github-actions
GitHub Actions Best Practices
Workflow Structure
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm test
Key Principles
- Always set
concurrencywithcancel-in-progress: trueon PR workflows — avoids wasting runners on outdated commits. - Pin action versions to a major tag (
@v4) or full SHA for security-critical actions. Never use@mainor@latest. - Use
npm ci(orpnpm install --frozen-lockfile) instead ofnpm install— ensures deterministic installs from the lockfile.
Caching
Node.js Dependencies
The built-in cache in actions/setup-node handles most cases:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm" # or "pnpm" or "yarn"
Custom Caching
For build outputs, Playwright browsers, or other artifacts:
- uses: actions/cache@v4
with:
path: |
.next/cache
node_modules/.cache
key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-
Turbo Cache
For Turborepo monorepos:
- uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
Parallel Jobs
Split independent checks into separate jobs for faster feedback:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: "npm" }
- run: npm ci
- run: npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: "npm" }
- run: npm ci
- run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: "npm" }
- run: npm ci
- run: npm test
build:
needs: [lint, typecheck, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: "npm" }
- run: npm ci
- run: npm run build
Use needs to gate deployment on all checks passing.
Matrix Builds
Test across multiple versions or platforms:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [18, 20, 22]
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm test
- Set
fail-fast: falseto run all combinations even if one fails. - Use
includeandexcludeto fine-tune the matrix.
Reusable Workflows
Extract common workflow patterns into callable workflows:
# .github/workflows/reusable-ci.yml
name: Reusable CI
on:
workflow_call:
inputs:
node-version:
type: number
default: 20
secrets:
NPM_TOKEN:
required: false
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
Call it from another workflow:
# .github/workflows/ci.yml
jobs:
ci:
uses: ./.github/workflows/reusable-ci.yml
with:
node-version: 20
secrets: inherit
- Use
workflow_calltrigger for reusable workflows. secrets: inheritpasses all secrets from the caller.- Reusable workflows can live in the same repo or a shared org repo.
Composite Actions
Bundle repeated steps into a single reusable action:
# .github/actions/setup-project/action.yml
name: "Setup Project"
description: "Checkout, setup Node, and install dependencies"
inputs:
node-version:
description: "Node.js version"
default: "20"
runs:
using: "composite"
steps:
- uses: actions/checkout@v4
shell: bash
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
shell: bash
- run: npm ci
shell: bash
Use in any workflow:
steps:
- uses: ./.github/actions/setup-project
with:
node-version: 20
- run: npm test
When to Use Each
| Pattern | Use When |
|---|---|
| Composite action | Reusing a group of steps within a job |
| Reusable workflow | Reusing entire jobs with their own runners |
| Workflow template | Providing starting-point workflows for new repos (org-level) |
Secrets and Security
- Never hardcode secrets — use
${{ secrets.SECRET_NAME }}. - Use environment protection rules for production deployments (require approvals, limit branches).
- Minimize permissions with the
permissionskey:
permissions:
contents: read
pull-requests: write
- Audit third-party actions before using them. Pin to a full SHA for critical workflows:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- Use
${{ github.token }}(auto-provisioned) instead of personal access tokens when possible.
Environment-Based Deployments
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npx deploy --env staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npx deploy --env production
Configure environment protection rules in GitHub settings to require manual approval before production deploys.
Path Filtering
Run workflows only when relevant files change:
on:
push:
paths:
- "src/**"
- "package.json"
- "package-lock.json"
paths-ignore:
- "docs/**"
- "**.md"
For monorepos, use path filters to run only affected package checks.
Artifacts
Share files between jobs:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 1
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- run: npx deploy dist/
Set retention-days to avoid accumulating large artifacts.
Common Patterns
PR Preview Deploys
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- id: deploy
run: echo "url=$(npx deploy --preview)" >> $GITHUB_OUTPUT
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview deployed: ${{ steps.deploy.outputs.url }}`
})
Scheduled Jobs
on:
schedule:
- cron: "0 9 * * 1" # Every Monday at 9am UTC
jobs:
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --production
Release on Tag
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- run: npm ci && npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
Debugging
- Enable debug logging: Set the
ACTIONS_RUNNER_DEBUGrepository secret totrue. - Use
actfor local testing:act pushsimulates a push event locally. - Add diagnostic steps when debugging:
- run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
Anti-Patterns
- Don't install dependencies in every job without caching — use
actions/setup-nodewithcacheor a composite setup action. - Don't use
@masteror@mainfor action versions — pin to a tagged release. - Don't store secrets in workflow files or commit them to the repo.
- Don't run all checks sequentially — parallelize independent jobs.
- Don't skip
concurrencyon PR workflows — stale runs waste minutes and can deploy outdated code.
More from grahamcrackers/skills
bulletproof-react-patterns
Bulletproof React architecture patterns for scalable, maintainable applications. Covers feature-based project structure, component patterns, state management boundaries, API layer design, error handling, security, and testing strategies. Use when structuring a React project, designing application architecture, organizing features, or when the user asks about React project structure or scalable patterns.
45react-aria-components
React Aria Components patterns for building accessible, unstyled UI with composition-based architecture. Covers component structure, styling with Tailwind and CSS, render props, collections, forms, selections, overlays, and drag-and-drop. Use when building accessible components, using react-aria-components, creating design systems, or when the user asks about React Aria, accessible UI primitives, or headless component libraries.
17clean-code-principles
Clean code principles for readable, maintainable TypeScript and React codebases. Covers naming, functions, abstraction, composition, error handling, comments, and code smells. Use when writing new code, refactoring, reviewing code quality, or when the user asks about clean code, readability, or maintainability.
10typescript-best-practices
Core TypeScript conventions for type safety, inference, and clean code. Use when writing TypeScript, reviewing TypeScript code, creating interfaces/types, or when the user asks about TypeScript patterns, conventions, or best practices.
9tanstack-query
TanStack Query v5 patterns for server state management, caching, mutations, optimistic updates, and query organization. Use when working with TanStack Query, React Query, server state, data fetching hooks, or when the user asks about caching strategies, query invalidation, or mutation patterns.
8zustand
Zustand state management patterns for React including store design, selectors, slices, middleware (immer, persist, devtools), and async actions. Use when managing client-side state, creating stores, working with Zustand, or when the user asks about global state management, store patterns, or state persistence.
7