Skip to main content

Advanced Workflows

Once you have the basics down, advanced patterns help you scale your CI/CD to larger teams and more complex projects. This lesson covers reusable workflows, custom actions, security scanning, and optimization techniques.

Reusable Workflows

Reusable workflows let you define a workflow once and call it from other workflows. This eliminates duplication across repositories.

Create a reusable workflow in .github/workflows/reusable-deploy.yml:

name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      url:
        required: true
        type: string
    secrets:
      SSH_KEY:
        required: true
      HOST:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.environment }}
      url: ${{ inputs.url }}
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to ${{ inputs.environment }}
        env:
          SSH_KEY: ${{ secrets.SSH_KEY }}
          HOST: ${{ secrets.HOST }}
        run: |
          echo "$SSH_KEY" > key.pem
          chmod 600 key.pem
          ssh -o StrictHostKeyChecking=no -i key.pem deploy@$HOST \
            "cd /var/www/myapp && git pull && npm ci && npm run build && pm2 restart all"

Call it from another workflow:

name: Deploy Pipeline

on:
  push:
    branches: [main]

jobs:
  staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      url: https://staging.myapp.com
    secrets:
      SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
      HOST: ${{ secrets.STAGING_HOST }}

  production:
    needs: staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      url: https://myapp.com
    secrets:
      SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
      HOST: ${{ secrets.PROD_HOST }}

Composite Actions

Composite actions bundle multiple steps into a single reusable action. Create .github/actions/setup-project/action.yml:

name: Setup Project
description: Install dependencies and prepare the project

inputs:
  node-version:
    description: Node.js version
    required: false
    default: "20"

runs:
  using: "composite"
  steps:
    - uses: pnpm/action-setup@v4
      with:
        version: 9

    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: "pnpm"

    - name: Install dependencies
      shell: bash
      run: pnpm install --frozen-lockfile

Use it in your workflows:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-project
        with:
          node-version: "20"
      - run: pnpm build

Security Scanning

Add security checks to your pipeline to catch vulnerabilities early:

name: Security

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: "0 8 * * 1"  # Weekly scan on Monday

jobs:
  dependency-audit:
    name: Dependency Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm audit --audit-level=high

  codeql:
    name: CodeQL Analysis
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: javascript

      - name: Perform CodeQL analysis
        uses: github/codeql-action/analyze@v3

  secret-scanning:
    name: Secret Detection
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Detect secrets
        uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

Monorepo Support

For repositories with multiple packages, trigger workflows only for changed packages:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.changes.outputs.frontend }}
      backend: ${{ steps.changes.outputs.backend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: changes
        with:
          filters: |
            frontend:
              - 'packages/frontend/**'
              - 'packages/shared/**'
            backend:
              - 'packages/backend/**'
              - 'packages/shared/**'

  test-frontend:
    needs: detect-changes
    if: needs.detect-changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cd packages/frontend && npm ci && npm test

  test-backend:
    needs: detect-changes
    if: needs.detect-changes.outputs.backend == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cd packages/backend && npm ci && npm test

Scheduled Maintenance Workflows

Automate routine tasks:

name: Maintenance

on:
  schedule:
    - cron: "0 9 * * 1"  # Every Monday at 9 AM UTC

jobs:
  update-dependencies:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Update dependencies
        run: |
          npx npm-check-updates -u
          npm install

      - name: Create PR if changes exist
        uses: peter-evans/create-pull-request@v6
        with:
          title: "chore: update dependencies"
          commit-message: "chore: update dependencies"
          branch: deps/weekly-update
          body: |
            Automated weekly dependency update.
            Please review the changes and run tests before merging.

  stale-issues:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/stale@v9
        with:
          days-before-stale: 60
          days-before-close: 7
          stale-issue-message: "This issue has been inactive for 60 days. It will be closed in 7 days unless there is activity."

Workflow Debugging

When workflows fail and you need to investigate:

steps:
  - name: Debug information
    run: |
      echo "Event: ${{ github.event_name }}"
      echo "Ref: ${{ github.ref }}"
      echo "SHA: ${{ github.sha }}"
      echo "Actor: ${{ github.actor }}"
      echo "Workflow: ${{ github.workflow }}"
      echo "Run ID: ${{ github.run_id }}"

  - name: List directory contents
    run: ls -la

  - name: Show environment
    run: env | sort

Enable debug logging by adding the secret ACTIONS_STEP_DEBUG with value true in your repository settings. This shows detailed output for every step.

Job Outputs and Dependencies

Pass data between jobs:

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      app-version: ${{ steps.get-version.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: get-version
        run: echo "version=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT

  build:
    needs: version
    runs-on: ubuntu-latest
    steps:
      - name: Build version ${{ needs.version.outputs.app-version }}
        run: echo "Building v${{ needs.version.outputs.app-version }}"

Workflow Permissions

Follow the principle of least privilege:

permissions:
  contents: read       # Read repository code
  packages: write      # Push Docker images to GHCR
  pull-requests: write # Comment on PRs
  issues: write        # Create issues
  security-events: write # Upload CodeQL results

Set minimal permissions at the workflow level and expand only where needed at the job level.

Best Practices Checklist

PracticeWhy
Pin action versions (@v4, not @main)Prevent supply chain attacks
Use --frozen-lockfile for installsEnsure reproducible builds
Set timeout-minutes on all jobsPrevent runaway jobs from burning minutes
Use concurrency groupsPrevent redundant parallel runs
Store secrets in GitHub SecretsNever hardcode credentials
Use environment protection rulesRequire approval for production deploys
Run security scans weeklyCatch new vulnerabilities
Keep workflows DRY with reusable componentsEasier maintenance

Summary

You now have the advanced skills to build scalable CI/CD pipelines — reusable workflows for consistency, composite actions for shared setup logic, security scanning for safety, monorepo support for complex projects, and optimization techniques for speed. These patterns form the backbone of professional DevOps automation.