Skip to main content
ship it and sleep

Reusable Workflows vs Composite Actions: When to Use Which

5 min read Chapter 8 of 66

Reusable Workflows vs Composite Actions

The Failure

The platform team builds a composite action that wraps the entire CI pipeline: build, test, scan, and promote. It works on paper. In practice, the composite action runs all steps sequentially in a single job on a single runner. Tests, scanning, and building cannot run in parallel. The pipeline that took 11 minutes as a DAG of parallel jobs now takes 17 minutes as a sequential composite action.

The team rewrites it as a reusable workflow with parallel jobs. It works. Then the payments team needs a custom security scan step that the reusable workflow does not support. They fork the reusable workflow into their own copy. The copy-paste problem returns.

The solution is two layers: reusable workflows for pipeline orchestration (the job graph) and composite actions for standardized operations within those jobs (Docker build, Trivy scan, infra repo update).

The Mechanism

Feature Comparison

FeatureReusable WorkflowComposite Action
Defines jobsYesNo (steps only)
Parallel executionYes (via job DAG)No (sequential steps)
Separate runner per jobYesNo (runs on caller’s runner)
Can be nestedYes (4 levels max)Yes (10 levels max)
Secrets handlingsecrets: keyword, inherit optionMust pass as inputs (visible in logs if echoed)
Runner selectionDefined in calleeInherits from caller
Matrix supportCallee defines its own matrixCaller wraps in matrix
Maximum workflow depth4 (caller → reusable → reusable → reusable)No practical limit for nesting
Caller visibilitySeparate workflow run in Actions UIInline within caller job

The Decision Rule

Use a reusable workflow when:

  • The shared logic involves multiple jobs that should run in parallel
  • The shared logic needs its own runner selection (e.g., a build job on a large runner)
  • The shared logic should appear as a separate workflow run in the GitHub Actions UI
  • Secrets must be handled without passing them through input parameters

Use a composite action when:

  • The shared logic is a sequence of 2-5 steps that belong inside a job
  • The caller needs to mix the shared steps with custom steps in the same job
  • The shared logic is an operation (Docker build, scan, deploy step) not a pipeline

When both could work, prefer composite actions for operations and reusable workflows for pipelines. One reusable workflow calls multiple composite actions. That is the two-layer architecture.

The Implementation

Two-Layer Architecture

Caller workflow (service repo)
  └── calls: Reusable workflow (shared repo)     # Pipeline orchestration
        ├── Job: build
        │     └── uses: composite action docker-build   # Operation
        ├── Job: test (matrix)
        ├── Job: scan
        │     └── uses: composite action trivy-scan     # Operation
        └── Job: promote
              └── uses: composite action update-infra   # Operation

The reusable workflow defines the job graph. Each job uses composite actions for standardized operations. The service team controls what enters the pipeline by choosing which reusable workflow to call and what inputs to provide. The platform team controls how each operation works by updating the composite actions.

Extending the Pipeline Without Forking

The payments team needs a SAST scan that other services do not. Instead of forking the reusable workflow, they use the reusable workflow for the standard pipeline and add a custom job in their caller workflow:

# HARDENED: Standard pipeline plus service-specific gate
name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  standard-ci:
    uses: acme/.github/workflows/ci-service.yml@v2
    with:
      service-name: payments
      image: ghcr.io/acme/payments-service
      run-contract-tests: true
    secrets:
      INFRA_REPO_TOKEN: ${{ secrets.INFRA_REPO_TOKEN }}
      REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run CodeQL analysis
        uses: github/codeql-action/analyze@v3
        with:
          languages: java

  promotion-gate:
    runs-on: ubuntu-latest
    needs: [standard-ci, sast-scan]
    if: github.ref == 'refs/heads/main'
    steps:
      - run: echo "All gates passed, including SAST"

The standard-ci job runs the shared pipeline. The sast-scan job runs in parallel. The promotion-gate job ensures both must pass. The payments team gets their custom gate without modifying the shared workflow or maintaining a fork.

The Gate

The version reference on the reusable workflow (@v2) acts as a stability gate. The platform team can develop v3 of the shared workflow on the main branch. Services continue using @v2 until the platform team validates the new version and services opt in to the migration.

Semantic versioning for reusable workflows:

  • Patch (v2.0.1): Bug fixes, no input/output changes. Safe to auto-update.
  • Minor (v2.1.0): New optional inputs, new jobs. Backward compatible.
  • Major (v3.0.0): Input changes, removed features, structural changes. Requires migration.
# Tag management in the shared workflow repo
git tag v2.1.0
git push origin v2.1.0

# Move the major version tag to the latest minor
git tag -f v2 v2.1.0
git push origin v2 --force

Services referencing @v2 automatically get minor and patch updates. Services referencing @v2.0.0 stay pinned to an exact version. The platform team documents which approach each service should use based on stability requirements.

The Recovery

When a shared workflow update breaks a service:

  1. The service team pins to the previous version: @v2.0.0 instead of @v2
  2. The platform team investigates the breakage using the service’s pipeline logs
  3. The fix is applied to the shared workflow and released as a new patch or minor version
  4. The service team updates their reference

When a composite action update breaks a step within a shared workflow, the platform team reverts the action change and releases a patch. Since composite actions are referenced by the reusable workflow (not by service repos directly), the fix propagates automatically.