From aea395ae65f991faeaa57d03256be0dc2ad5faef Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Thu, 14 May 2026 16:22:54 +0200 Subject: [PATCH] feat(ci): assert gzip transfer sizes against ADR-0017 budgets (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Closes the ADR-0017 follow-up that was sitting on an unpushed local branch (`feat/ci/gzip-transfer-size-budgets`) for ~5 days, while ADRs 0018–0021 and the auth/admin/security tracks landed. Cherry-picked onto current `main`, with a small follow-up fix to make the script work against the locale-split build layout introduced by ADR-0019 between the two commits. ## What lands ### Commit 1 — `feat(ci): assert gzip transfer sizes against ADR-0017 budgets` [`scripts/check-gzip-budgets.mjs`](scripts/check-gzip-budgets.mjs) — plain-Node, no deps, ~120 LOC at landing. Closes the ADR-0017 §Confirmation gap (Angular CLI's `budgets` only compare RAW sizes): - Parses Angular's emitted `index.html` to separate **initial** assets (anything referenced via `src=` / `href=`) from **lazy** chunks (the rest). - Gzips every JS / CSS file at level 9 — what most HTTP servers serve for static assets — and reports a per-file + per-bucket table. - Asserts the [ADR-0017](docs/decisions/0017-performance-budgets-lighthouse-ci.md) thresholds (`initial JS total ≤ 300 KB`, `any lazy chunk ≤ 100 KB`, `total CSS ≤ 150 KB`) and exits non-zero on any breach. - Wired through: - `pnpm ci:gzip-budgets` invokes the script with the default dist path. - `ci:perf` chains build → gzip check → Lighthouse, so a budget breach short-circuits before Lighthouse even runs. ADR-0017 §Confirmation also updated to point at the script instead of describing it as a "future follow-up". ### Commit 2 — `fix(ci): adapt gzip-budget check for the @angular/localize multi-bundle layout` The original commit predated PR #91 / [ADR-0019](docs/decisions/0019-internationalisation-angular-localize.md), which made `@angular/localize` emit one self-contained bundle per locale under `dist/apps/portal-shell/browser//`. The script looked for `index.html` directly under the dist root and failed with ENOENT on every build since. Fix: - Auto-detects layout. If `dist/index.html` exists → flat mode (unchanged behaviour, kept for single-locale apps like `portal-admin`). Otherwise enumerates immediate subdirectories with their own `index.html` and runs the check **per locale**. - Budget thresholds apply per locale — each bundle is what the user's browser actually downloads. Aggregating across locales would understate the worst case. - Violations across locales are collected + reported together with the locale tag prefixed so a single CI run surfaces every breach. - ADR-0017 §Confirmation amendment expanded to spell out the per-locale check + cite ADR-0019. ## Test plan - [x] `node --check scripts/check-gzip-budgets.mjs` — syntax OK. - [x] `pnpm ci:gzip-budgets` against the current production build: - 2 locale bundles detected (`en`, `fr`). - Initial JS total: ~141 KB / 300 KB budget ✓ (each locale). - CSS total: 4.62 KB / 150 KB budget ✓ (each locale). - Largest lazy chunk: 1.55 KB / 100 KB per-chunk budget ✓. - [x] No conflict on `package.json` despite 11 PRs touching it since the branch base — cherry-pick auto-merged cleanly. - [ ] CI: `pnpm ci:perf` end-to-end (build → gzip-budgets → Lighthouse). Validates on the runner. ## Notes for the reviewer - The script lives at the repo root under `scripts/` rather than as an Nx target — it's a pure CI helper, not project-scoped. Matches the existing `ci:audit` / `ci:commits` pattern in `package.json`. - No dependencies added. Uses `node:fs/promises` and `node:zlib`. - The original branch (`feat/ci/gzip-transfer-size-budgets`) is preserved in local-only state for paranoia; once this PR merges, it can be `git branch -D`'d. --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/133 --- .../0017-performance-budgets-lighthouse-ci.md | 3 +- package.json | 3 +- scripts/check-gzip-budgets.mjs | 229 ++++++++++++++++++ 3 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 scripts/check-gzip-budgets.mjs diff --git a/docs/decisions/0017-performance-budgets-lighthouse-ci.md b/docs/decisions/0017-performance-budgets-lighthouse-ci.md index 0508e32..5c45158 100644 --- a/docs/decisions/0017-performance-budgets-lighthouse-ci.md +++ b/docs/decisions/0017-performance-budgets-lighthouse-ci.md @@ -209,7 +209,8 @@ No browser-side RUM SDK in v1. The OTel browser tracing from [ADR-0012](0012-obs ### Confirmation - `lighthouserc.js` exists at the repo root with the critical-routes list, the assertions matching the thresholds above, and a 3-iteration median configuration. -- `apps/portal-shell/project.json` declares `budgets` with `maximumWarning == maximumError` (so an overshoot fails the build, matching `type: "error"` in spirit). Four entries: `initial` (≤ 1 MB raw), `anyScript` (≤ 300 KB raw), `anyComponentStyle` (≤ 6 KB raw, with 5 KB warning floor), `bundle name=styles` (≤ 150 KB raw). Angular CLI compares **raw** sizes; the values above are translated from the gzip-based ADR targets at the conventional ≈ 3× JS / ≈ 4× CSS compression ratios. A future follow-up will add a CI check that asserts the **actual** gzipped transfer size against the ADR thresholds — Angular CLI does not natively support a gzip-mode budget. +- `apps/portal-shell/project.json` declares `budgets` with `maximumWarning == maximumError` (so an overshoot fails the build, matching `type: "error"` in spirit). Four entries: `initial` (≤ 1 MB raw), `anyScript` (≤ 300 KB raw), `anyComponentStyle` (≤ 6 KB raw, with 5 KB warning floor), `bundle name=styles` (≤ 150 KB raw). Angular CLI compares **raw** sizes; the values above are translated from the gzip-based ADR targets at the conventional ≈ 3× JS / ≈ 4× CSS compression ratios. +- A complementary CI check in [`scripts/check-gzip-budgets.mjs`](../../scripts/check-gzip-budgets.mjs) (wired into `pnpm ci:perf` between the production build and Lighthouse) asserts the **actual** gzipped transfer size against the ADR thresholds: initial-JS-total ≤ 300 KB, any single lazy chunk ≤ 100 KB, total CSS ≤ 150 KB. Angular CLI does not natively support a gzip-mode budget — this script is the explicit gate. It parses `index.html` to distinguish initial assets from lazy chunks, gzips each file at level 9 (matching what HTTP servers typically serve), and exits non-zero on any breach. The check is **per locale bundle** — `@angular/localize` (per [ADR-0019](0019-internationalisation-angular-localize.md)) emits one self-contained bundle under `dist/.../browser//`, and each is what the user's browser actually downloads, so each is checked against the same budget independently. The script auto-detects locale-split vs flat layouts. - `package.json` exposes `ci:perf`, runnable locally with the same exit code as CI. - CI's `perf` gate from [ADR-0015](0015-cicd-gitea-actions.md) calls `pnpm ci:perf`. It is blocking on human PRs and on `push` to `main`; skipped on PRs authored by the Renovate bot user (`apf-portal-bot`) per the gating-policy subsection above. - `apps/portal-shell` exposes a Nx target `analyze` invoking `source-map-explorer` against the production build's source maps. diff --git a/package.json b/package.json index 152720a..d40b049 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "ci:check": "pnpm exec nx affected -t format:check lint test build", "ci:audit": "pnpm audit --audit-level=moderate", "ci:commits": "pnpm exec commitlint --from ${COMMIT_LINT_FROM:-origin/main} --to HEAD --verbose", - "ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm exec lhci autorun --config=./lighthouserc.js" + "ci:gzip-budgets": "node scripts/check-gzip-budgets.mjs", + "ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm ci:gzip-budgets && pnpm exec lhci autorun --config=./lighthouserc.js" }, "private": true, "lint-staged": { diff --git a/scripts/check-gzip-budgets.mjs b/scripts/check-gzip-budgets.mjs new file mode 100644 index 0000000..b0b8e06 --- /dev/null +++ b/scripts/check-gzip-budgets.mjs @@ -0,0 +1,229 @@ +#!/usr/bin/env node +/** + * Check the gzip-transfer size of an Angular production build against + * the ADR-0017 perf budgets. Complements Angular CLI's native + * `budgets` (which only compare RAW sizes — no built-in gzip mode). + * + * Strategy + * -------- + * 1. Parse the Angular `index.html` of the build output. Every + * `