Claude Skill

ci-cd-ops

CI/CD pipeline patterns with GitHub Actions, release automation, and testing strategies. Use for: github actions, workflow, CI, CD, pipeline, deploy, release, semantic release, changesets, goreleaser, matrix, cache, secrets, environment, artifact, reusable workflow, composite act

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_ci-cd-ops-3dfaf0b.zip · 20 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/ci-cd-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

CI/CD Operations

Comprehensive patterns for continuous integration, delivery, and deployment using GitHub Actions, release automation tools, and testing pipelines.

GitHub Actions Quick Reference

Workflow File Anatomy

name: CI                          # Display name in Actions tab
on:                               # Trigger events
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:                      # GITHUB_TOKEN scope (least privilege)
  contents: read
  pull-requests: write

concurrency:                      # Prevent duplicate runs
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:                              # Workflow-level environment variables
  NODE_VERSION: "20"

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

Core Syntax Elements

Element Purpose Example
on Event triggers push, pull_request, schedule
jobs.<id>.runs-on Runner selection ubuntu-latest, self-hosted
jobs.<id>.needs Job dependencies needs: [build, lint]
jobs.<id>.if Conditional execution if: github.event_name == 'push'
jobs.<id>.strategy.matrix Parallel variants node-version: [18, 20, 22]
jobs.<id>.environment Deployment target environment: production
jobs.<id>.permissions Token scope contents: write
steps[*].uses Use an action uses: actions/checkout@v4
steps[*].run Run a command run: npm test
steps[*].env Step environment env: { CI: true }

Trigger Decision Tree

Scenario Trigger Config
Run tests on every PR pull_request branches: [main]
Deploy on merge to main push branches: [main]
Release on version tag push tags: ['v*']
Nightly builds schedule cron: '0 2 * * *'
Manual deployment workflow_dispatch inputs: { environment: ... }
Called by another workflow workflow_call inputs:, secrets:
On PR label change pull_request types: [labeled]
On issue comment issue_comment types: [created]
On release published release types: [published]
On package push registry_package types: [published]

Trigger Filter Patterns

on:
  push:
    branches: [main, 'release/**']      # Branch patterns
    paths: ['src/**', '!src/**/*.test.*'] # Path filters (ignore tests)
    tags: ['v*']                          # Tag patterns
  pull_request:
    types: [opened, synchronize, reopened] # Default types
    paths-ignore: ['docs/**', '*.md']     # Ignore docs-only changes

Caching Strategies

Ecosystem Action / Key Path Restore Key
Node (npm) actions/setup-node with cache: npm Auto Auto
Node (pnpm) actions/setup-node with cache: pnpm Auto Auto
Go modules actions/setup-go with cache: true Auto Auto
Cargo actions/cache@v4 ~/.cargo/registry, target cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
pip / uv actions/setup-python with cache: pip Auto Auto
Docker layers docker/build-push-action Uses buildx cache type=gha or type=registry
Gradle actions/setup-java with cache: gradle Auto Auto
Composer actions/cache@v4 vendor composer-${{ hashFiles('composer.lock') }}

Manual Cache Example

- uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/bin
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
    restore-keys: |
      cargo-${{ runner.os }}-

Matrix Strategy

strategy:
  fail-fast: false                    # Don't cancel siblings on failure
  max-parallel: 4                     # Limit concurrent jobs
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node-version: [18, 20, 22]
    include:                          # Add specific combos
      - os: ubuntu-latest
        node-version: 22
        coverage: true
    exclude:                          # Remove specific combos
      - os: windows-latest
        node-version: 18

Dynamic Matrix

prepare:
  runs-on: ubuntu-latest
  outputs:
    matrix: ${{ steps.set.outputs.matrix }}
  steps:
    - id: set
      run: echo "matrix=$(jq -c . matrix.json)" >> "$GITHUB_OUTPUT"

test:
  needs: prepare
  strategy:
    matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}

Secrets Management

Scope Access Use Case
Repository secrets All workflows in repo API keys, tokens
Environment secrets Jobs targeting that environment Production credentials
Organization secrets Selected repos in org Shared service accounts
OIDC tokens Federated identity Cloud deployment (no stored secrets)

Secrets Best Practices

# Reference secrets - NEVER echo or log them
- run: deploy --token ${{ secrets.DEPLOY_TOKEN }}

# Mask custom values
- run: echo "::add-mask::$CUSTOM_SECRET"

# Use environments for deployment secrets
jobs:
  deploy:
    environment: production           # Requires approval + has secrets
    steps:
      - run: deploy --key ${{ secrets.PROD_API_KEY }}

OIDC for Cloud (No Stored Secrets)

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions
      aws-region: us-east-1

Common Workflow Patterns

Test on Pull Request

name: Test
on:
  pull_request:
    branches: [main]
concurrency:
  group: test-${{ github.head_ref }}
  cancel-in-progress: true
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage

Deploy on Merge to Main

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: npx wrangler deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}

Release on Tag

name: Release
on:
  push:
    tags: ['v*']
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: |
          gh release create ${{ github.ref_name }} \
            --generate-notes \
            --title "${{ github.ref_name }}"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Gotchas Table

Gotcha Problem Fix
Shallow clone git describe fails, history missing actions/checkout@v4 with fetch-depth: 0
Default permissions GITHUB_TOKEN is read-only by default Set permissions: explicitly
Action pinning @main can break without warning Pin to SHA: @abc123 or @v4
Fork PR secrets Secrets unavailable on fork PRs Use pull_request_target carefully
Concurrent deploys Race condition on production Use concurrency: groups
Stale caches Cache grows unbounded Include lockfile hash in key
Node.js version setup-node defaults vary Always specify node-version
Docker layer cache Rebuilds everything without cache Use cache-from: type=gha
Matrix + environment Each matrix job needs approval Use a single deploy job after matrix
Path filters + required checks Skipped jobs block merge Use paths-filter action or make checks non-required
GITHUB_TOKEN in PRs Cannot trigger other workflows Use a PAT or GitHub App token
Windows line endings Scripts fail with \r\n Use .gitattributes or core.autocrlf

Expression Syntax Quick Reference

Expression Result
${{ github.event_name }} push, pull_request, etc.
${{ github.ref_name }} Branch or tag name
${{ github.sha }} Full commit SHA
${{ github.actor }} User who triggered
${{ runner.os }} Linux, Windows, macOS
${{ contains(github.event.head_commit.message, '[skip ci]') }} Check commit message
${{ needs.build.outputs.version }} Output from prior job
${{ fromJson(steps.meta.outputs.json) }} Parse JSON output
${{ hashFiles('**/package-lock.json') }} Hash for cache keys
${{ format('refs/heads/{0}', matrix.branch) }} String formatting
${{ toJson(matrix) }} Debug: print matrix config

Step Outputs

steps:
  - id: version
    run: echo "value=$(cat VERSION)" >> "$GITHUB_OUTPUT"

  - run: echo "Version is ${{ steps.version.outputs.value }}"

Job Outputs (for Cross-Job Communication)

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-id: ${{ steps.upload.outputs.artifact-id }}
    steps:
      - id: upload
        run: echo "artifact-id=abc123" >> "$GITHUB_OUTPUT"

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying ${{ needs.build.outputs.artifact-id }}"

Reference Files

