From ae442a9a1dfaa9f63a0eac5065563b46b3913f85 Mon Sep 17 00:00:00 2001 From: farhan Date: Thu, 22 Oct 2026 13:41:36 +0500 Subject: [PATCH 1/4] build: support releases from older release lines Releases only ran on pushes to main, so patching an older line needed a manual upload with PyPI rights. A manual run on a release/ branch now releases that line from its own tags, without moving the latest GitHub release or npm dist-tag. See openedx/public-engineering#573. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 23 ++++++++++++++++++----- backend-plugin-sample/pyproject.toml | 8 ++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65b4c50..fffd807 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,14 @@ name: Semantic Release on: push: branches: [main] + # Run manually on an older branch to cut a patch release for that line. + workflow_dispatch: + inputs: + force: + description: 'Force a version bump (auto = derive from commits)' + type: choice + options: [auto, patch, minor, major] + default: auto jobs: # The same aggregate a pull request has to pass, so the release runs exactly @@ -18,7 +26,6 @@ jobs: # release half-finished. needs: [run_ci] runs-on: ubuntu-latest - if: github.ref_name == 'main' concurrency: group: ${{ github.workflow }}-release-${{ github.ref_name }} cancel-in-progress: false @@ -56,6 +63,7 @@ jobs: # We create it ourselves in the next step so that the distributions # are attached before the release is published. See that step for why. vcs_release: "false" + force: ${{ inputs.force != 'auto' && inputs.force || '' }} # Built before the release is created so that its distributions can be # attached as release assets too. See the release step for why that @@ -89,6 +97,7 @@ jobs: # publishes it -- all within this one command. gh release create "$TAG" \ --verify-tag \ + --latest=${{ github.ref_name == 'main' }} \ --title "$TAG" \ --notes-file "$RUNNER_TEMP/release_notes.md" \ backend-plugin-sample/dist/* \ @@ -121,7 +130,7 @@ jobs: # and it won't require reversing the release. runs-on: ubuntu-latest needs: release - if: github.ref_name == 'main' && needs.release.outputs.released == 'true' + if: needs.release.outputs.released == 'true' permissions: contents: read @@ -143,7 +152,7 @@ jobs: publish_tutor_plugin_to_pypi: runs-on: ubuntu-latest needs: release - if: github.ref_name == 'main' && needs.release.outputs.released == 'true' + if: needs.release.outputs.released == 'true' permissions: contents: read @@ -164,7 +173,7 @@ jobs: publish_to_npm: runs-on: ubuntu-latest needs: release - if: github.ref_name == 'main' && needs.release.outputs.released == 'true' + if: needs.release.outputs.released == 'true' permissions: contents: read id-token: write @@ -181,9 +190,13 @@ jobs: node-version-file: './frontend-plugin-sample/.nvmrc' - name: Update the package version and publish + # Only releases from main move the `latest` dist-tag; an older line + # (release/ulmo) is published under its own name (ulmo). + env: + DIST_TAG: ${{ github.ref_name == 'main' && 'latest' || github.ref_name }} run: | npm ci npm version --no-git-tag-version ${{ needs.release.outputs.version }} npm run build - npm publish + npm publish --tag "${DIST_TAG#release/}" working-directory: './frontend-plugin-sample' diff --git a/backend-plugin-sample/pyproject.toml b/backend-plugin-sample/pyproject.toml index ab3ba3b..9f504e2 100644 --- a/backend-plugin-sample/pyproject.toml +++ b/backend-plugin-sample/pyproject.toml @@ -126,6 +126,14 @@ where = ["src"] # code and files we need in this repo. build_command = "pip install build && SETUPTOOLS_SCM_PRETEND_VERSION=$NEW_VERSION python -m build" +[tool.semantic_release.branches.main] +match = "main" + +# Release branches (e.g. release/ulmo) can be released by running release.yml +# manually to patch an older release line. The version comes from that branch's tags. +[tool.semantic_release.branches.maintenance] +match = "^release/[a-z]+$" + [tool.semantic_release.commit_parser_options] # Because this repo is meant to be an example, docs changes are relevant # feature changes and so should produces new releases. From f4c4caf27c11198d866e26335ee12d20152b7021 Mon Sep 17 00:00:00 2001 From: farhan Date: Thu, 22 Oct 2026 13:54:30 +0500 Subject: [PATCH 2/4] build: fail a manual release run that releases nothing python-semantic-release exits successfully when the branch is not in a release group or the next version is already tagged on another branch, so a manual run could show green without publishing anything. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fffd807..cc6e5d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,6 +65,17 @@ jobs: vcs_release: "false" force: ${{ inputs.force != 'auto' && inputs.force || '' }} + # python-semantic-release exits successfully without releasing when the + # branch is not in a release group, there is nothing to release, or the + # next version is already tagged on another branch (e.g. main already + # released 3.8.5 and release/ulmo now computes 3.8.5 too). A manual run + # is always meant to release, so fail it rather than show it as green. + - name: Check | Fail a manual run that released nothing + if: github.event_name == 'workflow_dispatch' && steps.release.outputs.released != 'true' + run: | + echo "::error::No release was made. Run this on main or release/, with releasable commits, and make sure the next version is not already tagged on another branch." + exit 1 + # Built before the release is created so that its distributions can be # attached as release assets too. See the release step for why that # ordering matters. From 23147cd3f3c83185e521104c6fec3fc8568424ec Mon Sep 17 00:00:00 2001 From: farhan Date: Thu, 22 Oct 2026 15:28:30 +0500 Subject: [PATCH 3/4] build: drop version-clash wording from the release check Each release line keeps its own minor series, so a clash with a version tagged on another branch is not an expected cause of an empty release. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc6e5d8..4fff577 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,14 +66,13 @@ jobs: force: ${{ inputs.force != 'auto' && inputs.force || '' }} # python-semantic-release exits successfully without releasing when the - # branch is not in a release group, there is nothing to release, or the - # next version is already tagged on another branch (e.g. main already - # released 3.8.5 and release/ulmo now computes 3.8.5 too). A manual run - # is always meant to release, so fail it rather than show it as green. + # branch is not in a release group or there is nothing to release. A + # manual run is always meant to release, so fail it rather than show it + # as green. - name: Check | Fail a manual run that released nothing if: github.event_name == 'workflow_dispatch' && steps.release.outputs.released != 'true' run: | - echo "::error::No release was made. Run this on main or release/, with releasable commits, and make sure the next version is not already tagged on another branch." + echo "::error::No release was made. Run this on main or release/, with releasable commits." exit 1 # Built before the release is created so that its distributions can be From 48b2bcb6ce119259038f8f987476b0a955ea62b0 Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 24 Oct 2026 11:36:13 +0500 Subject: [PATCH 4/4] build: fail a release skipped because its tag is on another branch PSR exits successfully without releasing when the next version is already tagged on a release branch, so the run looked green even though nothing was published. Also document releasing from an older line in the README, with backports landing as fix: commits. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 15 +++++++++++++++ README.md | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4fff577..1225b77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,6 +75,21 @@ jobs: echo "::error::No release was made. Run this on main or release/, with releasable commits." exit 1 + # python-semantic-release also skips the release, and exits successfully, + # when the next version is already tagged on another branch, eg. main's + # next minor taken by a release branch. Fail so the skip doesn't look green. + - name: Check | Fail when the next version is tagged on another branch + if: steps.release.outputs.released != 'true' + env: + TAG: ${{ steps.release.outputs.tag }} + run: | + git fetch --tags --quiet + if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null \ + && ! git merge-base --is-ancestor "$TAG" HEAD; then + echo "::error::$TAG is already tagged on another branch, so this release was skipped." + exit 1 + fi + # Built before the release is created so that its distributions can be # attached as release assets too. See the release step for why that # ordering matters. diff --git a/README.md b/README.md index 36929ec..41c9fd0 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,21 @@ This path assumes you already have edx-platform running locally (bare-metal or d > TODO: a fully local brand-development flow without Tutor (recompile + serve from disk) is not yet documented. +## Releasing from an older release branch + +1. Create a `release/` branch from the release tag, e.g. `git switch -c release/ulmo v3.8.7`, and merge the fix into it as a `fix:` commit. + Only branches matching `release/` can be released. +2. Go to **Actions → Semantic Release → Run workflow** and select that branch. +3. Optionally set `force` to `patch`, `minor` or `major`. The default `auto` derives the bump from the commits. + +The version comes from the tags on that branch only, so a `fix:` on `release/ulmo` releases `3.8.8` even if `main` is on `3.9.x`. +Backports should land as `fix:` commits, so the line gets a patch and not the next minor that `main` will want. For the same reason, make `main`'s next release after cutting the branch at least a minor bump (a `feat:` commit, or `force: minor`). +The GitHub Release is not marked as latest, and the npm package is published under the `` dist-tag instead of `latest`. + +This works only for branches cut from a release that includes this workflow. Older release branches have to be released manually. + +See [Multibranch Releases](https://python-semantic-release.readthedocs.io/en/latest/concepts/multibranch_releases.html) for more details. + ## Getting help - Open edX [community Slack](https://openedx.org/slack) and [discussion forums](https://discuss.openedx.org)