Gsd-skill-creator ci-cd-patterns
Provides CI/CD pipeline best practices for GitHub Actions, deployment strategies, and pipeline optimization. Use when setting up pipelines, configuring GitHub Actions, managing deployments, or when user mentions 'CI', 'CD', 'pipeline', 'GitHub Actions', 'deploy', 'workflow', 'build'.
install
source · Clone the upstream repo
git clone https://github.com/Tibsfox/gsd-skill-creator
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Tibsfox/gsd-skill-creator "$T" && mkdir -p ~/.claude/skills && cp -r "$T/examples/skills/patterns/ci-cd-patterns" ~/.claude/skills/tibsfox-gsd-skill-creator-ci-cd-patterns && rm -rf "$T"
manifest:
examples/skills/patterns/ci-cd-patterns/SKILL.mdsource content
CI/CD Patterns
Best practices for building reliable, secure, and fast CI/CD pipelines with GitHub Actions.
Pipeline Stages
A well-structured pipeline follows this progression. Each stage gates the next.
lint --> test --> build --> security-scan --> deploy-staging --> deploy-production
| Stage | Purpose | Failure Means |
|---|---|---|
| Lint | Code style, formatting | Code doesn't meet standards |
| Test | Unit + integration tests | Broken functionality |
| Build | Compile, bundle | Code won't package |
| Security Scan | Dependency + code analysis | Vulnerabilities detected |
| Deploy Staging | Pre-production verification | Environment issue |
| Deploy Production | Live release | Requires approval gate |
GitHub Actions: Complete Workflow Templates
Standard CI Workflow
name: CI on: pull_request: branches: [main] push: branches: [main] # Cancel in-progress runs for the same branch/PR concurrency: group: ci-${{ github.ref }} cancel-in-progress: true permissions: contents: read 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 format:check test: runs-on: ubuntu-latest needs: lint strategy: 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: matrix.node-version == 20 with: name: coverage-report path: coverage/ retention-days: 7 build: runs-on: ubuntu-latest needs: test 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-output path: dist/ retention-days: 7
Deployment Workflow with Approval Gate
name: Deploy on: push: branches: [main] workflow_dispatch: inputs: environment: description: Target environment required: true default: staging type: choice options: - staging - production permissions: contents: read deployments: write jobs: build: 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 build - uses: actions/upload-artifact@v4 with: name: deploy-artifact path: dist/ deploy-staging: needs: build runs-on: ubuntu-latest environment: staging steps: - uses: actions/download-artifact@v4 with: name: deploy-artifact path: dist/ - name: Deploy to staging env: DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }} run: | # Deploy script here -- uses secret, never echo it echo "Deploying to staging..." deploy-production: needs: deploy-staging if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest # CRITICAL: Production requires manual approval via GitHub Environments environment: production steps: - uses: actions/download-artifact@v4 with: name: deploy-artifact path: dist/ - name: Deploy to production env: DEPLOY_TOKEN: ${{ secrets.PRODUCTION_DEPLOY_TOKEN }} run: | echo "Deploying to production..."
Docker Build and Push
name: Docker on: push: tags: ['v*'] permissions: contents: read packages: write jobs: docker: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v6 with: context: . push: true tags: | ghcr.io/${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:latest cache-from: type=gha cache-to: type=gha,mode=max
Secret Management
Rules
| Rule | Rationale |
|---|---|
| Never echo secrets in logs | CI logs are often accessible to contributors |
| Use GitHub Environment secrets for deploy tokens | Scoped to specific environments |
| Rotate secrets on schedule | Reduces blast radius of leaks |
| Use OIDC where possible | No long-lived credentials |
| Minimal secret scope | Each secret should access only what it needs |
Masking Secrets
steps: - name: Use secret safely env: # Secret is automatically masked in logs API_KEY: ${{ secrets.API_KEY }} run: | # NEVER do this: # echo "Key is $API_KEY" # SAFE: Use secret in commands without printing curl -s -H "Authorization: Bearer $API_KEY" https://api.example.com/health - name: Mask dynamic values run: | TOKEN=$(generate-token) echo "::add-mask::$TOKEN" # Now $TOKEN is masked in all subsequent log output echo "Token generated successfully"
OIDC for Cloud Providers (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-deploy aws-region: us-east-1 # No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY needed
Caching Strategies
Dependency Caching
# Node.js -- built into setup-node - uses: actions/setup-node@v4 with: node-version: 20 cache: npm # Python - uses: actions/setup-python@v5 with: python-version: '3.12' cache: pip # Go - uses: actions/setup-go@v5 with: go-version: '1.22' cache: true # Rust - uses: actions/cache@v4 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ target/ key: rust-${{ hashFiles('**/Cargo.lock') }} restore-keys: rust-
Custom Cache
- uses: actions/cache@v4 with: path: .cache/expensive-operation key: expensive-${{ hashFiles('src/**') }} restore-keys: | expensive-
Cache Sizing
| What to Cache | Impact | Size Concern |
|---|---|---|
(via npm ci) | HIGH | Use cache instead |
| Build output | MEDIUM | Only if build is slow (>2 min) |
| Docker layers | HIGH | Use |
| Test fixtures | LOW | Usually not worth caching |
Deployment Patterns
Blue-Green Deployment
Two identical environments. Switch traffic atomically.
Current traffic --> Blue (v1.0) Green (v1.1) <-- Deploy here, test, then switch After switch: Current traffic --> Green (v1.1) Blue (v1.0) <-- Rollback target
| Pros | Cons |
|---|---|
| Instant rollback | Requires 2x infrastructure |
| Zero downtime | Database migrations need care |
| Full environment testing | Higher cost |
Canary Deployment
Route a small percentage of traffic to the new version.
95% traffic --> v1.0 (stable) 5% traffic --> v1.1 (canary) Monitor metrics. If healthy: 50% --> v1.0, 50% --> v1.1 Then: 100% --> v1.1
| Pros | Cons |
|---|---|
| Low risk | Slower rollout |
| Real traffic testing | Complex routing setup |
| Gradual confidence | Stateful apps need care |
Rolling Deployment
Replace instances one at a time.
Instance 1: v1.0 --> v1.1 (update, health check, continue) Instance 2: v1.0 --> v1.1 Instance 3: v1.0 --> v1.1
| Pros | Cons |
|---|---|
| No extra infrastructure | Mixed versions during rollout |
| Simple to implement | Slower rollback (re-deploy) |
| Works with most platforms | Must be backward compatible |
Matrix Builds
Test across multiple versions and platforms efficiently.
strategy: fail-fast: false # Don't cancel other jobs if one fails matrix: os: [ubuntu-latest, macos-latest, windows-latest] node-version: [18, 20, 22] exclude: # Skip combinations that don't matter - os: macos-latest node-version: 18 include: # Add specific extra combinations - os: ubuntu-latest node-version: 20 coverage: true steps: - run: npm test - if: matrix.coverage run: npm run test:coverage
Pipeline Optimization
Speed Improvements
| Technique | Savings | Complexity |
|---|---|---|
| Dependency caching | 30-60s | Low |
| Parallel jobs | 40-70% | Low |
| Avoid wasted runs | Low |
| Docker layer caching | 1-5 min | Medium |
| Selective test running | Variable | Medium |
| Self-hosted runners | Variable | High |
Conditional Execution
# Only run when relevant files change on: push: paths: - 'src/**' - 'tests/**' - 'package.json' - 'package-lock.json' paths-ignore: - '**.md' - 'docs/**' # Skip CI for documentation-only changes jobs: test: if: | !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[docs only]')
Reusable Workflows
# .github/workflows/reusable-test.yml on: workflow_call: inputs: node-version: required: false type: string default: '20' secrets: NPM_TOKEN: required: false jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: npm - run: npm ci - run: npm test
# .github/workflows/ci.yml -- caller jobs: test: uses: ./.github/workflows/reusable-test.yml with: node-version: '20' secrets: inherit
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Force-push in deploy scripts | Can overwrite production state | Use atomic deploys, never in CI |
| Secrets in workflow files | Exposed in repo history | Use GitHub Secrets or OIDC |
in logs | Leaked credentials | Never echo; use for dynamic values |
| No approval gate for production | Accidental deploys | Use GitHub Environments with required reviewers |
instead of | Non-deterministic builds | Always in CI (uses lockfile) |
No control | Wasted compute, race conditions | Add for PR builds |
| Hardcoded versions in actions | Breaks without notice | Pin to major version () or SHA |
| Running tests only on main | Broken PRs get merged | Run on trigger |
| Single monolithic job | Slow, no parallelism | Split into lint/test/build/deploy jobs |
| No timeout on jobs | Hung builds waste minutes | Set on every job |
| Excessive permissions | Use minimal per job |
Workflow Security Checklist
-
block set with minimal scope on every workflowpermissions - Secrets used via
, never hardcoded${{ secrets.NAME }} - Third-party actions pinned to commit SHA or trusted major version
-
workflows do NOT checkout PR code (code injection risk)pull_request_target - Production deploys require approval via GitHub Environments
- No
push commands in any workflow--force -
groups prevent parallel deploys to same environmentconcurrency -
set on all jobs (default 360 min is too long)timeout-minutes - Artifacts have
set (don't accumulate forever)retention-days - OIDC used instead of long-lived cloud credentials where possible