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
149 changes: 147 additions & 2 deletions .github/workflows/performance-budget.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,12 +7,80 @@ on:
- 'packages/**'
- 'apps/console/**'
- 'pnpm-lock.yaml'
# This file itself (objectui#6245). Without it a change to this gate ships
# to `main` having never run once: the PR that edits it touches no
# `packages/**` path, so `Bundle Analysis` never appears on it, and a
# wiring bug surfaces on the NEXT `packages/**` PR — a required context
# turning red on someone else's diff, reading to them as a bundle problem
# of their own. Self-inclusion is this repo's convention for a
# path-filtered gate: measured on `origin/main`, 5 of the 7 workflows
# carrying a `paths:` filter list their own file, and this was one of the
# two that did not. The cost is honest and accepted — every edit of this
# file now pays a console build — and it is the same tax the other
# self-included gates already pay.
- '.github/workflows/performance-budget.yml'
# ...and the gate's RUNTIME CLOSURE: the files this job actually executes.
# Listing the YAML alone was still not enough — a PR touching only
# `check-eager-closure-budget.mjs`, the file that computes the verdict and
# the one this card is about, did not trigger this gate. The convention
# measured on `origin/main` is the closure, not just the workflow:
# `half-state-patrol.yml` lists its script AND `scripts/invoked-as.mjs`,
# the dependency of that script.
#
# `invoked-as.mjs` is here for the same reason it is there, and the reason
# is specific rather than tidy: `isEntrypoint` decides whether `main()`
# runs at all, so a regression in it makes this checker exit 0 having
# measured nothing — the exact silent-green failure the rest of this gate
# exists to prevent.
#
# NOT listed: `scripts/__tests__/*.test.ts`. This job never runs vitest —
# it runs two `node scripts/*.mjs` commands — so a test-only edit cannot
# change this gate's verdict, and those tests already run on every PR in
# the root vitest `unit` project. `published-dist-gate.yml` and
# `spec-range-floors.yml` draw the same line.
- 'scripts/check-eager-closure-budget.mjs'
- 'scripts/render-budget-comment.mjs'
- 'scripts/invoked-as.mjs'
pull_request:
branches: [main, develop]
paths:
- 'packages/**'
- 'apps/console/**'
- 'pnpm-lock.yaml'
# This file itself (objectui#6245). Without it a change to this gate ships
# to `main` having never run once: the PR that edits it touches no
# `packages/**` path, so `Bundle Analysis` never appears on it, and a
# wiring bug surfaces on the NEXT `packages/**` PR — a required context
# turning red on someone else's diff, reading to them as a bundle problem
# of their own. Self-inclusion is this repo's convention for a
# path-filtered gate: measured on `origin/main`, 5 of the 7 workflows
# carrying a `paths:` filter list their own file, and this was one of the
# two that did not. The cost is honest and accepted — every edit of this
# file now pays a console build — and it is the same tax the other
# self-included gates already pay.
- '.github/workflows/performance-budget.yml'
# ...and the gate's RUNTIME CLOSURE: the files this job actually executes.
# Listing the YAML alone was still not enough — a PR touching only
# `check-eager-closure-budget.mjs`, the file that computes the verdict and
# the one this card is about, did not trigger this gate. The convention
# measured on `origin/main` is the closure, not just the workflow:
# `half-state-patrol.yml` lists its script AND `scripts/invoked-as.mjs`,
# the dependency of that script.
#
# `invoked-as.mjs` is here for the same reason it is there, and the reason
# is specific rather than tidy: `isEntrypoint` decides whether `main()`
# runs at all, so a regression in it makes this checker exit 0 having
# measured nothing — the exact silent-green failure the rest of this gate
# exists to prevent.
#
# NOT listed: `scripts/__tests__/*.test.ts`. This job never runs vitest —
# it runs two `node scripts/*.mjs` commands — so a test-only edit cannot
# change this gate's verdict, and those tests already run on every PR in
# the root vitest `unit` project. `published-dist-gate.yml` and
# `spec-range-floors.yml` draw the same line.
- 'scripts/check-eager-closure-budget.mjs'
- 'scripts/render-budget-comment.mjs'
- 'scripts/invoked-as.mjs'

concurrency:
group: bundle-analysis-${{ github.event.pull_request.number || github.ref }}
Expand All@@ -31,6 +99,12 @@ jobs:
uses: actions/checkout@v7
with:
submodules: true
# Depth 2, not 1: on a `pull_request` run HEAD is the MERGE REF, whose
# first parent is the base commit this checkout was computed against.
# The ceiling-freshness half (objectui#6245) needs that commit's copy
# of the checker to tell a stale checkout from a PR that re-baselines
# on purpose, and one extra commit is cheaper than a second fetch.
fetch-depth: 2

- name: Enable Corepack
run: corepack enable
Expand DownExpand Up@@ -62,6 +136,72 @@ jobs:
- name: Build Console
run: pnpm --filter @object-ui/console build

