diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26781ede1..e8747f1a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,6 +60,34 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "Building tag=${TAG} version=${VERSION}" + - name: Verify tag is reachable from main + # Releases must be cut from `main`. Tagging a feature branch (e.g. when + # a stacked PR has merged into a parent feature branch but not yet into + # `main`) silently produces a release whose contents diverge from the + # canonical history. Precedent: v0.0.8/v0.0.9 were tagged from + # feat/embed-lmnr-key after PR #33 merged into the feature branch + # (not into main); main moved on without it and the bcode-laminar + # package had to be re-landed in PR #39. This guard fails the release + # before any binaries are uploaded. + # + # Resolves the tag name to a commit SHA via `git rev-parse` rather than + # using `$GITHUB_SHA`. For `push: tags` the two are equivalent, but for + # `workflow_dispatch` with `inputs.tag` `$GITHUB_SHA` is the dispatch + # ref's HEAD (typically main), not the selected tag's commit — using it + # would let a feature-branch tag pass the check trivially. + env: + TAG: ${{ steps.ver.outputs.tag }} + run: | + git fetch origin main --depth=1 + TAG_SHA=$(git rev-parse -q --verify "refs/tags/${TAG}^{commit}") || { + echo "::error::Tag ${TAG} does not exist locally. Create the tag on a main commit first (e.g. \`gh release create ${TAG} --target main\`), then re-run." + exit 1 + } + if ! git merge-base --is-ancestor "$TAG_SHA" origin/main; then + echo "::error::Tag ${TAG} points at $TAG_SHA which is not reachable from origin/main. Release tags must be cut from main." + exit 1 + fi + - name: Setup Bun uses: ./.github/actions/setup-bun