File Contents
references/github-actions.md Complete workflow syntax, reusable workflows, composite actions, OIDC, runners, debugging
references/release-automation.md Semantic versioning, semantic-release, changesets, goreleaser, changelog, publishing
references/testing-pipelines.md Test stages, parallelism, coverage, service containers, e2e in CI, deployment pipelines
Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
  • references
    • github-actions.md 17 KB
      # GitHub Actions Reference
      
      ## Table of Contents
      
      - [Workflow File Anatomy](#workflow-file-anatomy)
      - [Job Dependencies and Conditionals](#job-dependencies-and-conditionals)
      - [Reusable Workflows](#reusable-workflows)
      - [Composite Actions](#composite-actions)
      - [Matrix Strategy](#matrix-strategy)
      - [Artifacts](#artifacts)
      - [Environment Protection Rules](#environment-protection-rules)
      - [Concurrency Control](#concurrency-control)
      - [Self-Hosted Runners](#self-hosted-runners)
      - [OIDC for Cloud Deployment](#oidc-for-cloud-deployment)
      - [Common Action Recipes](#common-action-recipes)
      - [Debugging Workflows](#debugging-workflows)
      
      ---
      
      ## Workflow File Anatomy
      
      Every workflow lives in `.github/workflows/*.yml`. A complete annotated example:
      
      ```yaml
      # .github/workflows/ci.yml
      name: CI Pipeline                     # Name shown in Actions tab
      
      # ── Triggers ──────────────────────────────────────────────
      on:
        push:
          branches: [main, 'release/**']
          paths-ignore: ['docs/**', '*.md']
        pull_request:
          branches: [main]
          types: [opened, synchronize, reopened]
        schedule:
          - cron: '0 6 * * 1'              # Weekly Monday 6am UTC
        workflow_dispatch:                   # Manual trigger
          inputs:
            environment:
              description: 'Deploy target'
              required: true
              default: 'staging'
              type: choice
              options: [staging, production]
      
      # ── Token Permissions (least privilege) ───────────────────
      permissions:
        contents: read
        pull-requests: write
        checks: write
      
      # ── Concurrency ──────────────────────────────────────────
      concurrency:
        group: ${{ github.workflow }}-${{ github.ref }}
        cancel-in-progress: ${{ github.event_name == 'pull_request' }}
      
      # ── Workflow Environment ─────────────────────────────────
      env:
        CI: true
        NODE_ENV: test
      
      # ── Jobs ─────────────────────────────────────────────────
      jobs:
        lint:
          name: Lint & Format
          runs-on: ubuntu-latest
          timeout-minutes: 10               # Prevent hung jobs
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with:
                node-version-file: '.nvmrc'
                cache: npm
            - run: npm ci
            - run: npm run lint
            - run: npm run format:check
      
        test:
          name: Test (${{ matrix.node-version }})
          runs-on: ubuntu-latest
          timeout-minutes: 15
          needs: lint                       # Run after lint passes
          strategy:
            fail-fast: false
            matrix:
              node-version: [18, 20, 22]
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with:
                node-version: ${{ matrix.node-version }}
                cache: npm
            - run: npm ci
            - run: npm test -- --coverage
            - uses: actions/upload-artifact@v4
              if: always()                  # Upload even on failure
              with:
                name: coverage-${{ matrix.node-version }}
                path: coverage/
                retention-days: 7
      
        deploy:
          name: Deploy
          runs-on: ubuntu-latest
          needs: test
          if: github.ref == 'refs/heads/main' && github.event_name == 'push'
          environment: production           # Requires approval
          steps:
            - uses: actions/checkout@v4
            - run: ./deploy.sh
              env:
                DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
      ```
      
      ## Job Dependencies and Conditionals
      
      ### Job Dependencies with `needs`
      
      ```yaml
      jobs:
        build:
          runs-on: ubuntu-latest
          steps: [...]
      
        test:
          needs: build                      # Waits for build
          runs-on: ubuntu-latest
          steps: [...]
      
        deploy:
          needs: [build, test]              # Waits for both
          runs-on: ubuntu-latest
          steps: [...]
      ```
      
      ### Conditional Execution with `if`
      
      ```yaml
      jobs:
        deploy:
          if: github.ref == 'refs/heads/main'
          runs-on: ubuntu-latest
      
        notify:
          needs: deploy
          if: always()                      # Run even if deploy fails
          runs-on: ubuntu-latest
      
        release:
          if: startsWith(github.ref, 'refs/tags/v')
          runs-on: ubuntu-latest
      
      steps:
        - run: echo "Only on failure"
          if: failure()
      
        - run: echo "Only on success"
          if: success()
      
        - run: echo "Always run (cleanup)"
          if: always()
      
        - run: echo "Skip on forks"
          if: github.repository == 'owner/repo'
      
        - run: echo "Only for specific actor"
          if: github.actor == 'dependabot[bot]'
      
        - run: echo "Check PR label"
          if: contains(github.event.pull_request.labels.*.name, 'deploy')
      ```
      
      ### Accessing Outputs from `needs`
      
      ```yaml
      jobs:
        check:
          runs-on: ubuntu-latest
          outputs:
            should-deploy: ${{ steps.decision.outputs.deploy }}
          steps:
            - id: decision
              run: |
                if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
                  echo "deploy=true" >> "$GITHUB_OUTPUT"
                else
                  echo "deploy=false" >> "$GITHUB_OUTPUT"
                fi
      
        deploy:
          needs: check
          if: needs.check.outputs.should-deploy == 'true'
          runs-on: ubuntu-latest
          steps:
            - run: echo "Deploying..."
      ```
      
      ## Reusable Workflows
      
      ### Defining a Reusable Workflow
      
      ```yaml
      # .github/workflows/reusable-test.yml
      name: Reusable Test Workflow
      on:
        workflow_call:
          inputs:
            node-version:
              description: 'Node.js version'
              required: false
              default: '20'
              type: string
            working-directory:
              description: 'Directory to run tests in'
              required: false
              default: '.'
              type: string
          secrets:
            NPM_TOKEN:
              required: false
              description: 'NPM auth token'
          outputs:
            coverage-percent:
              description: 'Test coverage percentage'
              value: ${{ jobs.test.outputs.coverage }}
      
      jobs:
        test:
          runs-on: ubuntu-latest
          outputs:
            coverage: ${{ steps.cov.outputs.percent }}
          defaults:
            run:
              working-directory: ${{ inputs.working-directory }}
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with:
                node-version: ${{ inputs.node-version }}
                cache: npm
            - run: npm ci
              env:
                NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
            - run: npm test -- --coverage
            - id: cov
              run: |
                PERCENT=$(jq '.total.lines.pct' coverage/coverage-summary.json)
                echo "percent=$PERCENT" >> "$GITHUB_OUTPUT"
      ```
      
      ### Calling a Reusable Workflow
      
      ```yaml
      # .github/workflows/ci.yml
      name: CI
      on: [push, pull_request]
      
      jobs:
        test:
          uses: ./.github/workflows/reusable-test.yml
          with:
            node-version: '20'
          secrets:
            NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      
        # Or inherit all secrets
        test-inherit:
          uses: ./.github/workflows/reusable-test.yml
          secrets: inherit
      
        # Call from another repo
        test-external:
          uses: org/shared-workflows/.github/workflows/test.yml@main
          with:
            node-version: '20'
      
        report:
          needs: test
          runs-on: ubuntu-latest
          steps:
            - run: echo "Coverage was ${{ needs.test.outputs.coverage-percent }}%"
      ```
      
      ## Composite Actions
      
      ### Creating a Composite Action
      
      ```yaml
      # .github/actions/setup-project/action.yml
      name: 'Setup Project'
      description: 'Install dependencies and build'
      inputs:
        node-version:
          description: 'Node.js version'
          required: false
          default: '20'
        install-command:
          description: 'Install command'
          required: false
          default: 'npm ci'
      outputs:
        cache-hit:
          description: 'Whether cache was hit'
          value: ${{ steps.cache.outputs.cache-hit }}
      
      runs:
        using: composite
        steps:
          - uses: actions/setup-node@v4
            with:
              node-version: ${{ inputs.node-version }}
      
          - id: cache
            uses: actions/cache@v4
            with:
              path: node_modules
              key: node-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
      
          - if: steps.cache.outputs.cache-hit != 'true'
            run: ${{ inputs.install-command }}
            shell: bash
      
          - run: npm run build
            shell: bash                     # shell: is REQUIRED in composite
      ```
      
      ### Using a Composite Action
      
      ```yaml
      steps:
        - uses: actions/checkout@v4
        - uses: ./.github/actions/setup-project
          with:
            node-version: '22'
        - run: npm test
      ```
      
      ## Matrix Strategy
      
      ### Basic Matrix
      
      ```yaml
      strategy:
        matrix:
          os: [ubuntu-latest, windows-latest, macos-latest]
          node: [18, 20, 22]
          # Creates 3 x 3 = 9 jobs
      ```
      
      ### Include and Exclude
      
      ```yaml
      strategy:
        matrix:
          os: [ubuntu-latest, windows-latest]
          node: [18, 20]
          include:
            # Add a job with extra variables
            - os: ubuntu-latest
              node: 22
              experimental: true
            # Add variables to existing combo
            - os: windows-latest
              node: 20
              npm-version: 10
          exclude:
            # Remove a specific combo
            - os: windows-latest
              node: 18
      ```
      
      ### Matrix with `continue-on-error`
      
      ```yaml
      strategy:
        fail-fast: false
        matrix:
          node: [18, 20, 22]
          include:
            - node: 22
              experimental: true
      
      jobs:
        test:
          continue-on-error: ${{ matrix.experimental || false }}
      ```
      
      ### Single-Dimension Matrix (List of Configs)
      
      ```yaml
      strategy:
        matrix:
          include:
            - name: Unit Tests
              command: npm run test:unit
            - name: Integration Tests
              command: npm run test:integration
              timeout: 30
            - name: E2E Tests
              command: npm run test:e2e
              timeout: 60
      ```
      
      ## Artifacts
      
      ### Upload and Download Between Jobs
      
      ```yaml
      jobs:
        build:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - run: npm ci && npm run build
      
            - uses: actions/upload-artifact@v4
              with:
                name: dist
                path: dist/
                retention-days: 1           # Short-lived build artifacts
                if-no-files-found: error    # Fail if nothing to upload
      
        deploy:
          needs: build
          runs-on: ubuntu-latest
          steps:
            - uses: actions/download-artifact@v4
              with:
                name: dist
                path: dist/
      
            - run: ls -la dist/            # Verify download
      ```
      
      ### Multiple Artifact Upload (Matrix)
      
      ```yaml
      # Upload with unique names per matrix
      - uses: actions/upload-artifact@v4
        with:
          name: results-${{ matrix.os }}-${{ matrix.node }}
          path: test-results/
      
      # Download all in a later job
      - uses: actions/download-artifact@v4
        with:
          pattern: results-*
          merge-multiple: true
          path: all-results/
      ```
      
      ## Environment Protection Rules
      
      Environments provide deployment gates and scoped secrets.
      
      ### Setting Up Environments
      
      Environments are configured in **Settings > Environments** on GitHub. Options:
      
      | Setting | Purpose |
      |---------|---------|
      | Required reviewers | Manual approval before deployment (up to 6 reviewers) |
      | Wait timer | Delay in minutes before deployment proceeds |
      | Deployment branches | Restrict which branches can deploy (e.g., only `main`) |
      | Environment secrets | Secrets scoped to this environment only |
      | Environment variables | Variables scoped to this environment |
      
      ### Using Environments in Workflows
      
      ```yaml
      jobs:
        deploy-staging:
          runs-on: ubuntu-latest
          environment:
            name: staging
            url: https://staging.example.com   # Shown in deployment status
          steps:
            - run: deploy --env staging
              env:
                API_KEY: ${{ secrets.API_KEY }}  # Environment-scoped secret
      
        deploy-production:
          needs: deploy-staging
          runs-on: ubuntu-latest
          environment:
            name: production
            url: https://example.com
          steps:
            - run: deploy --env production
      ```
      
      ## Concurrency Control
      
      ### Cancel Previous Runs on Same Branch
      
      ```yaml
      concurrency:
        group: ${{ github.workflow }}-${{ github.ref }}
        cancel-in-progress: true
      ```
      
      ### Deployment Queue (No Cancellation)
      
      ```yaml
      concurrency:
        group: deploy-production
        cancel-in-progress: false           # Queue instead of cancel
      ```
      
      ### Per-PR Concurrency
      
      ```yaml
      concurrency:
        group: pr-${{ github.event.pull_request.number }}
        cancel-in-progress: true
      ```
      
      ## Self-Hosted Runners
      
      ### Runner Labels
      
      ```yaml
      jobs:
        build:
          runs-on: [self-hosted, linux, x64, gpu]    # Match all labels
      ```
      
      ### Runner Groups (Enterprise/Org)
      
      ```yaml
      jobs:
        build:
          runs-on:
            group: production-runners
            labels: [linux, x64]
      ```
      
      ### Hybrid Strategy
      
      ```yaml
      strategy:
        matrix:
          runner: [ubuntu-latest, self-hosted]
      
      jobs:
        test:
          runs-on: ${{ matrix.runner }}
      ```
      
      ## OIDC for Cloud Deployment
      
      OIDC eliminates stored cloud credentials. GitHub issues a short-lived JWT that your cloud provider trusts.
      
      ### AWS
      
      ```yaml
      permissions:
        id-token: write
        contents: read
      
      steps:
        - uses: aws-actions/configure-aws-credentials@v4
          with:
            role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
            aws-region: us-east-1
            # No access keys needed
      
        - run: aws s3 sync dist/ s3://my-bucket
      ```
      
      ### GCP
      
      ```yaml
      permissions:
        id-token: write
        contents: read
      
      steps:
        - uses: google-github-actions/auth@v2
          with:
            workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/github/providers/my-repo'
            service_account: 'deploy@my-project.iam.gserviceaccount.com'
      
        - uses: google-github-actions/setup-gcloud@v2
      
        - run: gcloud run deploy my-service --image gcr.io/my-project/app
      ```
      
      ### Azure
      
      ```yaml
      permissions:
        id-token: write
        contents: read
      
      steps:
        - uses: azure/login@v2
          with:
            client-id: ${{ secrets.AZURE_CLIENT_ID }}
            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      
        - run: az webapp deploy --name my-app --src-path dist/
      ```
      
      ## Common Action Recipes
      
      ### Checkout
      
      ```yaml
      # Standard checkout
      - uses: actions/checkout@v4
      
      # Full history (for changelogs, git describe)
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      # Checkout PR head (for pull_request_target)
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      
      # Checkout with submodules
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          token: ${{ secrets.PAT }}         # For private submodules
      ```
      
      ### Setup Node.js
      
      ```yaml
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm                        # Or pnpm, yarn
          registry-url: https://npm.pkg.github.com
      ```
      
      ### Setup Go
      
      ```yaml
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod           # Read from go.mod
          cache: true                       # Cache go modules
      ```
      
      ### Setup Python
      
      ```yaml
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip                        # Or pipenv, poetry
      ```
      
      ### Docker Build and Push
      
      ```yaml
      - uses: docker/setup-buildx-action@v3
      
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
      
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64
      ```
      
      ## Debugging Workflows
      
      ### Enable Debug Logging
      
      Set repository secret `ACTIONS_STEP_DEBUG` to `true` for verbose step output.
      
      Or re-run a failed job with "Enable debug logging" checkbox.
      
      ### Debug Expressions
      
      ```yaml
      - run: |
          echo "Event: ${{ github.event_name }}"
          echo "Ref: ${{ github.ref }}"
          echo "SHA: ${{ github.sha }}"
          echo "Actor: ${{ github.actor }}"
          echo "Matrix: ${{ toJson(matrix) }}"
          echo "Env: ${{ toJson(env) }}"
      
      # Dump full event payload
      - run: cat "$GITHUB_EVENT_PATH" | jq .
      ```
      
      ### Local Testing with `act`
      
      ```bash
      # Install act (https://github.com/nektos/act)
      brew install act                      # macOS
      choco install act-cli                 # Windows
      
      # Run default event (push)
      act
      
      # Run specific workflow
      act -W .github/workflows/ci.yml
      
      # Run specific job
      act -j test
      
      # Run with specific event
      act pull_request
      
      # Pass secrets
      act -s GITHUB_TOKEN="$(gh auth token)"
      
      # Use specific runner image
      act -P ubuntu-latest=catthehacker/ubuntu:act-latest
      
      # Dry run (show what would run)
      act -n
      ```
      
      ### Common Debugging Patterns
      
      ```yaml
      # Temporarily add to any step
      - run: |
          echo "::group::Debug Info"
          env | sort
          echo "::endgroup::"
      
      # Check file existence
      - run: |
          echo "::group::Workspace Contents"
          find . -maxdepth 3 -type f | head -50
          echo "::endgroup::"
      
      # Conditional debug step
      - if: runner.debug == '1'
        run: |
          echo "Debug mode enabled"
          cat package.json | jq '.scripts'
      ```
      
      ### Workflow Run Annotations
      
      ```yaml
      # Warning annotation
      - run: echo "::warning file=app.js,line=1::Missing error handling"
      
      # Error annotation
      - run: echo "::error file=app.js,line=10,col=5::Syntax error"
      
      # Notice annotation
      - run: echo "::notice::Deployment complete"
      
      # Group log lines
      - run: |
          echo "::group::Install Dependencies"
          npm ci
          echo "::endgroup::"
      ```
      
    • release-automation.md 14 KB
      # Release Automation Reference
      
      ## Table of Contents
      
      - [Semantic Versioning](#semantic-versioning)
      - [Conventional Commits](#conventional-commits)
      - [Tool Comparison](#tool-comparison)
      - [semantic-release](#semantic-release)
      - [changesets](#changesets)
      - [release-please](#release-please)
      - [goreleaser](#goreleaser)
      - [Changelog Generation](#changelog-generation)
      - [GitHub Releases](#github-releases)
      - [NPM Publishing](#npm-publishing)
      - [Docker Image Tagging](#docker-image-tagging)
      - [Monorepo Release Strategies](#monorepo-release-strategies)
      
      ---
      
      ## Semantic Versioning
      
      Format: `MAJOR.MINOR.PATCH` (e.g., `2.4.1`)
      
      | Increment | When | Example |
      |-----------|------|---------|
      | MAJOR | Breaking API changes | `1.9.0` -> `2.0.0` |
      | MINOR | New features (backward compatible) | `2.0.0` -> `2.1.0` |
      | PATCH | Bug fixes (backward compatible) | `2.1.0` -> `2.1.1` |
      
      Pre-release versions: `2.0.0-alpha.1`, `2.0.0-beta.3`, `2.0.0-rc.1`
      
      Build metadata: `2.0.0+build.123` (ignored in version precedence)
      
      ## Conventional Commits
      
      Format: `<type>(<scope>): <description>`
      
      | Type | Version Bump | Example |
      |------|-------------|---------|
      | `fix` | PATCH | `fix(auth): handle expired tokens` |
      | `feat` | MINOR | `feat(api): add user search endpoint` |
      | `feat` + `BREAKING CHANGE:` | MAJOR | `feat(api)!: change response format` |
      | `docs`, `chore`, `ci`, `style`, `refactor`, `test`, `perf` | None | `docs: update API reference` |
      
      Breaking changes can be indicated two ways:
      
      ```
      feat(api)!: remove legacy endpoint
      
      BREAKING CHANGE: The /v1/users endpoint has been removed. Use /v2/users instead.
      ```
      
      ## Tool Comparison
      
      | Feature | semantic-release | changesets | release-please | goreleaser |
      |---------|-----------------|------------|----------------|------------|
      | Language | Any (Node-based) | Any (Node-based) | Any | Go projects |
      | Versioning | Automatic from commits | Manual (developer intent) | Automatic from commits | From git tags |
      | Changelog | Auto-generated | Manual + auto | Auto-generated | Auto-generated |
      | Monorepo | Via plugins | Native | Native | N/A |
      | CI integration | Deep | Moderate | GitHub-native | Deep |
      | NPM publish | Built-in | Built-in | Via workflow | N/A |
      | GitHub Release | Built-in | Via script | Built-in | Built-in |
      | Human review | No (fully auto) | Yes (PR-based) | Yes (PR-based) | No |
      | Best for | Full automation | Monorepos, team review | Google-style, simple setup | Go binaries |
      
      ## semantic-release
      
      Fully automated versioning and publishing based on commit messages.
      
      ### Configuration
      
      ```json
      // .releaserc.json
      {
        "branches": [
          "main",
          { "name": "next", "prerelease": true },
          { "name": "beta", "prerelease": true }
        ],
        "plugins": [
          "@semantic-release/commit-analyzer",
          "@semantic-release/release-notes-generator",
          "@semantic-release/changelog",
          ["@semantic-release/npm", {
            "npmPublish": true
          }],
          ["@semantic-release/github", {
            "assets": ["dist/*.tar.gz"]
          }],
          ["@semantic-release/git", {
            "assets": ["CHANGELOG.md", "package.json"],
            "message": "chore(release): ${nextRelease.version} [skip ci]"
          }]
        ]
      }
      ```
      
      ### GitHub Actions Workflow
      
      ```yaml
      # .github/workflows/release.yml
      name: Release
      on:
        push:
          branches: [main, next, beta]
      
      permissions:
        contents: write
        issues: write
        pull-requests: write
        packages: write
      
      jobs:
        release:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
              with:
                fetch-depth: 0
                persist-credentials: false
      
            - uses: actions/setup-node@v4
              with:
                node-version: 20
                cache: npm
      
            - run: npm ci
      
            - run: npx semantic-release
              env:
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      ```
      
      ### Custom Commit Analyzer Rules
      
      ```json
      // .releaserc.json
      {
        "plugins": [
          ["@semantic-release/commit-analyzer", {
            "preset": "conventionalcommits",
            "releaseRules": [
              { "type": "perf", "release": "patch" },
              { "type": "refactor", "release": "patch" },
              { "type": "docs", "scope": "api", "release": "patch" }
            ]
          }]
        ]
      }
      ```
      
      ## changesets
      
      Developer-driven versioning with PR-based workflow. Ideal for monorepos.
      
      ### Setup
      
      ```bash
      npx @changesets/cli init
      # Creates .changeset/ directory with config.json
      ```
      
      ### Configuration
      
      ```json
      // .changeset/config.json
      {
        "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
        "changelog": "@changesets/cli/changelog",
        "commit": false,
        "fixed": [],
        "linked": [["@myorg/core", "@myorg/utils"]],
        "access": "public",
        "baseBranch": "main",
        "updateInternalDependencies": "patch",
        "ignore": ["@myorg/docs", "@myorg/dev-tools"]
      }
      ```
      
      ### Developer Workflow
      
      ```bash
      # 1. Create a changeset (interactive)
      npx changeset
      
      # 2. This creates a file like .changeset/brave-dogs-dance.md:
      # ---
      # "@myorg/core": minor
      # "@myorg/utils": patch
      # ---
      #
      # Add search functionality to core package
      
      # 3. Commit the changeset with your PR
      git add .changeset/brave-dogs-dance.md
      git commit -m "feat: add search functionality"
      ```
      
      ### GitHub Actions Workflow
      
      ```yaml
      # .github/workflows/release.yml
      name: Release
      on:
        push:
          branches: [main]
      
      permissions:
        contents: write
        pull-requests: write
        packages: write
      
      jobs:
        release:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
      
            - uses: actions/setup-node@v4
              with:
                node-version: 20
                cache: npm
      
            - run: npm ci
      
            - name: Create Release PR or Publish
              uses: changesets/action@v1
              with:
                publish: npx changeset publish
                version: npx changeset version
                title: 'chore: version packages'
                commit: 'chore: version packages'
              env:
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      ```
      
      ## release-please
      
      Google's release automation. Creates release PRs automatically from conventional commits.
      
      ### GitHub Actions Workflow
      
      ```yaml
      # .github/workflows/release.yml
      name: Release
      on:
        push:
          branches: [main]
      
      permissions:
        contents: write
        pull-requests: write
      
      jobs:
        release:
          runs-on: ubuntu-latest
          outputs:
            release_created: ${{ steps.release.outputs.release_created }}
            tag_name: ${{ steps.release.outputs.tag_name }}
          steps:
            - uses: googleapis/release-please-action@v4
              id: release
              with:
                release-type: node           # or python, go, simple, etc.
      
            # Steps that only run on release
            - uses: actions/checkout@v4
              if: ${{ steps.release.outputs.release_created }}
      
            - uses: actions/setup-node@v4
              if: ${{ steps.release.outputs.release_created }}
              with:
                node-version: 20
                registry-url: https://registry.npmjs.org
      
            - run: npm ci && npm publish
              if: ${{ steps.release.outputs.release_created }}
              env:
                NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      ```
      
      ### Configuration
      
      ```json
      // release-please-config.json
      {
        "packages": {
          ".": {
            "release-type": "node",
            "changelog-path": "CHANGELOG.md",
            "bump-minor-pre-major": true,
            "bump-patch-for-minor-pre-major": true
          }
        }
      }
      ```
      
      ## goreleaser
      
      Release automation for Go projects: cross-compilation, archives, Docker images, and more.
      
      ### Configuration
      
      ```yaml
      # .goreleaser.yml
      version: 2
      
      before:
        hooks:
          - go mod tidy
          - go generate ./...
      
      builds:
        - id: myapp
          main: ./cmd/myapp
          binary: myapp
          env:
            - CGO_ENABLED=0
          goos: [linux, darwin, windows]
          goarch: [amd64, arm64]
          ldflags:
            - -s -w
            - -X main.version={{.Version}}
            - -X main.commit={{.Commit}}
            - -X main.date={{.Date}}
      
      archives:
        - id: default
          format: tar.gz
          format_overrides:
            - goos: windows
              format: zip
          name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
      
      dockers:
        - image_templates:
            - "ghcr.io/owner/myapp:{{ .Version }}"
            - "ghcr.io/owner/myapp:latest"
          dockerfile: Dockerfile
          build_flag_templates:
            - "--build-arg=VERSION={{.Version}}"
      
      checksum:
        name_template: 'checksums.txt'
      
      changelog:
        sort: asc
        filters:
          exclude:
            - '^docs:'
            - '^chore:'
            - '^ci:'
      
      release:
        github:
          owner: myorg
          name: myapp
        draft: false
        prerelease: auto
      ```
      
      ### GitHub Actions Workflow
      
      ```yaml
      # .github/workflows/release.yml
      name: Release
      on:
        push:
          tags: ['v*']
      
      permissions:
        contents: write
        packages: write
      
      jobs:
        release:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
              with:
                fetch-depth: 0
      
            - uses: actions/setup-go@v5
              with:
                go-version-file: go.mod
      
            - uses: docker/login-action@v3
              with:
                registry: ghcr.io
                username: ${{ github.actor }}
                password: ${{ secrets.GITHUB_TOKEN }}
      
            - uses: goreleaser/goreleaser-action@v6
              with:
                version: '~> v2'
                args: release --clean
              env:
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      ```
      
      ### Local Testing
      
      ```bash
      # Dry run (no publish)
      goreleaser release --snapshot --clean
      
      # Check config
      goreleaser check
      
      # Build only (no release)
      goreleaser build --snapshot --clean
      ```
      
      ## Changelog Generation
      
      ### Standalone Changelog Tools
      
      ```bash
      # conventional-changelog-cli
      npx conventional-changelog -p conventionalcommits -i CHANGELOG.md -s
      
      # git-cliff (Rust, fast)
      git cliff -o CHANGELOG.md
      git cliff --latest                    # Only latest release
      git cliff --unreleased                # Only unreleased changes
      ```
      
      ### git-cliff Configuration
      
      ```toml
      # cliff.toml
      [changelog]
      header = "# Changelog\n\n"
      body = """
      {% for group, commits in commits | group_by(attribute="group") %}
      ### {{ group | upper_first }}
      {% for commit in commits %}
      - {{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})\
      {% endfor %}
      {% endfor %}
      """
      
      [git]
      conventional_commits = true
      filter_unconventional = true
      commit_parsers = [
        { message = "^feat", group = "Features" },
        { message = "^fix", group = "Bug Fixes" },
        { message = "^perf", group = "Performance" },
        { message = "^refactor", group = "Refactoring" },
      ]
      ```
      
      ## GitHub Releases
      
      ### Creating Releases with `gh`
      
      ```bash
      # Auto-generate notes from commits
      gh release create v1.2.0 --generate-notes
      
      # With title and custom notes
      gh release create v1.2.0 \
        --title "v1.2.0" \
        --notes "## What's New
      - Feature A
      - Bug fix B"
      
      # Upload assets
      gh release create v1.2.0 dist/*.tar.gz checksums.txt
      
      # Create draft release
      gh release create v1.2.0 --draft
      
      # Create pre-release
      gh release create v2.0.0-beta.1 --prerelease
      
      # Edit existing release
      gh release edit v1.2.0 --draft=false
      ```
      
      ### GitHub Actions Release
      
      ```yaml
      - run: |
          gh release create "$TAG" \
            --title "$TAG" \
            --generate-notes \
            dist/*
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ github.ref_name }}
      ```
      
      ## NPM Publishing
      
      ### Complete NPM Release Workflow
      
      ```yaml
      name: Publish to NPM
      on:
        push:
          tags: ['v*']
      
      permissions:
        contents: write
        id-token: write                     # For npm provenance
      
      jobs:
        publish:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
      
            - uses: actions/setup-node@v4
              with:
                node-version: 20
                registry-url: https://registry.npmjs.org
      
            - run: npm ci
            - run: npm test
            - run: npm publish --provenance --access public
              env:
                NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      ```
      
      ### Publishing to GitHub Packages
      
      ```yaml
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://npm.pkg.github.com
          scope: '@myorg'
      
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      ```
      
      ## Docker Image Tagging
      
      ### Tagging Strategy
      
      | Tag | Source | Example | Purpose |
      |-----|--------|---------|---------|
      | `latest` | Main branch | `myapp:latest` | Most recent stable |
      | `x.y.z` | Git tag | `myapp:1.2.3` | Immutable release |
      | `x.y` | Git tag | `myapp:1.2` | Latest patch |
      | `x` | Git tag | `myapp:1` | Latest minor |
      | `sha-abc1234` | Commit SHA | `myapp:sha-abc1234` | Exact build |
      | `pr-42` | PR number | `myapp:pr-42` | PR preview |
      | `edge` | Main branch | `myapp:edge` | Bleeding edge |
      
      ### docker/metadata-action
      
      ```yaml
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: |
            ghcr.io/${{ github.repository }}
            docker.io/myorg/myapp
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha,prefix=
            type=ref,event=branch
            type=ref,event=pr
            type=raw,value=latest,enable={{is_default_branch}}
      ```
      
      ## Monorepo Release Strategies
      
      ### Independent Versioning (changesets)
      
      Each package has its own version. Best for library monorepos.
      
      ```json
      // .changeset/config.json
      {
        "fixed": [],
        "linked": [["@myorg/client-*"]],   # These move together
        "access": "public"
      }
      ```
      
      ### Fixed Versioning (release-please)
      
      All packages share one version. Best for application monorepos.
      
      ```json
      // release-please-config.json
      {
        "packages": {
          "packages/core": { "release-type": "node" },
          "packages/cli": { "release-type": "node" },
          "packages/web": { "release-type": "node" }
        },
        "group-pull-requests-pattern": "chore: release main"
      }
      ```
      
      ### Path-Filtered Releases
      
      ```yaml
      on:
        push:
          branches: [main]
          paths:
            - 'packages/api/**'
      
      jobs:
        release-api:
          runs-on: ubuntu-latest
          defaults:
            run:
              working-directory: packages/api
          steps:
            - uses: actions/checkout@v4
            - run: npm ci
            - run: npm publish
      ```
      
      ### Turborepo + changesets
      
      ```yaml
      jobs:
        release:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npx turbo run build --filter='...[origin/main]'
            - uses: changesets/action@v1
              with:
                publish: npx changeset publish
              env:
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      ```
      
    • testing-pipelines.md 23.3 KB
      # Testing Pipelines Reference
      
      ## Table of Contents
      
      - [Test Stages](#test-stages)
      - [Parallel Test Execution](#parallel-test-execution)
      - [Test Splitting Strategies](#test-splitting-strategies)
      - [Code Coverage](#code-coverage)
      - [Database Testing in CI](#database-testing-in-ci)
      - [Docker in CI](#docker-in-ci)
      - [E2E Testing in CI](#e2e-testing-in-ci)
      - [Flaky Test Detection and Retry](#flaky-test-detection-and-retry)
      - [Performance Testing in CI](#performance-testing-in-ci)
      - [Status Checks and Branch Protection](#status-checks-and-branch-protection)
      - [Pull Request Checks Workflow](#pull-request-checks-workflow)
      - [Deployment Pipelines](#deployment-pipelines)
      
      ---
      
      ## Test Stages
      
      A typical CI pipeline progresses through these stages, failing fast on cheap checks:
      
      ```
      ┌─────────┐   ┌──────────┐   ┌─────────────┐   ┌──────────┐   ┌────────┐
      │  Lint    │──>│  Unit    │──>│ Integration │──>│  E2E     │──>│ Deploy │
      │  ~1 min  │   │  ~2 min  │   │  ~5 min     │   │  ~10 min │   │        │
      └─────────┘   └──────────┘   └─────────────┘   └──────────┘   └────────┘
      ```
      
      ### Stage Characteristics
      
      | Stage | Speed | Dependencies | Flakiness | What It Catches |
      |-------|-------|-------------|-----------|----------------|
      | Lint / Format | Fastest | None | None | Style, syntax, type errors |
      | Unit tests | Fast | None (mocked) | Low | Logic bugs, regressions |
      | Integration | Medium | Services (DB, cache) | Medium | API contracts, data flow |
      | E2E | Slow | Full environment | High | User-facing regressions |
      | Performance | Slow | Full environment | Medium | Performance regressions |
      
      ### Staged Workflow
      
      ```yaml
      jobs:
        lint:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm run lint
            - run: npm run typecheck
      
        unit:
          needs: lint
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm run test:unit -- --coverage
      
        integration:
          needs: lint
          runs-on: ubuntu-latest
          services:
            postgres:
              image: postgres:16
              env:
                POSTGRES_DB: test
                POSTGRES_USER: test
                POSTGRES_PASSWORD: test
              ports: ['5432:5432']
              options: >-
                --health-cmd pg_isready
                --health-interval 10s
                --health-timeout 5s
                --health-retries 5
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm run test:integration
              env:
                DATABASE_URL: postgresql://test:test@localhost:5432/test
      
        e2e:
          needs: [unit, integration]
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npx playwright install --with-deps chromium
            - run: npm run test:e2e
            - uses: actions/upload-artifact@v4
              if: failure()
              with:
                name: playwright-report
                path: playwright-report/
                retention-days: 7
      ```
      
      ## Parallel Test Execution
      
      ### Matrix-Based Parallelism
      
      ```yaml
      jobs:
        test:
          runs-on: ubuntu-latest
          strategy:
            fail-fast: false
            matrix:
              shard: [1, 2, 3, 4]
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm test -- --shard=${{ matrix.shard }}/${{ strategy.job-total }}
      ```
      
      ### Playwright Sharding
      
      ```yaml
      strategy:
        matrix:
          shard: [1/4, 2/4, 3/4, 4/4]
      steps:
        - run: npx playwright test --shard=${{ matrix.shard }}
      
        - uses: actions/upload-artifact@v4
          if: always()
          with:
            name: blob-report-${{ strategy.job-index }}
            path: blob-report/
      
      # Merge reports in a separate job
      merge-reports:
        needs: test
        runs-on: ubuntu-latest
        steps:
          - uses: actions/download-artifact@v4
            with:
              pattern: blob-report-*
              merge-multiple: true
              path: all-blob-reports
      
          - run: npx playwright merge-reports --reporter html all-blob-reports
      ```
      
      ### Jest Parallelism
      
      ```yaml
      # Jest auto-parallelizes across workers
      - run: npx jest --maxWorkers=50%      # Use half available CPUs
      - run: npx jest --maxWorkers=4        # Or specify exactly
      
      # With sharding (Jest 28+)
      - run: npx jest --shard=${{ matrix.shard }}/${{ strategy.job-total }}
      ```
      
      ## Test Splitting Strategies
      
      ### By File Count (Simple)
      
      ```bash
      # Split test files evenly across shards
      files=$(find src -name '*.test.ts' | sort)
      total=$(echo "$files" | wc -l)
      per_shard=$(( (total + SHARD_COUNT - 1) / SHARD_COUNT ))
      echo "$files" | sed -n "${start},${end}p"
      ```
      
      ### By Timing (Optimal)
      
      ```yaml
      # Use test timing data from previous runs
      - uses: actions/cache@v4
        with:
          path: .test-timings
          key: test-timings-${{ github.ref }}
          restore-keys: test-timings-
      
      - run: |
          npx jest --json --outputFile=results.json
          # Store timing data for next run
          jq '[.testResults[] | {file: .testFilePath, duration: .perfStats.runtime}]' \
            results.json > .test-timings
      ```
      
      ### By Test Type
      
      ```yaml
      strategy:
        matrix:
          include:
            - name: unit
              command: npm run test:unit
              timeout: 10
            - name: integration
              command: npm run test:integration
              timeout: 20
            - name: e2e
              command: npm run test:e2e
              timeout: 30
      
      jobs:
        test:
          timeout-minutes: ${{ matrix.timeout }}
          steps:
            - run: ${{ matrix.command }}
      ```
      
      ## Code Coverage
      
      ### Codecov
      
      ```yaml
      - run: npm test -- --coverage
      
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: coverage/lcov.info
          flags: unittests
          fail_ci_if_error: true
      ```
      
      ### Coveralls
      
      ```yaml
      - run: npm test -- --coverage
      
      - uses: coverallsapp/github-action@v2
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          path-to-lcov: coverage/lcov.info
      ```
      
      ### Coverage Gates
      
      ```yaml
      # Fail if coverage drops
      - run: |
          COVERAGE=$(jq '.total.lines.pct' coverage/coverage-summary.json)
          echo "Coverage: ${COVERAGE}%"
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "::error::Coverage ${COVERAGE}% is below 80% threshold"
            exit 1
          fi
      ```
      
      ### Multi-Platform Coverage Merge
      
      ```yaml
      # Upload per-shard coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.shard }}
          path: coverage/
      
      # Merge in separate job
      merge-coverage:
        needs: test
        runs-on: ubuntu-latest
        steps:
          - uses: actions/download-artifact@v4
            with:
              pattern: coverage-*
              merge-multiple: true
              path: all-coverage
      
          - run: npx nyc merge all-coverage merged-coverage.json
          - run: npx nyc report --reporter=lcov --temp-dir=.
      
          - uses: codecov/codecov-action@v4
            with:
              token: ${{ secrets.CODECOV_TOKEN }}
      ```
      
      ## Database Testing in CI
      
      ### Service Containers
      
      ```yaml
      services:
        postgres:
          image: postgres:16-alpine
          env:
            POSTGRES_DB: test_db
            POSTGRES_USER: test_user
            POSTGRES_PASSWORD: test_pass
          ports: ['5432:5432']
          options: >-
            --health-cmd pg_isready
            --health-interval 10s
            --health-timeout 5s
            --health-retries 5
      
        redis:
          image: redis:7-alpine
          ports: ['6379:6379']
          options: >-
            --health-cmd "redis-cli ping"
            --health-interval 10s
            --health-timeout 5s
            --health-retries 5
      
        mysql:
          image: mysql:8
          env:
            MYSQL_ROOT_PASSWORD: root
            MYSQL_DATABASE: test_db
          ports: ['3306:3306']
          options: >-
            --health-cmd "mysqladmin ping -h localhost"
            --health-interval 10s
            --health-timeout 5s
            --health-retries 5
      ```
      
      ### Testcontainers
      
      ```yaml
      # Testcontainers manages its own containers - just needs Docker
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-java@v4
          with: { java-version: 21, distribution: temurin }
      
        # Testcontainers needs Docker socket access (default on ubuntu-latest)
        - run: ./gradlew test
          env:
            TESTCONTAINERS_RYUK_DISABLED: false
      ```
      
      ### Database Migrations in CI
      
      ```yaml
      steps:
        - run: npm run db:migrate
          env:
            DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
      
        - run: npm run db:seed           # Optional test data
      
        - run: npm run test:integration
          env:
            DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
      ```
      
      ## Docker in CI
      
      ### Docker-in-Docker (DinD)
      
      ```yaml
      # Not recommended for GitHub Actions - use standard Docker
      # GitHub-hosted runners have Docker pre-installed
      
      steps:
        - uses: actions/checkout@v4
        - run: docker build -t myapp .
        - run: docker run myapp npm test
      ```
      
      ### Docker-outside-of-Docker (DooD)
      
      ```yaml
      # Mount the host Docker socket (for self-hosted runners)
      # GitHub-hosted runners use this by default
      steps:
        - run: docker compose up -d
        - run: docker compose run app npm test
        - run: docker compose down
      ```
      
      ### Docker Compose in CI
      
      ```yaml
      steps:
        - uses: actions/checkout@v4
      
        - run: docker compose -f docker-compose.test.yml up -d --wait
        - run: docker compose -f docker-compose.test.yml run app npm test
        - run: docker compose -f docker-compose.test.yml down -v
      
        # Alternative: use --exit-code-from
        - run: docker compose -f docker-compose.test.yml up --exit-code-from test
      ```
      
      ## E2E Testing in CI
      
      ### Playwright
      
      ```yaml
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: 20, cache: npm }
        - run: npm ci
      
        # Install browsers (cache for speed)
        - name: Cache Playwright browsers
          uses: actions/cache@v4
          id: playwright-cache
          with:
            path: ~/.cache/ms-playwright
            key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
      
        - if: steps.playwright-cache.outputs.cache-hit != 'true'
          run: npx playwright install --with-deps chromium
      
        - if: steps.playwright-cache.outputs.cache-hit == 'true'
          run: npx playwright install-deps chromium
      
        # Run tests
        - run: npx playwright test
          env:
            CI: true
      
        # Upload artifacts on failure
        - uses: actions/upload-artifact@v4
          if: failure()
          with:
            name: playwright-report
            path: |
              playwright-report/
              test-results/
            retention-days: 7
      ```
      
      ### Cypress
      
      ```yaml
      steps:
        - uses: actions/checkout@v4
      
        - uses: cypress-io/github-action@v6
          with:
            build: npm run build
            start: npm start
            wait-on: 'http://localhost:3000'
            wait-on-timeout: 120
            browser: chrome
            record: true                    # Cypress Cloud recording
          env:
            CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
      
        - uses: actions/upload-artifact@v4
          if: failure()
          with:
            name: cypress-screenshots
            path: cypress/screenshots/
      ```
      
      ### E2E with Containerized App
      
      ```yaml
      steps:
        - uses: actions/checkout@v4
      
        # Start the app in Docker
        - run: docker compose up -d --wait
      
        # Run E2E tests against containerized app
        - run: npm ci
        - run: npx playwright test
          env:
            BASE_URL: http://localhost:3000
      
        - run: docker compose down -v
          if: always()
      ```
      
      ## Flaky Test Detection and Retry
      
      ### GitHub Actions Retry
      
      ```yaml
      # Retry the entire job
      - uses: nick-fields/retry@v3
        with:
          timeout_minutes: 10
          max_attempts: 3
          command: npm run test:e2e
          retry_on: error
      ```
      
      ### Built-in Test Runner Retries
      
      ```bash
      # Playwright
      npx playwright test --retries=2
      
      # Jest
      npx jest --bail --forceExit          # Fail fast, clean exit
      
      # Vitest
      npx vitest --retry=2
      
      # pytest
      pip install pytest-rerunfailures
      pytest --reruns 3 --reruns-delay 1
      ```
      
      ### Flaky Test Quarantine Pattern
      
      ```yaml
      jobs:
        stable-tests:
          runs-on: ubuntu-latest
          steps:
            - run: npm test -- --testPathIgnorePatterns='flaky'
      
        flaky-tests:
          runs-on: ubuntu-latest
          continue-on-error: true           # Don't block PR
          steps:
            - run: npm test -- --testPathPattern='flaky' --retries=3
      ```
      
      ### Detect New Flaky Tests
      
      ```yaml
      # Run tests multiple times on PR to detect flakiness
      - run: |
          for i in {1..5}; do
            echo "Run $i of 5"
            npm test -- --bail || exit 1
          done
      ```
      
      ## Performance Testing in CI
      
      ### Benchmark Comparison
      
      ```yaml
      - uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'customBiggerIsBetter'
          output-file-path: benchmark-results.json
          github-token: ${{ secrets.GITHUB_TOKEN }}
          auto-push: true
          alert-threshold: '150%'           # Alert if 50% slower
          comment-on-alert: true
          fail-on-alert: true
      ```
      
      ### Lighthouse CI
      
      ```yaml
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: 20, cache: npm }
        - run: npm ci && npm run build
      
        - name: Start server
          run: npm start &
          env: { PORT: 3000 }
      
        - run: npx @lhci/cli autorun
          env:
            LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
      ```
      
      ### Lighthouse Configuration
      
      ```json
      // lighthouserc.json
      {
        "ci": {
          "collect": {
            "url": ["http://localhost:3000", "http://localhost:3000/about"],
            "numberOfRuns": 3
          },
          "assert": {
            "assertions": {
              "categories:performance": ["error", { "minScore": 0.9 }],
              "categories:accessibility": ["error", { "minScore": 0.95 }],
              "categories:best-practices": ["error", { "minScore": 0.9 }],
              "first-contentful-paint": ["warn", { "maxNumericValue": 2000 }]
            }
          },
          "upload": {
            "target": "temporary-public-storage"
          }
        }
      }
      ```
      
      ### Bundle Size Check
      
      ```yaml
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          # Reads config from .size-limit.json or package.json
      ```
      
      ## Status Checks and Branch Protection
      
      ### Required Status Checks
      
      Configure in **Settings > Branches > Branch protection rules**:
      
      | Setting | Purpose |
      |---------|---------|
      | Require status checks to pass | Block merge until CI passes |
      | Require branches to be up to date | Ensure tests run against latest main |
      | Status checks to require | Select specific job names |
      
      ### Handling Skipped Checks with Path Filters
      
      Problem: Path-filtered workflows skip jobs, blocking required checks.
      
      Solution 1: Paths-filter action with always-running workflow:
      
      ```yaml
      name: CI
      on: [push, pull_request]
      
      jobs:
        changes:
          runs-on: ubuntu-latest
          outputs:
            src: ${{ steps.filter.outputs.src }}
          steps:
            - uses: dorny/paths-filter@v3
              id: filter
              with:
                filters: |
                  src:
                    - 'src/**'
                    - 'package.json'
      
        test:
          needs: changes
          if: needs.changes.outputs.src == 'true'
          runs-on: ubuntu-latest
          steps:
            - run: npm test
      
        # Always passes - use this as the required check
        ci-success:
          needs: [test]
          if: always()
          runs-on: ubuntu-latest
          steps:
            - run: |
                if [[ "${{ needs.test.result }}" == "failure" ]]; then
                  exit 1
                fi
      ```
      
      Solution 2: Make the check non-required and use a merge queue.
      
      ### Merge Queue
      
      ```yaml
      on:
        merge_group:                        # Triggered by merge queue
          types: [checks_requested]
      
      jobs:
        test:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - run: npm ci && npm test
      ```
      
      ## Pull Request Checks Workflow
      
      Complete PR workflow with all common checks:
      
      ```yaml
      name: PR Checks
      on:
        pull_request:
          branches: [main]
      
      concurrency:
        group: pr-${{ github.event.pull_request.number }}
        cancel-in-progress: true
      
      permissions:
        contents: read
        pull-requests: write
        checks: write
      
      jobs:
        # ── Fast Checks ────────────────────────────────────────
        lint:
          name: Lint & Format
          runs-on: ubuntu-latest
          timeout-minutes: 5
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm run lint
            - run: npm run typecheck
            - run: npm run format:check
      
        # ── Unit Tests ─────────────────────────────────────────
        unit:
          name: Unit Tests
          needs: lint
          runs-on: ubuntu-latest
          timeout-minutes: 10
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm run test:unit -- --coverage
      
            - uses: codecov/codecov-action@v4
              with:
                token: ${{ secrets.CODECOV_TOKEN }}
                flags: unittests
      
        # ── Integration Tests ─────────────────────────────────
        integration:
          name: Integration Tests
          needs: lint
          runs-on: ubuntu-latest
          timeout-minutes: 15
          services:
            postgres:
              image: postgres:16-alpine
              env:
                POSTGRES_DB: test
                POSTGRES_USER: test
                POSTGRES_PASSWORD: test
              ports: ['5432:5432']
              options: >-
                --health-cmd pg_isready
                --health-interval 10s
                --health-timeout 5s
                --health-retries 5
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm run db:migrate
              env:
                DATABASE_URL: postgresql://test:test@localhost:5432/test
            - run: npm run test:integration
              env:
                DATABASE_URL: postgresql://test:test@localhost:5432/test
      
        # ── E2E Tests ─────────────────────────────────────────
        e2e:
          name: E2E Tests (${{ matrix.shard }})
          needs: [unit, integration]
          runs-on: ubuntu-latest
          timeout-minutes: 20
          strategy:
            fail-fast: false
            matrix:
              shard: [1/3, 2/3, 3/3]
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
      
            - name: Cache Playwright
              uses: actions/cache@v4
              with:
                path: ~/.cache/ms-playwright
                key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
      
            - run: npx playwright install --with-deps chromium
            - run: npx playwright test --shard=${{ matrix.shard }}
      
            - uses: actions/upload-artifact@v4
              if: failure()
              with:
                name: playwright-report-${{ strategy.job-index }}
                path: playwright-report/
                retention-days: 7
      
        # ── Build Check ────────────────────────────────────────
        build:
          name: Build
          needs: lint
          runs-on: ubuntu-latest
          timeout-minutes: 10
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npm run build
            - uses: actions/upload-artifact@v4
              with:
                name: build
                path: dist/
                retention-days: 1
      
        # ── Gate Check (required status check) ─────────────────
        ci-success:
          name: CI Success
          needs: [lint, unit, integration, e2e, build]
          if: always()
          runs-on: ubuntu-latest
          steps:
            - run: |
                results=("${{ needs.lint.result }}" "${{ needs.unit.result }}" \
                         "${{ needs.integration.result }}" "${{ needs.e2e.result }}" \
                         "${{ needs.build.result }}")
                for result in "${results[@]}"; do
                  if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
                    echo "::error::Job failed with result: $result"
                    exit 1
                  fi
                done
                echo "All checks passed"
      ```
      
      ## Deployment Pipelines
      
      ### Staging to Production
      
      ```yaml
      name: Deploy
      on:
        push:
          branches: [main]
      
      jobs:
        build:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - run: npm ci && npm run build
            - uses: actions/upload-artifact@v4
              with:
                name: build
                path: dist/
      
        deploy-staging:
          needs: build
          runs-on: ubuntu-latest
          environment:
            name: staging
            url: https://staging.example.com
          steps:
            - uses: actions/download-artifact@v4
              with: { name: build, path: dist/ }
            - run: ./deploy.sh staging
              env:
                DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
      
        smoke-test:
          needs: deploy-staging
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - run: npm ci
            - run: npx playwright test tests/smoke/
              env:
                BASE_URL: https://staging.example.com
      
        deploy-production:
          needs: smoke-test
          runs-on: ubuntu-latest
          environment:
            name: production                # Manual approval required
            url: https://example.com
          steps:
            - uses: actions/download-artifact@v4
              with: { name: build, path: dist/ }
            - run: ./deploy.sh production
              env:
                DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
      ```
      
      ### Blue/Green Deployment
      
      ```yaml
      deploy:
        runs-on: ubuntu-latest
        environment: production
        steps:
          - run: |
              # Deploy to inactive slot
              ACTIVE=$(curl -s https://example.com/slot)
              INACTIVE=$([[ "$ACTIVE" == "blue" ]] && echo "green" || echo "blue")
      
              # Deploy to inactive
              deploy --slot "$INACTIVE"
      
              # Health check on inactive
              curl -sf "https://${INACTIVE}.example.com/health" || exit 1
      
              # Swap traffic
              swap-slots "$ACTIVE" "$INACTIVE"
      
              echo "Swapped from $ACTIVE to $INACTIVE"
      ```
      
      ### Canary Deployment
      
      ```yaml
      deploy:
        runs-on: ubuntu-latest
        environment: production
        steps:
          - name: Deploy canary (10%)
            run: deploy --canary --weight=10
      
          - name: Monitor canary (5 min)
            run: |
              for i in {1..5}; do
                ERROR_RATE=$(curl -s https://metrics.example.com/error-rate)
                if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
                  echo "::error::Error rate ${ERROR_RATE}% exceeds threshold"
                  deploy --rollback
                  exit 1
                fi
                sleep 60
              done
      
          - name: Promote canary (100%)
            run: deploy --promote
      ```
      
      ### Rollback Pattern
      
      ```yaml
      on:
        workflow_dispatch:
          inputs:
            version:
              description: 'Version to rollback to'
              required: true
              type: string
      
      jobs:
        rollback:
          runs-on: ubuntu-latest
          environment: production
          steps:
            - run: |
                echo "Rolling back to ${{ inputs.version }}"
                deploy --version "${{ inputs.version }}"
      
            - run: |
                curl -sf https://example.com/health || {
                  echo "::error::Rollback health check failed"
                  exit 1
                }
      ```
      
      ### Multi-Region Deployment
      
      ```yaml
      jobs:
        deploy:
          runs-on: ubuntu-latest
          environment: production
          strategy:
            max-parallel: 1                 # Deploy one region at a time
            matrix:
              region: [us-east-1, eu-west-1, ap-southeast-1]
          steps:
            - run: deploy --region ${{ matrix.region }}
      
            - name: Region health check
              run: |
                curl -sf "https://${{ matrix.region }}.example.com/health" || {
                  echo "::error::Health check failed in ${{ matrix.region }}"
                  exit 1
                }
      ```
      
  • scripts
    • .gitkeep 0 B · in bundle
  • SKILL.md 10.2 KB
    ---
    name: ci-cd-ops
    description: "CI/CD pipeline patterns with GitHub Actions, release automation, and testing strategies. Use for: github actions, workflow, CI, CD, pipeline, deploy, release, semantic release, changesets, goreleaser, matrix, cache, secrets, environment, artifact, reusable workflow, composite action."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: git-ops, docker-ops, testing-ops
    ---
    
    # CI/CD Operations
    
    Comprehensive patterns for continuous integration, delivery, and deployment using GitHub Actions, release automation tools, and testing pipelines.
    
    ## GitHub Actions Quick Reference
    
    ### Workflow File Anatomy
    
    ```yaml
    name: CI                          # Display name in Actions tab
    on:                               # Trigger events
      push:
        branches: [main]
      pull_request:
        branches: [main]
    
    permissions:                      # GITHUB_TOKEN scope (least privilege)
      contents: read
      pull-requests: write
    
    concurrency:                      # Prevent duplicate runs
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true
    
    env:                              # Workflow-level environment variables
      NODE_VERSION: "20"
    
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: ${{ env.NODE_VERSION }}
              cache: npm
          - run: npm ci
          - run: npm test
    ```
    
    ### Core Syntax Elements
    
    | Element | Purpose | Example |
    |---------|---------|---------|
    | `on` | Event triggers | `push`, `pull_request`, `schedule` |
    | `jobs.<id>.runs-on` | Runner selection | `ubuntu-latest`, `self-hosted` |
    | `jobs.<id>.needs` | Job dependencies | `needs: [build, lint]` |
    | `jobs.<id>.if` | Conditional execution | `if: github.event_name == 'push'` |
    | `jobs.<id>.strategy.matrix` | Parallel variants | `node-version: [18, 20, 22]` |
    | `jobs.<id>.environment` | Deployment target | `environment: production` |
    | `jobs.<id>.permissions` | Token scope | `contents: write` |
    | `steps[*].uses` | Use an action | `uses: actions/checkout@v4` |
    | `steps[*].run` | Run a command | `run: npm test` |
    | `steps[*].env` | Step environment | `env: { CI: true }` |
    
    ## Trigger Decision Tree
    
    | Scenario | Trigger | Config |
    |----------|---------|--------|
    | Run tests on every PR | `pull_request` | `branches: [main]` |
    | Deploy on merge to main | `push` | `branches: [main]` |
    | Release on version tag | `push` | `tags: ['v*']` |
    | Nightly builds | `schedule` | `cron: '0 2 * * *'` |
    | Manual deployment | `workflow_dispatch` | `inputs: { environment: ... }` |
    | Called by another workflow | `workflow_call` | `inputs:`, `secrets:` |
    | On PR label change | `pull_request` | `types: [labeled]` |
    | On issue comment | `issue_comment` | `types: [created]` |
    | On release published | `release` | `types: [published]` |
    | On package push | `registry_package` | `types: [published]` |
    
    ### Trigger Filter Patterns
    
    ```yaml
    on:
      push:
        branches: [main, 'release/**']      # Branch patterns
        paths: ['src/**', '!src/**/*.test.*'] # Path filters (ignore tests)
        tags: ['v*']                          # Tag patterns
      pull_request:
        types: [opened, synchronize, reopened] # Default types
        paths-ignore: ['docs/**', '*.md']     # Ignore docs-only changes
    ```
    
    ## Caching Strategies
    
    | Ecosystem | Action / Key | Path | Restore Key |
    |-----------|-------------|------|-------------|
    | Node (npm) | `actions/setup-node` with `cache: npm` | Auto | Auto |
    | Node (pnpm) | `actions/setup-node` with `cache: pnpm` | Auto | Auto |
    | Go modules | `actions/setup-go` with `cache: true` | Auto | Auto |
    | Cargo | `actions/cache@v4` | `~/.cargo/registry`, `target` | `cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}` |
    | pip / uv | `actions/setup-python` with `cache: pip` | Auto | Auto |
    | Docker layers | `docker/build-push-action` | Uses buildx cache | `type=gha` or `type=registry` |
    | Gradle | `actions/setup-java` with `cache: gradle` | Auto | Auto |
    | Composer | `actions/cache@v4` | `vendor` | `composer-${{ hashFiles('composer.lock') }}` |
    
    ### Manual Cache Example
    
    ```yaml
    - uses: actions/cache@v4
      with:
        path: |
          ~/.cargo/bin
          ~/.cargo/registry
          ~/.cargo/git
          target
        key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          cargo-${{ runner.os }}-
    ```
    
    ## Matrix Strategy
    
    ```yaml
    strategy:
      fail-fast: false                    # Don't cancel siblings on failure
      max-parallel: 4                     # Limit concurrent jobs
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
        include:                          # Add specific combos
          - os: ubuntu-latest
            node-version: 22
            coverage: true
        exclude:                          # Remove specific combos
          - os: windows-latest
            node-version: 18
    ```
    
    ### Dynamic Matrix
    
    ```yaml
    prepare:
      runs-on: ubuntu-latest
      outputs:
        matrix: ${{ steps.set.outputs.matrix }}
      steps:
        - id: set
          run: echo "matrix=$(jq -c . matrix.json)" >> "$GITHUB_OUTPUT"
    
    test:
      needs: prepare
      strategy:
        matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
    ```
    
    ## Secrets Management
    
    | Scope | Access | Use Case |
    |-------|--------|----------|
    | Repository secrets | All workflows in repo | API keys, tokens |
    | Environment secrets | Jobs targeting that environment | Production credentials |
    | Organization secrets | Selected repos in org | Shared service accounts |
    | OIDC tokens | Federated identity | Cloud deployment (no stored secrets) |
    
    ### Secrets Best Practices
    
    ```yaml
    # Reference secrets - NEVER echo or log them
    - run: deploy --token ${{ secrets.DEPLOY_TOKEN }}
    
    # Mask custom values
    - run: echo "::add-mask::$CUSTOM_SECRET"
    
    # Use environments for deployment secrets
    jobs:
      deploy:
        environment: production           # Requires approval + has secrets
        steps:
          - run: deploy --key ${{ secrets.PROD_API_KEY }}
    ```
    
    ### OIDC for Cloud (No Stored Secrets)
    
    ```yaml
    permissions:
      id-token: write
      contents: read
    
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions
          aws-region: us-east-1
    ```
    
    ## Common Workflow Patterns
    
    ### Test on Pull Request
    
    ```yaml
    name: Test
    on:
      pull_request:
        branches: [main]
    concurrency:
      group: test-${{ github.head_ref }}
      cancel-in-progress: true
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with: { node-version: 20, cache: npm }
          - run: npm ci
          - run: npm run lint
          - run: npm test -- --coverage
    ```
    
    ### Deploy on Merge to Main
    
    ```yaml
    name: Deploy
    on:
      push:
        branches: [main]
    jobs:
      deploy:
        runs-on: ubuntu-latest
        environment: production
        steps:
          - uses: actions/checkout@v4
          - run: npm ci && npm run build
          - run: npx wrangler deploy
            env:
              CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
    ```
    
    ### Release on Tag
    
    ```yaml
    name: Release
    on:
      push:
        tags: ['v*']
    permissions:
      contents: write
    jobs:
      release:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with: { fetch-depth: 0 }
          - run: |
              gh release create ${{ github.ref_name }} \
                --generate-notes \
                --title "${{ github.ref_name }}"
            env:
              GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    ```
    
    ## Gotchas Table
    
    | Gotcha | Problem | Fix |
    |--------|---------|-----|
    | Shallow clone | `git describe` fails, history missing | `actions/checkout@v4` with `fetch-depth: 0` |
    | Default permissions | `GITHUB_TOKEN` is read-only by default | Set `permissions:` explicitly |
    | Action pinning | `@main` can break without warning | Pin to SHA: `@abc123` or `@v4` |
    | Fork PR secrets | Secrets unavailable on fork PRs | Use `pull_request_target` carefully |
    | Concurrent deploys | Race condition on production | Use `concurrency:` groups |
    | Stale caches | Cache grows unbounded | Include lockfile hash in key |
    | Node.js version | `setup-node` defaults vary | Always specify `node-version` |
    | Docker layer cache | Rebuilds everything without cache | Use `cache-from: type=gha` |
    | Matrix + environment | Each matrix job needs approval | Use a single deploy job after matrix |
    | Path filters + required checks | Skipped jobs block merge | Use `paths-filter` action or make checks non-required |
    | `GITHUB_TOKEN` in PRs | Cannot trigger other workflows | Use a PAT or GitHub App token |
    | Windows line endings | Scripts fail with `\r\n` | Use `.gitattributes` or `core.autocrlf` |
    
    ## Expression Syntax Quick Reference
    
    | Expression | Result |
    |------------|--------|
    | `${{ github.event_name }}` | `push`, `pull_request`, etc. |
    | `${{ github.ref_name }}` | Branch or tag name |
    | `${{ github.sha }}` | Full commit SHA |
    | `${{ github.actor }}` | User who triggered |
    | `${{ runner.os }}` | `Linux`, `Windows`, `macOS` |
    | `${{ contains(github.event.head_commit.message, '[skip ci]') }}` | Check commit message |
    | `${{ needs.build.outputs.version }}` | Output from prior job |
    | `${{ fromJson(steps.meta.outputs.json) }}` | Parse JSON output |
    | `${{ hashFiles('**/package-lock.json') }}` | Hash for cache keys |
    | `${{ format('refs/heads/{0}', matrix.branch) }}` | String formatting |
    | `${{ toJson(matrix) }}` | Debug: print matrix config |
    
    ## Step Outputs
    
    ```yaml
    steps:
      - id: version
        run: echo "value=$(cat VERSION)" >> "$GITHUB_OUTPUT"
    
      - run: echo "Version is ${{ steps.version.outputs.value }}"
    ```
    
    ### Job Outputs (for Cross-Job Communication)
    
    ```yaml
    jobs:
      build:
        runs-on: ubuntu-latest
        outputs:
          artifact-id: ${{ steps.upload.outputs.artifact-id }}
        steps:
          - id: upload
            run: echo "artifact-id=abc123" >> "$GITHUB_OUTPUT"
    
      deploy:
        needs: build
        runs-on: ubuntu-latest
        steps:
          - run: echo "Deploying ${{ needs.build.outputs.artifact-id }}"
    ```
    
    ## Reference Files
    
    | File | Contents |
    |------|----------|
    | `references/github-actions.md` | Complete workflow syntax, reusable workflows, composite actions, OIDC, runners, debugging |
    | `references/release-automation.md` | Semantic versioning, semantic-release, changesets, goreleaser, changelog, publishing |
    | `references/testing-pipelines.md` | Test stages, parallelism, coverage, service containers, e2e in CI, deployment pipelines |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related