Set up or align GitHub repo settings, branch/ruleset policy, templates, Actions hardening, Environments, release workflows, and deploy workflows for continuously publishable or deployable repositories.
97
100%
Does it follow best practices?
Impact
96%
1.35xAverage score across 7 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, merge_group when the repo uses merge queue, 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.pull_request_target for any workflow that checks out, installs, builds, tests, packages, signs, publishes, or otherwise executes project code. Keep fork and outsider code on pull_request with read-only credentials and no release secrets.workflow_dispatch is fine to add for verify; release paths still honor the [skip ci] gate.main, a published v* tag, or a separately validated protected ref.actions/checkout with.ref or git checkout input is trusted. Treat run ref and checkout ref as separate trust boundaries.workflow_dispatch inputs through env: into a secretless validation step. Validate shape/length/allowed values, emit the sanitized value as a step output, then use that output downstream.actions/checkout with with: { ref: ${{ steps.validate.outputs.ref }} } after validation.$GITHUB_ENV unless it is sanitized or written with a heredoc-safe delimiter.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: {}. Jobs opt into only the scopes they need.
Release job:
permissions:
contents: writeAdd id-token: write only when the job uses npm trusted publishing, provider OIDC, or keyless provenance. Add issues: write and pull-requests: write only when semantic-release is configured to comment on issues or pull requests. Add attestations: write only when producing GitHub build provenance:
id-token: write
attestations: writeubuntu-latest, windows-latest, and macos-latest.main rules, allowed push actors, release tag rules, Actions permission policy, Environment reviewers/branch policy, and publish secret location.release Environment to read publish secrets and vars. GitHub creates deployment records for jobs that declare an Environment; there is no supported deployment: false key. If deployment records are unacceptable, use trusted publishing/OIDC without a GitHub Environment or another narrowly scoped secret boundary the repo can justify.extra_plugins for workflow-owned release plugins, and reserve devDependencies for tooling the repo intentionally exposes through local scripts or lockfile-owned release wrappers.actions/checkout@<full-sha> # v6.0.2 with fetch-depth: 0. Semantic-release walks history to compute the next version; a shallow clone breaks it.persist-credentials: false through checkout, install, build, and pack steps whenever possible, especially before package-manager lifecycle scripts run. If @semantic-release/git must push a bump commit, add write credentials only at the narrow release boundary: use a release bot or GitHub App token that branch rules explicitly allow, configure the git remote or credential helper immediately before semantic-release, and avoid exposing that token to dependency install steps.[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 so bump commits are attributed to automation.
GIT_AUTHOR_*/GIT_COMMITTER_* with GITHUB_TOKEN still writes as github-actions[bot]. When branch push restrictions are not enabled, workflow GITHUB_TOKEN writeback is acceptable for low-risk repos. When branch push restrictions are required, use a narrowly scoped GitHub App release actor that branch rules or rulesets can explicitly allow.GIT_AUTHOR_*/GIT_COMMITTER_*. Checkout tokens do not override hardcoded metadata.pull_request and privileged push: main, workflow_dispatch, or tag-driven jobs. The dangerous shape is outsider-controlled code populating a cache that a later publish job consumes.Pods/, vendor/, dist/, build directories, or packaged runtime bundles inside secret-bearing release jobs.actions/upload-artifact / actions/download-artifact as the bridge from release to deploy when the payload is already published somewhere durable. Actions artifacts are CI scratch storage with quota and retention failure modes.timeout-minutes to verify and release jobs so stuck package managers, signing tools, or registry calls do not burn the default six-hour window.fail-fast: false for release matrices when every OS/archive result is useful evidence; use max-parallel when signing services, package registries, or tap repos rate-limit concurrent publishes.retention-days to 1-3, set if-no-files-found: error, use lane/package-specific names, and record the artifact digest.optionalDependencies, and package-root payloads such as router_init.js.id-token: write; remove NPM_TOKEN; and rely on npm's automatic provenance for public packages from public repos.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: explicitly.
Pick one matching the repo's toolchain and place it after actions/checkout. Use the repo's existing verify command (make verify, vp run verify, mise run verify, etc.).
For Node / TypeScript workflows, check in .node-version with the latest active LTS line, currently Node 24.x. Use .node-version consistently; do not introduce alternate Node version files or hardcode an older Node major in workflow YAML.
# Node / TypeScript
- uses: actions/setup-node@<full-sha> # v6.4.0
with: { node-version-file: ".node-version" }
- run: npm ci# Node via Vite+
- uses: voidzero-dev/setup-vp@<full-sha> # v1.10.0
with: { node-version-file: ".node-version", cache: false, run-install: false }
- run: vp install# Go CLI
- uses: jdx/mise-action@<full-sha> # v4.0.1
- run: mise run verify# Swift (CocoaPods + SwiftPM)
- uses: maxim-lobanov/setup-xcode@<full-sha> # v1.7.0
with: { xcode-version: latest-stable }
- uses: ruby/setup-ruby@<full-sha> # v1.310.0
with: { bundler-cache: false }