# 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 }} production ``` ## Canary deploy sketch ```bash # deploy.sh 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 done ``` ## Automated 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 1 ``` Record `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 schema ``` ## Gotchas - **`cancel-in-progress` on 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.