CI/CD Pipeline
by @gitgoodordietrying
Create, debug, and manage CI/CD pipelines with GitHub Actions. Use when the user needs to set up automated testing, deployment, releases, or workflows. Covers workflow syntax, common patterns, secrets management, caching, matrix builds, and troubleshooting.
clawhub install cicd-pipelineπ About This Skill
name: cicd-pipeline description: Create, debug, and manage CI/CD pipelines with GitHub Actions. Use when the user needs to set up automated testing, deployment, releases, or workflows. Covers workflow syntax, common patterns, secrets management, caching, matrix builds, and troubleshooting. metadata: {"clawdbot":{"emoji":"π","requires":{"anyBins":["gh","git"]},"os":["linux","darwin","win32"]}}
CI/CD Pipeline (GitHub Actions)
Set up and manage CI/CD pipelines using GitHub Actions. Covers workflow creation, testing, deployment, release automation, and debugging.
When to Use
Quick Start: Add CI to a Project
Node.js project
# .github/workflows/ci.yml
name: CIon:
push:
branches: [main]
pull_request:
branches: [main]
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 test
- run: npm run lint
Python project
# .github/workflows/ci.yml
name: CIon:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt
- run: pytest
- run: ruff check .
Go project
# .github/workflows/ci.yml
name: CIon:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go test ./...
- run: go vet ./...
Rust project
# .github/workflows/ci.yml
name: CIon:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test
- run: cargo clippy -- -D warnings
Common Patterns
Matrix builds (test across versions/OSes)
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Conditional jobs
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
Caching dependencies
# Node.js (automatic with setup-node)
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # or yarn, pnpmGeneric caching
uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.cargo/registry
node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-
Artifacts (save build outputs)
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7Download in another job
uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
Run on schedule (cron)
on:
schedule:
- cron: "0 6 * * 1" # Every Monday at 6 AM UTC
workflow_dispatch: # Also allow manual trigger
Deployment Workflows
Deploy to production on tag
name: Releaseon:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- run: npm test
# Create GitHub release
- uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
dist/*.js
dist/*.css
Deploy to multiple environments
name: Deployon:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
./deploy.sh production
else
./deploy.sh staging
fi
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Docker build and push
name: Dockeron:
push:
branches: [main]
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
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:
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
npm publish on release
name: Publishon:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
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
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Secrets Management
Set secrets via CLI
# Set a repository secret
gh secret set DEPLOY_TOKEN --body "my-secret-value"Set from a file
gh secret set SSH_KEY < ~/.ssh/deploy_keySet for a specific environment
gh secret set DB_PASSWORD --env production --body "p@ssw0rd"List secrets
gh secret listDelete a secret
gh secret delete OLD_SECRET
Use secrets in workflows
env:
# Available to all steps in this job
DATABASE_URL: ${{ secrets.DATABASE_URL }}steps:
- run: echo "Deploying..."
env:
# Available to this step only
API_KEY: ${{ secrets.API_KEY }}
Environment protection rules
Set up via GitHub UI or API:
# View environments
gh api repos/{owner}/{repo}/environments | jq '.environments[].name'
Workflow Debugging
Re-run failed jobs
# List recent workflow runs
gh run list --limit 10View a specific run
gh run view View failed job logs
gh run view --log-failedRe-run failed jobs only
gh run rerun --failedRe-run entire workflow
gh run rerun
Debug with SSH (using tmate)
# Add this step before the failing step
uses: mxschmitt/action-tmate@v3
if: failure()
with:
limit-access-to-actor: true
Common failures and fixes
"Permission denied" on scripts
- run: chmod +x ./scripts/deploy.sh && ./scripts/deploy.sh
"Node modules not found"
# Make sure npm ci runs before npm test
run: npm ci # Install exact lockfile versions
run: npm test # Now node_modules exists
"Resource not accessible by integration"
# Add permissions block
permissions:
contents: write
packages: write
pull-requests: write
Cache not restoring
# Check cache key matches - use hashFiles for lockfile
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
NOT: key: ${{ runner.os }}-node-${{ hashFiles('package.json') }}
Workflow not triggering
push vs pull_request)# Manually trigger a workflow
gh workflow run ci.yml --ref main
Workflow Validation
Validate locally before pushing
# Check YAML syntax
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo "Valid"Use actionlint (if installed)
actionlint .github/workflows/ci.ymlOr via Docker
docker run --rm -v "$(pwd):/repo" -w /repo rhysd/actionlint:latest
View workflow as graph
# List all workflows
gh workflow listView workflow definition
gh workflow view ci.ymlWatch a running workflow
gh run watch
Advanced Patterns
Reusable workflows
# .github/workflows/reusable-test.yml
name: Reusable Test
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: "20"
secrets:
npm-token:
required: falsejobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm test
# .github/workflows/ci.yml - caller
name: CI
on: [push, pull_request]
jobs:
test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
Concurrency (prevent duplicate runs)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel previous runs for same branch
Path filters (only run for relevant changes)
on:
push:
paths:
- "src/**"
- "package.json"
- "package-lock.json"
- ".github/workflows/ci.yml"
paths-ignore:
- "docs/**"
- "*.md"
Monorepo: only test changed packages
jobs:
changes:
runs-on: ubuntu-latest
outputs:
api: ${{ steps.filter.outputs.api }}
web: ${{ steps.filter.outputs.web }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'packages/api/**'
web:
- 'packages/web/**' test-api:
needs: changes
if: needs.changes.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd packages/api && npm ci && npm test
test-web:
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd packages/web && npm ci && npm test
Tips
workflow_dispatch on every workflow for manual triggering during debugginguses: actions/checkout@b4ffde...continue-on-error: true for non-critical steps (like linting)timeout-minutes on jobs to prevent runaway builds (default is 360 minutes)outputs: result: ${{ steps.step-id.outputs.value }}runs-on: self-hosted with labels for targeting specific machinesβ‘ When to Use
π Tips & Best Practices
workflow_dispatch on every workflow for manual triggering during debugginguses: actions/checkout@b4ffde...continue-on-error: true for non-critical steps (like linting)timeout-minutes on jobs to prevent runaway builds (default is 360 minutes)outputs: result: ${{ steps.step-id.outputs.value }}runs-on: self-hosted with labels for targeting specific machines