Introduction
As your CI/CD pipelines grow, copy-pasting workflow files across repositories becomes a maintenance nightmare. When you need to update a deployment step or add a new linting rule, you're hunting through dozens of repos making the same change over and over. Reusable GitHub Actions workflows solve this problem by letting you define pipelines once and share them across your entire organization. In this guide, we'll walk through building reusable GitHub Actions workflows—the practical patterns we use in production—so you can standardize deployment, testing, and compliance across your team.
Understanding workflow_call for reusable workflows
GitHub Actions lets you define a workflow as reusable by adding a workflow_call trigger. Unlike push or pull_request, workflow_call makes your workflow callable from other workflows, effectively turning it into a shareable template.
Start with a simple example. Let's say your team runs the same test suite across multiple services: Node.js app, Python service, Go microservice. Instead of maintaining three identical test workflows, create a single reusable workflow.
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
required: false
default: '18'
type: string
test-command:
required: true
type: string
secrets:
npm-token:
required: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
registry-url: 'https://npm.pkg.github.com'
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.npm-token }}
- run: ${{ inputs.test-command }}
Now, in each service repository, call this workflow from a simple caller workflow:
name: Test
on:
push:
branches: [ main, develop ]
pull_request:
jobs:
call-test:
uses: my-org/.github/workflows/test.yml@main
with:
node-version: '20'
test-command: 'npm run test:ci'
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
The key difference: workflow_call defines inputs and secrets that the reusable workflow accepts, while the caller repository provides concrete values via uses and with. This keeps your shared logic in one place and each repository's workflow minimal.
In production e-commerce systems, we've used this pattern for database migration workflows. The migration script varies per service, but the overall pattern—backup, migrate, verify, rollback-on-failure—is identical. By parameterizing the migration command and database credentials, we run the same workflow across payment, inventory, and order services.
Composite actions for shared steps
While workflow_call handles entire workflows, composite actions let you bundle a series of steps into a single reusable unit. Composite actions are lighter weight than full workflows and work well for multi-step operations that appear across many workflows.
Consider a common pattern: checking out code, installing dependencies, building Docker images, and pushing them to a registry. Rather than repeat this in every service's deployment workflow, wrap it in a composite action.
name: Build and Push Docker Image
description: Build a Docker image and push to registry
inputs:
registry:
description: Container registry URL
required: true
image-name:
description: Image name without registry
required: true
dockerfile-path:
description: Path to Dockerfile
required: false
default: ./Dockerfile
build-context:
description: Docker build context
required: false
default: .
outputs:
image-uri:
description: Full image URI with tag
value: ${{ steps.meta.outputs.tags }}
runs:
using: composite
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ inputs.registry }}/${{ inputs.image-name }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
- uses: docker/build-push-action@v5
with:
context: ${{ inputs.build-context }}
file: ${{ inputs.dockerfile-path }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
In your service workflow, use it like this:
- uses: my-org/actions/build-docker-image@v1
with:
registry: ghcr.io
image-name: my-org/payment-service
dockerfile-path: ./Dockerfile.prod
Composite actions return outputs, so you can chain them. After building the image, the next step might consume the image-uri output to deploy it. We've used composite actions for AWS credential setup, Slack notifications with context-specific messages, and terraform plan/apply cycles across infrastructure repos.
Version your reusable workflows with tags
The moment you share a workflow, versioning becomes critical. If you change a reusable workflow in your central repository, every consumer gets the update—which could break downstream pipelines. GitHub's tagging system lets you reference specific versions.
When a workflow or action is stable, tag it in your central repository:
git tag -a v1.0.0 -m "Release test workflow v1.0.0"
git push origin v1.0.0
Consumers reference the tag explicitly:
uses: my-org/.github/workflows/test.yml@v1.0.0
uses: my-org/actions/build-docker-image@v1.0.0
Use semantic versioning: increment major versions for breaking changes (removing inputs, changing secret names), minor for new features (new optional inputs), and patches for bug fixes. In our ride-hailing platform, we maintain parallel versions of deployment workflows during API migrations—v1 uses the old schema, v2 the new one. Services migrate at their own pace.
Avoid using @main in production. It's tempting for quick testing, but production pipelines should pin to specific versions to prevent surprise failures from central repo changes.
Building a central repository for organization standards
Most organizations eventually create a central repository—often named .github or platform-workflows—to house shared workflows and actions. This becomes your organization's CI/CD standard library.
Structure it like this:
.github/
├── workflows/
│ ├── test.yml
│ ├── deploy-to-staging.yml
│ ├── deploy-to-production.yml
│ └── security-scan.yml
├── actions/
│ ├── build-docker-image/
│ │ └── action.yml
│ ├── notify-slack/
│ │ └── action.yml
│ └── assume-aws-role/
│ │ └── action.yml
├── README.md
└── CONTRIBUTING.md
Document each workflow and action clearly. In the README, show examples of how to use them and explain the inputs, outputs, and any prerequisites.
In production SaaS systems, we use a central workflows repository as the single source of truth for:
- Build standards: Node version, npm cache strategy, artifact handling
- Deployment procedures: Blue-green rollout logic, health check endpoints, rollback criteria
- Security checks: Dependency scanning, container scanning, SAST tools
- Compliance: Audit logging, change tracking, approval workflows for production
When security discovers a new vulnerability class requiring scanning, you add it once to the central security workflow. Every service that uses that workflow immediately inherits the check on their next deployment.
Equally important: establish a process for updates. We tag changes and notify teams in Slack when major updates are available. Services have a grace period (usually 2 weeks) to migrate to new major versions. For critical security patches, we enforce migration immediately.
Common mistakes
- Over-parameterization: Adding too many inputs to handle every edge case makes workflows hard to understand. If a workflow needs more than 5-6 inputs, consider splitting it into smaller, composable units.
- Hardcoding paths and assumptions: A reusable workflow that assumes the project structure or language specifics of one team won't work for others. Use clear defaults and document assumptions in your README.
- Ignoring context in outputs: Composite actions can return outputs, but they only work within the same job. If you need output from a reusable workflow, you must pass it through job outputs explicitly or restructure to use composite actions instead.
- Storing secrets in the central repository: Never commit secrets to your shared workflows repo. Use the secrets mechanism in GitHub to pass them from the caller repository. The central repo references them by name, and each repository provides its own values.
- Not testing before tagging: Before releasing a new version, test it in a staging repository. We've had deployments fail because a typo in the central workflow went into production—it happens quickly to all of us.
- Forgetting to update documentation when workflows change: Outdated docs cause teams to make incorrect assumptions and use workflows in ways they weren't designed for. Update your README with every release.
Key takeaways
workflow_calltriggers turn workflows into reusable templates, shareable across repositories with parameterized inputs and secrets.- Composite actions bundle multi-step operations into single reusable units, perfect for common sequences like Docker builds or credential setup.
- Always version with semantic tags and avoid
@mainin production to prevent surprise failures from upstream changes. - A central repository becomes your organization's CI/CD standard library, the source of truth for build, deployment, and security procedures.
- Document thoroughly, test before releasing, and establish a clear update process so teams understand when and why to migrate to new versions.
- Keep workflows focused and minimal—complex logic belongs in scripts or configuration, not in the workflow YAML itself.






Comments
Loading comments…