Files
certctl/scripts/ci-guards/G-frontend-bundle-budget.sh
shankar0123 5c5bbedc7e feat(ci): SCALE-007 — frontend bundle-size budget via size-limit
Acquisition-audit SCALE-007 closure (Sprint 6 ACQ, 2026-05-16).

The web/src codebase has ~45 React.lazy() call sites (`grep -rE
'lazy\(' web/src --include='*.tsx' | wc -l`), heavily route-
splitting the SPA. Pre-2026-05-16 there was no CI guard on bundle
size, so unintended bloat in a vendor chunk or a page chunk would
slip in unnoticed until somebody profiled cold-start performance.

This commit adds:

- web/.size-limit.json — 11 budget entries: per-chunk caps on the
  load-bearing chunks (main entry, vendor-recharts, vendor-react,
  vendor-query, vendor-router, vendor-icons, OnboardingWizard,
  CommandPalette, Timestamp) + two roll-up tiers (total vendor JS,
  total app JS). Budgets tuned to current vite-build output +
  ~15% headroom in brotli-compressed bytes (the size-limit
  default measurement mode — closest analogue to what a real
  browser downloads).
- web/package.json + web/package-lock.json: `npm run size` script
  + size-limit + @size-limit/file devDeps.
- .github/workflows/ci.yml: new "Frontend bundle-size budget
  (size-limit)" step in the frontend-build job, runs immediately
  after the vite build.
- scripts/ci-guards/G-frontend-bundle-budget.sh: local-runnable
  wrapper matching the existing ci-guards/<id>.sh contract — exits
  0 on clean, non-zero with ::error:: prefix on regression.

Acceptance verified locally:
- npm install in web/ regenerates package-lock cleanly
- `npm run size` exits 0 against the committed web/dist/
- `bash scripts/ci-guards/G-frontend-bundle-budget.sh` exits 0
- All current chunks measured (brotli, kB): main entry 23.3
  (cap 30), vendor-recharts 91.2 (cap 110), vendor-react 37.4
  (cap 45), OnboardingWizard 28.6 (cap 35), total vendor 149.5
  (cap 180), total app 351.1 (cap 425)

A regression that bloats a chunk past its cap fails CI and forces
an explicit operator decision: fix the regression, or raise the
cap in web/.size-limit.json with a rationale comment in the
commit message. Do not raise caps blindly.
2026-05-16 19:45:10 +00:00

72 lines
2.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright 2026 certctl LLC. All rights reserved.
# SPDX-License-Identifier: BUSL-1.1
#
# Acquisition-audit SCALE-007 closure (Sprint 6 ACQ, 2026-05-16).
# Per-chunk frontend bundle-size budget guard.
#
# Reads web/.size-limit.json and asserts every chunk-size pattern
# matches its committed cap (brotli-compressed, the default
# size-limit measurement mode — closest analogue to what a real
# browser downloads). A regression that bloats a chunk past its
# cap fails the build and forces an explicit operator decision —
# either fix the regression, or raise the cap in
# web/.size-limit.json with a rationale comment in the commit
# message.
#
# Contract (matches the other scripts/ci-guards/<id>.sh files):
# - exit 0 on clean repo
# - non-zero with `::error::` prefix on regression
# - skips with exit 0 when the prerequisites aren't installed
# (npm missing, web/node_modules missing, web/dist not built)
# — CI's frontend-build job runs `npm ci` + `npx vite build`
# BEFORE invoking this guard, so the skip path only fires in
# local fast-loop runs where the operator hasn't built the
# frontend yet.
#
# Local run:
# cd web && npm ci && npm run build && cd ..
# bash scripts/ci-guards/G-frontend-bundle-budget.sh
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$REPO_ROOT"
fail() {
echo "::error::G-frontend-bundle-budget: $*" >&2
exit 1
}
if ! command -v npm >/dev/null 2>&1; then
echo "G-frontend-bundle-budget: skipped — npm not on PATH."
echo " Install: https://nodejs.org/ (CI uses Node 22 via actions/setup-node)."
exit 0
fi
if [ ! -f "web/.size-limit.json" ]; then
fail "web/.size-limit.json is missing — the size-limit config is the source of truth for per-chunk budgets."
fi
if [ ! -d "web/node_modules/size-limit" ]; then
echo "G-frontend-bundle-budget: skipped — web/node_modules/size-limit/ missing."
echo " Run \`cd web && npm ci\` first."
exit 0
fi
if [ ! -d "web/dist/assets" ]; then
echo "G-frontend-bundle-budget: skipped — web/dist/assets/ not present."
echo " Run \`cd web && npm run build\` first."
exit 0
fi
echo "G-frontend-bundle-budget: checking $(ls -1 web/dist/assets/*.js 2>/dev/null | wc -l) JS chunks against web/.size-limit.json..."
cd web
if ! npm run --silent size; then
echo "::error::G-frontend-bundle-budget: at least one chunk exceeded its budget in web/.size-limit.json. See output above for the offender."
echo " To fix: either reduce the chunk size (audit the imports / move code to a route-level lazy boundary / drop an unneeded dependency), or — only if the growth is intentional — raise the cap in web/.size-limit.json with a rationale comment in the commit message."
exit 1
fi
echo "G-frontend-bundle-budget: PASS — all chunks within budget."