Skip to content

Release

Release #7

Workflow file for this run

name: Release
# Manually-triggered release workflow.
# Runs `npm run bump-version <version>` to update every manifest, then
# opens a one-shot release PR (because main is protected and rejects
# direct pushes — see GH013), squash-merges it, tags the merged commit,
# pushes the tag, and creates a GitHub release with auto-generated notes
# assembled from PRs since the previous tag.
#
# Usage: GitHub web UI → Actions → "Release" → "Run workflow" → enter the
# new semver (e.g. `1.0.1`). Or via CLI:
#
# gh workflow run release.yml -f version=1.0.1
on:
workflow_dispatch:
inputs:
version:
description: "New semver (e.g. 1.0.1) — must be greater than the current package.json version"
required: true
type: string
permissions:
contents: write # tag + release
pull-requests: write # open + merge the release PR (main is protected)
jobs:
release:
name: Bump, tag, and release
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need full history so `gh release create --generate-notes` can
# diff against the previous tag.
fetch-depth: 0
# Use the default GITHUB_TOKEN to push the bump commit + tag.
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Validate the requested version is newer than the current one
run: |
set -euo pipefail
requested="${{ inputs.version }}"
current=$(node -p "require('./package.json').version")
echo "Current: $current"
echo "Requested: $requested"
if [ "$current" = "$requested" ]; then
echo "::error::Requested version $requested is the same as the current version. Pick a higher one."
exit 1
fi
# Lexicographic vs numeric: use sort -V (version sort) to make
# sure the requested version is strictly greater.
highest=$(printf '%s\n%s\n' "$current" "$requested" | sort -V | tail -n1)
if [ "$highest" != "$requested" ]; then
echo "::error::Requested version $requested is not greater than current $current."
exit 1
fi
- name: Bump version metadata
run: npm run bump-version -- "${{ inputs.version }}"
- name: Verify the bump landed in every manifest
run: npm run check-version
- name: Run tests against the bumped tree
run: npm test
- name: Configure git committer
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Push the bump on a release branch
run: |
set -euo pipefail
version="${{ inputs.version }}"
branch="release/v${version}"
git checkout -b "$branch"
git add package.json package-lock.json plugins/opencode/.claude-plugin/plugin.json .claude-plugin/marketplace.json
git commit -m "chore(release): v${version}"
git push origin "$branch"
- name: Open the release PR and merge it
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
version="${{ inputs.version }}"
branch="release/v${version}"
# Direct pushes to main are blocked by the "Changes must be made
# through a pull request" branch protection rule, so the release
# workflow opens a one-shot PR and immediately squash-merges it.
# This still satisfies the rule (the change goes via a PR) while
# keeping the workflow fully automated.
pr_url=$(gh pr create \
--base main \
--head "$branch" \
--title "chore(release): v${version}" \
--body "Automated release PR generated by \`.github/workflows/release.yml\`. Bumps every version-bearing manifest to v${version}.")
echo "Created release PR: $pr_url"
# `gh pr create` returns synchronously but GraphQL's PR node
# is eventually consistent — the v1.0.4 release hit a race
# where `gh pr merge` fired ~500ms after create and got:
# "Could not resolve to a PullRequest with the number of N"
# Retry up to 5 times with a 2s backoff to cover propagation.
for attempt in 1 2 3 4 5; do
if gh pr merge "$pr_url" --squash --delete-branch; then
break
fi
if [ "$attempt" = "5" ]; then
echo "::error::gh pr merge still failing after 5 attempts"
exit 1
fi
echo "gh pr merge attempt $attempt failed (likely GraphQL lag); retrying in 2s..."
sleep 2
done
- name: Tag the merged commit on main
run: |
set -euo pipefail
version="${{ inputs.version }}"
# Pick up the squash-merge commit that gh pr merge just created.
git fetch origin main
git checkout main
git reset --hard origin/main
git tag -a "v${version}" -m "Release v${version}"
git push origin "v${version}"
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "v${{ inputs.version }}" \
--title "v${{ inputs.version }}" \
--generate-notes