| name | github-actions |
| description | Master GitHub Actions CI/CD workflows with production-grade security and
performance patterns. Use ONLY when explicitly setting up CI/CD pipelines specifically via GitHub Actions,
setting up matrix strategies, caching dependencies, managing
artifacts, or implementing reusable workflows. For deployment requests, use ONLY when the deploy step is inside a .github/workflows file. NOT for GitLab or autonomous-dev. NOT for Render, AWS, Azure, or GCP deployments unless explicitly triggered from within a CI/CD workflow context. Do NOT trigger for generic "deploy my app" requests without clarifying the target platform.
|
GitHub Actions CI/CD Engineering Standards
This skill codifies 2026 GitHub Actions best practices — secure supply chains, efficient caching, reusable workflows, and hardened permission models.
1. Security: SHA Pinning (Mandatory)
Pin Every Third-Party Action to a Commit SHA
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde8c81c89c3166c0
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- ALWAYS pin to full-length commit SHAs — tags are mutable and can be hijacked
- ALWAYS add a trailing comment with the version for human readability
- Use tools like
step-security/harden-runner or pin-github-action CLI to automate SHA resolution
- Resolve SHA Manually: See templates.md for the
gh api command to resolve SHAs.
- Audit quarterly — review all pinned SHAs when updating workflow dependencies
Permission Hardening
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
- ALWAYS set
permissions: at the workflow level — use read-all or specify individually
- NEVER use
permissions: write-all — it grants maximum privileges
- Grant write only where needed — per-job, not per-workflow
2. Workflow Structure
Standard CI Template
See templates.md for a complete, production-ready CI workflow template.
Key Structural Rules
- ALWAYS set
concurrency with cancel-in-progress: true to prevent stale runs
- Use
needs: to create a dependency chain: lint → test → build → deploy
- Use
.node-version or .python-version files — never hardcode versions in workflows
- Use
--frozen-lockfile — never let CI modify the lock file
3. Caching
Package Manager Caching
- uses: actions/setup-node@<sha>
with:
node-version-file: .node-version
cache: pnpm
- uses: actions/setup-python@<sha>
with:
python-version-file: .python-version
- run: pip install uv
- uses: actions/cache@<sha>
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: uv-${{ runner.os }}-
- uses: actions/setup-go@<sha>
with:
go-version-file: go.mod
cache: true
Custom Caching Rules
- Key on lock file hash —
${{ hashFiles('pnpm-lock.yaml') }}
- Use
restore-keys for fallback to partial cache hits
- Cache the package manager's global cache, not
node_modules directly
- Don't cache everything — simplicity trumps marginal speedup
4. Matrix Strategy
Basic Matrix
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [20, 22]
exclude:
- os: windows-latest
node: 20
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@<sha>
with:
node-version: ${{ matrix.node }}
Dynamic Matrix
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set.outputs.matrix }}
steps:
- id: set
run: |
echo 'matrix={"include":[{"project":"api"},{"project":"web"}]}' >> "$GITHUB_OUTPUT"
build:
needs: prepare
strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- run: echo "Building ${{ matrix.project }}"
Rules
- Use
fail-fast: false for test matrices — you want to see all failures, not just the first
- Use
include/exclude to fine-tune — don't generate invalid combinations
- Use
max-parallel if jobs contend for shared resources (APIs, databases)
5. Reusable Workflows
Defining a Reusable Workflow
name: Reusable Build
on:
workflow_call:
inputs:
node-version:
type: string
default: '22'
secrets:
NPM_TOKEN:
required: true
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: actions/setup-node@<sha>
with:
node-version: ${{ inputs.node-version }}
registry-url: https://registry.npmjs.org
- run: pnpm install --frozen-lockfile
- run: pnpm build
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Calling a Reusable Workflow
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '22'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Rules
- Pass secrets explicitly — avoid
secrets: inherit (grants broader access than needed)
- Pin reusable workflows to SHA or tag in production
- Use
workflow_call inputs for all configuration — don't rely on env or file conventions
- Separate concerns: reusable workflows = entire jobs; composite actions = reusable steps
6. Artifacts (v4)
- uses: actions/upload-artifact@<sha>
with:
name: build-output
path: dist/
retention-days: 7
compression-level: 6
- uses: actions/download-artifact@<sha>
with:
name: build-output
path: dist/
Rules
- v4 artifacts are immutable — you cannot overwrite the same artifact name
- Use unique names per job — don't upload from parallel matrix jobs to the same name
- Set
retention-days — don't rely on org defaults (storage costs add up)
- Use
compression-level: 0 for already-compressed files (.zip, .tar.gz)
- v3 and v4 are incompatible — do not mix upload-artifact@v3 with download-artifact@v4
7. Environment Protection
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.example.com
steps:
- run: echo "Deploying to production"
- Use
environment: for production deployments — enables approval gates
- Configure required reviewers in repo Settings → Environments
- Use environment-scoped secrets — production secrets should not be accessible in CI
8. Anti-Patterns (Never Do These)
| Anti-Pattern | Why It's Wrong | Do This Instead |
|---|
uses: action@v4 | Mutable tag, supply chain risk | Pin to full commit SHA |
permissions: write-all | Maximum privilege, dangerous | Explicit per-job permissions |
continue-on-error: true on security steps | Suppresses critical failures | Hard-fail on security gates |
secrets: inherit | Over-broad secret access | Pass secrets explicitly |
Hardcoded node-version: 22 | Version drift across workflows | Use .node-version file |
No concurrency: | Stale runs waste minutes | Always set with cancel-in-progress |
if: always() on non-cleanup steps | Runs even after critical failures | Use if: success() (default) |
Caching node_modules directly | Fragile, platform-specific | Cache package manager global cache |