GitHub Actions workflows

Author YAML with minimized permissions, pinned actions (commit SHA preferred), reusable workflows, and environment protection rules.

The SKILL defines on triggers, path filters, and concurrency group names; third-party actions need vendor review and prefer official or pinned versions.

When using GITHUB_TOKEN, explicitly downgrade defaults; for cross-repo or cloud access use OIDC with provider trust—avoid long-lived secrets.

Document when to split reusable workflows vs composite actions; self-hosted runner labels and isolation belong in the SKILL when applicable.

  • Cache: actions/cache keys and restore-keys—avoid bad hits.
  • Container jobs: image provenance and pull policy—align with Dockerfile guidance.
  • Reuse: validate workflow_call inputs and document defaults.

GHA main flow (skill-flow-block)

  [ Event: push / PR / schedule / workflow_dispatch / workflow_call ]
                    │
                    ▼
         [ Match: workflow file + branch / paths / activity types ]
                    │
                    ▼
    [ Concurrency: group cancels or queues—watch deploy job races ]
                    │
                    ▼
         [ Job: runs-on, lowered permissions, environment, secrets ]
                    │
           ┌────────┴────────┐
           ▼                ▼
  [ Inline steps: pinned uses + explicit with ]   [ Reuse: workflow_call / composite / templates ]
           │                 │
           └────────┬────────┘
                    ▼
         [ Outputs: artifacts, summaries, checks—on failure keep logs + repro commands ]
When agents edit YAML, also verify default branch protection, required check names vs jobs.*.name, and unintended expansion of pull_request_target risk.

Reusable workflow comparison matrix

Compare common reuse patterns; click a row for SKILL talking points (local highlight only—no network).

Pattern Entry Inputs / secrets Permission boundary Typical use
Reusable workflow on.workflow_call, consumed via uses: org/repo/.github/workflows/x.yml@ref inputs / secrets explicit; optional outputs Caller and callee permissions are separate; cross-repo needs trusted ref + CODEOWNERS Shared CI or release templates across repos
Composite action action.yml with runs.using: composite; uses: ./path or published action inputs; secrets from calling workflow Same actor as calling job—good for bundling shell, not whole pipelines Repeated setup, toolchain install, cache wrappers
Docker / JS action uses: owner/action@vX or SHA; optional docker:// with; sensitive via secrets Vendor review + SHA pins; watch post steps and cache side effects Official or community single-purpose steps
Job matrix strategy.matrix / fail-fast inside one workflow Matrix dims as env; secrets not auto-shared beyond declared scopes Watch matrix explosion and minutes; split via workflow_call when huge Multi language versions, OS grids, parameterized tests

Selected: Click a row for SKILL emphasis.

Triggers, concurrency, and path filters

  • on: separate paths/paths-ignore for PR vs push; typed workflow_dispatch inputs with defaults and descriptions.
  • concurrency: group names include env/branch tokens so PRs don’t cancel each other; be careful with cancel-in-progress on deploy jobs.
  • Callable workflows: validate inputs enums; reject empty strings or dangerous default branch names.

Complete reusable workflow example with typed inputs, secrets, and outputs:

# .github/workflows/reusable-build.yml  — reusable workflow
name: Reusable Build

on:
  workflow_call:
    inputs:
      node-version:
        description: 'Node.js version'
        type: string
        default: '20'
        required: false
      environment:
        description: 'Target deployment environment'
        type: string
        required: true
    secrets:
      REGISTRY_TOKEN:
        required: true
    outputs:
      image-tag:
        description: 'Built image tag'
        value: ${{ jobs.build.outputs.tag }}

