Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 22 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
name: Release

# Release is triggered by pushing a vX.Y.Z tag.
#
# PREREQUISITE: Before tagging, merge develop → main via PR.
# The tag must point to a commit on main, NOT develop.
# GitFlow: develop (default) → PR → main → tag → release.
#
# This workflow does NOT sync main — main must already be up to date.

on:
push:
tags:
Expand All @@ -10,46 +18,32 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
sync-main:
name: Sync main ← tag commit
verify-main:
name: Verify tag is on main
runs-on: ubuntu-latest
# Blocking: main MUST be synced before release artifacts are built.
# If main can't be fast-forwarded, the release aborts to prevent
# stale install.sh / mismatched main branch.
# (Hardened after uteke issue #325 — same pattern caused main to get
# stuck at old version while release proceeded.)
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Fast-forward main to tag commit
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check tag is on main
run: |
# Fetch main branch ref so merge-base can check ancestry.
git fetch origin main

TAG_SHA=$(git rev-parse "${{ github.ref }}^{commit}")
echo "Tag commit: $TAG_SHA"

# Check if main is an ancestor of TAG_SHA (safe fast-forward).
# If main is NOT an ancestor, abort — force-updating would
# rewrite history and potentially lose commits.
if git merge-base --is-ancestor origin/main "$TAG_SHA" 2>/dev/null; then
git push origin "$TAG_SHA:refs/heads/main"
echo "✅ main fast-forwarded to $TAG_SHA"
MAIN_SHA=$(git rev-parse origin/main)
echo "Tag: $TAG_SHA"
echo "main: $MAIN_SHA"
if [ "$TAG_SHA" = "$MAIN_SHA" ]; then
echo "✅ Tag is main HEAD"
elif git merge-base --is-ancestor "$TAG_SHA" origin/main 2>/dev/null; then
echo "✅ Tag commit $TAG_SHA is in main history"
else
echo "::error::main is not an ancestor of tag commit. Refusing to force-update."
echo "Manual intervention required: merge develop into main first."
echo "::error::Tag commit $TAG_SHA is NOT on main."
echo "Merge develop → main via PR before tagging."
exit 1
fi

build-release:
needs: sync-main
needs: [verify-main]
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
Expand Down Expand Up @@ -125,7 +119,7 @@ jobs:

publish-crates:
name: Publish to crates.io
needs: [sync-main]
needs: [verify-main]
runs-on: ubuntu-latest
permissions:
id-token: write
Expand Down
Loading