Skip to content
All essays
CraftMarch 27, 202418 min

Advanced GitHub Actions Patterns and Techniques

Master advanced GitHub Actions patterns including composite actions, custom workflows, complex matrices, and enterprise-grade CI/CD

Ü
Ümit Uz
Mobile & Full Stack Developer

GitHub Actions has evolved into a sophisticated automation platform. Beyond basic CI/CD, it offers powerful features for building complex workflows, creating reusable components, and implementing enterprise-grade automation strategies.

Composite Actions

Creating reusable composite actions encapsulates complex logic into shareable units.

yaml
# .github/actions/setup-node-env/action.yml
name: 'Setup Node Environment'
description: 'Setup Node.js with caching and dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '20'
  package-manager:
    description: 'Package manager to use'
    required: false
    default: 'npm'

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: ${{ inputs.package-manager }}

    - name: Install dependencies
      shell: bash
      run: |
        if [ "${{ inputs.package-manager }}" = "npm" ]; then
          npm ci
        elif [ "${{ inputs.package-manager }}" = "yarn" ]; then
          yarn install --frozen-lockfile
        elif [ "${{ inputs.package-manager }}" = "pnpm" ]; then
          pnpm install --frozen-lockfile
        fi
yaml
# Using the composite action
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-node-env
        with:
          node-version: '20'
          package-manager: 'pnpm'

Complex Matrix Strategies

Dynamic Matrices

yaml
name: Dynamic Matrix Builds

on:
  push:
    branches: [main]

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - id: set-matrix
        run: |
          MATRIX='{"include":['
          MATRIX+='{"os":"ubuntu-latest","node":"18","arch":"x64"},'
          MATRIX+='{"os":"ubuntu-latest","node":"20","arch":"x64"},'
          MATRIX+='{"os":"windows-latest","node":"20","arch":"x64"},'
          MATRIX+='{"os":"macos-latest","node":"20","arch":"arm64"}'
          MATRIX+=']}'
          echo "matrix=$MATRIX" >> $GITHUB_OUTPUT

  test:
    needs: setup
    runs-on: ${{ matrix.os }}
    strategy:
      matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          architecture: ${{ matrix.arch }}
      - run: npm test

Exclusion and Inclusion Patterns

yaml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [16, 18, 20, 21]
        ruby: [2.7, 3.0, 3.1]
        exclude:
          - os: windows-latest
            node: 16
          - os: macos-latest
            node: 21
          - os: windows-latest
            ruby: 2.7
        include:
          - os: ubuntu-latest
            node: 20
            coverage: true
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm test
      - if: matrix.coverage
        run: npm run test:coverage

Reusable Workflows

Creating Reusable Workflows

yaml
# .github/workflows/reusable-ci.yml
name: Reusable CI Workflow

