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
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 |