Reusable GitHub Actions Workflows

DevOpsCI/CDGitHub Actions
Share on LinkedIn

You have fourteen repositories. Each has a 120-line CI YAML that's 80% identical — checkout, setup Node, install, test, build Docker, push to ECR. A security fix to the Node setup requires editing fourteen files and hoping nobody copy-pasted with slight variations. Reusable GitHub Actions workflows let you define the pipeline once and call it from every repo with inputs for what's different.

Reusable workflow definition

Central repo: org/github-actions-templates/.github/workflows/node-ci.yml

name: Node CI Pipeline

on:
  workflow_call:
    inputs:
      node-version:
        type: string
        required: false
        default: '20'
      run-e2e:
        type: boolean
        required: false
        default: false
      docker-image:
        type: string
        required: true
    secrets:
      NPM_TOKEN:
        required: true
      AWS_ROLE_ARN:
        required: true
    outputs:
      image-tag:
        description: "Built Docker image tag"
        value: ${{ jobs.build.outputs.image-tag }}

jobs:
  test:
    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
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - run: npm test

      - if: inputs.run-e2e
        run: npm run test:e2e

  build:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1

      - uses: docker/build-push-action@v5
        id: meta
        with:
          push: true
          tags: ${{ inputs.docker-image }}:${{ github.sha }}

Calling from a service repo

# my-service/.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  ci:
    uses: org/github-actions-templates/.github/workflows/node-ci.yml@main
    with:
      node-version: '20'
      run-e2e: true
      docker-image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-service
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}

Fourteen lines in the service repo. The template repo owns the pipeline logic.

Pinning workflow versions

# Pin to tag for stability
uses: org/github-actions-templates/.github/workflows/[email protected]

# Pin to SHA for maximum security
uses: org/github-actions-templates/.github/workflows/node-ci.yml@a1b2c3d4e5f6

Don't pin to @main in production service repos — a template change breaks all pipelines simultaneously. Pin to tags or SHAs; update deliberately.

Composing multiple reusable workflows

jobs:
  ci:
    uses: org/templates/.github/workflows/node-ci.yml@v2
    with:
      docker-image: registry/my-service
    secrets: inherit  # pass all secrets (GitHub 2023+)

  deploy-staging:
    needs: ci
    if: github.ref == 'refs/heads/main'
    uses: org/templates/.github/workflows/deploy-k8s.yml@v2
    with:
      environment: staging
      image-tag: ${{ needs.ci.outputs.image-tag }}
    secrets:
      KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}

Job outputs from reusable workflows flow to downstream jobs via needs.

secrets: inherit

jobs:
  ci:
    uses: org/templates/.github/workflows/node-ci.yml@v2
    secrets: inherit

Passes all caller secrets to the reusable workflow. Convenient but less explicit — the reusable workflow can access secrets not listed in its secrets declaration. Use for internal templates; explicit mapping for third-party reusable workflows.

Composite action for step reuse

When you need reusable steps within a job, not a full workflow:

# org/actions/setup-node-aws/action.yml
name: Setup Node with AWS
inputs:
  node-version:
    default: '20'
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
    - uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ env.AWS_ROLE_ARN }}
        aws-region: us-east-1
# In any workflow step
- uses: org/actions/setup-node-aws@v1
  with:
    node-version: '20'

Composite actions run in the same job — no separate runner.

Organization strategy

org/
├── github-actions-templates/     # Reusable workflows
│   └── .github/workflows/
│       ├── node-ci.yml
│       ├── deploy-k8s.yml
│       └── security-scan.yml
├── actions/                      # Composite actions
│   ├── setup-node-aws/
│   └── docker-build-push/
└── my-service/                   # Thin caller workflow
    └── .github/workflows/ci.yml  # 10-20 lines

Template repo changes go through PR review with test runs from a canary service repo before tagging a release.

Limitations

Pin reusable workflow refs to SHA or semver tag — @main reusable workflows break all consumers on every push.

Reusable workflow versioning

uses: org/.github/workflows/[email protected]  # pin tag, not @main

Breaking change in reusable workflow without version bump breaks all consumers. Semver tags + CHANGELOG.

Common production mistakes

Teams get github actions reusable workflows wrong in predictable ways:

CI/CD for github actions reusable workflows breaks merges when pipeline secrets rotate without updating OIDC trust, cache keys ignore lockfile changes, and deployment gates check build success but not smoke tests.

Debugging and triage workflow

When github actions reusable workflows misbehaves in production, work top-down instead of guessing:

  1. Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
  2. Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
  3. Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
  4. Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
  5. Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
  6. Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.

Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.

Resources

Frequently asked questions

What are reusable workflows in GitHub Actions?

Reusable workflows are YAML files with workflow_call trigger that other workflows invoke like functions. Define build-and-test once in a central repo; every service repo calls it with repo-specific inputs. Changes to the reusable workflow propagate to all callers on their next run.

How do inputs and secrets work in reusable workflows?

The reusable workflow declares inputs (typed, with defaults) and required secrets in the workflow_call section. The caller passes values via with: and secrets:. Secrets must be explicitly mapped — they are not inherited automatically. Organization-level secrets can be referenced by name.

Reusable workflows vs composite actions — which to use?

Reusable workflows are full workflow graphs with multiple jobs, runners, and job dependencies — use for entire pipelines (build, test, deploy). Composite actions bundle steps into one job — use for reusable step sequences (setup Node, configure AWS). Workflows compose jobs; actions compose steps.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →