mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
ci(types): block new TypeScript 7 diagnostics (#10134)
This commit is contained in:
11
.github/workflows/quality.yml
vendored
11
.github/workflows/quality.yml
vendored
@@ -275,15 +275,22 @@ jobs:
|
||||
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
|
||||
# (the hybrid is the officially documented pattern). Isolated npx on purpose:
|
||||
# installing an alias package could collide node_modules/.bin/tsc with 6.x.
|
||||
# Promote to the blocking gate after ~1 week of parity with the step above.
|
||||
# The full result stays advisory while #8484 has a backlog. The blocking
|
||||
# base-relative ratchet immediately below rejects only diagnostics added by
|
||||
# the PR, so existing release debt does not block unrelated work.
|
||||
- name: Typecheck (core) — TS7 native shadow (advisory)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
RC=0
|
||||
START=$(date +%s)
|
||||
npx -y -p typescript@7 tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
|
||||
npm exec --yes --package=typescript@7.0.2 -- tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
|
||||
echo "[ts7-shadow] exit=$RC elapsed=$(( $(date +%s) - START ))s — the 6.x step above stays authoritative"
|
||||
exit $RC
|
||||
- name: Typecheck (core) — TS7 zero-new-diagnostics ratchet
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
env:
|
||||
TS7_BASE_REF: ${{ github.event.pull_request.base.sha }}
|
||||
run: npm run check:ts7-diagnostics-ratchet -- --base-ref "$TS7_BASE_REF"
|
||||
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
|
||||
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
|
||||
# selector returns __RUN_ALL__ — full-suite authority is the parallel
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
|
||||
"check:dashboard-typecheck": "node scripts/check/check-dashboard-typecheck.mjs",
|
||||
"check:open-sse-typecheck": "node scripts/check/check-open-sse-typecheck.mjs",
|
||||
"check:ts7-diagnostics-ratchet": "node scripts/check/check-ts7-diagnostics-ratchet.mjs",
|
||||
"backfill-aggregation": "node --import tsx src/scripts/backfillAggregation.ts",
|
||||
"env:sync": "node scripts/dev/sync-env.mjs",
|
||||
"test:integration": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/integration/*.test.ts \"tests/integration/combo-matrix/*.test.ts\"",
|
||||
|
||||
322
scripts/check/check-ts7-diagnostics-ratchet.mjs
Normal file
322
scripts/check/check-ts7-diagnostics-ratchet.mjs
Normal file
@@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env node
|
||||
// Blocks TypeScript 7 diagnostic regressions without requiring the existing
|
||||
// migration backlog to be clean. The PR base and checked-out head are compiled
|
||||
// with the same compiler and tsconfig, then compared as duplicate-preserving
|
||||
// multisets of: relative file | TS code | normalized message.
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const DEFAULT_TSCONFIG = "tsconfig.typecheck-core.json";
|
||||
const DEFAULT_COMPILER_VERSION = "7.0.2";
|
||||
const DIAGNOSTIC_START = /^(.+?)\((\d+),(\d+)\): error (TS\d+):\s*(.*)$/;
|
||||
const GLOBAL_DIAGNOSTIC_START = /^error (TS\d+):\s*(.*)$/;
|
||||
|
||||
function normalizeSlashes(value) {
|
||||
return String(value).replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function stripRoot(value, root) {
|
||||
const normalizedValue = normalizeSlashes(value);
|
||||
const normalizedRoot = normalizeSlashes(path.resolve(root)).replace(/\/$/, "");
|
||||
return normalizedValue === normalizedRoot
|
||||
? "."
|
||||
: normalizedValue.startsWith(`${normalizedRoot}/`)
|
||||
? normalizedValue.slice(normalizedRoot.length + 1)
|
||||
: normalizedValue;
|
||||
}
|
||||
|
||||
export function normalizeDiagnosticMessage(message, root = ROOT) {
|
||||
const normalizedRoot = normalizeSlashes(path.resolve(root)).replace(/\/$/, "");
|
||||
return normalizeSlashes(message)
|
||||
.replaceAll(normalizedRoot, "<repo>")
|
||||
.replace(/((?:[A-Za-z]:)?[^()\s]+\.(?:[cm]?[jt]sx?|json))\(\d+,\d+\)/gi, "$1")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Parse complete `tsc --pretty false` diagnostic blocks. */
|
||||
export function parseTscDiagnostics(raw, { root = ROOT } = {}) {
|
||||
const diagnostics = [];
|
||||
let current = null;
|
||||
|
||||
const flush = () => {
|
||||
if (!current) return;
|
||||
const message = normalizeDiagnosticMessage(current.messageLines.join("\n"), root);
|
||||
diagnostics.push({
|
||||
file: current.file,
|
||||
code: current.code,
|
||||
message,
|
||||
key: `${current.file}\u0000${current.code}\u0000${message}`,
|
||||
});
|
||||
current = null;
|
||||
};
|
||||
|
||||
for (const line of String(raw).split(/\r?\n/)) {
|
||||
const located = DIAGNOSTIC_START.exec(line);
|
||||
if (located) {
|
||||
flush();
|
||||
current = {
|
||||
file: stripRoot(located[1], root),
|
||||
code: located[4],
|
||||
messageLines: [located[5]],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const global = GLOBAL_DIAGNOSTIC_START.exec(line);
|
||||
if (global) {
|
||||
flush();
|
||||
current = { file: "<global>", code: global[1], messageLines: [global[2]] };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current && /^\s/.test(line) && line.trim()) current.messageLines.push(line);
|
||||
}
|
||||
flush();
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
export function toDiagnosticMultiset(diagnostics) {
|
||||
const counts = new Map();
|
||||
for (const diagnostic of diagnostics) {
|
||||
const entry = counts.get(diagnostic.key) ?? { ...diagnostic, count: 0 };
|
||||
entry.count += 1;
|
||||
counts.set(diagnostic.key, entry);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function diffDiagnosticMultisets(baseDiagnostics, headDiagnostics) {
|
||||
const base = toDiagnosticMultiset(baseDiagnostics);
|
||||
const head = toDiagnosticMultiset(headDiagnostics);
|
||||
const added = [];
|
||||
const removed = [];
|
||||
|
||||
for (const [key, entry] of head) {
|
||||
const baseCount = base.get(key)?.count ?? 0;
|
||||
if (entry.count > baseCount) {
|
||||
added.push({ ...entry, baseCount, headCount: entry.count, delta: entry.count - baseCount });
|
||||
}
|
||||
}
|
||||
for (const [key, entry] of base) {
|
||||
const headCount = head.get(key)?.count ?? 0;
|
||||
if (entry.count > headCount) {
|
||||
removed.push({ ...entry, baseCount: entry.count, headCount, delta: entry.count - headCount });
|
||||
}
|
||||
}
|
||||
|
||||
const order = (a, b) => a.key.localeCompare(b.key);
|
||||
return { added: added.sort(order), removed: removed.sort(order) };
|
||||
}
|
||||
|
||||
export function hasParserPrerequisite(diagnostics) {
|
||||
return diagnostics.some((diagnostic) => diagnostic.code === "TS1005");
|
||||
}
|
||||
|
||||
function argument(name, fallback = "") {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return spawnSync(command, args, {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveCommit(ref) {
|
||||
const result = run("git", ["rev-parse", "--verify", `${ref}^{commit}`]);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`cannot resolve base ref ${ref}: ${result.stderr.trim()}`);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function sameLockfile(baseRoot) {
|
||||
const head = path.join(ROOT, "package-lock.json");
|
||||
const base = path.join(baseRoot, "package-lock.json");
|
||||
return (
|
||||
fs.existsSync(head) &&
|
||||
fs.existsSync(base) &&
|
||||
fs.readFileSync(head).equals(fs.readFileSync(base))
|
||||
);
|
||||
}
|
||||
|
||||
function linkDependencies(baseRoot) {
|
||||
const source = path.join(ROOT, "node_modules");
|
||||
const target = path.join(baseRoot, "node_modules");
|
||||
if (!fs.existsSync(source)) throw new Error("node_modules is missing; run npm ci first");
|
||||
|
||||
fs.mkdirSync(target);
|
||||
for (const entry of fs.readdirSync(source)) {
|
||||
if (entry === "@omniroute") continue;
|
||||
fs.symlinkSync(path.join(source, entry), path.join(target, entry), "junction");
|
||||
}
|
||||
|
||||
const scope = path.join(target, "@omniroute");
|
||||
fs.mkdirSync(scope);
|
||||
fs.symlinkSync(path.join(baseRoot, "open-sse"), path.join(scope, "open-sse"), "junction");
|
||||
fs.symlinkSync(
|
||||
path.join(baseRoot, "packages", "browser-pool"),
|
||||
path.join(scope, "browser-pool"),
|
||||
"junction"
|
||||
);
|
||||
}
|
||||
|
||||
function installBaseDependencies(baseRoot) {
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const result = run(
|
||||
npm,
|
||||
["ci", "--ignore-scripts", "--prefer-offline", "--no-audit", "--no-fund"],
|
||||
{ cwd: baseRoot, stdio: "inherit" }
|
||||
);
|
||||
if (result.status !== 0) throw new Error(`npm ci for the base worktree exited ${result.status}`);
|
||||
}
|
||||
|
||||
function runTypeScript(root, tsconfig, compilerVersion) {
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const result = run(
|
||||
npm,
|
||||
[
|
||||
"exec",
|
||||
"--yes",
|
||||
`--package=typescript@${compilerVersion}`,
|
||||
"--",
|
||||
"tsc",
|
||||
"--pretty",
|
||||
"false",
|
||||
"--noEmit",
|
||||
"-p",
|
||||
tsconfig,
|
||||
],
|
||||
{ cwd: root }
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
||||
const diagnostics = parseTscDiagnostics(output, { root });
|
||||
if (result.status !== 0 && diagnostics.length === 0) {
|
||||
throw new Error(
|
||||
`TypeScript exited ${result.status} without a parseable diagnostic:\n${output}`
|
||||
);
|
||||
}
|
||||
return { diagnostics, status: result.status ?? 0 };
|
||||
}
|
||||
|
||||
function formatEntry(entry) {
|
||||
return `${entry.file} ${entry.code}: ${entry.message} (${entry.baseCount} -> ${entry.headCount})`;
|
||||
}
|
||||
|
||||
function appendSummary({ baseRef, baseCount, headCount, added, removed, skipped }) {
|
||||
const summary = process.env.GITHUB_STEP_SUMMARY;
|
||||
if (!summary) return;
|
||||
const lines = [
|
||||
"## TypeScript 7 zero-new-diagnostics ratchet",
|
||||
"",
|
||||
`- Base: \`${baseRef}\` (${baseCount} diagnostics)`,
|
||||
`- Head: ${headCount} diagnostics`,
|
||||
`- Added: ${added.reduce((sum, entry) => sum + entry.delta, 0)}`,
|
||||
`- Removed: ${removed.reduce((sum, entry) => sum + entry.delta, 0)}`,
|
||||
];
|
||||
if (skipped) lines.push("- Status: parser prerequisite unresolved; comparison is advisory");
|
||||
if (added.length) {
|
||||
lines.push("", "### Added diagnostics", "", ...added.map((entry) => `- ${formatEntry(entry)}`));
|
||||
}
|
||||
fs.appendFileSync(summary, `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const baseRef = argument("--base-ref", process.env.TS7_BASE_REF ?? "");
|
||||
const tsconfig = argument("--tsconfig", DEFAULT_TSCONFIG);
|
||||
const compilerVersion = argument("--compiler-version", DEFAULT_COMPILER_VERSION);
|
||||
if (!baseRef) {
|
||||
console.log("[ts7-ratchet] SKIP — --base-ref is required outside a pull request");
|
||||
return 0;
|
||||
}
|
||||
if (!fs.existsSync(path.join(ROOT, tsconfig))) {
|
||||
throw new Error(`tsconfig not found: ${tsconfig}`);
|
||||
}
|
||||
|
||||
const baseCommit = resolveCommit(baseRef);
|
||||
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ts7-ratchet-"));
|
||||
const baseRoot = path.join(temporaryRoot, "base");
|
||||
let worktreeAdded = false;
|
||||
|
||||
try {
|
||||
const add = run("git", ["worktree", "add", "--detach", baseRoot, baseCommit]);
|
||||
if (add.status !== 0) throw new Error(`cannot create base worktree: ${add.stderr.trim()}`);
|
||||
worktreeAdded = true;
|
||||
|
||||
if (sameLockfile(baseRoot)) linkDependencies(baseRoot);
|
||||
else installBaseDependencies(baseRoot);
|
||||
|
||||
console.log(
|
||||
`[ts7-ratchet] TypeScript ${compilerVersion}; base=${baseCommit}; config=${tsconfig}`
|
||||
);
|
||||
const base = runTypeScript(baseRoot, tsconfig, compilerVersion);
|
||||
const head = runTypeScript(ROOT, tsconfig, compilerVersion);
|
||||
const { added, removed } = diffDiagnosticMultisets(base.diagnostics, head.diagnostics);
|
||||
const parserBlocked = hasParserPrerequisite(base.diagnostics);
|
||||
|
||||
console.log(`ts7DiagnosticsBase=${base.diagnostics.length}`);
|
||||
console.log(`ts7DiagnosticsHead=${head.diagnostics.length}`);
|
||||
console.log(`ts7DiagnosticsAdded=${added.reduce((sum, entry) => sum + entry.delta, 0)}`);
|
||||
console.log(`ts7DiagnosticsRemoved=${removed.reduce((sum, entry) => sum + entry.delta, 0)}`);
|
||||
|
||||
appendSummary({
|
||||
baseRef: baseCommit,
|
||||
baseCount: base.diagnostics.length,
|
||||
headCount: head.diagnostics.length,
|
||||
added,
|
||||
removed,
|
||||
skipped: parserBlocked,
|
||||
});
|
||||
|
||||
if (parserBlocked) {
|
||||
console.warn(
|
||||
"[ts7-ratchet] SKIP — the release base still has TS1005 parser diagnostics. " +
|
||||
"Resolve #10094 before making this comparison blocking; those errors are not accepted as baseline."
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (added.length) {
|
||||
console.error(
|
||||
`[ts7-ratchet] FAIL — the PR adds ${added.reduce((sum, entry) => sum + entry.delta, 0)} ` +
|
||||
`normalized TypeScript 7 diagnostic(s):\n${added.map((entry) => ` ✗ ${formatEntry(entry)}`).join("\n")}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[ts7-ratchet] OK — no new normalized diagnostics; ` +
|
||||
`${removed.reduce((sum, entry) => sum + entry.delta, 0)} removed.`
|
||||
);
|
||||
return 0;
|
||||
} finally {
|
||||
if (worktreeAdded) {
|
||||
const remove = run("git", ["worktree", "remove", "--force", baseRoot]);
|
||||
if (remove.status !== 0) {
|
||||
console.warn(`[ts7-ratchet] WARN — temporary worktree cleanup: ${remove.stderr.trim()}`);
|
||||
}
|
||||
run("git", ["worktree", "prune"]);
|
||||
}
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
try {
|
||||
process.exitCode = main();
|
||||
} catch (error) {
|
||||
console.error(`[ts7-ratchet] FAIL — ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exitCode = 2;
|
||||
}
|
||||
}
|
||||
93
tests/unit/build/check-ts7-diagnostics-ratchet.test.ts
Normal file
93
tests/unit/build/check-ts7-diagnostics-ratchet.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
diffDiagnosticMultisets,
|
||||
hasParserPrerequisite,
|
||||
parseTscDiagnostics,
|
||||
} from "../../../scripts/check/check-ts7-diagnostics-ratchet.mjs";
|
||||
|
||||
test("line and column movement does not create a new diagnostic", () => {
|
||||
const base = parseTscDiagnostics(
|
||||
"src/example.ts(10,2): error TS2322: Type 'string' is not assignable to type 'number'.\n"
|
||||
);
|
||||
const head = parseTscDiagnostics(
|
||||
"src/example.ts(80,19): error TS2322: Type 'string' is not assignable to type 'number'.\n"
|
||||
);
|
||||
|
||||
assert.deepEqual(diffDiagnosticMultisets(base, head), { added: [], removed: [] });
|
||||
});
|
||||
|
||||
test("the full normalized message distinguishes different diagnostics at one file and code", () => {
|
||||
const base = parseTscDiagnostics(
|
||||
"src/example.ts(1,1): error TS2339: Property 'old' does not exist on type 'Value'.\n"
|
||||
);
|
||||
const head = parseTscDiagnostics(
|
||||
"src/example.ts(1,1): error TS2339: Property 'new' does not exist on type 'Value'.\n"
|
||||
);
|
||||
const diff = diffDiagnosticMultisets(base, head);
|
||||
|
||||
assert.equal(diff.added.length, 1);
|
||||
assert.equal(diff.removed.length, 1);
|
||||
assert.match(diff.added[0].message, /Property 'new'/);
|
||||
});
|
||||
|
||||
test("duplicate diagnostics are compared as a multiset, not a set", () => {
|
||||
const line = "src/example.ts(1,1): error TS2304: Cannot find name 'missing'.\n";
|
||||
const base = parseTscDiagnostics(line);
|
||||
const head = parseTscDiagnostics(line + line.replace("(1,1)", "(2,1)"));
|
||||
const { added, removed } = diffDiagnosticMultisets(base, head);
|
||||
|
||||
assert.equal(added.length, 1);
|
||||
assert.equal(added[0].delta, 1);
|
||||
assert.equal(added[0].baseCount, 1);
|
||||
assert.equal(added[0].headCount, 2);
|
||||
assert.equal(removed.length, 0);
|
||||
});
|
||||
|
||||
test("multiline diagnostics normalize whitespace and nested coordinates", () => {
|
||||
const base = parseTscDiagnostics(
|
||||
"src/example.ts(1,1): error TS2345: Argument is invalid.\n" +
|
||||
" The expected type comes from property 'value' declared at src/types.ts(12,4).\n"
|
||||
);
|
||||
const head = parseTscDiagnostics(
|
||||
"src/example.ts(99,8): error TS2345: Argument is invalid.\n" +
|
||||
" The expected type comes from property 'value' declared at src/types.ts(20,9).\n"
|
||||
);
|
||||
|
||||
assert.deepEqual(diffDiagnosticMultisets(base, head), { added: [], removed: [] });
|
||||
});
|
||||
|
||||
test("an absolute worktree root is normalized out of file paths and messages", () => {
|
||||
const base = parseTscDiagnostics(
|
||||
"/tmp/base/src/example.ts(1,1): error TS2307: Cannot find module '/tmp/base/src/missing'.\n",
|
||||
{ root: "/tmp/base" }
|
||||
);
|
||||
const head = parseTscDiagnostics(
|
||||
"/tmp/head/src/example.ts(2,3): error TS2307: Cannot find module '/tmp/head/src/missing'.\n",
|
||||
{ root: "/tmp/head" }
|
||||
);
|
||||
|
||||
assert.deepEqual(diffDiagnosticMultisets(base, head), { added: [], removed: [] });
|
||||
assert.equal(base[0].file, "src/example.ts");
|
||||
});
|
||||
|
||||
test("reductions pass and report the removed multiplicity", () => {
|
||||
const line = "src/example.ts(1,1): error TS2304: Cannot find name 'missing'.\n";
|
||||
const base = parseTscDiagnostics(line + line.replace("(1,1)", "(2,1)"));
|
||||
const head = parseTscDiagnostics(line);
|
||||
const { added, removed } = diffDiagnosticMultisets(base, head);
|
||||
|
||||
assert.equal(added.length, 0);
|
||||
assert.equal(removed.length, 1);
|
||||
assert.equal(removed[0].delta, 1);
|
||||
});
|
||||
|
||||
test("TS1005 makes the unresolved parser prerequisite explicit", () => {
|
||||
const diagnostics = parseTscDiagnostics(
|
||||
"open-sse/config/providers/gateways.ts(10,2): error TS1005: ',' expected.\n"
|
||||
);
|
||||
|
||||
assert.equal(hasParserPrerequisite(diagnostics), true);
|
||||
assert.equal(hasParserPrerequisite([]), false);
|
||||
});
|
||||
@@ -55,6 +55,7 @@ test("fast-gates carries the deterministic ratchets and security scanners from t
|
||||
"secrets vuln-ratchet workflows openapi-breaking",
|
||||
"typecheck:core",
|
||||
"check:dashboard-typecheck",
|
||||
"check:ts7-diagnostics-ratchet",
|
||||
]) {
|
||||
assert.ok(block.includes(needle), `fast-gates must contain "${needle}"`);
|
||||
}
|
||||
@@ -87,6 +88,20 @@ test("the complexity ratchet stays on the release rail (G0's written validation
|
||||
);
|
||||
});
|
||||
|
||||
test("the TS7 shadow and zero-new-diagnostics ratchet use one pinned compiler and core scope", () => {
|
||||
const block = jobBlock("fast-gates");
|
||||
assert.match(block, /typescript@7\.0\.2/, "TS7 diagnostics must use the reviewed compiler");
|
||||
assert.match(
|
||||
block,
|
||||
/TS7_BASE_REF:\s*\$\{\{ github\.event\.pull_request\.base\.sha \}\}/,
|
||||
"the ratchet must compare against the exact pull-request base commit"
|
||||
);
|
||||
assert.ok(
|
||||
block.includes("tsconfig.typecheck-core.json"),
|
||||
"the advisory shadow must use the same core scope as the blocking ratchet"
|
||||
);
|
||||
});
|
||||
|
||||
test("the new gates run on jobs that stay pinned to hosted runners", () => {
|
||||
// Complements tests/unit/vps-runner-variable-scope.test.ts: neither gate job
|
||||
// may pick up a USE_VPS_RUNNER switch (setup-node measured 20m06s on .113 vs 16s hosted).
|
||||
|
||||
Reference in New Issue
Block a user