# objectui#6245: `Bundle Analysis` is a required context, and GitHub does
# not re-run a PR's checks when the base branch moves — so a green verdict
# can be computed against ceiling constants `main` has since replaced, and
# the merge is then gated on a ceiling that no longer exists. Observed
# live: run 32804357171 started at 03:13:27Z, 6m50s after `0409b766d`
# lowered MAX_EAGER_CLOSURE_GZIP_BYTES from 4,086,000 to 3,345,000, and
# published `BUDGET_CLOSURE_BUDGET_KB: 3990.2` — the retired ceiling — as a
# success.
#
# This step hands the checker THREE readings of the constants: the two
# below plus its own checkout. Two would not do — a re-baseline PR differs
# from the base branch deliberately and has to stay landable — so the
# verdict fires only when the base branch moved a ceiling AND this checkout
# does not carry the move. See `evaluateCeilingFreshness`.
#
# `continue-on-error` is deliberate and is NOT a fail-open: when this step
# cannot produce both files the checker finds the variables unset and
# reports a freshness ERROR (exit 2), which fails the job. Letting the step
# itself fail the job would kill it before the bundle is measured, and a
# run that hides its numbers is how this gate loses its readers
# (objectui#3152).
#
# The paths go out through `$GITHUB_ENV` rather than a step `env:` block:
# `render-budget-comment.test.ts` walks every `NAME: ${{ steps.* }}` line
# in this file and requires the COMMENT RENDERER to read it, which is the
# right rule for the comment's inputs and the wrong one for the checker's.
- name: Resolve the base-branch ceiling constants
id: base_ceilings
if: ${{ github.event_name == 'pull_request' }}
continue-on-error: true
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
CHECKER='scripts/check-eager-closure-budget.mjs'
OUT="${RUNNER_TEMP}/eager-closure-ceilings"
mkdir -p "$OUT"

# The base commit the merge ref was computed against. `fetch-depth: 2`
# normally makes it a local object already; the fetch is the fallback
# for a merge ref GitHub recomputed against a different base.
if ! git cat-file -e "${PR_BASE_SHA}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "$PR_BASE_SHA"
fi
git cat-file -p "${PR_BASE_SHA}:${CHECKER}" > "$OUT/pr-base.partial"
mv "$OUT/pr-base.partial" "$OUT/pr-base.mjs"

# The base branch as it stands right now — what the merge would land on.
git fetch --no-tags --depth=1 origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
BASE_SHA=$(git rev-parse "refs/remotes/origin/${BASE_REF}")
git cat-file -p "${BASE_SHA}:${CHECKER}" > "$OUT/base.partial"
mv "$OUT/base.partial" "$OUT/base.mjs"

# Written only after BOTH `mv`s: a half-populated pair would let the
# checker compare against a file it thinks is the base branch.
{
echo "EAGER_CLOSURE_PR_BASE_SOURCE=$OUT/pr-base.mjs"
echo "EAGER_CLOSURE_BASE_SOURCE=$OUT/base.mjs"
echo "EAGER_CLOSURE_PR_BASE_SHA=$PR_BASE_SHA"
echo "EAGER_CLOSURE_BASE_SHA=$BASE_SHA"
echo "EAGER_CLOSURE_BASE_REF=$BASE_REF"
} >> "$GITHUB_ENV"

echo "Ceilings resolved: ${PR_BASE_SHA} (this checkout's base) vs ${BASE_SHA} (${BASE_REF} now)"

- name: Check console performance budget
id: budget
run: |
Expand DownExpand Up@@ -159,7 +299,9 @@ jobs:
# cannot name, which is what objectui#6230 was (the per-chunk and
# sensitivity halves both went unread). A fourth half added here must
# be wired through too; `render-budget-comment.test.ts` fails if it is
# not.
# not. The fourth half — ceiling freshness, objectui#6245 — arrived
# through exactly that door: it publishes `closure_freshness_status`,
# and is wired into the comment step below.
#
# Exit codes are distinct on
# purpose: 1 = over budget (a verdict about the bundle), 2 = no
Expand All@@ -173,7 +315,7 @@ jobs:

if [ "$CLOSURE_CODE" -eq 2 ]; then
echo "budget_status=error" >> "$GITHUB_OUTPUT"
echo "budget_message=The entry chunk measured ${GZIP_KB} KB, but the eager-closure gauge produced no trustworthy measurement — see the step log. This is a broken gauge, not a passing budget." >> "$GITHUB_OUTPUT"
echo "budget_message=The entry chunk measured ${GZIP_KB} KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either." >> "$GITHUB_OUTPUT"
exit 1
fi

Expand DownExpand Up@@ -258,6 +400,9 @@ jobs:
# half did — the reader has to open the job log to find out.
BUDGET_CLOSURE_CHUNK_STATUS: ${{ steps.budget.outputs.closure_chunk_status }}
BUDGET_CLOSURE_HEADROOM_STATUS: ${{ steps.budget.outputs.closure_headroom_status }}
# The freshness half (objectui#6245). Empty on a run it does not apply
# to; the renderer filters an empty half out rather than rendering it.
BUDGET_CLOSURE_FRESHNESS_STATUS: ${{ steps.budget.outputs.closure_freshness_status }}
BUDGET_STEP_OUTCOME: ${{ steps.budget.outcome }}
BUILD_PACKAGES_OUTCOME: ${{ steps.build_packages.outcome }}
run: node scripts/render-budget-comment.mjs > budget-comment.md
Expand Down
Loading
Loading