ci: open release PR by open-state lookup, not branch name (#41) #33
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: Automated Release | |
| # PR-based release flow. | |
| # | |
| # Why: master has a ruleset requiring the "Validate Skill Files" status check. | |
| # A direct push of the release commit (the old flow) is rejected, and the | |
| # github-actions[bot] cannot be a ruleset bypass actor on a user-owned repo. | |
| # So the version bump goes through a PR whose required check actually runs. | |
| # | |
| # REQUIRES (one-time manual setup): | |
| # 1. Repo secret RELEASE_TOKEN = fine-grained PAT for this repo with | |
| # contents:write + pull-requests:write. The default GITHUB_TOKEN will NOT | |
| # work: PRs/branches it creates do not trigger workflows, so the required | |
| # "Validate Skill Files" check would never run and auto-merge would hang. | |
| # 2. Repo Settings -> "Allow auto-merge" enabled (for `gh pr merge --auto`). | |
| # | |
| # This workflow (A) opens/refreshes the release PR. tag-release.yml (B) tags + | |
| # publishes after the PR merges to master. | |
| on: | |
| push: | |
| branches: | |
| - master | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| release-pr: | |
| name: Open release PR | |
| runs-on: ubuntu-latest | |
| # Loop guard: do not re-release the merged release commit. | |
| if: ${{ github.event_name == 'workflow_dispatch' || !startsWith(github.event.head_commit.message, 'chore(release):') }} | |
| steps: | |
| - name: Checkout master | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.RELEASE_TOKEN }} | |
| # The changelog action commits on the CURRENT branch then runs | |
| # `git push origin release/next`, so a local `release/next` ref must be | |
| # checked out; its internal `git pull --ff-only` also needs an upstream. | |
| # Create release/next from master HEAD, publish it, and track it (its own | |
| # remote, never master - the action pushes explicitly to release/next). | |
| - name: Prepare release branch | |
| run: | | |
| git push origin --delete release/next 2>/dev/null || true | |
| git checkout -B release/next | |
| git push origin release/next | |
| git branch --set-upstream-to=origin/release/next release/next | |
| # Version calc + CHANGELOG + version.json + pre-commit sync | |
| # (SKILL.md frontmatter metadata.version + .codex-plugin/plugin.json), | |
| # committed onto release/next. No tag here (tag is created after merge by | |
| # tag-release.yml). skip-ci MUST be false so the PR runs the required check. | |
| - name: Conventional Changelog (release branch) | |
| id: changelog | |
| uses: TriPSs/conventional-changelog-action@v5 | |
| with: | |
| github-token: ${{ secrets.RELEASE_TOKEN }} | |
| git-message: 'chore(release): {version}' | |
| git-user-name: 'github-actions[bot]' | |
| git-user-email: 'github-actions[bot]@users.noreply.github.com' | |
| git-branch: 'release/next' | |
| preset: 'angular' | |
| tag-prefix: 'v' | |
| output-file: 'CHANGELOG.md' | |
| version-file: './version.json' | |
| version-path: 'version' | |
| skip-on-empty: 'true' | |
| skip-version-file: 'false' | |
| skip-commit: 'false' | |
| skip-tag: 'true' | |
| skip-ci: 'false' | |
| pre-commit: '.github/release/pre-commit.js' | |
| - name: Open / refresh release PR and enable auto-merge | |
| if: ${{ steps.changelog.outputs.skipped == 'false' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| TAG: ${{ steps.changelog.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| # Branch was reset+pushed by the changelog action. Reuse an OPEN PR | |
| # for release/next if one exists, else open a new one. Title drives | |
| # the squash-merge commit subject so the loop guard (Workflow A) | |
| # skips it and Workflow B's trigger matches it. | |
| # | |
| # Resolve by `gh pr list --state open`, NOT `gh pr view release/next`: | |
| # view/merge by branch name can resolve to a STALE MERGED PR (a prior | |
| # release reused the release/next head), so the script silently took | |
| # the edit-merged-PR path and never created a new PR while still | |
| # exiting 0. Scope to state=open and act on the explicit number. | |
| pr=$(gh pr list --head release/next --base master --state open \ | |
| --json number --jq '.[0].number // empty') | |
| if [ -z "$pr" ]; then | |
| url=$(gh pr create \ | |
| --base master \ | |
| --head release/next \ | |
| --title "chore(release): ${TAG}" \ | |
| --body "Automated release ${TAG}. Squash-merging publishes the tag and GitHub Release (tag-release.yml).") | |
| pr=$(printf '%s\n' "$url" | grep -oE '[0-9]+$') | |
| else | |
| gh pr edit "$pr" --title "chore(release): ${TAG}" | |
| fi | |
| gh pr merge "$pr" --auto --squash --delete-branch --subject "chore(release): ${TAG}" |