Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 039 / 100

Reusable GitHub Actions workflows: a step-by-step guide

Share pipelines across repositories.

Expona Admin7 min read

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.

bash
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:

bash
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.

bash
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:

bash
- 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:

bash
git tag -a v1.0.0 -m "Release test workflow v1.0.0"
git push origin v1.0.0

Consumers reference the tag explicitly:

bash
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:

bash
.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_call triggers 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 @main in 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles