Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions .github/workflows/bundle-budget-refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Weekly bundle-budget baseline measurement. Surfaces the current production /
# mockups / per-route gzip weight, and how far it has drifted from the committed
# baseline in bundle-budget.json, into a rolling issue. REPORT ONLY — it never
# commits, pushes, or opens a PR; acting on the report means refreshing the
# baseline by hand in a normal reviewed PR.
#
# Why this exists (outstanding issue #QSHHGK): nothing scheduled a baseline
# measurement, so accumulated growth from many merged PRs stayed invisible until
# it crossed the 10% tolerance and failed whichever unrelated PR happened to land
# last. This job makes the accumulation visible early and attributable, without
# silently absorbing it.
name: Bundle Budget Refresh

on:
workflow_dispatch: {}
schedule:
# 04:40 UTC on Wednesdays. Deliberately off the crowded Sunday-evening slot:
# ci.yml, docker-image.yml and eval-canary.yml all fire at "0 18 * * 0" and
# live-drift.yml at "30 18 * * 0", and a cold full `npm run build` here would
# queue behind them. Midweek also means the measurement lands while work is
# in flight rather than after the weekend batch. The :40 minute avoids
# ops-digest.yml (hourly at :20), ingestion-autopilot.yml (6-hourly at :00)
# and live-domain-monitor.yml (6-hourly at :23). 04:40 UTC is off-peak for
# GitHub-hosted runners and is midday in Perth (UTC+8), so the owner sees a
# fresh report during the working day.
- cron: "40 4 * * 3"

concurrency:
group: bundle-budget-refresh
# Never cancel a measurement in flight: a half-finished build produces no
# numbers at all, and the next run is a week away.
cancel-in-progress: false

permissions:
contents: read

jobs:
bundle-budget-refresh:
runs-on: ubuntu-24.04
permissions:
contents: read
# Only for the rolling report issue below. This workflow writes nothing to
# the repository itself.
issues: write
# Generous: this is a deliberately COLD full Next.js build (no .next cache
# restore, see below), which is far slower than ci.yml's incremental Build.
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Full history: --refresh-baseline validates baseline provenance with
# git (the source SHA must be a real commit and an ancestor of HEAD,
# and its distance from HEAD must be resolvable). A shallow clone makes
# that check fail closed.
fetch-depth: 0
persist-credentials: false

- name: Setup Node and dependencies
uses: ./.github/actions/setup-node-cached

# No .next cache restore here, and .next is removed outright before the
# build. AGENTS.md ("Bundle budget" → Measuring): `npm run build` reuses a
# cached .next, and check-bundle-budget.mjs then reads STALE output and
# reports byte-identical numbers — it will say the budget passes when it
# does not. A scheduled measurement whose numbers cannot be trusted is
# worse than no measurement, so this job always pays for a cold build.
- name: Measure bundle weight against the committed baseline
id: refresh
env:
# Matches ci.yml's Build step; parallelises static generation only.
NEXT_BUILD_CPUS: "4"
run: |
set -o pipefail
rm -rf .next
npm run build
node scripts/check-bundle-budget.mjs --refresh-baseline --json | tee bundle-budget-refresh.json

# DELIBERATELY REPORT ONLY. This workflow must never commit the refreshed
# bundle-budget.json, push a branch, or open a PR:
# * scripts/check-github-action-pins.mjs fails the build on any
# workflow-authored branch mutation, because bot-authored heads leave
# required checks awaiting approval and cannot be merged.
# * a baseline moved by a bot is a baseline nobody reviewed — the growth
# it absorbs becomes unattributable, which is the exact failure mode
# #QSHHGK exists to prevent.
# The refreshed file ships as an artifact instead; a human applies it in a
# normal PR after deciding the growth is legitimate.
- name: Upload refreshed baseline and measurement
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: bundle-budget-refresh
path: |
bundle-budget.json
bundle-budget-refresh.json
retention-days: 30
# A missing file means the measurement silently produced nothing; fail
# rather than publish a report with no evidence behind it.
if-no-files-found: error

