GitHub Actions’ YAML syntax is straightforward for basic pipelines but has subtleties that waste hours: expression syntax, concurrency groups, matrix strategies, reusable workflows, and the correct way to pass secrets between jobs. Claude Code generates GitHub Actions workflows with working syntax and patterns that avoid the common gotchas.
This guide covers advanced GitHub Actions with Claude Code: matrix builds, reusable workflows, environment promotion, secrets management, and cost optimization.
Advanced Matrix Builds
Run tests against Node.js 18, 20, and 22. Also test against
PostgreSQL 14 and 15 as a matrix. Only run the matrix on main branch PRs.
# .github/workflows/test-matrix.yml
name: Test Matrix
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false # Don't cancel other matrix jobs when one fails
matrix:
node-version: ['18', '20', '22']
postgres-version: ['14', '15']
# Exclude combinations that would be redundant
exclude:
- node-version: '18'
postgres-version: '14'
services:
postgres:
image: postgres:${{ matrix.postgres-version }}
env:
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- name: Run tests
run: npm test
env:
DATABASE_URL: postgres://postgres:testpassword@localhost:5432/test
NODE_VERSION: ${{ matrix.node-version }}
PG_VERSION: ${{ matrix.postgres-version }}
Reusable Workflows
We have 8 repositories that all need the same deploy workflow.
Create a reusable workflow that they can all call.
# .github/workflows/deploy-reusable.yml
name: Deploy (reusable)
on:
workflow_call:
inputs:
environment:
required: true
type: string # 'staging' | 'production'
app-name:
required: true
type: string
secrets:
CLOUDFLARE_API_TOKEN:
required: true
AWS_ROLE_ARN:
required: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }} # Uses GitHub Environment protection rules
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
if: inputs.environment == 'production'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
# OIDC — no long-lived secrets needed
- name: Deploy to ${{ inputs.environment }}
run: |
echo "Deploying ${{ inputs.app-name }} to ${{ inputs.environment }}"
# Your deploy commands here
env:
CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
ENVIRONMENT: ${{ inputs.environment }}
# In any repo that needs deployment:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-staging:
uses: your-org/shared-workflows/.github/workflows/deploy-reusable.yml@main
with:
environment: staging
app-name: my-app
secrets:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
deploy-production:
needs: deploy-staging
uses: your-org/shared-workflows/.github/workflows/deploy-reusable.yml@main
with:
environment: production
app-name: my-app
secrets: inherit # Pass all secrets from calling workflow
Environment Protection Rules
Set up staging and production environments.
Production requires two approvals from the platform team.
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: dist/
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging # Requires approval from 'staging-approvers' team (configured in UI)
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: build-${{ github.sha }}
path: dist/
- name: Deploy to staging
run: ./scripts/deploy.sh staging
integration-tests:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run test:integration
env:
APP_URL: https://staging.myapp.com
deploy-production:
needs: integration-tests
runs-on: ubuntu-latest
environment: production # Requires 2 approvals from 'platform-team' (configured in UI)
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: build-${{ github.sha }}
path: dist/
- name: Deploy to production
run: ./scripts/deploy.sh production
Concurrency and Cost Optimization
We're spending too much on GitHub Actions.
Concurrent PR pushes create multiple runs of the same workflow.
Only the latest commit's run should execute.
on:
pull_request:
# Cancel previous run when new commit pushed to same PR
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
# Use larger runners only for long tests
runs-on: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'ubuntu-latest-large' || 'ubuntu-latest' }}
steps:
# Cache dependencies between runs
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Cache npm install
# Cache build artifacts
- name: Cache build
uses: actions/cache@v4
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*.ts') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-
${{ runner.os }}-nextjs-
Workflow Dispatch for Manual Operations
Add a workflow that engineers can manually trigger to run database migrations
in production, with a dry-run option.
# .github/workflows/db-migrate.yml
name: Database Migration
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options: ['staging', 'production']
default: staging
dry-run:
description: 'Dry run (show what would change, do not execute)'
required: true
type: boolean
default: true
confirm-production:
description: 'Type "yes-i-am-sure" to confirm production migration'
required: false
type: string
jobs:
migrate:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Validate production confirmation
if: inputs.environment == 'production' && !inputs.dry-run
run: |
if [ "${{ inputs.confirm-production }}" != "yes-i-am-sure" ]; then
echo "Production migration requires typing 'yes-i-am-sure' in the confirmation field"
exit 1
fi
- name: Run migration
run: |
if [ "${{ inputs.dry-run }}" == "true" ]; then
npx knex migrate:status --env ${{ inputs.environment }}
else
npx knex migrate:latest --env ${{ inputs.environment }}
fi
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Notify on completion
if: always()
run: |
STATUS="${{ job.status }}"
DRY_RUN="${{ inputs.dry-run }}"
ENV="${{ inputs.environment }}"
echo "Migration job status: $STATUS (dry_run=$DRY_RUN, env=$ENV)"
# Send Slack notification here
For the CI/CD pipeline patterns including test stages and deployment gates, see the CI/CD guide. For infrastructure provisioning that integrates with these workflows, see the Terraform guide. The Claude Skills 360 bundle includes CI/CD automation skill sets for workflow generation, matrix configuration, and cost optimization. Start with the free tier to try workflow generation for your pipeline.