Share CI/CD Logic Across Repositories with GitHub Actions Reusable Workflows
Reusable workflows centralize CI/CD YAML across repositories. Learn to define, call, pin, and secure them with a worked example.
23 Nov 2025, 12:09 UTC

Start with the shared workflow, not duplicated YAML
Teams operating many repositories often copy the same build, test, and publish steps into every `.github/workflows` directory. When a dependency version changes or a security scan step is added, someone must edit every copy. GitHub Actions reusable workflows solve this by letting one repository publish a workflow that other repositories can call. The caller repository pays for the execution minutes; the repository that hosts the shared workflow is not billed for the caller's runs.
Key takeaway
Define the shared workflow with on: workflow_call, declare its inputs, outputs, and secrets explicitly, and call it from another repository using a full commit SHA. Do not use secrets: inherit unless every secret in the caller is safe to expose to the shared workflow.
Step 1: Define the reusable workflow
Create the file in the source repository, for example `org/ci-tools`, at `.github/workflows/build-and-test.yml`. The `workflow_call` trigger tells GitHub this workflow can be invoked by another repository.
# .github/workflows/build-and-test.yml in org/ci-tools
name: Build and Test
on:
workflow_call:
inputs:
node-version:
required: true
type: string
skip-tests:
required: false
type: boolean
default: false
outputs:
artifact-url:
description: Download URL for the build artifact
value: ${{ steps.upload.outputs.artifact_url }}
secrets:
NPM_TOKEN:
description: Token for publishing to npm
required: true
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-url: ${{ steps.upload.outputs.artifact_url }}
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
if: ${{ !inputs.skip-tests }}
run: npm test
- name: Upload build output
id: upload
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
- name: Expose artifact URL
run: echo "artifact_url=${{ steps.upload.outputs.artifact_url }}" >> $GITHUB_OUTPUT
A reusable workflow cannot read caller secrets automatically. The `secrets` block declares which secrets the caller must supply. `inputs` are typed values passed from the caller, and `outputs` let the caller consume values produced inside the shared job.
Step 2: Call the workflow from another repository
In the consuming repository, for example `org/service-a`, create a normal workflow job that uses the shared file. Pin the reference to a full commit SHA so a maintainer cannot silently change the executed logic by retargeting a branch or tag.
# .github/workflows/ci.yml in org/service-a
name: CI
on:
push:
branches: [main]
jobs:
call-build:
uses: org/ci-tools/.github/workflows/build-and-test.yml@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
with:
node-version: "20"
skip-tests: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
outputs:
artifact-url: ${{ jobs.call-build.outputs.artifact-url }}
deploy:
needs: call-build
runs-on: ubuntu-latest
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: build-output
path: ./dist
- name: Deploy
run: echo "Deploying from ${{ needs.call-build.outputs.artifact-url }}"
The `uses` value contains three parts: the owner and repository, the path to the workflow file, and the commit SHA after `@`. Replace `a1b2c3d4...` with the actual SHA of the commit in `org/ci-tools` that contains the shared workflow. The `secrets` mapping is explicit: the caller sends its own `NPM_TOKEN` to the secret name declared by the shared workflow.
Billing, access, and security boundaries
- Execution minutes: billed to the caller repository, not the repository that hosts the reusable workflow.
- Read access: the caller repository must be able to read the source repository. Organization policies can block cross-repository calls, resulting in authorization failures.
- Secrets: not automatically shared. Use an explicit `secrets` mapping. `secrets: inherit` forwards every caller secret and should only be used when you trust the shared workflow completely.
- No chaining: a reusable workflow cannot directly call another reusable workflow. Orchestrate multiple calls from the top-level caller job.
Common mistakes
- Pinning to a branch or tag
Using `@main` or `@v1` means a maintainer can change the workflow after you review it. A full commit SHA is the only reproducible reference. - Using `secrets: inherit` by default
This exposes all caller secrets, including deployment credentials, to the shared workflow. Prefer explicit secret names. - Assuming environment variables cross the boundary
Variables set inside the reusable workflow do not appear in the caller. Use declared `outputs` to pass data back. - Calling from a repository without read access
The run fails with a 404 or authorization error. Confirm the caller can read the source repository before debugging the YAML. - Missing `on: workflow_call`
Without this trigger, GitHub rejects external calls with an error indicating no `workflow_call` trigger is defined.
Limitations
- GitHub Enterprise Server versions older than 3.3 do not support reusable workflows. Check your instance version before adopting this pattern.
- A reusable workflow cannot access caller secrets unless they are declared in its `secrets` block or passed through `secrets: inherit`.
- Each caller job can invoke one reusable workflow. For multiple shared workflows, define multiple jobs in the caller.
- Reusable workflows run as separate workflow runs under the caller repository; they appear in the caller's Actions tab, not the source repository's.
Verification checklist
- Push a commit to the caller repository and open the Actions tab. Confirm a run appears for the caller workflow and that the reusable job is listed inside it.
- Pass a unique input string, echo it inside the shared workflow, and confirm the same value appears in the caller job log.
- Attempt to call the workflow from a repository that lacks read access to the source repository. Confirm the run is rejected rather than executed.
- Check the billing dashboard for the caller repository to confirm minutes are attributed there.
- Rotate the referenced commit SHA to an older commit and confirm the caller uses the older workflow definition on the next run.
Rollback
Because this change only modifies YAML files, rollback means reverting the pull request or commit that introduced the caller workflow or changed the pinned SHA. No database or infrastructure state is altered by adding a reusable workflow call.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.