Skip to content

Reusable Workflows

Overview

This document provides a comprehensive library of reusable GitHub Actions workflows designed for enterprise-grade CI/CD pipelines. These workflows follow the DRY principle and can be called from any repository workflow.


Build and Test Workflow

Reusable Build Workflow

# .github/workflows/reusable-build.yml
name: Reusable Build Workflow

on:
  workflow_call:
    inputs:
      node-version:
        description: 'Node.js version to use'
        required: false
        type: string
        default: '20'
      python-version:
        description: 'Python version to use'
        required: false
        type: string
        default: '3.11'
      language:
        description: 'Primary language (node, python, go, rust)'
        required: true
        type: string
      working-directory:
        description: 'Working directory for commands'
        required: false
        type: string
        default: '.'
      build-command:
        description: 'Build command to run'
        required: false
        type: string
        default: ''
      artifact-name:
        description: 'Name for build artifacts'
        required: false
        type: string
        default: 'build-output'
      artifact-path:
        description: 'Path to artifacts to upload'
        required: false
        type: string
        default: 'dist/'
      cache-dependency-path:
        description: 'Path to dependency lock file for caching'
        required: false
        type: string
        default: ''
    outputs:
      build-version:
        description: 'Version of the build'
        value: ${{ jobs.build.outputs.version }}
      artifact-name:
        description: 'Name of uploaded artifact'
        value: ${{ jobs.build.outputs.artifact }}
    secrets:
      NPM_TOKEN:
        required: false
      PYPI_TOKEN:
        required: false

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      artifact: ${{ inputs.artifact-name }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        if: inputs.language == 'node'
        uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
          cache-dependency-path: ${{ inputs.cache-dependency-path || format('{0}/package-lock.json', inputs.working-directory) }}

      - name: Setup Python
        if: inputs.language == 'python'
        uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: 'pip'
          cache-dependency-path: ${{ inputs.cache-dependency-path || format('{0}/requirements*.txt', inputs.working-directory) }}

      - name: Setup Go
        if: inputs.language == 'go'
        uses: actions/setup-go@v5
        with:
          go-version-file: '${{ inputs.working-directory }}/go.mod'
          cache-dependency-path: '${{ inputs.working-directory }}/go.sum'

      - name: Setup Rust
        if: inputs.language == 'rust'
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Rust dependencies
        if: inputs.language == 'rust'
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Install Node.js dependencies
        if: inputs.language == 'node'
        working-directory: ${{ inputs.working-directory }}
        run: npm ci
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Install Python dependencies
        if: inputs.language == 'python'
        working-directory: ${{ inputs.working-directory }}
        run: |
          python -m pip install --upgrade pip
          pip install build wheel
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
          if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
          if [ -f pyproject.toml ]; then pip install -e ".[dev]"; fi

      - name: Install Go dependencies
        if: inputs.language == 'go'
        working-directory: ${{ inputs.working-directory }}
        run: go mod download

      - name: Get version
        id: version
        working-directory: ${{ inputs.working-directory }}
        run: |
          if [ "${{ inputs.language }}" == "node" ]; then
            VERSION=$(node -p "require('./package.json').version")
          elif [ "${{ inputs.language }}" == "python" ]; then
            VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])" 2>/dev/null || echo "0.0.0")
          elif [ "${{ inputs.language }}" == "go" ]; then
            VERSION=$(git describe --tags --always 2>/dev/null || echo "0.0.0")
          elif [ "${{ inputs.language }}" == "rust" ]; then
            VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[0].version')
          else
            VERSION="0.0.0"
          fi
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Build (Node.js)
        if: inputs.language == 'node' && inputs.build-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: npm run build

      - name: Build (Python)
        if: inputs.language == 'python' && inputs.build-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: python -m build

      - name: Build (Go)
        if: inputs.language == 'go' && inputs.build-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: |
          CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${{ steps.version.outputs.version }}" -o dist/ ./...

      - name: Build (Rust)
        if: inputs.language == 'rust' && inputs.build-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: cargo build --release

      - name: Build (Custom)
        if: inputs.build-command != ''
        working-directory: ${{ inputs.working-directory }}
        run: ${{ inputs.build-command }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: ${{ inputs.artifact-name }}
          path: ${{ inputs.working-directory }}/${{ inputs.artifact-path }}
          retention-days: 7
          if-no-files-found: error

Reusable Test Workflow

# .github/workflows/reusable-test.yml
name: Reusable Test Workflow

on:
  workflow_call:
    inputs:
      language:
        description: 'Primary language (node, python, go, rust)'
        required: true
        type: string
      node-version:
        description: 'Node.js version'
        required: false
        type: string
        default: '20'
      python-version:
        description: 'Python version'
        required: false
        type: string
        default: '3.11'
      working-directory:
        description: 'Working directory'
        required: false
        type: string
        default: '.'
      test-command:
        description: 'Custom test command'
        required: false
        type: string
        default: ''
      coverage-threshold:
        description: 'Minimum coverage percentage'
        required: false
        type: number
        default: 80
      upload-coverage:
        description: 'Upload coverage to Codecov'
        required: false
        type: boolean
        default: true
    outputs:
      coverage:
        description: 'Test coverage percentage'
        value: ${{ jobs.test.outputs.coverage }}
      test-result:
        description: 'Test result (success/failure)'
        value: ${{ jobs.test.outputs.result }}
    secrets:
      CODECOV_TOKEN:
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    outputs:
      coverage: ${{ steps.coverage.outputs.percentage }}
      result: ${{ steps.result.outputs.status }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        if: inputs.language == 'node'
        uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'

      - name: Setup Python
        if: inputs.language == 'python'
        uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: 'pip'

      - name: Setup Go
        if: inputs.language == 'go'
        uses: actions/setup-go@v5
        with:
          go-version-file: '${{ inputs.working-directory }}/go.mod'

      - name: Setup Rust
        if: inputs.language == 'rust'
        uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview

      - name: Install dependencies (Node.js)
        if: inputs.language == 'node'
        working-directory: ${{ inputs.working-directory }}
        run: npm ci

      - name: Install dependencies (Python)
        if: inputs.language == 'python'
        working-directory: ${{ inputs.working-directory }}
        run: |
          python -m pip install --upgrade pip
          pip install pytest pytest-cov
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
          if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
          if [ -f pyproject.toml ]; then pip install -e ".[dev]"; fi

      - name: Install coverage tools (Rust)
        if: inputs.language == 'rust'
        run: cargo install cargo-llvm-cov

      - name: Run tests (Node.js)
        if: inputs.language == 'node' && inputs.test-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: npm test -- --coverage --coverageReporters=json-summary --coverageReporters=lcov

      - name: Run tests (Python)
        if: inputs.language == 'python' && inputs.test-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: |
          pytest --cov --cov-report=xml --cov-report=json --cov-report=term

      - name: Run tests (Go)
        if: inputs.language == 'go' && inputs.test-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: |
          go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
          go tool cover -func=coverage.out

      - name: Run tests (Rust)
        if: inputs.language == 'rust' && inputs.test-command == ''
        working-directory: ${{ inputs.working-directory }}
        run: cargo llvm-cov --lcov --output-path lcov.info

      - name: Run tests (Custom)
        if: inputs.test-command != ''
        working-directory: ${{ inputs.working-directory }}
        run: ${{ inputs.test-command }}

      - name: Extract coverage
        id: coverage
        working-directory: ${{ inputs.working-directory }}
        run: |
          if [ "${{ inputs.language }}" == "node" ]; then
            COVERAGE=$(jq '.total.lines.pct' coverage/coverage-summary.json 2>/dev/null || echo "0")
          elif [ "${{ inputs.language }}" == "python" ]; then
            COVERAGE=$(jq '.totals.percent_covered' coverage.json 2>/dev/null || echo "0")
          elif [ "${{ inputs.language }}" == "go" ]; then
            COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
          elif [ "${{ inputs.language }}" == "rust" ]; then
            COVERAGE=$(cargo llvm-cov report --json | jq '.data[0].totals.lines.percent' 2>/dev/null || echo "0")
          else
            COVERAGE="0"
          fi
          echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT

      - name: Check coverage threshold
        id: result
        run: |
          COVERAGE=${{ steps.coverage.outputs.percentage }}
          THRESHOLD=${{ inputs.coverage-threshold }}
          if (( $(echo "$COVERAGE >= $THRESHOLD" | bc -l) )); then
            echo "status=success" >> $GITHUB_OUTPUT
            echo "Coverage $COVERAGE% meets threshold $THRESHOLD%"
          else
            echo "status=failure" >> $GITHUB_OUTPUT
            echo "::error::Coverage $COVERAGE% is below threshold $THRESHOLD%"
            exit 1
          fi

      - name: Upload coverage to Codecov
        if: inputs.upload-coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: false
          verbose: true

Calling Build and Test Workflows

# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build:
    uses: ./.github/workflows/reusable-build.yml
    with:
      language: node
      node-version: '20'
      artifact-name: app-build
      artifact-path: dist/
    secrets: inherit

  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      language: node
      node-version: '20'
      coverage-threshold: 80
    secrets: inherit

  deploy:
    needs: [build, test]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: app-build
          path: dist/

      - name: Deploy
        run: ./deploy.sh

Docker Build and Push Workflow

Reusable Docker Workflow

# .github/workflows/reusable-docker.yml
name: Reusable Docker Build and Push

on:
  workflow_call:
    inputs:
      image-name:
        description: 'Docker image name'
        required: true
        type: string
      dockerfile:
        description: 'Path to Dockerfile'
        required: false
        type: string
        default: 'Dockerfile'
      context:
        description: 'Docker build context'
        required: false
        type: string
        default: '.'
      platforms:
        description: 'Target platforms (comma-separated)'
        required: false
        type: string
        default: 'linux/amd64,linux/arm64'
      push:
        description: 'Push image to registry'
        required: false
        type: boolean
        default: true
      registry:
        description: 'Container registry'
        required: false
        type: string
        default: 'ghcr.io'
      build-args:
        description: 'Build arguments (multiline KEY=VALUE)'
        required: false
        type: string
        default: ''
      cache-from:
        description: 'Cache source'
        required: false
        type: string
        default: 'type=gha'
      cache-to:
        description: 'Cache destination'
        required: false
        type: string
        default: 'type=gha,mode=max'
      scan-image:
        description: 'Run security scan on image'
        required: false
        type: boolean
        default: true
      sbom:
        description: 'Generate SBOM'
        required: false
        type: boolean
        default: true
    outputs:
      image-digest:
        description: 'Image digest'
        value: ${{ jobs.build.outputs.digest }}
      image-tags:
        description: 'Image tags'
        value: ${{ jobs.build.outputs.tags }}
    secrets:
      REGISTRY_USERNAME:
        required: false
      REGISTRY_PASSWORD:
        required: false

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      security-events: write
    outputs:
      digest: ${{ steps.build-push.outputs.digest }}
      tags: ${{ steps.meta.outputs.tags }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GitHub Container Registry
        if: inputs.push && inputs.registry == 'ghcr.io'
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Log in to Docker Hub
        if: inputs.push && inputs.registry == 'docker.io'
        uses: docker/login-action@v3
        with:
          registry: docker.io
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Log in to AWS ECR
        if: inputs.push && contains(inputs.registry, 'amazonaws.com')
        uses: docker/login-action@v3
        with:
          registry: ${{ inputs.registry }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ inputs.registry }}/${{ inputs.image-name }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha,prefix=
            type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}

      - name: Build and push
        id: build-push
        uses: docker/build-push-action@v6
        with:
          context: ${{ inputs.context }}
          file: ${{ inputs.dockerfile }}
          platforms: ${{ inputs.platforms }}
          push: ${{ inputs.push }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: ${{ inputs.build-args }}
          cache-from: ${{ inputs.cache-from }}
          cache-to: ${{ inputs.cache-to }}
          sbom: ${{ inputs.sbom }}
          provenance: mode=max

      - name: Run Trivy vulnerability scanner
        if: inputs.scan-image
        uses: aquasecurity/trivy-action@v0.34.1
        with:
          image-ref: ${{ inputs.registry }}/${{ inputs.image-name }}:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload Trivy scan results
        if: inputs.scan-image
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Generate build summary
        run: |
          echo "## Docker Build Summary" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Image:** \`${{ inputs.registry }}/${{ inputs.image-name }}\`" >> $GITHUB_STEP_SUMMARY
          echo "**Digest:** \`${{ steps.build-push.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY
          echo "**Platforms:** ${{ inputs.platforms }}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### Tags" >> $GITHUB_STEP_SUMMARY
          echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n' | sed 's/^/- /' >> $GITHUB_STEP_SUMMARY

Multi-Stage Docker Workflow with Scanning

# .github/workflows/reusable-docker-complete.yml
name: Complete Docker CI/CD

on:
  workflow_call:
    inputs:
      image-name:
        required: true
        type: string
      environments:
        description: 'Deployment environments (JSON array)'
        required: false
        type: string
        default: '["staging"]'
    secrets:
      REGISTRY_TOKEN:
        required: true

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

      - name: Lint Dockerfile
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning

  build:
    needs: lint
    uses: ./.github/workflows/reusable-docker.yml
    with:
      image-name: ${{ inputs.image-name }}
      push: ${{ github.event_name != 'pull_request' }}
      scan-image: true
    secrets: inherit

  deploy:
    needs: build
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: ${{ fromJson(inputs.environments) }}

    environment:
      name: ${{ matrix.environment }}
      url: https://${{ matrix.environment }}.example.com

    steps:
      - name: Deploy to ${{ matrix.environment }}
        run: |
          echo "Deploying ${{ needs.build.outputs.image-digest }} to ${{ matrix.environment }}"

Terraform Workflow

Reusable Terraform Workflow

# .github/workflows/reusable-terraform.yml
name: Reusable Terraform Workflow

on:
  workflow_call:
    inputs:
      working-directory:
        description: 'Terraform working directory'
        required: false
        type: string
        default: '.'
      terraform-version:
        description: 'Terraform version'
        required: false
        type: string
        default: '1.7.0'
      environment:
        description: 'Deployment environment'
        required: true
        type: string
      backend-config:
        description: 'Backend configuration file'
        required: false
        type: string
        default: ''
      var-file:
        description: 'Variable file to use'
        required: false
        type: string
        default: ''
      apply:
        description: 'Apply changes (true/false)'
        required: false
        type: boolean
        default: false
      destroy:
        description: 'Destroy infrastructure'
        required: false
        type: boolean
        default: false
      plan-artifact-name:
        description: 'Name for plan artifact'
        required: false
        type: string
        default: 'tfplan'
    outputs:
      plan-exit-code:
        description: 'Terraform plan exit code'
        value: ${{ jobs.terraform.outputs.plan-exit-code }}
      has-changes:
        description: 'Whether plan has changes'
        value: ${{ jobs.terraform.outputs.has-changes }}
    secrets:
      AWS_ACCESS_KEY_ID:
        required: false
      AWS_SECRET_ACCESS_KEY:
        required: false
      AZURE_CREDENTIALS:
        required: false
      GOOGLE_CREDENTIALS:
        required: false
      TF_API_TOKEN:
        required: false

jobs:
  terraform:
    runs-on: ubuntu-latest
    outputs:
      plan-exit-code: ${{ steps.plan.outputs.exitcode }}
      has-changes: ${{ steps.plan.outputs.has-changes }}

    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}

    environment:
      name: ${{ inputs.environment }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ inputs.terraform-version }}
          cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

      - name: Configure AWS credentials
        if: secrets.AWS_ACCESS_KEY_ID != ''
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Configure Azure credentials
        if: secrets.AZURE_CREDENTIALS != ''
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Configure GCP credentials
        if: secrets.GOOGLE_CREDENTIALS != ''
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GOOGLE_CREDENTIALS }}

      - name: Terraform Format Check
        id: fmt
        run: terraform fmt -check -recursive
        continue-on-error: true

      - name: Terraform Init
        id: init
        run: |
          if [ -n "${{ inputs.backend-config }}" ]; then
            terraform init -backend-config="${{ inputs.backend-config }}"
          else
            terraform init
          fi

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Setup TFLint
        uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: latest

      - name: Run TFLint
        run: |
          tflint --init
          tflint --recursive --format=compact

      - name: Terraform Plan
        id: plan
        run: |
          set +e
          VAR_FILE_ARG=""
          if [ -n "${{ inputs.var-file }}" ]; then
            VAR_FILE_ARG="-var-file=${{ inputs.var-file }}"
          fi

          if [ "${{ inputs.destroy }}" == "true" ]; then
            terraform plan -destroy -detailed-exitcode -no-color -out=tfplan $VAR_FILE_ARG
          else
            terraform plan -detailed-exitcode -no-color -out=tfplan $VAR_FILE_ARG
          fi

          EXIT_CODE=$?
          echo "exitcode=$EXIT_CODE" >> $GITHUB_OUTPUT

          if [ $EXIT_CODE -eq 2 ]; then
            echo "has-changes=true" >> $GITHUB_OUTPUT
          else
            echo "has-changes=false" >> $GITHUB_OUTPUT
          fi

          exit 0

      - name: Upload plan artifact
        if: steps.plan.outputs.has-changes == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: ${{ inputs.plan-artifact-name }}-${{ inputs.environment }}
          path: ${{ inputs.working-directory }}/tfplan
          retention-days: 7

      - name: Terraform Plan Summary
        run: |
          echo "## Terraform Plan Summary" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Environment:** ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY
          echo "**Working Directory:** ${{ inputs.working-directory }}" >> $GITHUB_STEP_SUMMARY
          echo "**Has Changes:** ${{ steps.plan.outputs.has-changes }}" >> $GITHUB_STEP_SUMMARY

      - name: Terraform Apply
        if: inputs.apply && steps.plan.outputs.has-changes == 'true'
        run: terraform apply -auto-approve tfplan

      - name: Terraform Output
        if: inputs.apply && steps.plan.outputs.has-changes == 'true'
        run: terraform output -json > outputs.json

      - name: Upload outputs
        if: inputs.apply && steps.plan.outputs.has-changes == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: terraform-outputs-${{ inputs.environment }}
          path: ${{ inputs.working-directory }}/outputs.json
          retention-days: 30

Terraform Multi-Environment Workflow

# .github/workflows/terraform-multi-env.yml
name: Terraform Multi-Environment

on:
  push:
    branches: [main]
    paths:
      - 'terraform/**'
  pull_request:
    branches: [main]
    paths:
      - 'terraform/**'
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options:
          - dev
          - staging
          - production
      action:
        description: 'Action to perform'
        required: true
        type: choice
        options:
          - plan
          - apply
          - destroy

jobs:
  plan-dev:
    if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'dev')
    uses: ./.github/workflows/reusable-terraform.yml
    with:
      working-directory: terraform/environments/dev
      environment: development
      var-file: dev.tfvars
      apply: false
    secrets: inherit

  plan-staging:
    if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'staging')
    uses: ./.github/workflows/reusable-terraform.yml
    with:
      working-directory: terraform/environments/staging
      environment: staging
      var-file: staging.tfvars
      apply: false
    secrets: inherit

  apply-dev:
    if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.action == 'apply' && inputs.environment == 'dev')
    uses: ./.github/workflows/reusable-terraform.yml
    with:
      working-directory: terraform/environments/dev
      environment: development
      var-file: dev.tfvars
      apply: true
    secrets: inherit

  apply-staging:
    needs: apply-dev
    if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.action == 'apply' && inputs.environment == 'staging')
    uses: ./.github/workflows/reusable-terraform.yml
    with:
      working-directory: terraform/environments/staging
      environment: staging
      var-file: staging.tfvars
      apply: true
    secrets: inherit

  apply-production:
    needs: apply-staging
    if: github.event_name == 'workflow_dispatch' && inputs.action == 'apply' && inputs.environment == 'production'
    uses: ./.github/workflows/reusable-terraform.yml
    with:
      working-directory: terraform/environments/production
      environment: production
      var-file: production.tfvars
      apply: true
    secrets: inherit

Security Scanning Workflow

Comprehensive Security Scanning

# .github/workflows/reusable-security.yml
name: Reusable Security Scanning

on:
  workflow_call:
    inputs:
      languages:
        description: 'Languages to scan (JSON array)'
        required: false
        type: string
        default: '["python", "javascript"]'
      scan-dependencies:
        description: 'Scan dependencies for vulnerabilities'
        required: false
        type: boolean
        default: true
      scan-secrets:
        description: 'Scan for secrets'
        required: false
        type: boolean
        default: true
      scan-sast:
        description: 'Run SAST scanning'
        required: false
        type: boolean
        default: true
      scan-containers:
        description: 'Scan container images'
        required: false
        type: boolean
        default: false
      container-image:
        description: 'Container image to scan'
        required: false
        type: string
        default: ''
      fail-on-severity:
        description: 'Fail on vulnerability severity (CRITICAL, HIGH, MEDIUM, LOW)'
        required: false
        type: string
        default: 'CRITICAL,HIGH'
    outputs:
      vulnerabilities-found:
        description: 'Whether vulnerabilities were found'
        value: ${{ jobs.summary.outputs.vulnerabilities }}
      secrets-found:
        description: 'Whether secrets were found'
        value: ${{ jobs.summary.outputs.secrets }}

jobs:
  codeql:
    if: inputs.scan-sast
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        language: ${{ fromJson(inputs.languages) }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: security-extended,security-and-quality

      - name: Autobuild
        uses: github/codeql-action/autobuild@v3

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}"

  dependency-review:
    if: github.event_name == 'pull_request' && inputs.scan-dependencies
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Dependency Review
        uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: ${{ inputs.fail-on-severity }}
          deny-licenses: GPL-3.0, AGPL-3.0

  secret-scanning:
    if: inputs.scan-secrets
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: TruffleHog Secret Scan
        uses: trufflesecurity/trufflehog@v3.93.4
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
          extra_args: --only-verified

      - name: Gitleaks Scan
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

  semgrep:
    if: inputs.scan-sast
    runs-on: ubuntu-latest
    permissions:
      security-events: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Run Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/default
            p/owasp-top-ten
            p/security-audit
          generateSarif: true

      - name: Upload Semgrep results
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

  container-scan:
    if: inputs.scan-containers && inputs.container-image != ''
    runs-on: ubuntu-latest
    permissions:
      security-events: write

    steps:
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@v0.34.1
        with:
          image-ref: ${{ inputs.container-image }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: ${{ inputs.fail-on-severity }}

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

      - name: Run Grype scanner
        uses: anchore/scan-action@v4
        with:
          image: ${{ inputs.container-image }}
          fail-build: true
          severity-cutoff: high

  sbom:
    if: inputs.scan-dependencies
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          format: spdx-json
          output-file: sbom.spdx.json

      - name: Upload SBOM
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.spdx.json
          retention-days: 30

  summary:
    needs: [codeql, dependency-review, secret-scanning, semgrep, container-scan, sbom]
    if: always()
    runs-on: ubuntu-latest
    outputs:
      vulnerabilities: ${{ steps.check.outputs.vulnerabilities }}
      secrets: ${{ steps.check.outputs.secrets }}

    steps:
      - name: Check results
        id: check
        run: |
          echo "vulnerabilities=${{ contains(needs.*.result, 'failure') }}" >> $GITHUB_OUTPUT
          echo "secrets=${{ needs.secret-scanning.result == 'failure' }}" >> $GITHUB_OUTPUT

      - name: Generate security summary
        run: |
          echo "## Security Scan Summary" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "| Scan | Status |" >> $GITHUB_STEP_SUMMARY
          echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
          echo "| CodeQL | ${{ needs.codeql.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| Dependency Review | ${{ needs.dependency-review.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| Secret Scanning | ${{ needs.secret-scanning.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| Semgrep | ${{ needs.semgrep.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| Container Scan | ${{ needs.container-scan.result }} |" >> $GITHUB_STEP_SUMMARY
          echo "| SBOM Generation | ${{ needs.sbom.result }} |" >> $GITHUB_STEP_SUMMARY

Cloud Deployment Workflows

AWS Deployment Workflow

# .github/workflows/reusable-deploy-aws.yml
name: Reusable AWS Deployment

on:
  workflow_call:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        type: string
      aws-region:
        description: 'AWS region'
        required: false
        type: string
        default: 'us-east-1'
      deployment-type:
        description: 'Deployment type (ecs, lambda, s3, eks)'
        required: true
        type: string
      service-name:
        description: 'Service/function name'
        required: true
        type: string
      cluster-name:
        description: 'ECS/EKS cluster name'
        required: false
        type: string
        default: ''
      image-uri:
        description: 'Container image URI'
        required: false
        type: string
        default: ''
      s3-bucket:
        description: 'S3 bucket for deployment'
        required: false
        type: string
        default: ''
      artifact-path:
        description: 'Path to deployment artifacts'
        required: false
        type: string
        default: 'dist/'
      health-check-url:
        description: 'URL for health check'
        required: false
        type: string
        default: ''
      rollback-on-failure:
        description: 'Rollback on deployment failure'
        required: false
        type: boolean
        default: true
    outputs:
      deployment-id:
        description: 'Deployment identifier'
        value: ${{ jobs.deploy.outputs.deployment-id }}
      deployment-url:
        description: 'Deployment URL'
        value: ${{ jobs.deploy.outputs.url }}
    secrets:
      AWS_ACCESS_KEY_ID:
        required: true
      AWS_SECRET_ACCESS_KEY:
        required: true
      AWS_ROLE_ARN:
        required: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    outputs:
      deployment-id: ${{ steps.deploy.outputs.id }}
      url: ${{ steps.deploy.outputs.url }}

    environment:
      name: ${{ inputs.environment }}
      url: ${{ steps.deploy.outputs.url }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ inputs.aws-region }}
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}

      - name: Download build artifacts
        if: inputs.deployment-type == 's3' || inputs.deployment-type == 'lambda'
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: ${{ inputs.artifact-path }}

      - name: Deploy to ECS
        id: deploy-ecs
        if: inputs.deployment-type == 'ecs'
        run: |
          aws ecs update-service \
            --cluster ${{ inputs.cluster-name }} \
            --service ${{ inputs.service-name }} \
            --force-new-deployment

          aws ecs wait services-stable \
            --cluster ${{ inputs.cluster-name }} \
            --services ${{ inputs.service-name }}

          SERVICE_URL=$(aws ecs describe-services \
            --cluster ${{ inputs.cluster-name }} \
            --services ${{ inputs.service-name }} \
            --query 'services[0].loadBalancers[0].targetGroupArn' \
            --output text)

          echo "id=${{ github.run_id }}" >> $GITHUB_OUTPUT
          echo "url=https://${{ inputs.service-name }}.${{ inputs.environment }}.example.com" >> $GITHUB_OUTPUT

      - name: Deploy to Lambda
        id: deploy-lambda
        if: inputs.deployment-type == 'lambda'
        run: |
          cd ${{ inputs.artifact-path }}
          zip -r function.zip .

          aws lambda update-function-code \
            --function-name ${{ inputs.service-name }} \
            --zip-file fileb://function.zip

          aws lambda wait function-updated \
            --function-name ${{ inputs.service-name }}

          FUNCTION_URL=$(aws lambda get-function-url-config \
            --function-name ${{ inputs.service-name }} \
            --query 'FunctionUrl' \
            --output text 2>/dev/null || echo "")

          echo "id=${{ github.run_id }}" >> $GITHUB_OUTPUT
          echo "url=$FUNCTION_URL" >> $GITHUB_OUTPUT

      - name: Deploy to S3
        id: deploy-s3
        if: inputs.deployment-type == 's3'
        run: |
          aws s3 sync ${{ inputs.artifact-path }} s3://${{ inputs.s3-bucket }}/ \
            --delete \
            --cache-control "max-age=31536000"

          aws s3 cp ${{ inputs.artifact-path }}/index.html s3://${{ inputs.s3-bucket }}/index.html \
            --cache-control "no-cache, no-store, must-revalidate"

          if [ -n "${{ inputs.health-check-url }}" ]; then
            aws cloudfront create-invalidation \
              --distribution-id $(aws cloudfront list-distributions --query "DistributionList.Items[?Origins.Items[0].DomainName=='${{ inputs.s3-bucket }}.s3.amazonaws.com'].Id" --output text) \
              --paths "/*"
          fi

          echo "id=${{ github.run_id }}" >> $GITHUB_OUTPUT
          echo "url=https://${{ inputs.s3-bucket }}.s3-website-${{ inputs.aws-region }}.amazonaws.com" >> $GITHUB_OUTPUT

      - name: Deploy to EKS
        id: deploy-eks
        if: inputs.deployment-type == 'eks'
        run: |
          aws eks update-kubeconfig --name ${{ inputs.cluster-name }} --region ${{ inputs.aws-region }}

          kubectl set image deployment/${{ inputs.service-name }} \
            ${{ inputs.service-name }}=${{ inputs.image-uri }} \
            --record

          kubectl rollout status deployment/${{ inputs.service-name }} --timeout=5m

          SERVICE_URL=$(kubectl get svc ${{ inputs.service-name }} -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')

          echo "id=${{ github.run_id }}" >> $GITHUB_OUTPUT
          echo "url=https://$SERVICE_URL" >> $GITHUB_OUTPUT

      - name: Set deployment output
        id: deploy
        run: |
          if [ "${{ inputs.deployment-type }}" == "ecs" ]; then
            echo "id=${{ steps.deploy-ecs.outputs.id }}" >> $GITHUB_OUTPUT
            echo "url=${{ steps.deploy-ecs.outputs.url }}" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.deployment-type }}" == "lambda" ]; then
            echo "id=${{ steps.deploy-lambda.outputs.id }}" >> $GITHUB_OUTPUT
            echo "url=${{ steps.deploy-lambda.outputs.url }}" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.deployment-type }}" == "s3" ]; then
            echo "id=${{ steps.deploy-s3.outputs.id }}" >> $GITHUB_OUTPUT
            echo "url=${{ steps.deploy-s3.outputs.url }}" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.deployment-type }}" == "eks" ]; then
            echo "id=${{ steps.deploy-eks.outputs.id }}" >> $GITHUB_OUTPUT
            echo "url=${{ steps.deploy-eks.outputs.url }}" >> $GITHUB_OUTPUT
          fi

      - name: Health check
        if: inputs.health-check-url != ''
        run: |
          for i in {1..30}; do
            if curl -sf "${{ inputs.health-check-url }}" > /dev/null; then
              echo "Health check passed"
              exit 0
            fi
            echo "Attempt $i: Health check failed, retrying..."
            sleep 10
          done
          echo "Health check failed after 30 attempts"
          exit 1

      - name: Rollback on failure
        if: failure() && inputs.rollback-on-failure && inputs.deployment-type == 'eks'
        run: |
          kubectl rollout undo deployment/${{ inputs.service-name }}
          kubectl rollout status deployment/${{ inputs.service-name }} --timeout=5m

Azure Deployment Workflow

# .github/workflows/reusable-deploy-azure.yml
name: Reusable Azure Deployment

on:
  workflow_call:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        type: string
      resource-group:
        description: 'Azure resource group'
        required: true
        type: string
      deployment-type:
        description: 'Deployment type (webapp, function, aks, storage)'
        required: true
        type: string
      app-name:
        description: 'App/Function name'
        required: true
        type: string
      cluster-name:
        description: 'AKS cluster name'
        required: false
        type: string
        default: ''
      image-uri:
        description: 'Container image URI'
        required: false
        type: string
        default: ''
      artifact-path:
        description: 'Path to deployment artifacts'
        required: false
        type: string
        default: 'dist/'
      storage-account:
        description: 'Storage account name'
        required: false
        type: string
        default: ''
    outputs:
      deployment-url:
        description: 'Deployment URL'
        value: ${{ jobs.deploy.outputs.url }}
    secrets:
      AZURE_CREDENTIALS:
        required: true
      AZURE_SUBSCRIPTION_ID:
        required: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    outputs:
      url: ${{ steps.deploy.outputs.url }}

    environment:
      name: ${{ inputs.environment }}
      url: ${{ steps.deploy.outputs.url }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Azure Login
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Download build artifacts
        if: inputs.deployment-type == 'webapp' || inputs.deployment-type == 'function' || inputs.deployment-type == 'storage'
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: ${{ inputs.artifact-path }}

      - name: Deploy to Web App
        id: deploy-webapp
        if: inputs.deployment-type == 'webapp'
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ inputs.app-name }}
          package: ${{ inputs.artifact-path }}

      - name: Deploy to Azure Functions
        id: deploy-function
        if: inputs.deployment-type == 'function'
        uses: azure/functions-action@v1
        with:
          app-name: ${{ inputs.app-name }}
          package: ${{ inputs.artifact-path }}

      - name: Deploy to AKS
        id: deploy-aks
        if: inputs.deployment-type == 'aks'
        run: |
          az aks get-credentials --resource-group ${{ inputs.resource-group }} --name ${{ inputs.cluster-name }}

          kubectl set image deployment/${{ inputs.app-name }} \
            ${{ inputs.app-name }}=${{ inputs.image-uri }}

          kubectl rollout status deployment/${{ inputs.app-name }} --timeout=5m

      - name: Deploy to Azure Storage (Static Website)
        id: deploy-storage
        if: inputs.deployment-type == 'storage'
        run: |
          az storage blob upload-batch \
            --account-name ${{ inputs.storage-account }} \
            --destination '$web' \
            --source ${{ inputs.artifact-path }} \
            --overwrite

      - name: Set deployment output
        id: deploy
        run: |
          if [ "${{ inputs.deployment-type }}" == "webapp" ]; then
            URL="https://${{ inputs.app-name }}.azurewebsites.net"
          elif [ "${{ inputs.deployment-type }}" == "function" ]; then
            URL="https://${{ inputs.app-name }}.azurewebsites.net"
          elif [ "${{ inputs.deployment-type }}" == "aks" ]; then
            URL=$(kubectl get svc ${{ inputs.app-name }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
            URL="http://$URL"
          elif [ "${{ inputs.deployment-type }}" == "storage" ]; then
            URL=$(az storage account show --name ${{ inputs.storage-account }} --query "primaryEndpoints.web" --output tsv)
          fi
          echo "url=$URL" >> $GITHUB_OUTPUT

GCP Deployment Workflow

# .github/workflows/reusable-deploy-gcp.yml
name: Reusable GCP Deployment

on:
  workflow_call:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        type: string
      project-id:
        description: 'GCP project ID'
        required: true
        type: string
      region:
        description: 'GCP region'
        required: false
        type: string
        default: 'us-central1'
      deployment-type:
        description: 'Deployment type (cloudrun, appengine, gke, cloudfunctions, storage)'
        required: true
        type: string
      service-name:
        description: 'Service/function name'
        required: true
        type: string
      image-uri:
        description: 'Container image URI'
        required: false
        type: string
        default: ''
      cluster-name:
        description: 'GKE cluster name'
        required: false
        type: string
        default: ''
      bucket-name:
        description: 'GCS bucket for deployment'
        required: false
        type: string
        default: ''
      artifact-path:
        description: 'Path to deployment artifacts'
        required: false
        type: string
        default: 'dist/'
      min-instances:
        description: 'Minimum instances'
        required: false
        type: number
        default: 0
      max-instances:
        description: 'Maximum instances'
        required: false
        type: number
        default: 100
    outputs:
      deployment-url:
        description: 'Deployment URL'
        value: ${{ jobs.deploy.outputs.url }}
    secrets:
      GCP_CREDENTIALS:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    outputs:
      url: ${{ steps.deploy.outputs.url }}

    environment:
      name: ${{ inputs.environment }}
      url: ${{ steps.deploy.outputs.url }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_CREDENTIALS }}

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v2
        with:
          project_id: ${{ inputs.project-id }}

      - name: Download build artifacts
        if: inputs.deployment-type == 'appengine' || inputs.deployment-type == 'cloudfunctions' || inputs.deployment-type == 'storage'
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: ${{ inputs.artifact-path }}

      - name: Deploy to Cloud Run
        id: deploy-cloudrun
        if: inputs.deployment-type == 'cloudrun'
        run: |
          gcloud run deploy ${{ inputs.service-name }} \
            --image ${{ inputs.image-uri }} \
            --region ${{ inputs.region }} \
            --platform managed \
            --allow-unauthenticated \
            --min-instances ${{ inputs.min-instances }} \
            --max-instances ${{ inputs.max-instances }}

          URL=$(gcloud run services describe ${{ inputs.service-name }} \
            --region ${{ inputs.region }} \
            --format 'value(status.url)')
          echo "url=$URL" >> $GITHUB_OUTPUT

      - name: Deploy to App Engine
        id: deploy-appengine
        if: inputs.deployment-type == 'appengine'
        working-directory: ${{ inputs.artifact-path }}
        run: |
          gcloud app deploy --quiet --promote

          URL="https://${{ inputs.project-id }}.appspot.com"
          echo "url=$URL" >> $GITHUB_OUTPUT

      - name: Deploy to GKE
        id: deploy-gke
        if: inputs.deployment-type == 'gke'
        run: |
          gcloud container clusters get-credentials ${{ inputs.cluster-name }} \
            --region ${{ inputs.region }} \
            --project ${{ inputs.project-id }}

          kubectl set image deployment/${{ inputs.service-name }} \
            ${{ inputs.service-name }}=${{ inputs.image-uri }}

          kubectl rollout status deployment/${{ inputs.service-name }} --timeout=5m

          SERVICE_IP=$(kubectl get svc ${{ inputs.service-name }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
          echo "url=http://$SERVICE_IP" >> $GITHUB_OUTPUT

      - name: Deploy Cloud Function
        id: deploy-function
        if: inputs.deployment-type == 'cloudfunctions'
        working-directory: ${{ inputs.artifact-path }}
        run: |
          gcloud functions deploy ${{ inputs.service-name }} \
            --gen2 \
            --runtime nodejs20 \
            --region ${{ inputs.region }} \
            --trigger-http \
            --allow-unauthenticated \
            --min-instances ${{ inputs.min-instances }} \
            --max-instances ${{ inputs.max-instances }}

          URL=$(gcloud functions describe ${{ inputs.service-name }} \
            --region ${{ inputs.region }} \
            --gen2 \
            --format 'value(serviceConfig.uri)')
          echo "url=$URL" >> $GITHUB_OUTPUT

      - name: Deploy to Cloud Storage
        id: deploy-storage
        if: inputs.deployment-type == 'storage'
        run: |
          gsutil -m rsync -r -d ${{ inputs.artifact-path }} gs://${{ inputs.bucket-name }}

          gsutil web set -m index.html -e 404.html gs://${{ inputs.bucket-name }}

          echo "url=https://storage.googleapis.com/${{ inputs.bucket-name }}/index.html" >> $GITHUB_OUTPUT

      - name: Set deployment output
        id: deploy
        run: |
          if [ "${{ inputs.deployment-type }}" == "cloudrun" ]; then
            echo "url=${{ steps.deploy-cloudrun.outputs.url }}" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.deployment-type }}" == "appengine" ]; then
            echo "url=${{ steps.deploy-appengine.outputs.url }}" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.deployment-type }}" == "gke" ]; then
            echo "url=${{ steps.deploy-gke.outputs.url }}" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.deployment-type }}" == "cloudfunctions" ]; then
            echo "url=${{ steps.deploy-function.outputs.url }}" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.deployment-type }}" == "storage" ]; then
            echo "url=${{ steps.deploy-storage.outputs.url }}" >> $GITHUB_OUTPUT
          fi

Notification Workflow

Reusable Notification Workflow

# .github/workflows/reusable-notify.yml
name: Reusable Notification Workflow

on:
  workflow_call:
    inputs:
      status:
        description: 'Deployment/build status'
        required: true
        type: string
      environment:
        description: 'Environment name'
        required: false
        type: string
        default: ''
      deployment-url:
        description: 'Deployment URL'
        required: false
        type: string
        default: ''
      message:
        description: 'Custom message'
        required: false
        type: string
        default: ''
      notify-slack:
        description: 'Send Slack notification'
        required: false
        type: boolean
        default: true
      notify-teams:
        description: 'Send Teams notification'
        required: false
        type: boolean
        default: false
      notify-discord:
        description: 'Send Discord notification'
        required: false
        type: boolean
        default: false
    secrets:
      SLACK_WEBHOOK_URL:
        required: false
      TEAMS_WEBHOOK_URL:
        required: false
      DISCORD_WEBHOOK_URL:
        required: false

jobs:
  notify:
    runs-on: ubuntu-latest

    steps:
      - name: Set status emoji
        id: emoji
        run: |
          if [ "${{ inputs.status }}" == "success" ]; then
            echo "emoji=:white_check_mark:" >> $GITHUB_OUTPUT
            echo "color=good" >> $GITHUB_OUTPUT
            echo "teams_color=00FF00" >> $GITHUB_OUTPUT
          elif [ "${{ inputs.status }}" == "failure" ]; then
            echo "emoji=:x:" >> $GITHUB_OUTPUT
            echo "color=danger" >> $GITHUB_OUTPUT
            echo "teams_color=FF0000" >> $GITHUB_OUTPUT
          else
            echo "emoji=:warning:" >> $GITHUB_OUTPUT
            echo "color=warning" >> $GITHUB_OUTPUT
            echo "teams_color=FFFF00" >> $GITHUB_OUTPUT
          fi

      - name: Send Slack notification
        if: inputs.notify-slack && secrets.SLACK_WEBHOOK_URL != ''
        uses: slackapi/slack-github-action@v1.26.0
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "attachments": [
                {
                  "color": "${{ steps.emoji.outputs.color }}",
                  "blocks": [
                    {
                      "type": "header",
                      "text": {
                        "type": "plain_text",
                        "text": "${{ steps.emoji.outputs.emoji }} ${{ github.repository }} - ${{ inputs.status }}"
                      }
                    },
                    {
                      "type": "section",
                      "fields": [
                        {
                          "type": "mrkdwn",
                          "text": "*Workflow:*\n${{ github.workflow }}"
                        },
                        {
                          "type": "mrkdwn",
                          "text": "*Branch:*\n${{ github.ref_name }}"
                        },
                        {
                          "type": "mrkdwn",
                          "text": "*Environment:*\n${{ inputs.environment || 'N/A' }}"
                        },
                        {
                          "type": "mrkdwn",
                          "text": "*Triggered by:*\n${{ github.actor }}"
                        }
                      ]
                    },
                    {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": "${{ inputs.message || format('Commit: {0}', github.event.head_commit.message) }}"
                      }
                    },
                    {
                      "type": "actions",
                      "elements": [
                        {
                          "type": "button",
                          "text": {
                            "type": "plain_text",
                            "text": "View Run"
                          },
                          "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                        },
                        {
                          "type": "button",
                          "text": {
                            "type": "plain_text",
                            "text": "View Deployment"
                          },
                          "url": "${{ inputs.deployment-url }}"
                        }
                      ]
                    }
                  ]
                }
              ]
            }

      - name: Send Teams notification
        if: inputs.notify-teams && secrets.TEAMS_WEBHOOK_URL != ''
        run: |
          curl -H 'Content-Type: application/json' \
            -d '{
              "@type": "MessageCard",
              "@context": "http://schema.org/extensions",
              "themeColor": "${{ steps.emoji.outputs.teams_color }}",
              "summary": "${{ github.repository }} - ${{ inputs.status }}",
              "sections": [{
                "activityTitle": "${{ github.repository }} - ${{ inputs.status }}",
                "facts": [
                  {"name": "Workflow", "value": "${{ github.workflow }}"},
                  {"name": "Branch", "value": "${{ github.ref_name }}"},
                  {"name": "Environment", "value": "${{ inputs.environment || 'N/A' }}"},
                  {"name": "Triggered by", "value": "${{ github.actor }}"}
                ],
                "text": "${{ inputs.message || github.event.head_commit.message }}"
              }],
              "potentialAction": [
                {
                  "@type": "OpenUri",
                  "name": "View Run",
                  "targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}]
                }
              ]
            }' \
            ${{ secrets.TEAMS_WEBHOOK_URL }}

      - name: Send Discord notification
        if: inputs.notify-discord && secrets.DISCORD_WEBHOOK_URL != ''
        run: |
          curl -H "Content-Type: application/json" \
            -d '{
              "embeds": [{
                "title": "${{ github.repository }} - ${{ inputs.status }}",
                "color": ${{ inputs.status == 'success' && '65280' || inputs.status == 'failure' && '16711680' || '16776960' }},
                "fields": [
                  {"name": "Workflow", "value": "${{ github.workflow }}", "inline": true},
                  {"name": "Branch", "value": "${{ github.ref_name }}", "inline": true},
                  {"name": "Environment", "value": "${{ inputs.environment || 'N/A' }}", "inline": true},
                  {"name": "Triggered by", "value": "${{ github.actor }}", "inline": true}
                ],
                "description": "${{ inputs.message || github.event.head_commit.message }}",
                "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
              }]
            }' \
            ${{ secrets.DISCORD_WEBHOOK_URL }}

Complete CI/CD Pipeline Example

Full Pipeline Using Reusable Workflows

# .github/workflows/complete-pipeline.yml
name: Complete CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy to environment'
        required: true
        type: choice
        options:
          - staging
          - production

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  # Stage 1: Build and Test
  build:
    uses: ./.github/workflows/reusable-build.yml
    with:
      language: node
      node-version: '20'
      artifact-name: app-build
    secrets: inherit

  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      language: node
      node-version: '20'
      coverage-threshold: 80
    secrets: inherit

  # Stage 2: Security Scanning
  security:
    needs: [build, test]
    uses: ./.github/workflows/reusable-security.yml
    with:
      languages: '["javascript", "typescript"]'
      scan-dependencies: true
      scan-secrets: true
      scan-sast: true
    secrets: inherit

  # Stage 3: Docker Build
  docker:
    needs: [build, test, security]
    if: github.event_name != 'pull_request'
    uses: ./.github/workflows/reusable-docker.yml
    with:
      image-name: ${{ github.repository }}
      push: true
      scan-image: true
    secrets: inherit

  # Stage 4: Deploy to Staging
  deploy-staging:
    needs: docker
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
    uses: ./.github/workflows/reusable-deploy-aws.yml
    with:
      environment: staging
      deployment-type: ecs
      service-name: myapp
      cluster-name: staging-cluster
      image-uri: ghcr.io/${{ github.repository }}:${{ github.sha }}
      health-check-url: https://staging.example.com/health
    secrets: inherit

  # Stage 5: Integration Tests
  integration-tests:
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run integration tests
        run: |
          npm ci
          npm run test:integration
        env:
          TEST_URL: https://staging.example.com

  # Stage 6: Deploy to Production
  deploy-production:
    needs: [integration-tests]
    if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || inputs.environment == 'production')
    uses: ./.github/workflows/reusable-deploy-aws.yml
    with:
      environment: production
      deployment-type: ecs
      service-name: myapp
      cluster-name: production-cluster
      image-uri: ghcr.io/${{ github.repository }}:${{ github.sha }}
      health-check-url: https://example.com/health
      rollback-on-failure: true
    secrets: inherit

  # Stage 7: Notifications
  notify-success:
    needs: [deploy-staging, deploy-production]
    if: success()
    uses: ./.github/workflows/reusable-notify.yml
    with:
      status: success
      environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
      deployment-url: ${{ github.ref == 'refs/heads/main' && 'https://example.com' || 'https://staging.example.com' }}
      notify-slack: true
    secrets: inherit

  notify-failure:
    needs: [build, test, security, docker, deploy-staging, deploy-production]
    if: failure()
    uses: ./.github/workflows/reusable-notify.yml
    with:
      status: failure
      message: 'Pipeline failed - please investigate'
      notify-slack: true
    secrets: inherit

Best Practices

Workflow Organization

.github/
  workflows/
    # Reusable workflows (called by others)
    reusable-build.yml
    reusable-test.yml
    reusable-docker.yml
    reusable-terraform.yml
    reusable-security.yml
    reusable-deploy-aws.yml
    reusable-deploy-azure.yml
    reusable-deploy-gcp.yml
    reusable-notify.yml

    # Main workflows (entry points)
    ci.yml                    # Pull request CI
    cd.yml                    # Continuous deployment
    release.yml               # Release workflow
    scheduled-security.yml    # Scheduled security scans

Input/Output Design

# Good - Typed inputs with defaults
inputs:
  language:
    description: 'Programming language'
    required: true
    type: string
  version:
    description: 'Language version'
    required: false
    type: string
    default: 'latest'
  coverage-threshold:
    description: 'Minimum coverage'
    required: false
    type: number
    default: 80

# Good - Clear outputs
outputs:
  artifact-name:
    description: 'Name of the uploaded artifact'
    value: ${{ jobs.build.outputs.artifact }}
  version:
    description: 'Built version'
    value: ${{ jobs.build.outputs.version }}

Secret Inheritance

# Caller workflow
jobs:
  build:
    uses: ./.github/workflows/reusable-build.yml
    secrets: inherit  # Pass all secrets

# Or explicit secrets
jobs:
  deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    secrets:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

References


Status: Active