The Slim CI Rebuild: One Pipeline Per Push, One Browser Download, Zero Redundant Installs
Every push to an MR branch ran two GitLab pipelines and every Playwright job downloaded its own Chromium. Commit 1f3a658 fixed all three with workflow rules, lockfile-keyed caching, and a shared browser artifact — here is the exact diff and what it cannot prove.
Every push to an MR branch ran the pipeline twice: once for the branch, once for the MR. That doubled GitLab Free runner-minute burn on the exact pushes where CI matters most. Each of the three Playwright jobs then downloaded its own ~150 MB Chromium. And two audit jobs each paid a full npm ci to run npm audit.
Commit 1f3a658 (ci: slim pipeline) fixed all three structurally. No benchmarks were captured before/after. What follows is the diff and the mechanism, not measured minutes.
Fix 1: one pipeline per push
# .gitlab-ci.yml — the whole dedup
workflow:
auto_cancel:
on_new_commit: interruptible
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- when: never
Branch pipelines for non-main branches are dropped. Nothing else is lost because this repo has no tags, schedules, or merge trains. MR and main pushes are the only ones that gate or release anything. The old shape (image: node:24, seven stages incl. audit/security/lint/typecheck/test) is collapsed to four: validate → build → verify → deploy.
Fix 2: lockfile-keyed cache
cache:
key:
files:
- package-lock.json
prefix: node-modules
fallback_keys:
- node-modules-
paths:
- node_modules/
The old key was ${CI_COMMIT_REF_SLUG}: cold on every new branch, exactly when CI matters most. A stale lockfile-keyed cache only ever costs minutes, never correctness, because every consumer still runs a full npm ci. Jobs with no node tooling (semgrep-sast, secret_detection, the three Playwright verify jobs) set cache: [] explicitly so they skip the restore entirely.
Fix 3: one Chromium download, one audit install
variables:
PLAYWRIGHT_BROWSERS_PATH: "$CI_PROJECT_DIR/.pw-browsers"
The build job runs the single npx playwright install chromium (Darwin-guarded: --with-deps is apt-only and fails on the mac-shell runner); the artifact .pw-browsers/ travels to the verify jobs, which top up system libs only (~30–60 s) instead of re-downloading. And the two audit jobs (info + critical, each with its own npm ci) merged into one npm-audit running both levels.
The Playwright side needed a matching change in playwright.config.ts:
command: process.env.PW_USE_EXISTING_BUILD
? 'npx serve dist/client -p 4321 --no-clipboard'
: 'npm run build && npx serve dist/client -p 4321 --no-clipboard',
CI sets PW_USE_EXISTING_BUILD=1 because dist/client already arrives via the build artifact. Rebuilding there wastes ~2–3 runner minutes. Unset locally, the config rebuilds hermetically as before.
The stage map after the rebuild
validate holds npm-audit (informational, allow_failure: true), semgrep-sast, secret_detection, lint, and typecheck (blocking since bundle-4 fixed 48 astro check errors; the config comments say NEVER relax it). build installs once, installs Chromium once, runs npm run build, and artifacts dist/, .csp-nonce, .vercel/output/, node_modules/, .pw-browsers/. verify holds csp-check, transitions-check, and test: all thin (cache: [], needs: [build]), all blocking. deploy runs finalize-csp.mjs then vercel deploy --prebuilt.
Also fixed in the same commit: the test job had been allow_failure: true, so a fully-red suite (including both axe-core accessibility specs) still produced a green pipeline. Since test is in deploy-production’s needs with when: on_success, a red suite now actually stops the release.
Honest limitations
- No measured before/after times. I did not capture runner-minute totals or per-job durations before the rebuild, so “saves N minutes” would be invented. The savings are structural (one pipeline not two, one Chromium not three, three
npm cinot five). Count the eliminated redundant work, not a stopwatch I never ran. - The
~150 MBand~30–60 sfigures are the comments’ own estimates in.gitlab-ci.yml, not measured in this repo’s runner logs. - Single-runner assumption. Sharing
node_modules/and.pw-browsers/via artifacts assumes themac-shelltag pool; a multi-runner setup would need cache-fallback tuning.
Maintainers on metered CI runners (GitLab Free minutes, single Mac shells) should care, because the failure this rebuild prevents is death by duplication: two pipelines per MR push, three Chromium downloads per run, five npm ci installs for one tree. Each invisible in isolation, each burning the minutes that gate your releases. Without the dedup, the deeper rot is the allow_failure: true test job: a fully-red suite, including the axe-core accessibility specs, still shipping green, so the pipeline’s approval means nothing. Structural savings need no stopwatch. Eliminating a redundant pipeline, two redundant browser downloads, and two redundant installs is arithmetic, not benchmarking. The gates this pipeline runs include the build attestation checks, and the stack contract is in the GitLab CI + Playwright/Vitest notes.
Related: the uses entry for this exact stack (GitLab CI + Playwright/Vitest), and the TweakDeals case study whose prebuild-data.ts empty-DB gate is the deploy-time sibling of these CI gates.
Related logs
What Prometheus Sees: Monitoring 10 Branches from One Grafana
The unified-infrastructure case claims Prometheus and Grafana observability — this is the architecture behind the claim: what gets scraped, what the dashboard shows, what fires alerts, and the one blind spot the design still carries.
Tally Prime on RDS RemoteApp: The Day-2 Shape (Sessions, Licenses, Certificates)
The RDS cutover posts cover the migration night — this is what operating it looks like after: session-broker license management, timeout policies at peak hours, the 60-day certificate cycle, and the connector you still have to watch.