Get the next live webinar in your inbox

One email a month: the upcoming live event + free recording access for subscribers. No spam, unsubscribe anytime.

Lesson 2 of 10 ~30 min
Course progress
0%

Reusable Workflows and Composite Actions

Create DRY workflows with reusable components

Reusable Workflows and Composite Actions

Eliminate duplication by creating reusable workflow components.

Reusable Workflow

Create .github/workflows/reusable-deploy.yml:

name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      node-version:
        required: false
        type: string
        default: '18'
    secrets:
      deploy-token:
        required: true
    outputs:
      deployment-url:
        description: "Deployed URL"
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    outputs:
      url: ${{ steps.deploy.outputs.url }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      
      - name: Deploy
        id: deploy
        run: |
          echo "Deploying to ${{ inputs.environment }}"
          URL=$(./deploy.sh)
          echo "url=$URL" >> $GITHUB_OUTPUT
        env:
          DEPLOY_TOKEN: ${{ secrets.deploy-token }}

Using Reusable Workflow

name: Deploy All Environments

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      node-version: '18'
    secrets:
      deploy-token: ${{ secrets.STAGING_TOKEN }}

  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      node-version: '20'
    secrets:
      deploy-token: ${{ secrets.PRODUCTION_TOKEN }}

Composite Actions

Create .github/actions/setup-project/action.yml:

name: 'Setup Project'
description: 'Setup Node.js and install dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '18'
  cache:
    description: 'Cache package manager'
    required: false
    default: 'npm'

outputs:
  cache-hit:
    description: 'Whether cache was hit'
    value: ${{ steps.cache.outputs.cache-hit }}

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: ${{ inputs.cache }}
    
    - name: Cache dependencies
      id: cache
      uses: actions/cache@v3
      with:
        path: node_modules
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    
    - name: Install dependencies
      if: steps.cache.outputs.cache-hit != 'true'
      shell: bash
      run: npm ci

Using Composite Action

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup project
        uses: ./.github/actions/setup-project
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Run tests
        run: npm test

Matrix with Reusable Workflows

name: Test Matrix

on: [push]

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [16, 18, 20]
    uses: ./.github/workflows/reusable-test.yml
    with:
      os: ${{ matrix.os }}
      node-version: ${{ matrix.node }}

Docker Composite Action

Create .github/actions/docker-build/action.yml:

name: 'Docker Build and Push'
description: 'Build and push Docker image'

inputs:
  registry:
    required: true
  image-name:
    required: true
  tag:
    required: false
    default: 'latest'

runs:
  using: 'composite'
  steps:
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
    
    - name: Log in to registry
      uses: docker/login-action@v3
      with:
        registry: ${{ inputs.registry }}
        username: ${{ github.actor }}
        password: ${{ github.token }}
    
    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        push: true
        tags: ${{ inputs.registry }}/${{ inputs.image-name }}:${{ inputs.tag }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

Call External Reusable Workflows

jobs:
  deploy:
    uses: my-org/workflows/.github/workflows/deploy.yml@main
    with:
      environment: production
    secrets: inherit

Conditional Reusable Workflows

jobs:
  deploy-preview:
    if: github.event_name == 'pull_request'
    uses: ./.github/workflows/deploy-preview.yml
    with:
      pr-number: ${{ github.event.number }}

  deploy-production:
    if: github.ref == 'refs/heads/main'
    uses: ./.github/workflows/deploy-prod.yml
    secrets: inherit

Best Practices

Centralize common logic - Reusable workflows
Parameterize inputs - Make workflows flexible
Document actions - Clear descriptions
Version reusable workflows - Use tags or branches
Test composite actions - Validate locally
Share across repos - Organization-level workflows

Reusable vs Composite

Reusable Workflows:

  • Entire workflow files
  • Can have multiple jobs
  • Called with uses: in jobs
  • Good for complete processes

Composite Actions:

  • Groups of steps
  • Single job context
  • Called with uses: in steps
  • Good for repeated step sequences

Key Takeaways

Reusable workflows and composite actions eliminate duplication, ensure consistency, and make maintenance easier. Use reusable workflows for complete processes and composite actions for step sequences.