Files
OmniRoute/tests/unit/vps-runner-variable-scope.test.ts
diegosouzapw 0ae87c8b67 fix(ci): finish gap 19 — pin fast-gates and give USE_VPS_RUNNER one meaning
This was left deliberately partial because `fast-gates` had never been measured,
and guessing is what produced gap 19 in the first place. Measured now, and the
evidence is cleaner than expected:

    fast-gates, 160 quality.yml runs .... ZERO self-hosted samples
                                          every non-skipped one is "GitHub Actions NNNN"
    median duration, 72 successful runs .. 5.6 min hosted

The classifier is not at fault — in the same window ci.yml's Build demonstrably
ran on omniroute-113-7 and omniroute-113-6, so self-hosted runs are visible when
they happen. The USE_VPS_RUNNER expression on this job was dead configuration.

And had it ever fired it would have inherited the measured penalty, because this
job's first two steps are exactly the bottleneck:

    actions/setup-node on .113 with 4 concurrent runners .... 20m06s
    actions/setup-node hosted ..............................     16s

So it is pinned rather than switched, and the second variable the gap proposed
(USE_VPS_RUNNER_BUILD / _TESTS) turns out to be unnecessary. After this the
variable governs exactly five jobs, all of them build-like:

    ci.yml:build · quality.yml:build · npm-publish:publish
    nightly-release-green: release-green, main-green

One variable, one meaning: "this job needs the .113's memory". A guard test pins
that — it fails if the variable is ever attached to a test-like job again, and it
also asserts the build KEEPS it, so nobody closes this gap by removing the
variable outright.

    node --import tsx/esm --test tests/unit/vps-runner-variable-scope.test.ts   # 3 pass
    check:workflows --ratchet → 178, baseline 190
2026-07-30 12:35:36 -03:00

83 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Guard for what `USE_VPS_RUNNER` is allowed to govern (gap 19).
*
* One variable used to switch the build AND the test jobs, which want OPPOSITE machines. The
* build needs the .113's RAM — the production `next build` wants 1820 GB and a hosted runner
* has 16, which killed the same step nine times in the v3.8.49 cycle. The test jobs need the
* hosted runner's link:
*
* actions/setup-node on .113, 4 concurrent runners ..... 20m06s
* actions/setup-node hosted ............................ 16s
* the tests themselves .................. 2m54 vs 2m31 — a tie
*
* So there was no setting that served both, and flipping the variable for a release traded a
* working build for twenty-minute test jobs.
*
* The fix is not a second variable. Self-hosted is strictly worse for anything whose cost is
* dominated by `setup-node` + `npm ci`, so those jobs are pinned to hosted and the variable now
* means exactly one thing: "this job needs the .113's memory".
*
* `fast-gates` was the last holdout and the evidence for pinning it is unusually clean: across
* 160 quality.yml runs it NEVER landed on a self-hosted runner — every non-skipped sample is
* `GitHub Actions NNNN`. That was dead configuration, not a working escape hatch, and the
* classifier is not at fault: ci.yml's Build ran on omniroute-113-7 and -6 in the same window.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const workflowDir = path.join(repoRoot, ".github/workflows");
/** Map every `runs-on:` line mentioning USE_VPS_RUNNER back to the job that owns it. */
function jobsSwitchedByVpsVar(): Array<{ file: string; job: string }> {
const out: Array<{ file: string; job: string }> = [];
for (const name of fs.readdirSync(workflowDir)) {
if (!name.endsWith(".yml") && !name.endsWith(".yaml")) continue;
const lines = fs.readFileSync(path.join(workflowDir, name), "utf-8").split("\n");
let job = "";
for (const line of lines) {
const m = /^ {2}([A-Za-z0-9_-]+):\s*$/.exec(line);
if (m) job = m[1];
if (line.includes("USE_VPS_RUNNER") && line.includes("runs-on")) {
out.push({ file: name, job });
}
}
}
return out;
}
/** Jobs whose cost is dominated by setup-node + npm ci, never by memory. */
const TEST_LIKE = /(^|-)(test|tests|vitest|unit|gates)($|-)/i;
test("USE_VPS_RUNNER never governs a test-like job", () => {
const offenders = jobsSwitchedByVpsVar().filter((j) => TEST_LIKE.test(j.job));
assert.deepEqual(
offenders.map((j) => `${j.file}:${j.job}`),
[],
"self-hosted is strictly worse for these — setup-node measured 20m06s there vs 16s hosted, " +
"while the tests themselves tie. Pin the job to ubuntu-latest instead of switching it."
);
});
test("it still governs the build-like jobs — the ones that actually need the RAM", () => {
const jobs = jobsSwitchedByVpsVar().map((j) => j.job);
assert.ok(
jobs.includes("build"),
"the build is the whole reason the variable exists: 18-20 GB working set vs a 16 GB hosted runner"
);
assert.ok(jobs.includes("publish"), "npm-publish falls back to a full build when the artifact is missing");
});
test("fast-gates specifically stays hosted", () => {
// The holdout. 160 runs, zero self-hosted samples — dead configuration, and it would have
// inherited the setup-node penalty the day it fired.
const wf = fs.readFileSync(path.join(workflowDir, "quality.yml"), "utf-8");
const block = wf.split(/^ {2}fast-gates:\s*$/m)[1] ?? "";
const runsOn = /^ {4}runs-on:\s*(.+)$/m.exec(block);
assert.ok(runsOn, "fast-gates must declare runs-on");
assert.match(runsOn[1].trim(), /^ubuntu-latest$/, "fast-gates must be pinned, not switched");
});