Building Reusable Workflows

What are Reusable Workflows?

Reusable workflows are GitHub Actions workflows that can be called from other workflows, similar to functions in programming. They eliminate duplication by centralizing common CI/CD logic in a single place, making workflows easier to maintain and standardize across repositories.

Instead of copying the same workflow configuration across multiple repositories, define it once and reference it wherever needed.


Why Use Reusable Workflows?


Reusable Workflow Structure

graph TD A["Caller Workflow
Repository A"] -->|calls| B["Reusable Workflow
Central Repository"] C["Caller Workflow
Repository B"] -->|calls| B D["Caller Workflow
Repository C"] -->|calls| B B -->E["Job 1: Build"] B -->F["Job 2: Test"] B -->G["Job 3: Deploy"] style A fill:#e1f5ff style C fill:#e1f5ff style D fill:#e1f5ff style B fill:#fff3e0

Creating a Reusable Workflow

Basic Reusable Workflow

# .github/workflows/reusable-build.yml name: Reusable Build Workflow on: workflow_call: inputs: node-version: description: 'Node.js version to use' required: false default: '18' type: string working-directory: description: 'Working directory' required: false default: '.' type: string outputs: build-status: description: 'Build completion status' value: ${{ jobs.build.outputs.status }} jobs: build: runs-on: ubuntu-latest outputs: status: ${{ steps.build-step.outputs.status }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: 'npm' - name: Install dependencies working-directory: ${{ inputs.working-directory }} run: npm ci - name: Build application id: build-step working-directory: ${{ inputs.working-directory }} run: | npm run build echo "status=success" >> $GITHUB_OUTPUT

Key components:


Calling a Reusable Workflow

Simple Call

# .github/workflows/main.yml name: Main Workflow on: push: branches: - main jobs: call-build: uses: organization/repo/.github/workflows/reusable-build.yml@main with: node-version: '20' working-directory: './app'

Calling from Same Repository

jobs: build: uses: ./.github/workflows/reusable-build.yml with: node-version: '18'

Passing Secrets to Reusable Workflows

Secrets must be explicitly passed to reusable workflows.

Reusable Workflow with Secrets

# .github/workflows/reusable-deploy.yml name: Reusable Deploy on: workflow_call: inputs: environment: required: true type: string secrets: DEPLOY_TOKEN: required: true API_KEY: required: false jobs: deploy: runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - name: Deploy application run: | echo "Deploying to ${{ inputs.environment }}" # Use secrets in deployment env: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} API_KEY: ${{ secrets.API_KEY }}

Calling Workflow with Secrets

jobs: deploy-prod: uses: org/repo/.github/workflows/reusable-deploy.yml@v1 with: environment: production secrets: DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }} API_KEY: ${{ secrets.PROD_API_KEY }}

Using inherit for All Secrets

jobs: deploy: uses: org/repo/.github/workflows/reusable-deploy.yml@v1 with: environment: production secrets: inherit

Workflow Call Sequence

sequenceDiagram participant Caller as Caller Workflow participant Reusable as Reusable Workflow participant Runner as GitHub Runner Caller->>Reusable: Call with inputs Reusable->>Runner: Execute jobs Runner->>Runner: Run steps Runner->>Reusable: Return outputs Reusable->>Caller: Pass outputs back Caller->>Caller: Continue execution

Real-World Example: Build, Test, and Deploy Pipeline

Reusable Build Workflow

# .github/workflows/reusable-node-ci.yml name: Node.js CI on: workflow_call: inputs: node-version: type: string default: '18' run-tests: type: boolean default: true run-lint: type: boolean default: true outputs: artifact-name: value: ${{ jobs.build.outputs.artifact }} jobs: build: runs-on: ubuntu-latest outputs: artifact: build-${{ github.sha }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: 'npm' - run: npm ci - name: Run linter if: ${{ inputs.run-lint }} run: npm run lint - name: Run tests if: ${{ inputs.run-tests }} run: npm test - run: npm run build - uses: actions/upload-artifact@v4 with: name: build-${{ github.sha }} path: dist/

Reusable Deploy Workflow

# .github/workflows/reusable-deploy.yml name: Deploy on: workflow_call: inputs: environment: required: true type: string artifact-name: required: true type: string secrets: DEPLOY_KEY: required: true jobs: deploy: runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - uses: actions/download-artifact@v4 with: name: ${{ inputs.artifact-name }} - name: Deploy to server run: | echo "Deploying to ${{ inputs.environment }}" # Deployment logic here env: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

Main Workflow Calling Both

# .github/workflows/main.yml name: Main Pipeline on: push: branches: - main jobs: build: uses: ./.github/workflows/reusable-node-ci.yml with: node-version: '20' run-tests: true run-lint: true deploy-staging: needs: build uses: ./.github/workflows/reusable-deploy.yml with: environment: staging artifact-name: ${{ needs.build.outputs.artifact-name }} secrets: DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }} deploy-production: needs: [build, deploy-staging] uses: ./.github/workflows/reusable-deploy.yml with: environment: production artifact-name: ${{ needs.build.outputs.artifact-name }} secrets: DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

Versioning Reusable Workflows

Pin workflows to specific versions for stability.

jobs: # Pin to specific commit SHA build: uses: org/repo/.github/workflows/build.yml@a1b2c3d4 # Pin to tag test: uses: org/repo/.github/workflows/test.yml@v1.2.3 # Pin to branch (less stable) deploy: uses: org/repo/.github/workflows/deploy.yml@main

Best practices:


Workflow Templates vs Reusable Workflows

graph TD A["Workflow Templates"] -->|Starter files| B["Copy template
to new repo"] C["Reusable Workflows"] -->|Reference| D["Call workflow
from central repo"] B -->E["Independent copy
maintained separately"] D -->F["Shared workflow
centrally maintained"] style A fill:#ffcccc style C fill:#ccffcc

Workflow Templates:

Reusable Workflows:


Best Practices

Organization-Level Workflows

Store reusable workflows in a central repository:

org/workflows-repo/ .github/workflows/ reusable-ci.yml reusable-deploy.yml reusable-security.yml

Reference them across all organization repositories.

Input Validation

on: workflow_call: inputs: environment: type: string required: true deploy-region: type: choice options: - us-east-1 - us-west-2 - eu-west-1

Documentation

Document inputs, outputs, and usage in workflow files:

# Reusable Node.js CI Workflow # # Usage: # uses: org/repo/.github/workflows/node-ci.yml@v1 # with: # node-version: '18' # # Inputs: # - node-version: Node.js version (default: '18') # - run-tests: Run test suite (default: true) # # Outputs: # - artifact-name: Name of uploaded build artifact

Common Patterns

Matrix Build Reusable Workflow

on: workflow_call: inputs: node-versions: type: string default: '["16", "18", "20"]' jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: ${{ fromJSON(inputs.node-versions) }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm test

Conditional Deployment

on: workflow_call: inputs: deploy-enabled: type: boolean default: false jobs: deploy: if: ${{ inputs.deploy-enabled }} runs-on: ubuntu-latest steps: - run: echo "Deploying..."

Limitations and Considerations


Key Takeaways

Next Steps: Identify duplicated workflow patterns across repositories and refactor them into reusable workflows for centralized maintenance.