jobs:
  build:
    runs-on: ubuntu-24.04
    timeout-minutes: 20
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'

      - name: Restore build cache
        uses: actions/cache@v4
        with:
          path: |
            .next/cache
            node_modules/.cache
          key: build-${{ runner.os }}-${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            build-${{ runner.os }}-${{ inputs.node-version }}-
            build-${{ runner.os }}-

      - run: npm ci --prefer-offline
      - run: npm run build

      - id: tag
        run: |
          echo "tag=$(node -p "require('./package.json').version")-${{ github.sha }}" >> "$GITHUB_OUTPUT"

# ---- caller workflow ----
# jobs:
#   build:
#     uses: ./.github/workflows/reusable-build.yml
#     with:
#       node-version: '20'
#       environment: staging
#     secrets:
#       REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}

Matrix testing: multiple Node.js versions across multiple platforms in parallel:

# Matrix test: Node 18/20/22 x ubuntu/windows
jobs:
  test:
    strategy:
      fail-fast: false      # one failing combo won't stop others
      matrix:
        node: ['18', '20', '22']
        os: [ubuntu-24.04, windows-latest]
        exclude:
          - node: '18'
            os: windows-latest   # exclude specific combo
    runs-on: ${{ matrix.os }}
    name: Test Node ${{ matrix.node }} on ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

permissions, OIDC, and action pins

  • Least privilege: tighten top-level permissions, widen per job only as needed; justify contents: write.
  • OIDC: cloud trust policies bind sub/aud; don’t rely on spoofable claims alone.
  • Pins: prefer full commit SHAs; tags need org allow lists or Dependabot update policy.

Minimal permissions + environment protection with required reviewers and pinned action:

# Tighten top-level permissions, loosen per job + environment approval gate
permissions: {}   # close everything at top level

jobs:
  test:
    runs-on: ubuntu-24.04
    permissions:
      contents: read    # read-only source
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  deploy-prod:
    runs-on: ubuntu-24.04
    needs: test
    # environment binds protection rules — set in repo Settings > Environments
    # Required reviewers: at least 1 approval before job proceeds
    environment:
      name: production
      url: https://myapp.example.com
    permissions:
      id-token: write   # OIDC token
      contents: read
    steps:
      - uses: actions/checkout@v4

      # Pin action to full commit SHA, not @v4 tag
      - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
        with:
          role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ${{ vars.AWS_REGION }}

      - run: |
          # Secrets are scoped to the production environment job only
          aws ssm put-parameter             --name "/myapp/prod/db-password"             --value "${{ secrets.DB_PASSWORD }}"             --type SecureString --overwrite

Cache, containers, and environments

  • Cache: keys include lockfile hash + runner OS; document acceptable partial restore-keys hits.
  • Container jobs: image digest, non-root user, SBOM/scan gates per supply-chain policy.
  • Environments: protection rules, reviewers, secrets per env; separate prod deploy workflows from plain CI.

Cache optimization: correct key design for npm, Docker layers, and Terraform providers:

# npm dependency cache: exact key + two-level fallback
- name: Cache npm dependencies
  uses: actions/cache@v4
  id: npm-cache
  with:
    path: ~/.npm
    # Exact key: OS + node version + lockfile hash
    key: npm-${{ runner.os }}-node20-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-node20-
      npm-${{ runner.os }}-
  # cache-hit: true skips npm install entirely

- run: npm ci --prefer-offline
  # --prefer-offline: use ~/.npm cache before hitting registry

# Docker layer cache (BuildKit)
- name: Build with layer cache
  uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    cache-from: type=gha,scope=myapp-build
    cache-to: type=gha,mode=max,scope=myapp-build
    tags: myapp:${{ github.sha }}

# Terraform provider cache
- name: Cache Terraform providers
  uses: actions/cache@v4
  with:
    path: ~/.terraform.d/plugin-cache
    key: tf-${{ runner.os }}-${{ hashFiles('**/.terraform.lock.hcl') }}
    restore-keys: |
      tf-${{ runner.os }}-
---
name: github-actions-workflows
description: Secure, reusable GitHub Actions YAML with OIDC and caching
tags: [github-actions, ci-cd, security]
---
# Triggers and Concurrency
- Configure paths filters separately for push and PR to avoid noisy re-runs
- concurrency.group = workflow + ref; PRs must not cancel each other
- workflow_dispatch inputs: require type, description, and sane defaults
- workflow_call inputs: validate enums, reject empty strings or reserved branch names

# Least Privilege
- Top-level permissions: {} closed; each job opens only what it needs
- OIDC federation: id-token: write + cloud provider configure-credentials action
- Trust policy binds sub (repo:org/repo:ref:refs/heads/main) and aud together
- Action pins: full commit SHA preferred; pair with Dependabot for updates

# Reuse and Caching
- Reusable workflows: declare inputs/secrets/outputs explicitly; caller uses ./.github/workflows/x.yml
- Composite actions: bundle shell steps only, no independent deploy permissions
- actions/cache key = OS + tool version + lockfile hash; two-level restore-keys fallback
- Docker layer cache via BuildKit type=gha; scope per service

# Environments and Secrets
- environment binds required_reviewers; production deploy needs manual approval
- Production secrets visible only in jobs bound to the production environment
- pull_request_target: never checkout or run fork code in the same job
- Never echo or cat secrets; they must not appear in logs

Back to skills More skills