Patterns — Shipping with CI/CD
Contents
- GitHub Actions: fail-fast pipeline with artifact promotion
- Environment promotion job
- Canary deploy sketch
- Automated rollback gate
- Migration-before-code ordering
- Gotchas
GitHub Actions: fail-fast pipeline with artifact promotion
yaml
name: ci
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # stale runs waste the <10 min budget
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm run lint && npm run typecheck
test:
needs: lint # fail-fast ordering
runs-on: ubuntu-24.04
strategy:
matrix: { shard: [1, 2, 3, 4] } # parallel shards keep wall-clock low
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm test -- --shard=${{ matrix.shard }}/4
build:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
permissions: { id-token: write, contents: read } # OIDC, no long-lived keys
steps:
- uses: actions/checkout@v4
- run: docker build -t registry.example.com/svc:${{ github.sha }} .
- run: docker push registry.example.com/svc:${{ github.sha }}Environment promotion job
yaml
deploy-staging:
needs: build
environment: staging
runs-on: ubuntu-24.04
steps:
- run: ./deploy.sh registry.example.com/svc:${{ github.sha }} staging
deploy-prod:
needs: deploy-staging
environment: production # requires reviewer approval in repo settings
runs-on: ubuntu-24.04
steps:
# SAME artifact — promotion, not rebuild
- run: ./deploy.sh registry.example.com/svc:${{ github.sha }} productionCanary deploy sketch
bash
# deploy.sh <image> production — canary ramp with health gates
set -euo pipefail
IMAGE=$1
for PCT in 5 25 100; do
set_traffic_split "$IMAGE" "$PCT"
sleep 120 # observation window per step
ERR=$(error_rate_last_2m)
if (( $(echo "$ERR > 0.01" | bc -l) )); then # >1% errors aborts the ramp
set_traffic_split "$PREVIOUS_IMAGE" 100
echo "canary failed at ${PCT}% (err=${ERR}), rolled back" >&2
exit 1
fi
doneAutomated rollback gate
yaml
- name: verify and rollback
run: |
for i in $(seq 1 30); do
if curl -fsS https://svc.example.com/healthz; then exit 0; fi
sleep 10
done
./deploy.sh "$PREVIOUS_SHA" production # 5 min without health = revert
exit 1Record PREVIOUS_SHA before deploying — rollback needs a target, not a rebuild.
Migration-before-code ordering
yaml
migrate:
needs: build
runs-on: ubuntu-24.04
steps:
- run: ./run-migrations.sh # expand-phase only; contract ships a release later
deploy-staging:
needs: migrate # code never deploys onto an unmigrated schemaGotchas
cancel-in-progresson main deploys can abort a half-finished rollout — scope the concurrency group to PRs, or use a deploy queue for main.- Cache poisoning: keyed only on lockfile hash, a cache never picks up new OS packages; include the base-image tag in the key when builds depend on it.
environment:protection is the gate; branch protection is not — a promoted artifact needs its own approval step.- OIDC beats stored cloud keys:
permissions: id-token: write+ cloud trust policy removes the leakable secret entirely. - Rollback ≠ redeploy old branch: it is re-pointing to the previous artifact; rebuilding old code can produce a different binary than what ran yesterday.
- Migrations in the same job as deploy hide ordering failures — separate jobs make "schema first" visible and retryable.