원클릭으로
gh-actions
// GitHub Actions best practices — current action versions, caching, security, and common patterns. Activate when writing or modifying GitHub Actions workflows.
// GitHub Actions best practices — current action versions, caching, security, and common patterns. Activate when writing or modifying GitHub Actions workflows.
| name | gh-actions |
| description | GitHub Actions best practices — current action versions, caching, security, and common patterns. Activate when writing or modifying GitHub Actions workflows. |
When writing or modifying GitHub Actions workflows (.github/workflows/*.yml), follow these guidelines.
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.
permissions to least privilege. Start with permissions: {} and add only what's needed:
permissions:
contents: read
permissions: write-all or omit permissions entirelypull_request, not pull_request_target. Only use pull_request_target when the workflow must write to the base repo from a fork — it runs with write access and secrets from the base branchrun: 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 }}
actions/ and github/) to a full commit SHA to prevent tag-rewriting attacks. Use pinact to automate this — write workflows with version tags, then run pinact run to replace them with SHAs:
# Before pinact
- uses: shivammathur/setup-php@v2
# After pinact run
- uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
pull_request workflows from forks — this is intentional; do not work around it with pull_request_targetactions/setup-node, actions/setup-python, actions/setup-go, and actions/setup-java all have built-in caching via the cache input — prefer this over separate actions/cache steps:
- uses: actions/setup-node@v6 # check latest version
with:
node-version-file: .node-version
cache: npm
actions/cache directly when you need custom cache keys or pathsCancel 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
Use fail-fast: false when you want all matrix combinations to complete:
strategy:
fail-fast: false
matrix:
node-version: [22, 24]
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
pull_request runs against the merge commit — use this for CI validationpush on the default branch runs post-merge — use this for deployments, publishing, or cache warmingon:
push:
paths:
- 'src/**'
- 'package.json'
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.