on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
      run-tests:
        required: false
        type: boolean
        default: true
    secrets:
      npm-token:
        required: true
    outputs:
      test-result:
        description: 'Test results'
        value: ${{ jobs.test.outputs.result }}

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
      - run: npm run lint

  test:
    if: inputs.run-tests
    runs-on: ubuntu-latest
    outputs:
      result: ${{ steps.test.outputs.result }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - id: test
        run: |
          npm test
          echo "result=success" >> $GITHUB_OUTPUT
      - run: npm run test:coverage

Calling Reusable Workflows

yaml
name: CI Pipeline

on:
  push:
    branches: [main, develop]

jobs:
  call-ci:
    uses: ./.github/workflows/reusable-ci.yml
    with:
      node-version: '20'
      run-tests: true
    secrets:
      npm-token: ${{ secrets.NPM_TOKEN }}

Advanced Caching

Multi-Level Caching Strategy

yaml
name: Advanced Caching

on:
  push:
    branches: [main]

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

      - name: Cache node modules
        uses: actions/cache@v3
        id: npm-cache
        with:
          path: |
            ~/.npm
            node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Cache build outputs
        uses: actions/cache@v3
        with:
          path: |
            .next
            dist
            build
          key: ${{ runner.os }}-build-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-build-

      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      - name: Build Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

Advanced Deployment Strategies

Canary Deployments

yaml
name: Canary Deployment

on:
  push:
    branches: [main]

jobs:
  deploy-canary:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy canary (10% traffic)
        run: |
          kubectl apply -f k8s/canary-deployment.yaml
          kubectl patch service myapp -p '{"spec":{"selector":{"version":"canary"}}}'
          # Update service to route 10% to canary

      - name: Wait for stability
        run: sleep 300

      - name: Monitor metrics
        run: |
          ERROR_RATE=$(curl -s https://api.monitoring.com/error-rate)
          if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
            echo "Error rate too high, rolling back"
            kubectl rollout undo deployment/myapp
            exit 1
          fi

  deploy-full:
    needs: deploy-canary
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        run: |
          kubectl apply -f k8s/production-deployment.yaml
          kubectl rollout status deployment/myapp

A/B Testing Deployment

yaml
jobs:
  deploy-variant-a:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy variant A
        run: |
          kubectl apply -f k8s/variant-a.yaml
          kubectl patch service myapp \
            -p '{"spec":{"selector":{"variant":"a"}}}'

  deploy-variant-b:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy variant B
        run: |
          kubectl apply -f k8s/variant-b.yaml
          kubectl patch service myapp \
            -p '{"spec":{"selector":{"variant":"b"}}}'

  analyze:
    needs: [deploy-variant-a, deploy-variant-b]
    runs-on: ubuntu-latest
    steps:
      - name: Run A/B test analysis
        run: |
          # Collect metrics and determine winner
          npm run analyze-ab-test

Environment-Specific Workflows

Multi-Environment Deployments

yaml
name: Multi-Environment Deployment

on:
  push:
    branches: [main]

environments:
  staging:
    url: https://staging.example.com
  production:
    url: https://example.com

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
      deployment_url: ${{ steps.deploy.outputs.url }}
    outputs:
      version: ${{ steps.deploy.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: deploy
        run: |
          VERSION=$(npm version patch)
          npm run build
          npm run deploy:staging
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "url=https://staging.example.com" >> $GITHUB_OUTPUT

  test-staging:
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - name: Run integration tests
        run: |
          npm run test:integration -- --env=staging

  promote-production:
    needs: [deploy-staging, test-staging]
    runs-on: ubuntu-latest
    environment:
      name: production
    steps:
      - uses: actions/checkout@v4
      - run: |
          VERSION=${{ needs.deploy-staging.outputs.version }}
          npm run deploy:production -- --version=$VERSION

Complex Workflow Orchestration

Fan-Out/Fan-In Pattern

yaml
name: Fan-Out/Fan-In Pattern

on:
  push:
    branches: [main]

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      versions: ${{ steps.versions.outputs.versions }}
    steps:
      - uses: actions/checkout@v4
      - id: versions
        run: |
          VERSIONS=$(node -e "console.log(require('./package.json').versions.join(','))")
          echo "versions=$VERSIONS" >> $GITHUB_OUTPUT

  build-variants:
    needs: setup
    runs-on: ubuntu-latest
    strategy:
      matrix:
        variant: ${{ fromJson(needs.setup.outputs.versions) }}
    steps:
      - uses: actions/checkout@v4
      - run: npm run build -- --variant=${{ matrix.variant }}
      - uses: actions/upload-artifact@v3
        with:
          name: build-${{ matrix.variant }}
          path: dist/

  combine:
    needs: build-variants
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v3
        with:
          path: artifacts/
      - run: |
          # Combine all artifacts
          npm run combine -- --source=artifacts/ --output=final/
      - uses: actions/upload-artifact@v3
        with:
          name: final-build
          path: final/

Advanced Testing Patterns

Flaky Test Detection

yaml
jobs:
  test-with-retry:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run tests with retry
        uses: nick-fields/retry-action@v2
        with:
          timeout_minutes: 10
          max_attempts: 3
          retry_on: error
          command: npm test

      - name: Report flaky tests
        if: failure()
        run: |
          # Log flaky test detection
          echo "::warning::Tests failed after 3 attempts"

Parallel Test Execution

yaml
jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      files: ${{ steps.split.outputs.files }}
    steps:
      - uses: actions/checkout@v4
      - id: split
        run: |
          FILES=$(find . -name "*.test.ts" -printf '%p,' | sed 's/,$//')
          echo "files=$FILES" >> $GITHUB_OUTPUT

  test:
    needs: setup
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

Security and Compliance

Security Scanning Pipeline

yaml
name: Security Pipeline

on:
  push:
    branches: [main]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run npm audit
        run: npm audit --audit-level=high
        continue-on-error: true

      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v3
        with:
          fail-on-severity: high

Performance Optimization

Parallel Job Optimization

yaml
jobs:
  test-unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:unit

  test-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:integration

  test-e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:e2e

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run type-check

  build:
    runs-on: ubuntu-latest
    needs: [test-unit, test-integration, test-e2e, lint, type-check]
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

Conclusion

Advanced GitHub Actions patterns enable sophisticated automation strategies. By leveraging composite actions, reusable workflows, complex matrices, and advanced deployment patterns, you can build enterprise-grade CI/CD pipelines that scale with your team's needs.

The key is to iterate gradually: start simple, measure performance, and optimize based on real data.

Related essays

Next essay
Mutation Testing: The Ultimate Test Quality Assurance