Standardize CI Across Repositories with GitHub Actions Reusable Workflows
Stop duplicating CI YAML. Call reusable workflows across repositories with pinned refs, scoped permissions, and job outputs.
01 Oct 2025, 19:00 UTC

One definition, many repositories
Your team maintains several repositories, and each one contains a near-identical copy of the build, test, and deploy YAML. When a dependency version changes or a new approval step is required, someone edits one repository and forgets the others. The result is silent drift: pipelines that look the same but behave differently.
The practical fix is a reusable workflow: a GitHub Actions workflow declared with on: workflow_call in a template repository. Other repositories call it with uses:, pass only the inputs and secrets it needs, and read its job outputs in later steps. One definition stays in one place, while each consuming repository keeps control of its own triggers and permissions.
Declare the reusable workflow
In the template repository, create .github/workflows/reusable-ci.yml. The workflow_call trigger defines the contract: inputs for configuration, secrets for credentials, and outputs that callers can consume.
name: Reusable CI
on:
workflow_call:
inputs:
node-version:
required: true
type: string
description: "Node.js version for setup-node"
outputs:
build-id:
description: "Unique build identifier"
value: ${{ jobs.build.outputs.build-id }}
secrets:
NPM_TOKEN:
required: true
description: "Token for private npm packages"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
outputs:
build-id: ${{ steps.set-id.outputs.build-id }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm run build
- id: set-id
run: echo "build-id=$(date +%s)" >> $GITHUB_OUTPUT
permissions: contents: read sets least-privilege token access for this workflow. The effective GITHUB_TOKEN permissions are the intersection of the caller's permissions and the called workflow's permissions, so both files should be restricted.
Call it from another repository
In a consuming repository, invoke the template with a full commit SHA. Branch or tag references can move after your pull request is reviewed, so a SHA pin is the reproducible choice.
name: Deploy
on:
push:
branches: [main]
permissions:
contents: read
jobs:
ci:
uses: org/template/.github/workflows/reusable-ci.yml@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
with:
node-version: "20"
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
release:
needs: ci
runs-on: ubuntu-latest
steps:
- run: echo "Build ${{ needs.ci.outputs.build-id }} is ready for release"
Avoid secrets: inherit by default. It passes every caller secret to the reusable workflow, including credentials that job does not need. List only required secrets explicitly. Replace the placeholder SHA with the full commit hash from the template repository's default branch.
Understand caller context and outputs
Reusable workflow jobs run in the calling repository's context. Expressions such as github.repository, github.event_name, and environment names resolve to the caller unless you explicitly pass values as inputs. Job outputs from the called workflow are available to later caller jobs through jobs.<job_id>.outputs.<output_id>, which enables fan-out and fan-in without duplicating YAML.
Verify the result
- Open the Actions tab in the calling repository.
- Run the workflow and confirm the
cijob completes. - Confirm the
releasejob displays thebuild-idvalue produced by the called workflow. - If the output is empty, check that the called workflow declares
outputsat both the job andworkflow_calllevels, and that the caller referencesneeds.ci.outputs.build-id.
Trade-offs and limitations
- Sharing scope: Reusable workflows are organization or enterprise scoped. Cross-organization reuse depends on repository visibility and sharing settings, so verify the target model before building a central template repository.
- Immutability: Only full commit SHAs are immutable. A force-pushed tag or new default-branch commit can change the executed workflow after review.
- Self-hosted runners: Runner groups and labels must allow the calling repository to use them; otherwise jobs can pend without an obvious permission error.
- Context: Do not assume
github.repositoryrefers to the template repository. It refers to the caller.
Closing checklist
- Create a template repository with
on: workflow_call, explicit inputs, outputs, and secrets. - Pin every
uses:reference in calling repositories to a full commit SHA. - Pass only the secrets the called job requires; avoid
secrets: inherit. - Set least-privilege
permissionsin both the caller and the called workflow. - Verify a downstream caller job can read the called workflow's output.
Reverting a commit restores the previous YAML if a new template version introduces a failure, so treat template changes like any other infrastructure change: review, pin, and verify before rolling out.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.