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
107 changes: 76 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,39 @@ on:
permissions:
contents: write

# Ordering invariant: the version-bump commit lands on main only AFTER the
# matching release is published and all binaries are uploaded. This closes
# the ~80s race window in which CC would sync plugin.json at the new version
# while the GitHub release was still a draft (asset URLs return 404 for
# unauthenticated downloads, so the wrapper's first-run download fails and
# CC caches the MCP startup failure until restart).

jobs:
# ===========================================================================
# Step 1: Determine version and commit bump
# Step 1: Determine the target version (no push yet)
# ===========================================================================
version:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
commit_sha: ${{ steps.bump.outputs.commit_sha }}
bumped: ${{ steps.version.outputs.bumped }}
merge_sha: ${{ steps.resolve.outputs.merge_sha }}

steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.VERSION_BUMP_KEY }}
fetch-depth: 0

- name: Resolve PR merge commit
id: resolve
run: |
SHA="${{ github.event.pull_request.merge_commit_sha }}"
if [ -z "$SHA" ]; then
SHA=$(git rev-parse HEAD)
fi
echo "merge_sha=$SHA" >> "$GITHUB_OUTPUT"

- name: Determine version
id: version
run: |
Expand Down Expand Up @@ -69,29 +85,14 @@ jobs:
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
fi

- name: Sync version files
run: |
VERSION="${{ steps.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' .claude-plugin/plugin.json > tmp.json && mv tmp.json .claude-plugin/plugin.json

- name: Commit version bump
id: bump
run: |
VERSION="${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .claude-plugin/plugin.json
if git diff --cached --quiet; then
echo "Version files already at $VERSION — no commit needed"
else
git commit -m "bump to v${VERSION}"
git push
fi
echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

# ===========================================================================
# Step 2: Create draft release (tag + release atomically)
# Step 2: Create draft release, tagging the PR-merge commit
# ===========================================================================
# The tag points to the merge SHA so the tag exists before any bump-commit
# push. For auto-bumps this means the tagged tree has plugin.json one
# version behind the tag name (the bump lands in Step 5); for manual bumps
# the merge commit already contains the new plugin.json so the tagged tree
# matches the tag.
create-release:
needs: version
runs-on: ubuntu-latest
Expand All @@ -101,7 +102,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
ref: ${{ needs.version.outputs.merge_sha }}

- name: Generate release notes
id: notes
Expand Down Expand Up @@ -129,17 +130,18 @@ jobs:
RELEASE_NOTES: ${{ steps.notes.outputs.notes }}
run: |
TAG="v${{ needs.version.outputs.version }}"
COMMIT="${{ needs.version.outputs.commit_sha }}"
MERGE_SHA="${{ needs.version.outputs.merge_sha }}"
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then
echo "Tag $TAG already exists — skipping release"
echo "release_created=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Tag the version-bumped commit, not the stale checkout
git tag "$TAG" "$COMMIT"
# Tag the PR-merge commit so the tag is in place before any
# plugin.json bump-commit lands on main.
git tag "$TAG" "$MERGE_SHA"
git push origin "$TAG"
# Create release; clean up tag on failure
if ! gh release create "$TAG" --title "$TAG" --draft --notes "$RELEASE_NOTES"; then
if ! gh release create "$TAG" --title "$TAG" --target "$MERGE_SHA" --draft --notes "$RELEASE_NOTES"; then
echo "::warning::Release creation failed — cleaning up orphaned tag"
git push origin --delete "$TAG" || true
echo "release_created=false" >> "$GITHUB_OUTPUT"
Expand All @@ -163,6 +165,8 @@ jobs:
arch: [amd64, arm64]
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.version.outputs.merge_sha }}

- uses: actions/setup-go@v5
with:
Expand All @@ -186,7 +190,7 @@ jobs:
working-directory: src

# ===========================================================================
# Step 4: Generate checksums and publish release
# Step 4: Generate checksums and publish (un-draft) the release
# ===========================================================================
publish:
needs: [version, create-release, binaries]
Expand All @@ -210,11 +214,52 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release edit "v${{ needs.version.outputs.version }}" --draft=false

# ===========================================================================
# Step 5: Push the version-bump commit (auto-bump only)
# ===========================================================================
# Runs last so plugin.json on main only advertises the new version after
# the release is fully published. Skipped for manual bumps where the
# merge commit already carries the bumped plugin.json.
bump:
needs: [version, create-release, binaries, publish]
if: >-
needs.create-release.outputs.release_created == 'true' &&
needs.version.outputs.bumped == 'auto'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.VERSION_BUMP_KEY }}
fetch-depth: 0
ref: main

- name: Sync plugin.json
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' .claude-plugin/plugin.json > tmp.json && mv tmp.json .claude-plugin/plugin.json

- name: Commit and push version bump
run: |
VERSION="${{ needs.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .claude-plugin/plugin.json
if git diff --cached --quiet; then
echo "Version files already at $VERSION — no commit needed"
else
git commit -m "bump to v${VERSION}"
git push
fi

# ===========================================================================
# Cleanup on failure
# ===========================================================================
# Note: bump failures do NOT trigger cleanup — by that point the release
# is already published, so the artifacts should stay. A failed bump leaves
# plugin.json on main one version behind; the next merge's auto-bump logic
# walks forward from the latest tag, so recovery is automatic on next merge.
cleanup-on-failure:
needs: [version, create-release, binaries, publish]
needs: [version, create-release, binaries, publish, bump]
if: >-
always() &&
needs.create-release.outputs.release_created == 'true' &&
Expand Down
Loading