← Back to Blog
DevOps June 2026 7 min read

GitHub Actions: Practical Patterns Beyond the Basics

Past "lint, test, build" lies a set of patterns that actually matter in production pipelines: reusable workflows, matrix builds, caching, and OIDC-based cloud auth with no stored secrets.

Reusable workflows vs. composite actions

Once you have more than a couple of repos, copy-pasted CI YAML becomes its own maintenance burden — exactly the Terraform DRY problem, just in pipeline form. GitHub Actions gives you two mechanisms to avoid it, and picking the right one matters:

  • Composite actions — bundle a sequence of steps into a single reusable action, called from within a job. Good for "these five steps always go together" (e.g. checkout + setup + cache restore).
  • Reusable workflows — an entire workflow called via workflow_call, with its own inputs/secrets, and its own jobs. Good for "every service repo should run this exact CI pipeline shape."
# .github/workflows/reusable-build.yml
on:
  workflow_call:
    inputs:
      node-version: { type: string, default: '20' }
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ inputs.node-version }} }
      - run: npm ci && npm run build

# calling-repo/.github/workflows/ci.yml
jobs:
  build:
    uses: my-org/.github/.github/workflows/reusable-build.yml@main
    with: { node-version: '22' }

Matrix builds for real coverage

Testing across multiple versions or platforms shouldn't mean duplicated job definitions. The matrix strategy fans a single job definition out across every combination automatically, including failing fast or continuing on error per-combination as needed.

strategy:
  matrix:
    node: [18, 20, 22]
    os: [ubuntu-latest, macos-latest]
  fail-fast: false
runs-on: ${{ matrix.os }}
steps:
  - uses: actions/setup-node@v4
    with: { node-version: ${{ matrix.node }} }

Caching: the single biggest speed lever

Dependency installation (npm, pip, Go modules) is usually the slowest, most repetitive part of a pipeline. actions/cache keyed on a lockfile hash means cache hits skip the slow install entirely, and cache misses only happen when dependencies actually change.

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: npm-${{ runner.os }}-
✓ Many setup-* actions (setup-node, setup-go) now have a built-in cache: input that wraps this pattern for you — check before hand-rolling it.

OIDC: cloud auth without stored secrets

The biggest security upgrade most pipelines are still missing: instead of storing long-lived AWS/GCP/Azure credentials as repo secrets, GitHub Actions can present a short-lived OIDC token that your cloud provider trusts directly — no static credential sitting in secrets storage waiting to leak.

permissions:
  id-token: write
  contents: read
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
      aws-region: eu-west-1

The IAM role trust policy restricts which repo/branch can assume it — meaning a leaked workflow log can't be used to exfiltrate a standing credential, because there isn't one.

Concurrency control: stop wasting runner minutes

Without concurrency settings, every push to a PR branch queues a new full pipeline run, even if three are already in flight for the same branch. Cancelling superseded runs automatically saves real runner minutes (and real money on hosted runners).

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Final thoughts

Most teams get the "run tests on push" basics right immediately and never revisit the pipeline again. The patterns that actually save time and reduce risk at scale — reusable workflows instead of copy-paste, caching, OIDC instead of stored secrets, and concurrency cancellation — tend to get adopted only after the pain of their absence becomes obvious. Worth doing earlier than that.