GitHub Actions Tutorial — Composition Patterns
Composition Patterns
Reusable workflows become powerful when composed together into pipelines. Here are proven patterns for building maintainable CI/CD systems.
Pattern 1 — Linear Pipeline
Chain reusable workflows sequentially with needs::
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
lint:
uses: ./.github/workflows/lint.yml
test:
needs: lint
uses: ./.github/workflows/test.yml
with:
node-version: '20'
build:
needs: test
uses: ./.github/workflows/build.yml
secrets: inherit
deploy:
needs: build
uses: ./.github/workflows/deploy.yml
with:
environment: production
secrets: inherit
lint → test → build → deploy
Each stage must pass before the next starts.
Pattern 2 — Fan-Out / Fan-In
Run multiple workflows in parallel, then converge:
name: CI
on: push
jobs:
lint:
uses: ./.github/workflows/lint.yml
unit-test:
uses: ./.github/workflows/unit-test.yml
integration-test:
uses: ./.github/workflows/integration-test.yml
build:
needs: [lint, unit-test, integration-test] # fan-in
uses: ./.github/workflows/build.yml
┌── lint ──────────┐
push ──┼── unit-test ─────┼── build
└── integration ───┘
All three checks run in parallel. build waits for all three.
Pattern 3 — Environment Promotion
Progressive deployment through environments:
name: Deploy
on:
push:
branches: [main]
jobs:
build:
uses: ./.github/workflows/build.yml
secrets: inherit
deploy-dev:
needs: build
uses: ./.github/workflows/deploy.yml
with:
environment: dev
secrets: inherit
deploy-staging:
needs: deploy-dev
uses: ./.github/workflows/deploy.yml
with:
environment: staging
secrets: inherit
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/deploy.yml
with:
environment: production
secrets: inherit
build → deploy-dev → deploy-staging → deploy-production
The same deploy.yml is called three times with different inputs.
Pattern 4 — Matrix + Reusable
Call a reusable workflow for each matrix combination:
name: Cross-Platform
on: push
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
uses: ./.github/workflows/test.yml
with:
runner: ${{ matrix.os }}
This creates 3 calls to the reusable workflow, each with a different runner.
Pattern 5 — Shared CI Template
One reusable workflow serves as a template for all services in a monorepo:
# .github/workflows/service-ci.yml (reusable)
on:
workflow_call:
inputs:
service-name:
type: string
required: true
working-directory:
type: string
required: true
jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.service-name }}-build
path: ${{ inputs.working-directory }}/dist
# .github/workflows/ci.yml (caller)
on:
push:
branches: [main]
jobs:
api:
uses: ./.github/workflows/service-ci.yml
with:
service-name: api
working-directory: services/api
web:
uses: ./.github/workflows/service-ci.yml
with:
service-name: web
working-directory: services/web
auth:
uses: ./.github/workflows/service-ci.yml
with:
service-name: auth
working-directory: services/auth
Three services, same CI pipeline, different directories.
Common Mistakes
| Mistake | Fix |
|---|---|
| Circular dependencies | Reusable workflows can’t call themselves (directly or indirectly) |
| Too many nested calls | Keep nesting under 4 levels; flatten when possible |
| Duplicating logic in callers | Extract shared logic into a reusable workflow |
| No version pinning | Use tags or SHAs for cross-repo reusable workflows |
Quick Reference
| Pattern | Structure | Use Case |
|---|---|---|
| Linear | A → B → C | Sequential pipeline |
| Fan-out/in | A ⟶ [B,C,D] → E | Parallel checks |
| Promotion | Same workflow × N envs | Environment rollout |
| Matrix + reusable | Matrix calls reusable | Cross-platform |
| Template | One workflow, many callers | Monorepo CI |
← Previous: Calling Workflows
Next: Org Shared Workflows →