Release #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |