diff --git a/docs/ci.md b/docs/ci.md index b5950f431d21..dff5bc9ee6ee 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -37,7 +37,7 @@ dispatch. | `pnpm-store-warmup` | Warm the lockfile-pinned Actions cache for fork PRs, manual runs, and same-repo docs-only PRs | Node or docs-check lanes without an exact-cache writer | | `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, startup memory, and embedded built-artifact checks | Node-relevant changes | | `control-ui-i18n` | Verify generated Control UI locale bundles, metadata, and translation memory; advisory on automatic runs, blocking on manual release CI | Control UI i18n-relevant changes and manual CI | -| `checks-fast-core` | Fast Linux correctness lanes: max-lines and assertion-safety baseline ratchets, bundled + protocol, Bun launcher, and the CI-routing fast task | Node-relevant changes | +| `checks-fast-core` | Fast Linux correctness lanes: environment-variable, max-lines, and assertion-safety baseline ratchets, bundled + protocol, Bun launcher, and the CI-routing fast task | Node-relevant changes | | `qa-smoke-ci-profile` | Self-contained balanced parts of the automatic QA Smoke coverage set; one private-overlay build per part (the smoke set has no docker-lane or Control UI scenarios; the run step fails closed if one returns) | Pushes and manual runs; PRs only on QA-owned surfaces | | `checks-fast-contracts-plugins-*` | Two weighted plugin contract shards | Node-relevant changes | | `checks-fast-contracts-channels-*` | Two weighted channel contract shards | Node-relevant changes | @@ -173,7 +173,7 @@ Barnacle treats bug-labeled issues as verification candidates rather than inacti Ordinary manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI/native app i18n. Node 22 compatibility runs in Full Release Validation and manual dispatches only; push and pull request CI skip it. The exact-head `release_gate` fallback instead keeps the pull request's macOS and iOS scope, including conservative release screenshot capture for screenshot-pipeline owners. Automatic source PRs verify native extraction inventory and Android/Apple localization safety without requiring translated or platform-generated output in the same PR. The serialized Native App Locale Refresh workflow rebuilds those artifacts in one isolated PR and enables exact-head auto-merge after required checks pass. Full native parity remains blocking for generated-artifact PRs, manual CI, Full Release Validation, and release prep. Control UI locale parity remains advisory on automatic PR and `main` runs and blocking on manual/release CI. Standalone manual CI dispatches run Android only with `include_android=true` (the `release_gate` input also forces Android); the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled. -PR baseline ratchets derive their comparison state from the checked-out synthetic merge tree and verify its head parent against the event head. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref; ratchet baselines are compared with the target's merge base against the default-branch head resolved for that run. The `release_gate` input is an exact-SHA maintainer fallback for capacity-stalled PR CI: it requires `target_ref` to be a full commit SHA that matches the dispatched branch head and `pull_request_number` to identify the open PR whose merge tree is validated. +PR baseline ratchets derive their comparison state from the checked-out synthetic merge tree and verify its head parent against the event head. The max-lines entry chains the environment-variable budget with the same fork-point ref before the assertion-safety check, so production source growth cannot first surface on `main`. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref; ratchet baselines are compared with the target's merge base against the default-branch head resolved for that run. The `release_gate` input is an exact-SHA maintainer fallback for capacity-stalled PR CI: it requires `target_ref` to be a full commit SHA that matches the dispatched branch head and `pull_request_number` to identify the open PR whose merge tree is validated. ```bash gh workflow run ci.yml --ref release/YYYY.M.PATCH diff --git a/scripts/check-max-lines-ratchet.mts b/scripts/check-max-lines-ratchet.mts index 1e3d20e35316..a8d6d7d12d9a 100644 --- a/scripts/check-max-lines-ratchet.mts +++ b/scripts/check-max-lines-ratchet.mts @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import ts from "typescript"; +import { main as checkEnvVarCount } from "./check-env-var-count.mts"; import { resolveRatchetBase } from "./lib/ratchet-base.mts"; const BASELINE_PATH = "config/max-lines-baseline.txt"; @@ -314,6 +315,11 @@ function parseArgs(argv: string[]) { return args; } +function envVarCountArgs(argv: string[]) { + const args = parseArgs(argv); + return [...(args.staged ? ["--staged"] : []), ...(args.base ? ["--base", args.base] : [])]; +} + function printEntries(title: string, entries: string[]) { console.error(title); for (const entry of entries) { @@ -381,6 +387,22 @@ export function main(root = process.cwd(), argv: string[] = process.argv.slice(2 } } -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - process.exitCode = main(); +function runBaselineRatchets(root = process.cwd(), argv: string[] = process.argv.slice(2)) { + const maxLinesStatus = main(root, argv); + if (maxLinesStatus !== 0) { + return maxLinesStatus; + } + try { + // CI invokes this entry with its frozen fork-point ref. Carry the same snapshot + // into the env budget so every baseline ratchet judges one tested tree. + checkEnvVarCount(envVarCountArgs(argv), root); + return 0; + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + return 1; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exitCode = runBaselineRatchets(); } diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 86505449f08a..092aaf047cc7 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -5725,8 +5725,9 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" ).toContain("--only=core --split-core --core-stripe=${{ matrix.stripe }}/5 --threads=1"); }); - it("runs both baseline ratchets against the exact tested tree", () => { + it("runs all baseline ratchets against the exact tested tree", () => { const workflow = readCiWorkflow(); + const maxLinesRatchet = readFileSync("scripts/check-max-lines-ratchet.mts", "utf8"); const checksFastJob = workflow.jobs["checks-fast-core"]; const checksFastSteps = checksFastJob.steps; const checkout = checksFastSteps.find((step: WorkflowStep) => step.name === "Checkout"); @@ -5827,6 +5828,10 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" expect(checksFastRun.run).not.toContain("+${merge_base}:refs/remotes/origin/ci-ratchet-base"); expect(checksFastRun.run).toContain('pnpm check:max-lines-ratchet --base "$base_ref"'); expect(checksFastRun.run).toContain('pnpm check:assertion-safety --base "$base_ref"'); + expect(maxLinesRatchet).toContain( + 'import { main as checkEnvVarCount } from "./check-env-var-count.mts";', + ); + expect(maxLinesRatchet).toContain("checkEnvVarCount(envVarCountArgs(argv), root);"); expect(checksFastRun.run).toContain( 'if [[ "${RATCHET_RELEASE_MERGE_TREE:-}" == "true" ]]; then', );