Set up or align a GitHub Actions release pipeline for a versioned package, library, CLI, or marketplace action. Use when standardizing repos around the verify-then-release shape: push to main → guardrails → semantic-release tags + publishes → version-bump commit back to main with [skip ci].
99
100%
Does it follow best practices?
Impact
98%
1.55xAverage score across 4 eval scenarios
Passed
No known issues
Use when aligning GitHub Actions release workflow files.
.github/workflows/ci.yml with verify and release jobs.verify.yml + release.yml only when verify must run on a different cadence (e.g., scheduled) or when release needs a runner the verify path does not.pull_request and push to main.push to main only. Encode this as an if: on the release job rather than a separate on: block, so verify and release stay coupled.workflow_dispatch is fine to add for verify but must not bypass [skip ci] for release.Workflow-level cancellable group for verify:
concurrency:
group: verify-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueJob-level non-cancellable group for release:
concurrency:
group: release-${{ github.repository }}-main
cancel-in-progress: falseCancelling a release mid-tag corrupts the tag/release pairing. Always queue.
Workflow default: permissions: read-all (or just omit and rely on the default token's read scope).
Release job:
permissions:
contents: write
issues: write
pull-requests: writeAdd only when producing build provenance (e.g., GoReleaser + actions/attest-build-provenance):
id-token: write
attestations: writeactions/checkout@v6 with fetch-depth: 0. Semantic-release walks history to compute the next version; a shallow clone breaks it.persist-credentials: true (the default) so @semantic-release/git can push the bump commit using GITHUB_TOKEN.[skip ci] GateBoth jobs must short-circuit when the head commit is the bot's bump commit:
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}Apply on both verify and release. Skipping it on verify means the bump commit re-runs the verify suite for nothing; skipping it on release means the bump commit recursively triggers a new release.
Set inside the release step's env: (not at the job level — only semantic-release uses these):
env:
GIT_AUTHOR_NAME: release-bot
GIT_AUTHOR_EMAIL: release-bot@users.noreply.github.com
GIT_COMMITTER_NAME: release-bot
GIT_COMMITTER_EMAIL: release-bot@users.noreply.github.comUse a noreply.github.com address or a dedicated bot account. Do not attribute bump commits to a human contributor.
When the verify path has parallel jobs (e.g., verify-unit, verify-consumer-surface):
release:
needs: [verify-unit, verify-consumer-surface]Release waits for all verify jobs. Adding a new verify job means adding it to needs: — do not rely on a single umbrella job.
Pick one matching the repo's toolchain and place it after actions/checkout. The verify command is whatever the repo already uses (make verify, vp run verify, mise run verify, etc.) — do not invent a new one.
# Node / TypeScript
- uses: actions/setup-node@v5
with: { node-version-file: ".nvmrc", cache: "npm" }
- run: npm ci# Node via VitePlus
- uses: voidzero-dev/setup-vp@v1
with: { node-version-file: ".node-version", cache: true }
- run: vp install# Go CLI
- uses: jdx/mise-action@v4
- run: mise run verify# Swift (CocoaPods + SwiftPM)
- uses: maxim-lobanov/setup-xcode@v1
with: { xcode-version: latest-stable }
- uses: ruby/setup-ruby@v1
with: { bundler-cache: true }