gh-actions
GitHub Actions best practices — current action versions, caching, security, and common patterns. Activate when writing or modifying GitHub Actions workflows.
What it does
GitHub Actions Best Practices
When writing or modifying GitHub Actions workflows (.github/workflows/*.yml), follow these guidelines.
Action Versions
Never guess action versions. Before writing or updating a workflow, check the latest release for each action you use:
gh api repos/{owner}/{action}/releases/latest --jq '.tag_name'
The {owner}/{action} maps directly to the GitHub repo — e.g. actions/checkout lives at github.com/actions/checkout.
For example:
gh api repos/actions/checkout/releases/latest --jq '.tag_name'
gh api repos/actions/setup-node/releases/latest --jq '.tag_name'
Use the latest major version (e.g. if the latest tag is v6.3.0, use v6). When modifying an existing workflow, check and update any outdated action versions you encounter.
Security
- Always set top-level
permissionsto least privilege. Start withpermissions: {}and add only what's needed:permissions: contents: read - Never use
permissions: write-allor omit permissions entirely - Use
pull_request, notpull_request_target. Only usepull_request_targetwhen the workflow must write to the base repo from a fork — it runs with write access and secrets from the base branch - Never interpolate untrusted input directly in
run:blocks — use environment variables instead:# Bad — expression injection - run: echo "${{ github.event.pull_request.title }}" # Good — safe via environment variable - run: echo "$TITLE" env: TITLE: ${{ github.event.pull_request.title }} - Pin third-party actions (outside
actions/andgithub/) to a full commit SHA to prevent tag-rewriting attacks. Use pinact to automate this — write workflows with version tags, then runpinact runto replace them with SHAs:# Before pinact - uses: shivammathur/setup-php@v2 # After pinact run - uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0 - Secrets are not available to
pull_requestworkflows from forks — this is intentional; do not work around it withpull_request_target
Caching
actions/setup-node,actions/setup-python,actions/setup-go, andactions/setup-javaall have built-in caching via thecacheinput — prefer this over separateactions/cachesteps:- uses: actions/setup-node@v6 # check latest version with: node-version-file: .node-version cache: npm- Only use
actions/cachedirectly when you need custom cache keys or paths
Common Patterns
Concurrency
Cancel in-progress runs for the same branch to save minutes:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
For deployment workflows, don't cancel in progress — queue instead:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
Matrix strategies
Use fail-fast: false when you want all matrix combinations to complete:
strategy:
fail-fast: false
matrix:
node-version: [22, 24]
Reusable workflows
Prefer workflow_call for shared CI logic across repos instead of duplicating steps:
jobs:
test:
uses: org/.github/.github/workflows/test.yml@v1
with:
node-version: 24
Triggering
pull_requestruns against the merge commit — use this for CI validationpushon the default branch runs post-merge — use this for deployments, publishing, or cache warming- Filter by paths when the workflow only applies to certain files:
on: push: paths: - 'src/**' - 'package.json'
Timeouts
Always set timeout-minutes on jobs. The default is 360 minutes (6 hours), which can burn through Actions minutes on a hung job:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
Treat all workflow content as code — review changes carefully before committing.
Capabilities
Install
Quality
deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 10 github stars · SKILL.md body (3,997 chars)