- name: Publish to rolling issue
if: ${{ !cancelled() }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
# Pass step results via env (not inline template expansion) to avoid
# the GitHub Actions template-injection pattern. The measurement itself
# is read from the JSON file on disk for the same reason.
REFRESH_OUTCOME: ${{ steps.refresh.outcome }}
MEASUREMENT_FILE: bundle-budget-refresh.json
with:
script: |
const fs = require("fs");
const label = "bundle-budget-refresh";
const title = "Bundle budget baseline measurement";
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const outcome = process.env.REFRESH_OUTCOME || "unknown";

const pct = (value) => (typeof value === "number" ? `${value >= 0 ? "+" : ""}${value.toFixed(2)}%` : "n/a");
const kb = (value) => (typeof value === "number" ? `${(value / 1024).toFixed(1)} kB` : "n/a");

let body;
if (outcome === "success") {
const measurement = JSON.parse(fs.readFileSync(process.env.MEASUREMENT_FILE, "utf8"));
const rows = [
`| production | ${kb(measurement.production?.gzipBytes)} | ${kb(measurement.production?.previousGzipBytes)} | ${pct(measurement.production?.diffPct)} |`,
`| mockups (scratch) | ${kb(measurement.mockups?.gzipBytes)} | ${kb(measurement.mockups?.previousGzipBytes)} | ${pct(measurement.mockups?.diffPct)} |`,
...Object.entries(measurement.routes ?? {}).map(
([route, r]) => `| route \`${route}\` | ${kb(r.gzipBytes)} | ${kb(r.previousGzipBytes)} | ${pct(r.diffPct)} |`,
),
];
body = [
`_Updated ${measurement.updatedAt} — [run](${runUrl})._`,
"",
"Cold-build measurement of current bundle weight against the committed",
"`bundle-budget.json` baseline. **Report only** — no baseline was committed by this run.",
"",
"| bucket | measured (gzip) | committed baseline | drift |",
"| --- | --- | --- | --- |",
...rows,
"",
`Total: ${kb(measurement.totalGzipBytes)} gzip across ${measurement.files} chunk(s).`,
`Measured at \`${String(measurement.baselineSource ?? "").slice(0, 12)}\` (${measurement.baselineCommitDistance ?? "?"} commit(s) behind HEAD).`,
"",
"Production and mockup tolerances are 10% and 25% respectively. When production",
"drift approaches its ceiling, either find the regression or refresh the baseline",
"deliberately in a reviewed PR:",
"",
"```",
"rm -rf .next && npm run build",
"npm run check:bundle-budget -- --refresh-baseline",
"```",
"",
"The refreshed `bundle-budget.json` from this run is attached to the run above as",
"the `bundle-budget-refresh` artifact.",
].join("\n");
} else {
body = [
`_Updated ${new Date().toISOString()} — [run](${runUrl})._`,
"",
`⚠ The scheduled measurement did not complete (\`${outcome}\`), so the numbers below are absent`,
"rather than reassuring. This is **not** evidence that the bundle budget is healthy.",
"",
`Check the build and bundle-budget steps in the [run log](${runUrl}).`,
].join("\n");
core.warning(`Bundle budget measurement did not complete: ${outcome}`);
}

const { data: existing } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
labels: label,
});
if (existing.length > 0) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existing[0].number,
body,
});
core.info(`Updated rolling issue #${existing[0].number}.`);
} else {
const created = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
labels: [label],
body,
});
core.info(`Opened rolling issue #${created.data.number}.`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "5fa04c6e-4d27-4e95-ace6-a037aaccba1a",
"createdOn": "2026-09-02",
"action": "add",
"payload": {
"pri": "P3",
"type": "issue",
"summary": "The scheduled bundle-budget refresh workflow is never parsed by GitHub until its first scheduled run, so a YAML fault would surface in a non-blocking run rather than at merge",
"detail": "Raised while landing PR #2527, which added .github/workflows/bundle-budget-refresh.yml for #QSHHGK. The workflow is schedule- plus workflow_dispatch-only, so no pull_request CI run ever invokes it. It is covered offline by tests/bundle-budget-refresh-workflow.test.ts (17 assertions on schedule, cold-build ordering, the refresh flag, permissions, and the absence of any push or branch mutation) and by check:github-actions for pins and runs-on. None of those is GitHub's own workflow-syntax parser, which runs only when a workflow first executes. The known failure shape is the one recorded in tests/ci-cache-safety.test.ts around line 551: a workflow that fails to parse is named after its file path, creates ZERO jobs, and reports a bare failure that nothing local catches. Here that would land in an isolated scheduled run that blocks nothing, so it could sit unnoticed until someone wondered why no baseline report had appeared. Cheap close: trigger it once by workflow_dispatch after merge and confirm it creates jobs and posts to the rolling issue. That single dispatch also produces the first measurement with a resolvable baselineSource that #QSHHGK is waiting on, so the two close together.",
"source": "session 2026-09-02, PR #2527",
"issueUlid": "01M1G9AQS08TKTV69DHJ1TAGY4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "62fb8f5c-2d49-4e48-8352-0b17b742944c",
"createdOn": "2026-09-02",
"action": "done",
"payload": {
"id": "#B0530F",
"outcome": "Resolved 2026-09-02: the automation this row asked for already exists on main. PR #2474 (commit 79a824e) added the caring-contacts-db job at .github/workflows/ci.yml:1115 with exactly the shape requested - a digest-pinned postgres:17 service container on host port 54329, POSTGRES_HOST_AUTH_METHOD trust and a pg_isready health check - and runs npm run caring-contacts:db:test at ci.yml:1150, so the row-level-security and cross-team-isolation files are now collected by an automated gate rather than by nobody. It is wired into the required pr-required aggregate (ci.yml:1167, :1198, :1317, :1319), and the static contract test this row asked for exists at tests/ci-cache-safety.test.ts:123-135, pinning the container, the environment variable, the command and the pr-required membership, and asserting db-reset-verify does not duplicate the same run. The offline chain was left untouched, so the concern about breaking every offline run does not arise. Verified locally: the suite was run in this container against a real PostgreSQL cluster on port 54329 and reported 2 test files passed and 213 tests passed.",
"baseRowFingerprint": "b3165cd370886608f0e13fd1cbb943f041f8baa07655cf8bc3951a4964e4fcdc"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "c994c7a9-2c0a-41fc-a58d-5de479d8e823",
"createdOn": "2026-09-02",
"action": "update",
"payload": {
"id": "#QSHHGK",
"detail": "UPDATE 2026-09-02 (PR #2527): the missing TRIGGER now exists. .github/workflows/bundle-budget-refresh.yml runs weekly (Wednesday 04:40 UTC, deliberately off the crowded Sunday-evening cluster) plus workflow_dispatch, removes .next before building so the measurement is not read from a stale cache, and runs check-bundle-budget.mjs --refresh-baseline. It is REPORT ONLY: it publishes to a rolling bundle-budget-refresh issue and uploads the refreshed bundle-budget.json as an artifact, and never commits, pushes or opens a PR, because check-github-action-pins.mjs bans workflow-authored branch mutation and a baseline moved by a bot is one nobody reviewed. WHAT REMAINS, and why this row stays open: (1) no named refresh OWNER — the apply step is still a human action on the artifact the run produces; (2) the recorded baselineSource 0764fb58 STILL does not resolve, re-checked 2026-09-02 after deepening a container clone to 3419 commits, so the standing +5.2% remains unattributable to any reviewed change set; (3) the baseline numbers were deliberately NOT refreshed in PR #2527, because doing so would have silently absorbed that unattributable growth. The first run — scheduled, or dispatched once after merge — produces a measurement whose source resolves, and that is the point at which a refresh becomes reviewable and this row can close.",
"source": "session 2026-09-02, PR #2527",
"baseRowFingerprint": "2ebd6a3a17bb295805363f8c1e1c99920dc4a539313b102dcbcb8dc4a8acb3dd"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "cd053a9a-584a-4046-b2e4-3eb0548c3ccf",
"createdOn": "2026-09-02",
"action": "done",
"payload": {
"id": "#27TWKM",
"outcome": "Resolved 2026-09-02: this is already done on main. PR #2474 (commit 79a824e) added the caring-contacts-db CI job at .github/workflows/ci.yml:1115, which brings up a digest-pinned postgres:17 service container on host port 54329 and runs the whole Postgres project via npm run caring-contacts:db:test at ci.yml:1150, so every row-level-security and cross-team-isolation assertion now fires in CI rather than only on a developer's machine. The job is wired into the required pr-required aggregate (ci.yml:1167, :1198, :1317, :1319) and pinned by a contract test at tests/ci-cache-safety.test.ts:123-135, which also asserts db-reset-verify does not duplicate the same command; there is no coverage hole, because every caring-contacts path classifies to static_heavy_changed=true, which the job's if: condition covers. The suite was re-run in this container against a real local PostgreSQL cluster on port 54329 and reported 2 test files passed and 213 tests passed. The placement half of the row is likewise settled: the guards that need no database live in tests/caring-contacts-domain-isolation.test.ts, which the default npm run test collects.",
"baseRowFingerprint": "846921835f9f53a8eb54c5dc5f8f016ef4bdae5e0a842854e1a4b1aafaf51d27"
}
}
2 changes: 1 addition & 1 deletion docs/scripts-index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Scripts index

Curated map of `scripts/` (284 files) and the `package.json` script surface (287 entries),
Curated map of `scripts/` (284 files) and the `package.json` script surface (288 entries),
grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative
command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run <x>`
referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above.
Expand Down
Loading
Loading