@rightkit/release 0.6.15 → 0.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/admission-host.mjs +9 -5
- package/app-closure.mjs +9 -3
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +469 -102
- package/bin/rightkit-linux-setup.mjs +15 -0
- package/broker-admission.mjs +184 -0
- package/broker-wire.mjs +15 -3
- package/build-launch.mjs +4 -1
- package/build-ownership.mjs +125 -36
- package/build-release.mjs +39 -7
- package/cargo-contract.mjs +4 -1
- package/cargo-guard.mjs +25 -5
- package/cargo-target-shim.mjs +56 -7
- package/clippy-route.mjs +8 -7
- package/crate-graph.mjs +5 -1
- package/credential-provider.mjs +174 -0
- package/deterministic-scans.mjs +256 -41
- package/development.mjs +8 -0
- package/disk-policy.mjs +2 -2
- package/gate-executor.mjs +5 -3
- package/gate-stages.mjs +34 -3
- package/gradle-lock.mjs +206 -0
- package/heavy-command.mjs +33 -2
- package/linux-artifacts.mjs +2 -1
- package/linux-setup.mjs +106 -0
- package/local-publish.mjs +21 -10
- package/local-qualify.mjs +9 -3
- package/managed-cargo.mjs +54 -18
- package/npm-license-notices.mjs +49 -1
- package/ownership-lockfiles.mjs +27 -4
- package/ownership.mjs +5 -1
- package/package.json +17 -11
- package/protected-credentials.mjs +13 -5
- package/publication-gate.mjs +9 -4
- package/python-lock.mjs +209 -0
- package/qa-admission-policy.json +32 -0
- package/qa-layout.mjs +229 -0
- package/qa-runner.mjs +156 -0
- package/release-artifact-path.mjs +29 -3
- package/release-config.mjs +1 -1
- package/release-qa-command.mjs +58 -8
- package/release-qa-policy.mjs +5 -3
- package/release-source-identity.mjs +1 -0
- package/release.mjs +10 -2
- package/rightkit-ownership.json +1465 -79
- package/rightkit-registry-resolver.mjs +7 -3
- package/rightkit-versions.json +173 -36
- package/runtime-budget-classes.json +4 -0
- package/runtime-budgets.mjs +5 -1
- package/shell-contract.mjs +1 -1
- package/snapshot-broker.mjs +107 -9
- package/snapshot-check.mjs +2 -0
- package/snapshot-request.mjs +20 -3
- package/snapshot.mjs +11 -4
- package/stage-commands.mjs +2 -1
- package/stage-ledger.mjs +15 -0
- package/stage-sidecar.mjs +17 -3
- package/suite-signing.mjs +40 -4
- package/swift-lock.mjs +334 -0
- package/test-command.mjs +13 -3
- package/tool-build.mjs +156 -0
- package/unit-gate.mjs +9 -2
- package/windows-identity-plan.mjs +119 -0
- package/windows-identity.mjs +136 -0
package/bin/rightkit-gate.mjs
CHANGED
|
@@ -61,8 +61,12 @@
|
|
|
61
61
|
// rust.<crate>.testInputs lists the paths they read, for npm, node and docs scans, and whenever a reference cannot be resolved
|
|
62
62
|
// statically) are all identical. Stored in <git common dir>/rightkit-gate-cache; a hit prints
|
|
63
63
|
// `cached` and is listed in the result's cacheHits. Failures, deferrals and class A scans are never cached.
|
|
64
|
-
// RK_GATE_JOBS (default 4) sets class A scan concurrency.
|
|
65
|
-
// broker
|
|
64
|
+
// RK_GATE_JOBS (default 4) sets class A scan concurrency. Class B on a broker host (rightkit#1367 step 2): the gate does not count builds; every
|
|
65
|
+
// component's broker-carried scans (cargo, rustright, carried Swift, and npm suites that declare a broker call) start without a limit, each component in plan order,
|
|
66
|
+
// and the broker queues and orders them. A carried scan's run limit is a stopwatch that stops while the broker holds all its requests QUEUED (broker-admission.mjs),
|
|
67
|
+
// so queue time is neither charged to the scan nor bounded by a gate allowance. Only class B suites the broker never sees (npm or node tests run on this host)
|
|
68
|
+
// keep a local limit of 3, one slot per suite. On a host with no broker RK_GATE_CLASS_B_JOBS (default 3) sets how many components' class B scans run at once; one
|
|
69
|
+
// component's scans stay in order (rightkit#1285).
|
|
66
70
|
// A crate whose ci/gate.config.json entry says `quietTests: true` has its test scans run last,
|
|
67
71
|
// one at a time, under the RustRight quiet lease (`rustright quiet acquire`, RUSTRIGHT_QUIET_LEASE in their environment).
|
|
68
72
|
// SIGINT/SIGTERM stop scheduling, kill every running scan's process tree and exit non-zero listing the scans not run.
|
|
@@ -74,13 +78,16 @@ import os from "node:os";
|
|
|
74
78
|
import path from "node:path";
|
|
75
79
|
import { FOREIGN_PRE_COMMIT, retireHooksIn } from "../hook-retirement.mjs";
|
|
76
80
|
import { fileURLToPath } from "node:url";
|
|
77
|
-
import { BIOME_CONFIG_FILES, ESLINT_CONFIG_FILES, WORKSPACE_RUST_FILE, ancestorConfigChanged, ancestorDirsOf, canonicalJson, defaultSpawn, defaultSpawnAsync, digestScanSet, fileScanCache, findRustfmt, metadataArgs, planScans, runScans, scanToolPolicyFromPipeline, scanDeferrals, scanFailures } from "../deterministic-scans.mjs";
|
|
81
|
+
import { BIOME_CONFIG_FILES, ESLINT_CONFIG_FILES, LOCAL_CLASS_B_JOBS, WORKSPACE_RUST_FILE, ancestorConfigChanged, ancestorDirsOf, canonicalJson, defaultSpawn, defaultSpawnAsync, digestScanSet, fileScanCache, findRustfmt, metadataArgs, planScans, runScans, scanToolPolicyFromPipeline, scanDeferrals, scanFailures } from "../deterministic-scans.mjs";
|
|
78
82
|
import { changedLockNames, parseLock, reach } from "../lock-graph.mjs";
|
|
79
83
|
import { resolveExecutor } from "../gate-executor.mjs";
|
|
84
|
+
// The gate's own executor: an outer gate's cargo wrapper on PATH is never this gate's cargo (gate-executor.mjs `gateWrappers`).
|
|
85
|
+
const gateResolveExecutor = (args) => resolveExecutor({ ...args, gateWrappers: true });
|
|
80
86
|
import { ALL_UNITS_ERRORS, ALL_UNITS_EXCEPTIONS_FILE, BROKER_EXCEPTIONS_FILE, BROKER_EXCEPTION_ERRORS, CARGO_WORKSPACE_LIST_FILE, CARGO_WORKSPACE_LIST_KEY, RIGHTKIT_IDENTITY_FILE, authorizeAllUnits, authorizeNoBroker, checkCargoWorkspaceList, describeAllUnitsException, describeBrokerException, isRightKitIdentity, unitsOfSelector } from "../broker-exception.mjs";
|
|
81
87
|
import { SIGNING_REFUSAL_CODES, suiteSigningRefusals } from "../suite-signing-policy.mjs";
|
|
82
88
|
import { gitListRustFiles } from "../signing-files.mjs";
|
|
83
|
-
import { LINUX_BROKER_ROUTE, NO_BROKER_ENV, directPlatform, linuxBrokerDeclared } from "../managed-cargo.mjs";
|
|
89
|
+
import { LINUX_BROKER_ROUTE, NO_BROKER_ENV, directPlatform, linuxBrokerDeclared, managedRuntimeClient, realCargoOf } from "../managed-cargo.mjs";
|
|
90
|
+
import { createBrokerLive } from "../broker-admission.mjs";
|
|
84
91
|
import { bindingOf, compileOutputsRecord, compileReuseProblem, endRun, readReceipt, RECEIPT_SCHEMA, receiptKey, sameBinding, sameSource, sealedFor, stagesTo, startRun, STAGES, STAGE_SOURCES, writeReceipt } from "../gate-stages.mjs";
|
|
85
92
|
import { spawnSync as spawnInherit } from "../background-process.mjs";
|
|
86
93
|
import { readDiskPolicy, scratchRoots } from "../disk-policy.mjs";
|
|
@@ -103,7 +110,7 @@ export const PARTIAL_EXIT = 3;
|
|
|
103
110
|
/** The platforms a door run can package: desktop (signed, sealed, installed QA, publisher upload) and mobile (signed, sealed, store test track). */
|
|
104
111
|
export const PLATFORMS = Object.freeze(["mac", "win", "ios", "android"]);
|
|
105
112
|
export const MOBILE_PLATFORMS = Object.freeze(new Set(["ios", "android"]));
|
|
106
|
-
const USAGE = `usage: rightkit-gate --to check|compile|package|qualify|release [--platform mac|win|ios|android] [--tier patch|update] [--receipt-in <file>] [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify|release --unit npm:@rightkit/<name>|cargo:rightkit-<name> [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify --units-file <json {"units":[<selector>,...]} written for this run> | rightkit-gate --to check|compile|package|qualify --all-units-exception <id> (RightKit only: its unit is named on every run, or every unit by Adrian's entry in ci/all-units-exceptions.json of the commit) | rightkit-gate --to check|compile|package|qualify [--unit <selector>] --no-broker <exception-id> (Mac and Dell only; Adrian's recorded exception in .rightkit/broker-exceptions.json of the commit) | rightkit-gate --to dev|test [--commit <rev>] [-- <right-release args>] | rightkit-gate --to check|compile|package|qualify|dev|test --route [--platform mac|win|ios|android] [--commit <rev>] | rightkit-gate stage --platform mac|win [--tier patch|update] | rightkit-gate [--base <ref>] [--plan] [--no-cache] | rightkit-gate install-hook | rightkit-gate env-digest | rightkit-gate snapshot [--commit <rev>] [--verify [--env <sha256>]] [--json] | rightkit-gate live-scans --repo <root> | rightkit-gate clippy [<cargo clippy args>] | rightkit-gate env-digest [--snapshot <dir> --commit <sha>] | rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] | rightkit-gate check --commit <sha> --full-a --snapshot <dir> | --version`;
|
|
113
|
+
const USAGE = `usage: rightkit-gate --to check|compile|package|qualify|release [--platform mac|win|ios|android] [--tier patch|update] [--receipt-in <file>] [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify|release --unit npm:@rightkit/<name>|cargo:rightkit-<name> [--receipt-out <file>] | rightkit-gate --to tool --unit cargo:rightkit-qa|cargo:rightkit-credentials [--no-broker <exception-id>] [--receipt-out <file>] [--commit <rev>] (builds the unit's release binary from the commit and enrols it: cargo:rightkit-qa as the managed rightkit-qa runner, cargo:rightkit-credentials as the managed OS credential provider updater signing runs; Mac and Windows) | rightkit-gate --to check|compile|package|qualify --units-file <json {"units":[<selector>,...]} written for this run> | rightkit-gate --to check|compile|package|qualify --all-units-exception <id> (RightKit only: its unit is named on every run, or every unit by Adrian's entry in ci/all-units-exceptions.json of the commit) | rightkit-gate --to check|compile|package|qualify [--unit <selector>] --no-broker <exception-id> (Mac and Dell only; Adrian's recorded exception in .rightkit/broker-exceptions.json of the commit) | rightkit-gate --to dev|test [--commit <rev>] [-- <right-release args>] | rightkit-gate --to check|compile|package|qualify|dev|test --route [--platform mac|win|ios|android] [--commit <rev>] | rightkit-gate stage --platform mac|win [--tier patch|update] | rightkit-gate [--base <ref>] [--plan] [--no-cache] | rightkit-gate install-hook | rightkit-gate env-digest | rightkit-gate snapshot [--commit <rev>] [--verify [--env <sha256>]] [--json] | rightkit-gate live-scans --repo <root> | rightkit-gate clippy [<cargo clippy args>] | rightkit-gate env-digest [--snapshot <dir> --commit <sha>] | rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] | rightkit-gate check --commit <sha> --full-a --snapshot <dir> | --version`;
|
|
107
114
|
/** The host platform (process.platform) that constructs each platform's artifact; a platform missing here (android) builds on any host. */
|
|
108
115
|
const STAGE_NATIVE_HOST = Object.freeze({ mac: "darwin", ios: "darwin", win: "win32" });
|
|
109
116
|
|
|
@@ -235,7 +242,13 @@ function workspaceMetadata({ root, spawn, cargoCommand, dir, realpath }) {
|
|
|
235
242
|
const members = (meta.packages ?? []).map((p) => {
|
|
236
243
|
const pkgDir = rel(path.dirname(p.manifest_path));
|
|
237
244
|
if (pkgDir === ".." || pkgDir.startsWith("../")) throw new Error(`workspace member ${p.name} lives outside the repository (${pkgDir})`);
|
|
238
|
-
|
|
245
|
+
// Bin targets (rustright#209 item 5) and whether `cargo test -p <name>` links them: Cargo builds a package's bins only for the integration tests
|
|
246
|
+
// it selects, so a package with none leaves `debug/<bin>` to whatever an earlier build wrote. A bin with required-features is not counted.
|
|
247
|
+
const kinds = (t) => (Array.isArray(t.kind) ? t.kind : []);
|
|
248
|
+
const bins = (p.targets ?? []).filter((t) => kinds(t).includes("bin") && !(t["required-features"] ?? t.required_features ?? []).length).map((t) => t.name).sort();
|
|
249
|
+
// Only an integration test `cargo test` selects (`test` not false, no required-features) makes it link the bins; a bench is test = false by default.
|
|
250
|
+
const testLinksBins = (p.targets ?? []).some((t) => kinds(t).includes("test") && t.test !== false && !(t["required-features"] ?? t.required_features ?? []).length);
|
|
251
|
+
return { name: p.name, dir: pkgDir, manifest: pkgDir === "." ? "Cargo.toml" : `${pkgDir}/Cargo.toml`, publish: !(Array.isArray(p.publish) && p.publish.length === 0), bins, testLinksBins };
|
|
239
252
|
}).sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
240
253
|
return { cargoDir, members };
|
|
241
254
|
}
|
|
@@ -272,6 +285,7 @@ export function gateTmpRoot({ env = process.env, platform = process.platform, ex
|
|
|
272
285
|
*/
|
|
273
286
|
/** Longest the class A environment waits for `pnpm --version`. */
|
|
274
287
|
export const PNPM_PROBE_TIMEOUT_MS = 60_000;
|
|
288
|
+
export const PNPM_PIN_REFUSAL = "RK_PNPM_VERSION_MISMATCH";
|
|
275
289
|
export const TOOL_CACHE_DIRS = new Set([".vite", ".vite-temp", ".vitest-cache", ".cache"]);
|
|
276
290
|
export const INSTALL_STATE_NOT_COVERED = new Set([".pnpm-task-run-state-v1"]);
|
|
277
291
|
const nodeModulesDirs = (root, files) => [...new Set(files.filter((f) => /(^|\/)package\.json$/.test(f)).map((f) => path.join(path.dirname(path.join(root, f)), "node_modules")))].sort();
|
|
@@ -433,6 +447,37 @@ export function classAEnvironmentDigest({ root, files, spawn = defaultSpawn, inc
|
|
|
433
447
|
return createHash("sha256").update([`node ${process.version}`, `pnpm ${pnpmVersion}`, `platform ${process.platform}-${process.arch}`, ...gateFiles, ...installState, `installed ${installedDependencyDigest({ root, files, includeCaches })}`].join("\n")).digest("hex");
|
|
434
448
|
}
|
|
435
449
|
|
|
450
|
+
/** The pnpm a package.json pins (`packageManager: "pnpm@12.16.1+sha512.."` -> `12.16.1`), or null for no pnpm pin. */
|
|
451
|
+
export function pnpmPinOf(packageJsonText) {
|
|
452
|
+
let pm; try { pm = JSON.parse(packageJsonText)?.packageManager; } catch { return null; }
|
|
453
|
+
const m = typeof pm === "string" ? /^pnpm@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?$/.exec(pm.trim()) : null;
|
|
454
|
+
return m ? m[1] : null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Class A runs the pnpm it bound, never a pnpm that relaunches another one: a pnpm older than a package's `packageManager` pin switches
|
|
459
|
+
* itself to the pinned version through a nested shell (on Windows `cmd /d /s /c` with ^-escaped text), which the class A launch guard
|
|
460
|
+
* refuses mid-row as an unreadable launch. This compares the pnpm on PATH, asked from a directory with no package.json so it cannot
|
|
461
|
+
* switch, with every pnpm pin of the checked tree BEFORE any class A row launches, and returns a typed problem naming both versions
|
|
462
|
+
* and the fix, or null. Nothing is admitted: the guard keeps refusing any relaunch.
|
|
463
|
+
*/
|
|
464
|
+
export function pnpmPinProblem({ root, files, spawn = defaultSpawn, readFile = (f) => fs.readFileSync(f, "utf8"), neutralCwd = os.tmpdir() }) {
|
|
465
|
+
const pins = new Map();
|
|
466
|
+
for (const f of files.filter((x) => /(^|\/)package\.json$/.test(x))) {
|
|
467
|
+
let pin = null; try { pin = pnpmPinOf(readFile(path.join(root, f))); } catch { pin = null; }
|
|
468
|
+
if (pin) pins.set(f, pin);
|
|
469
|
+
}
|
|
470
|
+
if (!pins.size) return null;
|
|
471
|
+
const r = spawn("pnpm", ["--version"], { cwd: neutralCwd, boundedMs: PNPM_PROBE_TIMEOUT_MS });
|
|
472
|
+
const onPath = failed(r) ? "" : text(r).trim();
|
|
473
|
+
const fix = (pin) => `install the pinned pnpm as the pnpm on PATH (for example \`pnpm self-update ${pin}\`) and run again`;
|
|
474
|
+
if (!onPath) { const [pin] = new Set(pins.values()); return { code: PNPM_PIN_REFUSAL, message: `${PNPM_PIN_REFUSAL}: cannot read the version of the pnpm on PATH (${r?.error ? r.error.code ?? r.error.message : why(r)}); ${fix(pin)}` }; }
|
|
475
|
+
const bad = [...pins].filter(([, pin]) => pin !== onPath);
|
|
476
|
+
if (!bad.length) return null;
|
|
477
|
+
const [file, pin] = bad[0];
|
|
478
|
+
return { code: PNPM_PIN_REFUSAL, message: `${PNPM_PIN_REFUSAL}: the pnpm on PATH is ${onPath} but ${file} pins packageManager pnpm@${pin}${bad.length > 1 ? ` (${bad.length} package.json files differ)` : ""}; a different pnpm relaunches the pinned one through a shell, which class A refuses (RK_CLASS_A_COMPILE_LAUNCH); ${fix(pin)}` };
|
|
479
|
+
}
|
|
480
|
+
|
|
436
481
|
/** Split pathspecs into batches whose joined length stays under `limit` characters (well below Windows' 32767 command line). */
|
|
437
482
|
export function pathspecBatches(paths, limit = 8000) {
|
|
438
483
|
const batches = []; let current = []; let size = 0;
|
|
@@ -635,7 +680,7 @@ export function discoverRustWorkspaces({ root, spawn, cargoCommand, tracked, unt
|
|
|
635
680
|
for (const member of found.members) {
|
|
636
681
|
covered.add(member.manifest);
|
|
637
682
|
if (!trackedSet.has(member.manifest)) { skipped.push({ manifest: member.manifest, why: "a workspace member that is untracked or ignored, so it is not part of the gated commit" }); continue; }
|
|
638
|
-
units.push({ name: rustUnitName(found.cargoDir, member.name), crate: member.name, dir: member.dir, publish: member.publish, cargoDir: found.cargoDir });
|
|
683
|
+
units.push({ name: rustUnitName(found.cargoDir, member.name), crate: member.name, dir: member.dir, publish: member.publish, cargoDir: found.cargoDir, bins: member.bins ?? [], testLinksBins: member.testLinksBins === true });
|
|
639
684
|
}
|
|
640
685
|
workspaces.push({ cargoDir: found.cargoDir, units });
|
|
641
686
|
}
|
|
@@ -1262,6 +1307,17 @@ export function readUnitList(readFile, file) {
|
|
|
1262
1307
|
* units, so a repository that is not RightKit refuses them. Returns `{ exit }` when the run ends here, or `{}` to go on as a whole door run (the
|
|
1263
1308
|
* all-units flag and its id are removed from `args`). An unresolvable commit is runStages' own refusal, so it goes on.
|
|
1264
1309
|
*/
|
|
1310
|
+
/** The RightKit sample app the run's working directory is in, when the commit's ci/sample-apps.json lists it; else null. */
|
|
1311
|
+
export function sampleAppOf({ o, root, git, commit }) {
|
|
1312
|
+
const shown = git("show", `${commit}:ci/sample-apps.json`);
|
|
1313
|
+
if (failed(shown)) return null;
|
|
1314
|
+
let apps;
|
|
1315
|
+
try { apps = JSON.parse(text(shown)).apps; } catch { return null; }
|
|
1316
|
+
if (!Array.isArray(apps)) return null;
|
|
1317
|
+
const rel = path.relative(root, path.resolve(o.cwd)).split(path.sep).join("/");
|
|
1318
|
+
return apps.find((dir) => typeof dir === "string" && dir && !dir.includes("..") && (rel === dir || rel.startsWith(`${dir}/`))) ?? null;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1265
1321
|
export async function rightkitDoorRule(o, args, root, git, io, { unitsFileFlag, allUnitsFlag, unitList }) {
|
|
1266
1322
|
const tag = "rightkit-gate:";
|
|
1267
1323
|
const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
|
|
@@ -1299,6 +1355,13 @@ export async function rightkitDoorRule(o, args, root, git, io, { unitsFileFlag,
|
|
|
1299
1355
|
o.out(`${tag} ALL UNITS EXCEPTION ${describeAllUnitsException(granted)}`);
|
|
1300
1356
|
return {};
|
|
1301
1357
|
}
|
|
1358
|
+
// RightKit's own sample apps (ci/sample-apps.json of the commit: the all-packages e2e app) are built as apps, from inside their directory:
|
|
1359
|
+
// the run is scoped to that app (appRootOf), never the whole repository, exactly as a product app's run is.
|
|
1360
|
+
const sample = sampleAppOf({ o, root, git, commit: pinned.commit });
|
|
1361
|
+
if (sample) {
|
|
1362
|
+
o.out(`${tag} RightKit sample app ${sample}: built as an app, scoped to ${sample} (ci/sample-apps.json)`);
|
|
1363
|
+
return {};
|
|
1364
|
+
}
|
|
1302
1365
|
o.err(`${tag} BLOCKED: RK_UNIT_NOT_NAMED: RightKit names its unit on every run: --unit <cargo:rightkit-name|npm:@rightkit/name>, --units-file <json written for this run>, or --all-units-exception <id> (Adrian's entry in ${ALL_UNITS_EXCEPTIONS_FILE}); a whole-repository run is never the default; nothing ran`);
|
|
1303
1366
|
return { exit: 1 };
|
|
1304
1367
|
}
|
|
@@ -1311,7 +1374,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1311
1374
|
// of the snapshot), then right-release dev|test with the snapshot's app root as its cwd. They have no stage list of their own.
|
|
1312
1375
|
const devTest = to === "dev" || to === "test";
|
|
1313
1376
|
let stages = devTest ? [] : stagesTo(to);
|
|
1314
|
-
if (!stages) { o.err(`${tag} --to must be one of ${[...STAGES, "dev", "test"].join(", ")}`); return 2; }
|
|
1377
|
+
if (!stages) { o.err(`${tag} --to must be one of ${[...STAGES, "dev", "test"].join(", ")}${to === "tool" ? " (--to tool builds one named tool unit: --to tool --unit cargo:rightkit-qa or --to tool --unit cargo:rightkit-credentials)" : ""}`); return 2; }
|
|
1315
1378
|
// Every stage the door runs has a row naming where it reads its source from (gate-stages.mjs STAGE_SOURCES, drift-tested in
|
|
1316
1379
|
// door-stages.test.mjs); a stage added without one is refused rather than run with an unstated source.
|
|
1317
1380
|
if (!STAGE_SOURCES[to]) { o.err(`${tag} BLOCKED: stage ${to} has no STAGE_SOURCES row naming its source; nothing ran`); return 2; }
|
|
@@ -1345,7 +1408,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1345
1408
|
// `--route`: which route this stage takes here and whether it refuses, answered without running anything (rightkit#1297).
|
|
1346
1409
|
if (o.routeProbe) return await runRouteProbe(o, args, root, git, { to, platform, stages: devTest ? [] : stages, devTest, appDir });
|
|
1347
1410
|
// Commit-only build (GATE-CONTRACT §1): on every host (not hosted CI, whose runner checkout is the commit) the door selects a commit
|
|
1348
|
-
// (--commit <rev>, default HEAD), takes the
|
|
1411
|
+
// (--commit <rev>, default HEAD), takes the repository's build ownership, syncs the commit's gate-owned snapshot once, and every stage
|
|
1349
1412
|
// below reads that snapshot; the checkout may hold anything and is never a build source. Every non-hosted host takes this route: Mac
|
|
1350
1413
|
// and Dell compile it through the RustRight snapshot carrier (`deps.snapshotBroker`, wired by `productionEntryDeps`; item 7 rows 7.20
|
|
1351
1414
|
// to 7.24, rightkit#1301), right-release's own children included. Once it is selected any failure refuses; there is no retry on the
|
|
@@ -1354,7 +1417,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1354
1417
|
// also answers from (rightkit#1297 L1): the probe and the real run cannot drift, and the door-route parity test compares the two.
|
|
1355
1418
|
if (!o.doorSnapshot) {
|
|
1356
1419
|
const whichRoute = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
1357
|
-
const routeExecutor = (o.resolveExecutor ??
|
|
1420
|
+
const routeExecutor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which: whichRoute, readFile: o.readFile });
|
|
1358
1421
|
let decided;
|
|
1359
1422
|
try { decided = routePlan(o, { to, stages: devTest ? [] : stages, devTest, executor: routeExecutor, stagePlatform: platform, capabilities: await brokerCapabilityStateWithHandshake(o, { to, executor: routeExecutor }), appConfig: selected && to !== "check" ? routeAppConfig({ git, commit: selected.commit, appDir, platform: devTestPlatform(args) }) : null }); }
|
|
1360
1423
|
catch (e) { o.err(`${tag} ${e.message}`); return 2; }
|
|
@@ -1373,7 +1436,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1373
1436
|
const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
|
|
1374
1437
|
const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
|
|
1375
1438
|
let own;
|
|
1376
|
-
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: `door:${to}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit: selected.commit, platform: o.platform, env: o.env }); }
|
|
1439
|
+
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: snap.repositoryOf(root).key }), stage: `door:${to}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit: selected.commit, platform: o.platform, env: o.env }); }
|
|
1377
1440
|
catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`${tag} BLOCKED: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 1; }
|
|
1378
1441
|
o.out(`${tag} build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
|
|
1379
1442
|
try {
|
|
@@ -1400,7 +1463,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1400
1463
|
if (!ds && (failed(dirty) || text(dirty).trim())) { o.err(`${tag} BLOCKED: the checkout differs from HEAD; every stage works on the commit, so commit or stash first`); return 1; }
|
|
1401
1464
|
|
|
1402
1465
|
const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
1403
|
-
const executor = (o.resolveExecutor ??
|
|
1466
|
+
const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
1404
1467
|
if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
1405
1468
|
const version = (cmd, a) => { const r = o.spawn(cmd, a, { cwd: root }); return failed(r) ? null : text(r).trim().split(/\r?\n/)[0]; };
|
|
1406
1469
|
const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
@@ -1474,13 +1537,16 @@ async function runStages(o, args, root, git, io) {
|
|
|
1474
1537
|
// the ones they sealed, so they are not run again.
|
|
1475
1538
|
// The release configs of the commit (under this app) that declare `sidecars`: the commit's config decides on the snapshot route, never the checkout's.
|
|
1476
1539
|
const sidecarConfigsOf = () => committedSidecarConfigs({ o, git, commit, appDir, base: ds ? ds.path : root });
|
|
1477
|
-
|
|
1540
|
+
// right-release keeps its outputs (`.right-release`: sealed sets, QA receipts, state) at the checkout's git root, never a nested app's
|
|
1541
|
+
// directory (build-release.mjs gitRootOf): every lookup of a sealed set or a QA receipt reads there.
|
|
1542
|
+
const releaseRoot = root;
|
|
1543
|
+
const sealedNow = receipt.stages.package?.ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
|
|
1478
1544
|
// The protected-receipt verifier pulls in the ownership policy modules (and their dependencies): loaded only for a run that qualifies or releases,
|
|
1479
1545
|
// so an installed copy that only checks or compiles never needs them (GB-14).
|
|
1480
1546
|
const qualifyReuse = stages.includes("qualify") || stages.includes("release") ? await import("../qualify-reuse.mjs") : null;
|
|
1481
1547
|
const installedQaReceiptDigest = (...a) => qualifyReuse.installedQaReceiptDigest(...a);
|
|
1482
1548
|
const qualifyReuseProblem = (...a) => qualifyReuse.qualifyReuseProblem(...a);
|
|
1483
|
-
const qualifyRecordProblem = (recorded) => (o.qualifyReuseProblem ?? qualifyReuseProblem)({ recorded, sealedNow, repoRoot:
|
|
1549
|
+
const qualifyRecordProblem = (recorded) => (o.qualifyReuseProblem ?? qualifyReuseProblem)({ recorded, sealedNow, repoRoot: releaseRoot, platform, ...(o.verifyQa !== undefined ? { verify: o.verifyQa } : (o.env.GITHUB_ACTIONS === "true" && o.env.RIGHTKIT_PUBLISHER === "github-environment" ? { verify: null } : {})) });
|
|
1484
1550
|
const sealedIntact = Boolean(sealedNow && sealedNow.id === receipt.stages.package.sealed && sealedNow.digest === receipt.stages.package.manifestSha256);
|
|
1485
1551
|
// Every deterministic scan (full class A) runs before any compile, for every door stage. The app rules run at the tier the stage
|
|
1486
1552
|
// owes: dev, test and package take the compile-tier rules; qualify and release add the release-only rules (approvals, ratified
|
|
@@ -1544,12 +1610,18 @@ async function runStages(o, args, root, git, io) {
|
|
|
1544
1610
|
// On the snapshot route sidecars are staged ONCE per door run by the ledger bookkeeping (attribution diff, immutable staged entries).
|
|
1545
1611
|
if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1546
1612
|
else for (const config of ok ? sidecarConfigs : []) if (!await rightRelease(stage, ["stage-sidecar", "--config", path.join(root, config)])) { ok = false; break; }
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1613
|
+
// rightkit#1331: the declared compile inputs that are the app's web build output are produced in the snapshot before the compile reads them.
|
|
1614
|
+
if (ok && ds) ok = await stageWebBuildOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1615
|
+
// The Windows package-identity MSIX a Tauri app bundles (its release config's windows.identity), produced in the snapshot on Windows only.
|
|
1616
|
+
if (ok && ds) ok = await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1617
|
+
let compiledDirs = []; let compiledBins = null; let qualifyOwed = [];
|
|
1618
|
+
const reportOutputs = (dirs, bins = null) => { compiledDirs = [...dirs]; compiledBins = bins; };
|
|
1619
|
+
if (ok) ok = (await (o.runCompile ?? (() => main(ds ? ["--all", "--commit", commit, "--snapshot", ds.path] : ["--all"], { ...io, cwd: root, readStdin: () => "", ...(ds ? { buildOwnership: ds.own, classAEarlier: true, snapshotLedger: ds.ledger?.entries ?? [], onCompileLedger: (entries) => { ds.compileAdmitted = entries; }, onDirectReceipt: (r) => { directReceipt = r; }, onCompileOutputs: reportOutputs, onQualifyOwed: (list) => { qualifyOwed = list; } } : {}) })))({ reportOutputs })) === 0;
|
|
1550
1620
|
if (directReceipt) extra = { ...extra, direct: directReceipt };
|
|
1621
|
+
// What the plan left to the qualify stage is recorded with the compile, so qualification can prove it is not lost (rightkit#1332, #1333).
|
|
1622
|
+
if (qualifyOwed.length) extra = { ...extra, qualifyOwed };
|
|
1551
1623
|
// What this compile produced, recorded with its result so a later run can prove the outputs are still there (GB-13).
|
|
1552
|
-
if (ok) extra = { ...extra, outputs: compileOutputsRecord({ dirs: compiledDirs, env: o.env }) };
|
|
1624
|
+
if (ok) extra = { ...extra, outputs: compileOutputsRecord({ dirs: compiledDirs, env: o.env, builtBins: compiledBins }) };
|
|
1553
1625
|
// The cross-check with `.rightkit/compile-inputs.json`: a declared input that covers ledger entries must carry the digest of those
|
|
1554
1626
|
// very bytes in the compile receipt (the broker digests declared inputs in place in the snapshot, BC:39).
|
|
1555
1627
|
if (ok && ds && directReceipt?.declaredInputDigests) {
|
|
@@ -1579,13 +1651,24 @@ async function runStages(o, args, root, git, io) {
|
|
|
1579
1651
|
// GB-17: several Cargo workspaces: each of right-release's and its children's Cargo calls is bound to ITS workspace's target through the per-call wrapper.
|
|
1580
1652
|
let packageMulti = null;
|
|
1581
1653
|
if (ok && packagePin.dirs?.length > 1) {
|
|
1582
|
-
|
|
1654
|
+
// The package stage builds the workspace that holds the app's release config (its Tauri app), never a test-only workspace beside it.
|
|
1655
|
+
const builtWorkspaces = [...new Set(sidecarConfigsOf().map((c) => path.posix.dirname(String(c).replaceAll("\\", "/"))))];
|
|
1656
|
+
packageMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), onlyWorkspaces: builtWorkspaces, ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
|
|
1583
1657
|
if (packageMulti.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: [package] ${packageMulti.refusal}; nothing was built`); ok = false; }
|
|
1584
1658
|
else if (packageMulti.note) o.out(`${tag} [package] ${packageMulti.note}`);
|
|
1585
1659
|
}
|
|
1586
1660
|
const packageEnv = packagePin.dir ? { CARGO_TARGET_DIR: packagePin.dir } : (packageMulti?.env ?? {});
|
|
1587
|
-
|
|
1588
|
-
|
|
1661
|
+
// The QA shell the compile built is source QA's binary (rightkit#1309): the compile's recorded output directories are handed to `right-release qa` only
|
|
1662
|
+
// while that record still proves the outputs intact; it stages the declared layout from them in a run-scoped directory, never in the snapshot (qa-layout.mjs).
|
|
1663
|
+
const compileDirs = receipt.stages.compile?.outputs?.dirs;
|
|
1664
|
+
const compiledProblem = (o.compileReuseProblem ?? compileReuseProblem)(receipt.stages.compile, { env: o.env });
|
|
1665
|
+
// A direct host's suites (qa.layout.build) have no carried receipts: what they build lands in the output directories pinned here (test-command.mjs).
|
|
1666
|
+
const pinnedDirs = packagePin.dirs ?? (packagePin.dir ? [packagePin.dir] : []);
|
|
1667
|
+
const buildEnv = pinnedDirs.length ? { ...packageEnv, RIGHTKIT_GATE_TARGET_DIRS: JSON.stringify(pinnedDirs) } : packageEnv;
|
|
1668
|
+
const qaEnv = !mobile && !compiledProblem && Array.isArray(compileDirs) && compileDirs.length ? { ...buildEnv, RIGHT_RELEASE_QA_COMPILED_DIRS: JSON.stringify(compileDirs) } : buildEnv;
|
|
1669
|
+
if (ok && !mobile && compiledProblem && Array.isArray(compileDirs)) o.out(`${tag} [package] the compile outputs are not handed to source QA: ${compiledProblem}`);
|
|
1670
|
+
ok = ok && (mobile || await rightRelease(stage, ["qa", "--stage", "source", "--platform", platform], qaEnv)) && await rightRelease(stage, ["build", "--platform", platform], packageEnv);
|
|
1671
|
+
const sealed = ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
|
|
1589
1672
|
if (ok && !sealed) { o.err(`${tag} [package] the build reported success but sealed nothing for ${commit.slice(0, 8)} on ${platform}`); ok = false; }
|
|
1590
1673
|
if (sealed) extra = { platform, sealed: sealed.id, manifestSha256: sealed.digest };
|
|
1591
1674
|
// The cross-check: the Cargo targets the sealed manifest records are the directory this door resolved itself. A different one voids the stage.
|
|
@@ -1630,7 +1713,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1630
1713
|
}
|
|
1631
1714
|
} else {
|
|
1632
1715
|
const sealed = receipt.stages.package?.ok ? receipt.stages.package : null;
|
|
1633
|
-
const current = sealed ? (o.sealedFor ?? sealedFor)({ root:
|
|
1716
|
+
const current = sealed ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
|
|
1634
1717
|
if (!sealed || !current || current.id !== sealed.sealed || current.digest !== sealed.manifestSha256) {
|
|
1635
1718
|
o.err(`${tag} BLOCKED: [${stage}] needs the sealed set the package stage produced for this commit, unchanged (sealed manifest digest)`);
|
|
1636
1719
|
ok = false;
|
|
@@ -1649,9 +1732,14 @@ async function runStages(o, args, root, git, io) {
|
|
|
1649
1732
|
? (o.out(`${tag} [qualify] the release tier already passed at check for this exact input; not run again`), 0)
|
|
1650
1733
|
: await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...appScope, "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
|
|
1651
1734
|
if (releaseTier !== 0) { o.err(`${tag} [qualify] the release tier (every rule and scan) is red for ${commit.slice(0, 12)}; nothing was qualified`); ok = false; }
|
|
1652
|
-
else
|
|
1735
|
+
else {
|
|
1736
|
+
// rightkit#1332, #1333: what compile left to qualification (end-to-end crates, declared Swift packages) must have a declared run route here.
|
|
1737
|
+
const owedProblems = qualifyOwedProblem({ o, git, commit, appDir, base: ds ? ds.path : root, owed: receipt.stages.compile?.qualifyOwed ?? [] });
|
|
1738
|
+
for (const p of owedProblems) o.err(`${tag} BLOCKED: [qualify] ${p}`);
|
|
1739
|
+
ok = owedProblems.length === 0 && await rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
|
|
1740
|
+
}
|
|
1653
1741
|
// The protected installed-QA receipt this run produced is journaled by digest (GB-14), so the record can be re-verified before any reuse.
|
|
1654
|
-
extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, ruleTier: "release", ...(ok ? { qaReceiptDigest: (o.installedQaReceiptDigest ?? installedQaReceiptDigest)({ repoRoot:
|
|
1742
|
+
extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, ruleTier: "release", ...(ok ? { qaReceiptDigest: (o.installedQaReceiptDigest ?? installedQaReceiptDigest)({ repoRoot: releaseRoot, releaseId: sealed.sealed, platform }) } : {}) };
|
|
1655
1743
|
} else {
|
|
1656
1744
|
if (!receipt.stages.qualify?.ok || receipt.stages.qualify.manifestSha256 !== sealed.manifestSha256) { o.err(`${tag} BLOCKED: [release] the sealed set was not qualified (installed QA) in this door run's receipt`); ok = false; }
|
|
1657
1745
|
else if (qualifyRecordProblem(receipt.stages.qualify)) { o.err(`${tag} BLOCKED: [release] the recorded qualification no longer stands: ${qualifyRecordProblem(receipt.stages.qualify)}; nothing was uploaded`); ok = false; }
|
|
@@ -1699,7 +1787,9 @@ function stageForPublisher(o, args, root, git) {
|
|
|
1699
1787
|
const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, configDirs: inventory.dirs });
|
|
1700
1788
|
const receipt = (o.readReceipt ?? readReceipt)({ commonDir, commit, app: appDir });
|
|
1701
1789
|
const pack = receipt?.stages?.package; const qual = receipt?.stages?.qualify;
|
|
1702
|
-
|
|
1790
|
+
// right-release's outputs live at the checkout's git root (see runStages).
|
|
1791
|
+
const releaseRoot = root;
|
|
1792
|
+
const sealed = pack?.ok ? (o.sealedFor ?? sealedFor)({ root: releaseRoot, commit, platform }) : null;
|
|
1703
1793
|
if (!pack?.ok || !qual?.ok || pack.platform !== platform || qual.manifestSha256 !== pack.manifestSha256 || !sealed || sealed.id !== pack.sealed || sealed.digest !== pack.manifestSha256) {
|
|
1704
1794
|
o.err(`${tag} BLOCKED: stage needs this host's receipt for ${commit.slice(0, 12)} with package and qualify passed on ${platform} and the sealed set unchanged; run rightkit-gate --to qualify --platform ${platform} first`);
|
|
1705
1795
|
return 1;
|
|
@@ -1711,7 +1801,7 @@ function stageForPublisher(o, args, root, git) {
|
|
|
1711
1801
|
const receiptFile = path.join(work, `receipt-${platform}.json`);
|
|
1712
1802
|
fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1713
1803
|
const archive = path.join(work, `sealed-${platform}.tar.gz`);
|
|
1714
|
-
const tar = o.spawn("tar", ["-czf", archive, "-C",
|
|
1804
|
+
const tar = o.spawn("tar", ["-czf", archive, "-C", releaseRoot, path.join(".right-release", "sealed", sealed.id, platformDirOf(platform))]);
|
|
1715
1805
|
if (failed(tar)) { o.err(`${tag} could not pack ${sealed.id}/${platform}`); return 1; }
|
|
1716
1806
|
const gh = (...a) => o.spawn("gh", a, { cwd: root });
|
|
1717
1807
|
if (failed(gh("release", "view", releaseTag))) {
|
|
@@ -1782,7 +1872,12 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
|
|
|
1782
1872
|
const prior = new Map((ds.ledger?.entries ?? []).map((e) => [e.path, e]));
|
|
1783
1873
|
const now = new Map(walked.entries.map((e) => [e.path, e]));
|
|
1784
1874
|
const problems = [...walked.problems];
|
|
1785
|
-
|
|
1875
|
+
// The package stage builds for the package triple (universal-apple-darwin on a Mac recipe with `stageSidecars`), so right-release's own
|
|
1876
|
+
// stage-sidecar may stage again there. A staged entry it changed or removed is admitted only through the receipt it wrote: a valid receipt
|
|
1877
|
+
// of the same config, every file it names present with that digest; the files it names become this stage's staged entries.
|
|
1878
|
+
const restaged = stage === "package" ? await restagedSidecars({ o, ds, prior, now }) : { paths: new Set(), receipts: new Set(), producers: new Set(), problems: [] };
|
|
1879
|
+
problems.push(...restaged.problems);
|
|
1880
|
+
for (const [p, e] of prior) if (e.kind === "staged" && !restaged.paths.has(p) && !restaged.receipts.has(p) && !(restaged.producers.has(e.producer?.step) && !now.has(p)) && (!now.has(p) || entryKey(now.get(p)) !== entryKey(e))) problems.push(`${p} is a staged sidecar output and ${now.has(p) ? "changed" : "was removed"} after it was staged (a staged entry is immutable for the door run)`);
|
|
1786
1881
|
if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${p}`); return { ok: false, entries: ds.ledger?.entries ?? [] }; }
|
|
1787
1882
|
// The compile's own verify (snapshot-check.mjs, 5.09d) returned the entries it admitted: they must be the entries this walk found.
|
|
1788
1883
|
if (Array.isArray(ds.compileAdmitted) && stage === "compile") {
|
|
@@ -1793,6 +1888,7 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
|
|
|
1793
1888
|
ds.compileAdmitted = null;
|
|
1794
1889
|
const entries = walked.entries.map((e) => {
|
|
1795
1890
|
const was = prior.get(e.path);
|
|
1891
|
+
if (restaged.paths.has(e.path) || restaged.receipts.has(e.path)) return { ...e, kind: "staged", producer: { stage, step: "stage-sidecar" } };
|
|
1796
1892
|
if (was?.kind === "staged") return { ...e, kind: "staged", producer: was.producer };
|
|
1797
1893
|
return { ...e, kind: "generated", producer: was && entryKey(was) === entryKey(e) ? was.producer : { stage, step } };
|
|
1798
1894
|
});
|
|
@@ -1809,6 +1905,39 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
|
|
|
1809
1905
|
return { ok: true, entries };
|
|
1810
1906
|
}
|
|
1811
1907
|
|
|
1908
|
+
/**
|
|
1909
|
+
* The sidecar re-staging a package stage did, proven by its receipts: for each staged receipt (`<config dir>/.../stage-sidecar.json`) that
|
|
1910
|
+
* changed, the new receipt must be a valid stage-sidecar receipt and every file it names must be on disk with its recorded digest. Returns the
|
|
1911
|
+
* snapshot paths it names (`paths`), the receipts themselves (`receipts`), the producer steps whose earlier staged files it replaces
|
|
1912
|
+
* (`producers`, whose removed files are admitted) and the problems found.
|
|
1913
|
+
*/
|
|
1914
|
+
async function restagedSidecars({ o, ds, prior, now }) {
|
|
1915
|
+
const { entryKey } = await import("../stage-ledger.mjs");
|
|
1916
|
+
const { validateStageSidecarReceipt } = await import("../stage-sidecar.mjs");
|
|
1917
|
+
const out = { paths: new Set(), receipts: new Set(), producers: new Set(), problems: [] };
|
|
1918
|
+
for (const [p, e] of prior) {
|
|
1919
|
+
if (e.kind !== "staged" || !/(^|\/)stage-sidecar\.json$/.test(p) || !now.has(p) || entryKey(now.get(p)) === entryKey(e)) continue;
|
|
1920
|
+
const config = e.producer?.step;
|
|
1921
|
+
const configDir = typeof config === "string" ? path.posix.dirname(config.replaceAll("\\", "/")) : ".";
|
|
1922
|
+
let receipt;
|
|
1923
|
+
try { receipt = validateStageSidecarReceipt(JSON.parse(String(o.readFile(path.join(ds.path, ...p.split("/")))))); }
|
|
1924
|
+
catch (error) { out.problems.push(`${p} was rewritten in the package stage but is not a valid stage-sidecar receipt (${error.message})`); continue; }
|
|
1925
|
+
const named = [];
|
|
1926
|
+
for (const file of receipt.files) {
|
|
1927
|
+
const rel = (configDir === "." ? file.path : `${configDir}/${file.path}`).replaceAll("\\", "/");
|
|
1928
|
+
const entry = now.get(rel);
|
|
1929
|
+
if (!entry) { out.problems.push(`${p} names ${rel}, which is not in the snapshot`); continue; }
|
|
1930
|
+
if (entry.sha256 !== undefined && entry.sha256 !== file.sha256) { out.problems.push(`${rel} does not have the digest ${p} records for it`); continue; }
|
|
1931
|
+
named.push(rel);
|
|
1932
|
+
}
|
|
1933
|
+
if (named.length !== receipt.files.length) continue;
|
|
1934
|
+
out.receipts.add(p);
|
|
1935
|
+
for (const rel of named) out.paths.add(rel);
|
|
1936
|
+
if (typeof config === "string") out.producers.add(config);
|
|
1937
|
+
}
|
|
1938
|
+
return out;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1812
1941
|
/** The release configs of `commit` (under `appDir`) that declare `sidecars`, read from `base` (the snapshot on the snapshot route): the commit's config decides, never the checkout's. */
|
|
1813
1942
|
export function committedSidecarConfigs({ o, git, commit, appDir = ".", base }) {
|
|
1814
1943
|
const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
@@ -1848,7 +1977,11 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
|
|
|
1848
1977
|
try { result = JSON.parse(String(r.stdout ?? "").trim()); } catch { o.err(`${tag} RK_CHECK_ERROR: [${stage}] stage-sidecar --json for ${config} did not print its result; its writes cannot be attributed`); return false; }
|
|
1849
1978
|
let walked;
|
|
1850
1979
|
try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
|
|
1851
|
-
|
|
1980
|
+
// stage-sidecar reports its files relative to ITS app root, the directory of the config it was given (stage-sidecar-command.mjs
|
|
1981
|
+
// appRoot = dirname(config)), not the gate's appDir: a gate run at the repository root with the config in tauri-app-next/ would
|
|
1982
|
+
// otherwise find every staged file "not reported".
|
|
1983
|
+
const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
|
|
1984
|
+
const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`).replaceAll("\\", "/");
|
|
1852
1985
|
const receiptAbs = typeof result.receiptPath === "string" ? path.relative(ds.path, result.receiptPath).split(path.sep).join("/") : null;
|
|
1853
1986
|
const problems = [...walked.problems, ...attributionProblems({ before, after: walked.entries, reported: [...reportedPaths(result)].map(underApp), receipt: receiptAbs })];
|
|
1854
1987
|
if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] stage-sidecar ${config}: ${p}`); return false; }
|
|
@@ -1859,6 +1992,137 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
|
|
|
1859
1992
|
return true;
|
|
1860
1993
|
}
|
|
1861
1994
|
|
|
1995
|
+
/**
|
|
1996
|
+
* rightkit#1332, #1333: what the compile stage left to qualification must not be lost. Each owed item (an end-to-end crate whose tests need
|
|
1997
|
+
* the built app; a Swift package declared `qualifyOnly`) needs the run route the pipeline has for it: the app's release config declares
|
|
1998
|
+
* `qa.journeys`, so `right-release qa --stage installed` runs `rightkit-qa journeys run` against the sealed app and validates its receipt
|
|
1999
|
+
* (release-qa-command.mjs). Without it each owed item is named and qualification stops. Returns the problems (empty when nothing is owed or covered).
|
|
2000
|
+
*/
|
|
2001
|
+
export function qualifyOwedProblem({ o, git, commit, appDir = ".", base, owed }) {
|
|
2002
|
+
if (!Array.isArray(owed) || !owed.length) return [];
|
|
2003
|
+
const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
2004
|
+
const configs = (failed(listed) ? [] : text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)));
|
|
2005
|
+
const declared = configs.some((f) => { try { return /\bqa\b[\s\S]*(^|[\s,{])["']?journeys["']?\s*:/m.test(o.readFile(path.join(base, f))); } catch { return false; } });
|
|
2006
|
+
if (declared) return [];
|
|
2007
|
+
return owed.map((x) => `${x.id} (${x.kind}${x.dir ? ` ${x.dir}` : ""}) is owed to qualification (${x.reason}) but no release config under ${appDir} declares qa.journeys, so nothing would run it; declare qa.journeys with a journeys.toml that covers it`);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
/** The release configs of `commit` (under `appDir`) that declare a `web` block (`web.build` steps), read from `base`: the commit's config decides, never the checkout's. */
|
|
2011
|
+
export function committedWebBuildConfigs({ o, git, commit, appDir = ".", base }) {
|
|
2012
|
+
const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
2013
|
+
return (failed(listedConfigs) ? [] : text(listedConfigs).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)))
|
|
2014
|
+
.filter((f) => { try { return /(^|[\s,{])["']?web["']?\s*:\s*\{/.test(o.readFile(path.join(base, f))); } catch { return false; } });
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* rightkit#1331: the declared compile inputs (`.rightkit/compile-inputs.json`) that are an app's web build output (a Tauri `frontendDist`)
|
|
2019
|
+
* exist in the snapshot before the compile reads them. The broker's capture copies a declared path in from the checkout; the snapshot route
|
|
2020
|
+
* has no copy ("digested in place", GATE-CONTRACT GC-I14), so the door produces them the one way the contract allows: the app's own
|
|
2021
|
+
* `right-release web-build` (its release config's `web.build` steps) runs INSIDE the snapshot, once per door run, before the compile.
|
|
2022
|
+
* What it wrote may remain only under a declared input (declaredOutputProblems), ignored by the commit's own rules and recorded by this
|
|
2023
|
+
* gate's own walk as `generated` ledger entries (never `staged`: the package stage's own web build rewrites them). The compile's declared
|
|
2024
|
+
* input digest (compile-inputs.mjs, the broker's algorithm) then covers those very bytes and declaredInputProblems cross-checks it.
|
|
2025
|
+
* Nothing runs when every declared input is already present or the app declares no `web.build`; an input still absent afterwards is left
|
|
2026
|
+
* to the compile, which refuses a missing input and names it. Returns true when nothing is owed or the build passed.
|
|
2027
|
+
*/
|
|
2028
|
+
export async function stageWebBuildOnce({ o, ds, root, git = null, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
|
|
2029
|
+
if (ds.webBuilt) return true;
|
|
2030
|
+
ds.webBuilt = true;
|
|
2031
|
+
const { readDeclaredInputs, SnapshotRequestError } = await import("../snapshot-request.mjs");
|
|
2032
|
+
let declared;
|
|
2033
|
+
try { declared = readDeclaredInputs(ds.path).map((x) => x.split(/[\\/]/).join("/")); }
|
|
2034
|
+
catch (e) { if (e instanceof SnapshotRequestError) return true; throw e; } // a malformed declaration refuses in the compile, which names it
|
|
2035
|
+
const absent = () => declared.filter((rel) => !(o.exists ?? fs.existsSync)(path.join(ds.path, ...rel.split("/"))));
|
|
2036
|
+
if (!absent().length) return true;
|
|
2037
|
+
const configs = git ? committedWebBuildConfigs({ o, git, commit, appDir, base: ds.path }) : [];
|
|
2038
|
+
if (!configs.length) { o.out(`${tag} [${stage}] declared compile input(s) ${absent().join(", ")} are absent from the snapshot and no release config under ${appDir} declares web.build; the compile will report them missing`); return true; }
|
|
2039
|
+
const { declaredOutputProblems } = await import("../stage-ledger.mjs");
|
|
2040
|
+
const source = sourceSplitOf({ root, ds, appDir });
|
|
2041
|
+
const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited;
|
|
2042
|
+
for (const config of configs) {
|
|
2043
|
+
if (!absent().length) break;
|
|
2044
|
+
const before = ds.ledger?.entries ?? [];
|
|
2045
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
2046
|
+
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the web build did not run`); return false; }
|
|
2047
|
+
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
2048
|
+
let r;
|
|
2049
|
+
try {
|
|
2050
|
+
o.out(`${tag} [${stage}] right-release web-build --config ${path.join(ds.path, config)} (from the snapshot ${ds.path}; declared input(s) ${absent().join(", ")} are absent)`);
|
|
2051
|
+
r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, "web-build", "--config", path.join(ds.path, config)], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
|
|
2052
|
+
} finally { endRun(run); }
|
|
2053
|
+
const carriedOk = await carrier.finish({ stage });
|
|
2054
|
+
if (!r || r.status !== 0) { o.err(`${tag} [${stage}] web-build for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
|
|
2055
|
+
if (!carriedOk) return false;
|
|
2056
|
+
let walked;
|
|
2057
|
+
try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
|
|
2058
|
+
const problems = [...walked.problems, ...declaredOutputProblems({ before, after: walked.entries, declared })];
|
|
2059
|
+
if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] web-build ${config}: ${p}`); return false; }
|
|
2060
|
+
const priorByPath = new Map(before.map((e) => [e.path, e]));
|
|
2061
|
+
ds.ledger = { entries: walked.entries.map((e) => (priorByPath.get(e.path)?.sha256 === e.sha256 ? priorByPath.get(e.path) : { ...e, kind: "generated", producer: { stage: "web-build", step: config } })) };
|
|
2062
|
+
}
|
|
2063
|
+
return true;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* The Windows package-identity (sparse MSIX) an app bundles, produced INSIDE the snapshot before the compile, like stageWebBuildOnce: the
|
|
2068
|
+
* app's committed release config declares `windows.identity` (windows-identity-plan.mjs) and windows-identity.mjs (RightKit's msix tooling, the
|
|
2069
|
+
* provisioned signer; never app code) builds and signs it. Windows hosts only: elsewhere nothing runs and a declared input that stays absent is
|
|
2070
|
+
* left to the compile, which names it (platform scoping of the declared input itself is a separate decision). The child reports what it wrote
|
|
2071
|
+
* (`--json`); this gate's own walk must then find additions only under the producer's `out`, which must lie under a declared compile input
|
|
2072
|
+
* (identityOutputProblems). They become `generated` ledger entries, and the compile's declared-input digest covers those very bytes.
|
|
2073
|
+
* Nothing runs when every declared input is present or no committed config declares windows.identity (the package itself already being
|
|
2074
|
+
* present is the child's skip). Returns true when nothing is owed or the build passed.
|
|
2075
|
+
*/
|
|
2076
|
+
export async function stageWindowsIdentityOnce({ o, ds, root, git = null, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
|
|
2077
|
+
if (ds.identityBuilt) return true;
|
|
2078
|
+
ds.identityBuilt = true;
|
|
2079
|
+
const { readDeclaredInputs, SnapshotRequestError } = await import("../snapshot-request.mjs");
|
|
2080
|
+
let declared;
|
|
2081
|
+
try { declared = readDeclaredInputs(ds.path).map((x) => x.split(/[\\/]/).join("/")); }
|
|
2082
|
+
catch (e) { if (e instanceof SnapshotRequestError) return true; throw e; }
|
|
2083
|
+
const absent = () => declared.filter((rel) => !(o.exists ?? fs.existsSync)(path.join(ds.path, ...rel.split("/"))));
|
|
2084
|
+
if (!absent().length || !git) return true;
|
|
2085
|
+
const { committedWindowsIdentityConfigs } = await import("../windows-identity-plan.mjs");
|
|
2086
|
+
const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
2087
|
+
const all = failed(listed) ? [] : text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`));
|
|
2088
|
+
const configs = committedWindowsIdentityConfigs({ readFile: o.readFile, listConfigs: all, base: ds.path });
|
|
2089
|
+
if (!configs.length) return true;
|
|
2090
|
+
if ((o.platform ?? process.platform) !== "win32") { o.out(`${tag} [${stage}] ${configs.join(", ")} declares windows.identity; not a Windows host, so the identity package is not built here`); return true; }
|
|
2091
|
+
const { identityOutputProblems } = await import("../windows-identity-plan.mjs");
|
|
2092
|
+
const source = sourceSplitOf({ root, ds, appDir });
|
|
2093
|
+
const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited;
|
|
2094
|
+
const producer = path.join(path.dirname(cli), "..", "windows-identity.mjs");
|
|
2095
|
+
for (const config of configs) {
|
|
2096
|
+
const before = ds.ledger?.entries ?? [];
|
|
2097
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
2098
|
+
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the Windows identity package did not build`); return false; }
|
|
2099
|
+
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
2100
|
+
let r;
|
|
2101
|
+
try {
|
|
2102
|
+
o.out(`${tag} [${stage}] windows-identity --config ${path.join(ds.path, config)} (from the snapshot ${ds.path})`);
|
|
2103
|
+
r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [producer, "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
|
|
2104
|
+
} finally { endRun(run); }
|
|
2105
|
+
const carriedOk = await carrier.finish({ stage });
|
|
2106
|
+
if (r?.stderr) o.err(String(r.stderr).trimEnd());
|
|
2107
|
+
if (!r || r.status !== 0) { o.err(`${tag} [${stage}] windows-identity for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
|
|
2108
|
+
if (!carriedOk) return false;
|
|
2109
|
+
let result;
|
|
2110
|
+
try { result = JSON.parse(String(r.stdout ?? "").trim()); } catch { o.err(`${tag} RK_CHECK_ERROR: [${stage}] windows-identity --json for ${config} did not print its result; its writes cannot be attributed`); return false; }
|
|
2111
|
+
let walked;
|
|
2112
|
+
try { walked = await walkDoorSnapshot(o, ds, root); } catch (e) { o.err(`${tag} RK_CHECK_ERROR: [${stage}] ${e.message}`); return false; }
|
|
2113
|
+
const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
|
|
2114
|
+
const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`);
|
|
2115
|
+
let problems = walked.problems;
|
|
2116
|
+
if (result.skipped) problems = [...problems, ...diffProblems(before, walked.entries)];
|
|
2117
|
+
else problems = [...problems, ...identityOutputProblems({ before, after: walked.entries, out: underApp(result.out), declared, packageFile: result.package })];
|
|
2118
|
+
if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] windows-identity ${config}: ${p}`); return false; }
|
|
2119
|
+
const priorByPath = new Map(before.map((e) => [e.path, e]));
|
|
2120
|
+
ds.ledger = { entries: walked.entries.map((e) => (priorByPath.get(e.path)?.sha256 === e.sha256 ? priorByPath.get(e.path) : { ...e, kind: "generated", producer: { stage: "windows-identity", step: config } })) };
|
|
2121
|
+
}
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
const diffProblems = (before, after) => { const b = new Map(before.map((e) => [e.path, e.sha256])); return after.filter((e) => b.get(e.path) !== e.sha256).map((e) => `${e.path} was written although the identity build reported a skip`); };
|
|
2125
|
+
|
|
1862
2126
|
/** Hosted CI scans its committed runner checkout. Raw-tree verification defeats filters or a stale index hiding edits. */
|
|
1863
2127
|
async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
|
|
1864
2128
|
const head = git("rev-parse", "--verify", "HEAD^{commit}");
|
|
@@ -1873,7 +2137,7 @@ async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
|
|
|
1873
2137
|
}
|
|
1874
2138
|
|
|
1875
2139
|
/** Select output-binding workspaces from committed source before any Cargo metadata process runs. */
|
|
1876
|
-
async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
|
|
2140
|
+
async function committedCargoManifests({ o, git, root, commit, scopeDirs, extraWorkspaces = [] }) {
|
|
1877
2141
|
const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
1878
2142
|
if (failed(listed)) throw new Error(`cannot list the files of ${commit.slice(0, 12)}: ${why(listed)}`);
|
|
1879
2143
|
const files = text(listed).split("\0").filter(Boolean).map(posix);
|
|
@@ -1900,7 +2164,9 @@ async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
|
|
|
1900
2164
|
const inventory = discoverRustWorkspaces({ root, tracked, fixtures, readFile, metadata: sourceMetadata });
|
|
1901
2165
|
const npm = discoverNpmUnits({ root, tracked, readFile, exists });
|
|
1902
2166
|
const closure = tools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: inventory.units, readFile: readCommitted });
|
|
1903
|
-
|
|
2167
|
+
// Workspaces the unit's own compile builds beside its closure (a crate's consumer rows under ci/consumers/): bound when the commit tracks their manifest.
|
|
2168
|
+
const extra = extraWorkspaces.filter((dir) => fileSet.has(`${dir}/Cargo.toml`));
|
|
2169
|
+
const workspaces = [...new Set([...inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir), ...extra])].sort();
|
|
1904
2170
|
o.out(`rightkit-gate: Cargo output scope ${scopeDirs.join(", ")}: ${workspaces.length} workspace(s) (${workspaces.join(", ") || "none"}); dependency directories ${closure.dependencyDirs.join(", ")}`);
|
|
1905
2171
|
return { manifests: workspaces.map((dir) => path.posix.join(dir, "Cargo.toml")), hasCargo: allManifests.length > 0 };
|
|
1906
2172
|
}
|
|
@@ -1911,9 +2177,9 @@ async function cargoOutputPinForScope(options) {
|
|
|
1911
2177
|
}
|
|
1912
2178
|
|
|
1913
2179
|
/** Bind only committed closure workspaces to checkout outputs; runtime Cargo outside that map refuses. */
|
|
1914
|
-
async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
|
|
2180
|
+
async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs, extraWorkspaces = [], onlyWorkspaces = null }) {
|
|
1915
2181
|
let selection;
|
|
1916
|
-
try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs }); }
|
|
2182
|
+
try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs, extraWorkspaces }); }
|
|
1917
2183
|
catch (error) { return { refusal: error.message }; }
|
|
1918
2184
|
const { manifests, hasCargo } = selection;
|
|
1919
2185
|
if (!hasCargo) return { env: {}, note: null, cleanup() {} };
|
|
@@ -1959,8 +2225,12 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
|
|
|
1959
2225
|
const workspaces = Object.keys(targetDirs);
|
|
1960
2226
|
if (o.platform === "win32" && executor.kind !== "broker") {
|
|
1961
2227
|
if (workspaces.length === 0) return { refusal: "the selected closure has no Cargo workspace; a direct Windows host cannot wrap undeclared Cargo calls" };
|
|
2228
|
+
// A stage that builds exactly one of them (the package stage builds the app's own workspace, the one holding its release config) pins
|
|
2229
|
+
// that workspace's target; the others are not built by it (HeardRight: tauri-app-next built, tauri-app-next/qa-e2e not).
|
|
2230
|
+
const only = Array.isArray(onlyWorkspaces) ? workspaces.filter((w) => onlyWorkspaces.includes(w)) : [];
|
|
2231
|
+
if (workspaces.length > 1 && only.length === 1) return { targetDirs, env: { CARGO_TARGET_DIR: targetDirs[only[0]] }, note: `output directory for the Cargo workspace ${only[0]}: ${targetDirs[only[0]]} (the checkout's); this stage builds no other workspace (${workspaces.filter((w) => w !== only[0]).join(", ")})`, cleanup() {} };
|
|
1962
2232
|
if (workspaces.length > 1) return { refusal: `the commit holds ${workspaces.length} Cargo workspaces (${workspaces.join(", ")}) and a direct Windows host has no per-call wrapper to bind each to its own target directory` };
|
|
1963
|
-
return { env: { CARGO_TARGET_DIR: Object.values(targetDirs)[0] }, note: `output directory for the Cargo workspace: ${Object.values(targetDirs)[0]} (the checkout's)`, cleanup() {} };
|
|
2233
|
+
return { targetDirs, env: { CARGO_TARGET_DIR: Object.values(targetDirs)[0] }, note: `output directory for the Cargo workspace: ${Object.values(targetDirs)[0]} (the checkout's)`, cleanup() {} };
|
|
1964
2234
|
}
|
|
1965
2235
|
const shimModule = await import("../cargo-target-shim.mjs");
|
|
1966
2236
|
let broker = null; let realCargo = null;
|
|
@@ -1972,7 +2242,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
|
|
|
1972
2242
|
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
1973
2243
|
broker = { checkoutDirs: Object.fromEntries(workspaces.map((w) => [w, path.join(checkoutReal, ...(w === "." ? [] : w.split("/")))])), section: (o.brokerSection ?? brokerSection)(sb) };
|
|
1974
2244
|
} else {
|
|
1975
|
-
realCargo = which("cargo")
|
|
2245
|
+
realCargo = realCargoOf(which("cargo"));
|
|
1976
2246
|
if (!realCargo) return { refusal: "no cargo on PATH to wrap for the unit stages" };
|
|
1977
2247
|
}
|
|
1978
2248
|
let installed; let dir = null;
|
|
@@ -1982,6 +2252,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
|
|
|
1982
2252
|
const delta = Object.fromEntries(Object.entries(scan).filter(([k, v]) => o.env[k] !== v));
|
|
1983
2253
|
const { CARRIED_CARGO_MAP_ENV } = await import("../carried-cargo.mjs");
|
|
1984
2254
|
return {
|
|
2255
|
+
targetDirs, // { [cargoDir]: the checkout's target directory } on a direct host (null per workspace on a broker host, whose carried receipt names the output directory)
|
|
1985
2256
|
env: { ...delta, ...(installed.mode === "broker" ? { [CARRIED_CARGO_MAP_ENV]: installed.mapFile } : {}) }, note: `runtime cargo wrapper ${installed.shim} (${installed.mode}): each cargo call a unit stage reaches binds its workspace's checkout target directory or is refused`,
|
|
1986
2257
|
// Every call the wrapper decided is in the gate-owned ledger: a refused or unverified call fails the stage even when the test ignored cargo's exit (7.10, decision C).
|
|
1987
2258
|
check: async () => { const { readLedger, UNSCOPED } = await import("../cargo-ledger.mjs"); return readLedger(installed.ledgerDir, UNSCOPED).problems ?? []; },
|
|
@@ -1991,7 +2262,7 @@ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
|
|
|
1991
2262
|
|
|
1992
2263
|
/**
|
|
1993
2264
|
* `--to <stage> --unit <selector> [--commit <rev>]` on the snapshot route (row 7.06a): select a commit (HEAD by default), take the
|
|
1994
|
-
*
|
|
2265
|
+
* repository's build ownership, sync its snapshot, run class A of the snapshot (`check --commit <sha> --full-a --snapshot <dir>`), and build
|
|
1995
2266
|
* the unit from the snapshot (`--workspace <snapshot>`). Commit and tree come from the selection; no HEAD or status is read. The unit
|
|
1996
2267
|
* manifest and the local-build grant are read from the selected commit, so one present only in the checkout is refused. Each stage is
|
|
1997
2268
|
* bracketed by the ledger verify (7.08); Cargo's output is pinned to the checkout's target directory (7.09); `--to release` keeps its
|
|
@@ -2004,6 +2275,12 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
2004
2275
|
const match = /^(?:npm:@rightkit\/([a-z0-9-]+)|cargo:(rightkit-[a-z0-9-]+))$/.exec(selector);
|
|
2005
2276
|
const manifest = match && (match[1] ? `packages/${match[1]}/package.json` : `crates/${match[2]}/Cargo.toml`);
|
|
2006
2277
|
if (!manifest) { o.err(`${tag} BLOCKED: unit needs npm:@rightkit/<name> or cargo:rightkit-<name>`); return 1; }
|
|
2278
|
+
// `--to tool` (tool-build.mjs) builds and enrols one named tool binary; its unit and host are decided here, before ownership, a snapshot or any process.
|
|
2279
|
+
const toolRun = option("--to") === "tool";
|
|
2280
|
+
if (toolRun) {
|
|
2281
|
+
const { toolPlan } = await import("../tool-build.mjs");
|
|
2282
|
+
try { toolPlan({ selector, platform: o.platform }); } catch (e) { o.err(`${tag} BLOCKED: ${e.message}; nothing ran`); return 1; }
|
|
2283
|
+
}
|
|
2007
2284
|
const snap = await import("../snapshot.mjs");
|
|
2008
2285
|
const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
|
|
2009
2286
|
let selected;
|
|
@@ -2013,7 +2290,7 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
2013
2290
|
// The canonical manifest and the local-build grant must be tracked in the SELECTED COMMIT (read with the object database, never the checkout).
|
|
2014
2291
|
for (const file of [".rightkit-local-build.json", manifest]) if (failed(git("cat-file", "-e", `${commit}:${file}`))) { o.err(`${tag} BLOCKED: unit and local grant must be tracked canonical files of ${commit.slice(0, 12)} (${file} is not)`); return 1; }
|
|
2015
2292
|
const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
2016
|
-
const executor = (o.resolveExecutor ??
|
|
2293
|
+
const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
2017
2294
|
if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
2018
2295
|
{ const decided = routePlan(o, { to: option("--to"), stages: [], devTest: true, executor, capabilities: await brokerCapabilityStateWithHandshake(o, { to: option("--to"), executor }) });
|
|
2019
2296
|
if (decided.refusal?.code === "RK_BROKER_CAPABILITY_MISSING") { o.err(`${tag} BLOCKED: ${decided.refusal.code}: ${decided.refusal.message}; nothing ran`); return 1; } }
|
|
@@ -2023,7 +2300,7 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
2023
2300
|
const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
|
|
2024
2301
|
{ const mismatch = await declaredSnapshotRootRefusal(o, executor, snapshotsAt); if (mismatch) { o.err(`${tag} BLOCKED: ${mismatch.code}: ${mismatch.message}; nothing ran`); return 1; } }
|
|
2025
2302
|
let own;
|
|
2026
|
-
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: `door:unit:${option("--to")}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit, platform: o.platform, env: o.env }); }
|
|
2303
|
+
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: snap.repositoryOf(root).key }), stage: `door:unit:${option("--to")}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit, platform: o.platform, env: o.env }); }
|
|
2027
2304
|
catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`${tag} BLOCKED: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 1; }
|
|
2028
2305
|
o.out(`${tag} build ownership ${own.joined ? "joined" : "held"} ${own.handle}`);
|
|
2029
2306
|
try {
|
|
@@ -2035,7 +2312,14 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
2035
2312
|
// A unit's test or pack that reaches Cargo must never write `target/` into the snapshot (npm units included): see bindUnitCargo.
|
|
2036
2313
|
// A check starts no unit command: its own Class A path binds read-only Cargo calls. Resolving every unit's compile output
|
|
2037
2314
|
// here would run unrelated metadata before the scoped check even begins.
|
|
2038
|
-
|
|
2315
|
+
// A crate's consumer rows (ci/gate.config.json rust.<crate>.consumerRows, local-publish.mjs) build their own Cargo workspaces under ci/consumers/: they
|
|
2316
|
+
// are part of this unit's compile, so they are bound beside the crate's workspace (read from the snapshot, which is the commit's tree).
|
|
2317
|
+
let consumerScope = [];
|
|
2318
|
+
if (match[2] && !toolRun) {
|
|
2319
|
+
try { consumerScope = [...new Set((JSON.parse(fs.readFileSync(path.join(ds.path, "ci", "gate.config.json"), "utf8")).rust?.[match[2]]?.consumerRows ?? []).map((row) => path.posix.dirname(String(row.manifestPath))))]; }
|
|
2320
|
+
catch { consumerScope = []; }
|
|
2321
|
+
}
|
|
2322
|
+
const bound = option("--to") === "check" ? { env: {}, cleanup() {} } : await bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs: [path.posix.dirname(manifest)], extraWorkspaces: consumerScope, ...(toolRun ? { onlyWorkspaces: [path.posix.dirname(manifest)] } : {}) });
|
|
2039
2323
|
if (bound.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${bound.refusal}; nothing ran`); return 1; }
|
|
2040
2324
|
if (bound.note) o.out(`${tag} ${bound.note}`);
|
|
2041
2325
|
const envExtra = bound.env;
|
|
@@ -2045,17 +2329,27 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
2045
2329
|
const reportOut = option("--receipt-out");
|
|
2046
2330
|
const { runUnitGate, resolveUnit } = await import("../unit-gate.mjs");
|
|
2047
2331
|
const { localBuildGrantedAt } = await import("../local-publish.mjs");
|
|
2048
|
-
|
|
2049
|
-
resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
|
|
2050
|
-
...(o.loadUnitOwner ? { loadOwner: o.loadUnitOwner } : {}),
|
|
2051
|
-
...(o.unitGateDeps ?? {}), // test seam: fetchReceipt, stageCommands, verifyReceipts, selfGit
|
|
2052
|
-
stageGuard: { start: (stage) => openSnapshotStage({ o, ds, root, stage: `unit:${stage}`, tag }), end: async (stage) => {
|
|
2332
|
+
const stageGuard = { start: (stage) => openSnapshotStage({ o, ds, root, stage: `unit:${stage}`, tag }), end: async (stage) => {
|
|
2053
2333
|
if (!(await closeSnapshotStage({ o, ds, root, stage: `unit:${stage}`, step: `unit:${stage}`, tag })).ok) return false;
|
|
2054
2334
|
const calls = bound.check ? await bound.check() : [];
|
|
2055
2335
|
for (const p of calls.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [unit:${stage}] a cargo call the unit stage made did not complete: ${typeof p === "string" ? p : JSON.stringify(p)}`);
|
|
2056
2336
|
return calls.length === 0;
|
|
2057
|
-
} }
|
|
2058
|
-
|
|
2337
|
+
} };
|
|
2338
|
+
const runCheck = (tier) => (o.runCheck ?? (() => main(["check", "--commit", ds.commit, "--full-a", "--snapshot", ds.path, "--rule-tier", tier, "--scope", path.posix.dirname(manifest)], { ...io, cwd: root, buildOwnership: own })))(tier);
|
|
2339
|
+
if (toolRun) {
|
|
2340
|
+
const { runUnitTool } = await import("../tool-build.mjs");
|
|
2341
|
+
return await runUnitTool({ selector, root: ds.path, commonDir: path.resolve(root, text(common).trim()), commit: ds.commit, tree: ds.tree, executor, env: o.env, platform: o.platform, source, envExtra, targetDirs: bound.targetDirs ?? {},
|
|
2342
|
+
resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
|
|
2343
|
+
...(o.toolBuildDeps ?? {}), // test seam: run, enroll, dataRoot, readKnownRed
|
|
2344
|
+
stageGuard, check: runCheck, out: o.out, err: o.err,
|
|
2345
|
+
...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
return await runUnitGate({ selector, to: option("--to"), root: ds.path, commonDir: path.resolve(root, text(common).trim()), commit: ds.commit, tree: ds.tree, executor, env: o.env, platform: o.platform, source, envExtra,
|
|
2349
|
+
resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
|
|
2350
|
+
...(o.loadUnitOwner ? { loadOwner: o.loadUnitOwner } : {}),
|
|
2351
|
+
...(o.unitGateDeps ?? {}), // test seam: fetchReceipt, stageCommands, verifyReceipts, selfGit
|
|
2352
|
+
stageGuard, check: runCheck, out: o.out, err: o.err,
|
|
2059
2353
|
...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
|
|
2060
2354
|
});
|
|
2061
2355
|
} finally { bound.cleanup(); }
|
|
@@ -2095,7 +2389,7 @@ function sourceSplitOf({ root, ds, appDir }) {
|
|
|
2095
2389
|
*/
|
|
2096
2390
|
export function snapshotRouteRefusal(o) {
|
|
2097
2391
|
const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
2098
|
-
const executor = (o.resolveExecutor ??
|
|
2392
|
+
const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
|
|
2099
2393
|
if (executor.kind === "refuse") return { code: "RK_SNAPSHOT_BROKER_UNAVAILABLE", message: executor.problem };
|
|
2100
2394
|
if (executor.kind === "broker" && !o.snapshotBroker) return { code: "RK_SNAPSHOT_CARRIER_NOT_WIRED", message: `a ${o.platform} host compiles a commit snapshot only through the RustRight snapshot carrier (item 7 rows 7.20 to 7.24), and this gate was started without it` };
|
|
2101
2395
|
return null;
|
|
@@ -2270,7 +2564,7 @@ async function runRouteProbe(o, args, root, git, { to, platform, stages, devTest
|
|
|
2270
2564
|
try { selected = snap.resolveCommit(root, option("--commit") ?? "HEAD"); }
|
|
2271
2565
|
catch (e) { o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
|
|
2272
2566
|
const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
2273
|
-
const executor = (o.resolveExecutor ??
|
|
2567
|
+
const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
|
|
2274
2568
|
let plan;
|
|
2275
2569
|
const capabilities = await brokerCapabilityStateWithHandshake(o, { to, executor });
|
|
2276
2570
|
try { plan = routePlan(o, { to, stages, devTest, executor, stagePlatform: platform, capabilities, appConfig: to !== "check" ? routeAppConfig({ git, commit: selected.commit, appDir, platform: devTestPlatform(args) }) : null }); }
|
|
@@ -2544,7 +2838,7 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
|
|
|
2544
2838
|
if (failed(dirty) || text(dirty).trim()) { o.err(`${tag} BLOCKED: the checkout differs from HEAD; every stage works on the commit, so commit or stash first`); return 1; }
|
|
2545
2839
|
}
|
|
2546
2840
|
const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
2547
|
-
const executor = (o.resolveExecutor ??
|
|
2841
|
+
const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
2548
2842
|
if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
2549
2843
|
o.out(`${tag} --to ${to} for ${commit.slice(0, 12)}${appDir === "." ? "" : ` (app ${appDir})`}: check (class A) -> right-release ${to}; executor ${executor.kind}${ds ? `; snapshot ${ds.path}` : ""}`);
|
|
2550
2844
|
// Full class A of the commit before right-release compiles anything: of the snapshot, the checks that execute repository code included.
|
|
@@ -2686,7 +2980,7 @@ export async function main(argv, io = {}) {
|
|
|
2686
2980
|
// commit, never a diff. Nothing compiles. The RustRight broker runs exactly this before it admits any compile. `--list`,
|
|
2687
2981
|
// `--print-baseline` and `--only` stay app-check-only views.
|
|
2688
2982
|
// `check --commit <sha>` without `--snapshot` reads the gate's one warm snapshot of that commit, selected the way
|
|
2689
|
-
// every `--to` stage selects it:
|
|
2983
|
+
// every `--to` stage selects it: per-repository build ownership first, then one sync of the commit into the gate-owned source directory, then the
|
|
2690
2984
|
// check of that snapshot below. There is no export and no copy of the source (docs/GATE-CONTRACT.md, 2026-10-29); the checkout's own files are
|
|
2691
2985
|
// never read, so an uncommitted edit in it is ignored. Hosted CI has no warm snapshot: its runner checkout is the commit, scanned in place.
|
|
2692
2986
|
// `check --commit <sha> --full-a` takes the same route (GB-48, GATE 34/36/44, E17): the selected commit's snapshot, never a conversion to
|
|
@@ -2706,7 +3000,7 @@ export async function main(argv, io = {}) {
|
|
|
2706
3000
|
const selected = sc.commitOf(checkout, option("--commit") ?? "HEAD", o.spawn);
|
|
2707
3001
|
const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
|
|
2708
3002
|
if (!own) {
|
|
2709
|
-
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: "check", snapshot: path.join(snapshotsAt, snap.repositoryOf(checkout).key), commit: selected.commit, platform: o.platform, env: o.env }); ownHere = true; }
|
|
3003
|
+
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: snap.repositoryOf(checkout).key }), stage: "check", snapshot: path.join(snapshotsAt, snap.repositoryOf(checkout).key), commit: selected.commit, platform: o.platform, env: o.env }); ownHere = true; }
|
|
2710
3004
|
catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`rightkit-gate: RK_CHECK_ERROR: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 2; }
|
|
2711
3005
|
o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
|
|
2712
3006
|
}
|
|
@@ -2722,12 +3016,19 @@ export async function main(argv, io = {}) {
|
|
|
2722
3016
|
finally { if (ownHere) { const r = own.release(); if (r?.released === false && !own.joined) o.err(`rightkit-gate: build ownership ${own.handle} kept: ${r.why}`); } }
|
|
2723
3017
|
}
|
|
2724
3018
|
if ((sub === "check" || (!sub && args.includes("--all"))) && args.includes("--snapshot") && !o.buildOwnership) {
|
|
2725
|
-
//
|
|
3019
|
+
// Per-repository build ownership (../build-ownership.mjs; GATE-CONTRACT §1): held from before the first snapshot read through the
|
|
2726
3020
|
// end of the check, so no other request can sync the snapshot while class A runs in it. A nested gate whose
|
|
2727
3021
|
// RIGHTKIT_BUILD_OWNERSHIP names the current owner joins it. Busy (or liveness unknown) runs nothing.
|
|
2728
3022
|
const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
|
|
3023
|
+
const snap = await import("../snapshot.mjs");
|
|
3024
|
+
let key;
|
|
3025
|
+
try { key = snap.repositoryOf(option("--repo") ? path.resolve(o.cwd, option("--repo")) : o.cwd).key; }
|
|
3026
|
+
catch (e) { o.err(`rightkit-gate: RK_CHECK_ERROR: the repository whose build ownership this check takes cannot be identified: ${e.message}; nothing ran`); return 2; }
|
|
3027
|
+
// The record is keyed by the repository --repo/cwd names while --snapshot names a separate directory: they must be one repository.
|
|
3028
|
+
const snapshotRootHere = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
|
|
3029
|
+
if (path.basename(option("--snapshot") ?? "") !== key || path.resolve(o.cwd, path.dirname(option("--snapshot"))) !== path.resolve(o.cwd, snapshotRootHere)) { o.err(`rightkit-gate: RK_CHECK_ERROR: --snapshot ${option("--snapshot")} is not the snapshot of repository ${key} (its directory name must be the repository key, inside the snapshot root ${snapshotRootHere}); nothing ran`); return 2; }
|
|
2729
3030
|
let own;
|
|
2730
|
-
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: sub ?? "compile", snapshot: option("--snapshot") ?? null, commit: option("--commit") ?? null, platform: o.platform, env: o.env }); }
|
|
3031
|
+
try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: key }), stage: sub ?? "compile", snapshot: option("--snapshot") ?? null, commit: option("--commit") ?? null, platform: o.platform, env: o.env }); }
|
|
2731
3032
|
catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`rightkit-gate: RK_CHECK_ERROR: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 2; }
|
|
2732
3033
|
o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
|
|
2733
3034
|
try { return await main(argv, { ...io, buildOwnership: own }); }
|
|
@@ -2900,7 +3201,11 @@ export async function main(argv, io = {}) {
|
|
|
2900
3201
|
const scopeDirs = args.flatMap((a, i) => a === "--scope" ? [args[i + 1]] : []);
|
|
2901
3202
|
if (scopeDirs.length) {
|
|
2902
3203
|
const bad = scopeDirs.find((d) => typeof d !== "string" || !d || d.startsWith("--") || path.isAbsolute(d) || /^[A-Za-z]:/.test(d) || /(^|[\\/])\.\.([\\/]|$)/.test(d));
|
|
2903
|
-
|
|
3204
|
+
// Full Class A needs a snapshot or the committed hosted checkout. The content pass (`check --commit <sha> --scope <dir>`: publication
|
|
3205
|
+
// admission, GC-A03) reads the commit the same way it always did (the warm snapshot, or the runner checkout proven to be that commit
|
|
3206
|
+
// in place) and only narrows which executing workspace scans run, so it is the one other form --scope accepts; never the live tree.
|
|
3207
|
+
const scopeForm = fullA ? Boolean(snapshotA || hostedCommitA) : classAOnly && !args.includes("--worktree");
|
|
3208
|
+
if (!scopeForm || bad !== undefined || scopeDirs.some((d) => d === undefined)) { o.err("rightkit-gate: --scope needs full Class A of a snapshot or committed hosted CI checkout & repository-relative directories"); return 2; }
|
|
2904
3209
|
}
|
|
2905
3210
|
if (args.includes("--base") && (!option("--base") || option("--base").startsWith("--"))) { o.err("rightkit-gate: --base needs a ref"); return 2; }
|
|
2906
3211
|
|
|
@@ -2981,7 +3286,7 @@ export async function main(argv, io = {}) {
|
|
|
2981
3286
|
if (!e.code?.startsWith("RK_BROKER_EXCEPTION_")) { o.err(`rightkit-gate: cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
|
|
2982
3287
|
o.err(`rightkit-gate: BLOCKED: ${e.message}; nothing ran`); return e.code === BROKER_EXCEPTION_ERRORS.usage ? 2 : 1;
|
|
2983
3288
|
}
|
|
2984
|
-
const brokerResolver = o.resolveExecutor ??
|
|
3289
|
+
const brokerResolver = o.resolveExecutor ?? gateResolveExecutor;
|
|
2985
3290
|
o.resolveExecutor = (a) => brokerResolver({ ...a, forceDirect: true });
|
|
2986
3291
|
o.brokerException = granted;
|
|
2987
3292
|
io = { ...io, resolveExecutor: o.resolveExecutor, brokerException: granted };
|
|
@@ -3022,7 +3327,7 @@ export async function main(argv, io = {}) {
|
|
|
3022
3327
|
const manifest = match && (match[1] ? `packages/${match[1]}/package.json` : `crates/${match[2]}/Cargo.toml`);
|
|
3023
3328
|
if (!manifest || failed(git("ls-files", "--error-unmatch", "--", ".rightkit-local-build.json", manifest))) { o.err("rightkit-gate: BLOCKED: unit and local grant must be tracked canonical files"); return 1; }
|
|
3024
3329
|
const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
3025
|
-
const executor = (o.resolveExecutor ??
|
|
3330
|
+
const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
3026
3331
|
if (executor.kind === "refuse") { o.err(`rightkit-gate: BLOCKED: ${executor.problem}`); return 1; }
|
|
3027
3332
|
const reportOut = option("--receipt-out");
|
|
3028
3333
|
// Loaded here, not at the top: the unit path reads Cargo manifests (smol-toml), which an installed copy of this package without
|
|
@@ -3082,7 +3387,7 @@ export async function main(argv, io = {}) {
|
|
|
3082
3387
|
// The executor is chosen here, never by the caller (Adrian, 2026-10-25): the RustRight broker where this machine has it, a refusal
|
|
3083
3388
|
// on a broker-managed machine whose broker cannot be reached, direct cargo on any other host (a cloud container, a CI runner).
|
|
3084
3389
|
const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
|
|
3085
|
-
const executor = (o.resolveExecutor ??
|
|
3390
|
+
const executor = (o.resolveExecutor ?? gateResolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
3086
3391
|
// A broker-managed host whose broker is unreachable refuses the moment a Cargo manifest is in the snapshot (below): a repository
|
|
3087
3392
|
// with no Rust owes nothing to the broker (a git hook's minimal PATH, a JS-only app), and the class A scans never compile.
|
|
3088
3393
|
const cargoCommand = executor.cargo ?? ["cargo"];
|
|
@@ -3245,6 +3550,59 @@ export async function main(argv, io = {}) {
|
|
|
3245
3550
|
// digested in the snapshot, and the stage's own timings.
|
|
3246
3551
|
let outputResolution = {}; let directDeclared = []; let directDeclaredNotes = [];
|
|
3247
3552
|
const directTiming = { bindStart: 0, bindEnd: 0, cargoStart: 0, cargoEnd: 0, afterStart: 0, afterEnd: 0, diagnostics: 0 };
|
|
3553
|
+
// The wrapper and each workspace's output directory, resolved before ANY scan runs (rightkit#1328): Class A runs once, before the compile,
|
|
3554
|
+
// under this wrapper and its per-scan sentinel, so a cargo that bypasses it fails the Class A row, not a later pass. Nothing compiles here.
|
|
3555
|
+
// `wrapRoot` is the verified snapshot, `wrapBroker` the broker of a broker host (the compile's, or the check's discovery broker).
|
|
3556
|
+
const bindCargoWrapper = async (wrapRoot, wrapBroker) => {
|
|
3557
|
+
targetDirs = {};
|
|
3558
|
+
// A scoped run (--unit or a nested app) wraps its closure's workspaces (GC-A03) and, always, every workspace inside a scope
|
|
3559
|
+
// directory: a unit's own workspace is wrapped even when closure membership names its crates differently.
|
|
3560
|
+
const inScope = (ws) => scopeDirs.some((d) => { const s = d.replace(/\/+$/, ""); return ws.cargoDir === s || ws.cargoDir.startsWith(`${s}/`) || s.startsWith(`${ws.cargoDir}/`); });
|
|
3561
|
+
const wrapped = closure ? rust.workspaces.filter((ws) => inScope(ws) || ws.units.some((u) => closure.rust.has(u.name))) : rust.workspaces;
|
|
3562
|
+
for (const ws of wrapBroker ? [] : wrapped) {
|
|
3563
|
+
const manifest = path.join(root, ws.cargoDir, "Cargo.toml");
|
|
3564
|
+
if (!o.exists(manifest)) { o.err(`${tag} BLOCKED: the checkout has no ${posix(path.relative(root, manifest))}, so the output directory of workspace ${ws.cargoDir} cannot be resolved; nothing compiled`); return 1; }
|
|
3565
|
+
// Resolved in the PRIMARY CHECKOUT with the stage's environment, before the source location changes (row 7.09: CARGO_TARGET_DIR,
|
|
3566
|
+
// a relative or absolute build.target-dir, or the default target/ all resolve as for a developer's own build); the cwd and
|
|
3567
|
+
// environment used are recorded. A directory inside the snapshot is refused before anything compiles (a second output).
|
|
3568
|
+
let resolved;
|
|
3569
|
+
try { resolved = resolveTargetDirectory({ spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, manifest, env: o.env, checkoutRoot: root, snapshotRoot: wrapRoot, workspace: ws.cargoDir }); }
|
|
3570
|
+
catch (error) { o.err(`${tag} BLOCKED: ${error.message}; nothing compiled`); return 1; }
|
|
3571
|
+
targetDirs[ws.cargoDir] = resolved.dir; outputResolution[ws.cargoDir] = resolved.inputs;
|
|
3572
|
+
o.out(`${tag} output directory for workspace ${ws.cargoDir}: ${targetDirs[ws.cargoDir]} (the checkout's)`);
|
|
3573
|
+
}
|
|
3574
|
+
// CARGO_HOME, sccache & temp are inherited as absolute values, never snapshot-relative (GATE-CONTRACT "Output directory & cache"):
|
|
3575
|
+
// a relative one would be resolved inside the snapshot, a second cache or output location. Refused before anything compiles.
|
|
3576
|
+
const relativeEnv = SNAPSHOT_ABSOLUTE_ENV.filter((name) => typeof o.env[name] === "string" && o.env[name] !== "" && !path.isAbsolute(o.env[name]));
|
|
3577
|
+
if (relativeEnv.length) { o.err(`${tag} BLOCKED: ${relativeEnv.map((n) => `${n}=${o.env[n]}`).join(", ")} ${relativeEnv.length > 1 ? "are" : "is"} relative; a snapshot compile inherits CARGO_HOME, sccache and temp only as absolute paths, never resolved inside the snapshot; nothing compiled`); return 1; }
|
|
3578
|
+
// The runtime cargo wrapper (item 7): first on PATH and named in CARGO for every scan, it binds each real cargo invocation to its
|
|
3579
|
+
// own workspace's checkout target directory or refuses it (RK_CARGO_*), whatever path reaches cargo. Direct POSIX hosts only (a
|
|
3580
|
+
// broker host never reaches here; a direct Windows host keeps the fail-closed static walk). Removed when the gate ends (cleanup).
|
|
3581
|
+
if (o.platform !== "win32" && rust.workspaces.length) {
|
|
3582
|
+
const realCargo = realCargoOf(which("cargo"));
|
|
3583
|
+
if (!realCargo && !wrapBroker) { o.err(`${tag} BLOCKED: no cargo on PATH to wrap for the snapshot compile; nothing compiled`); return 1; }
|
|
3584
|
+
try {
|
|
3585
|
+
const shimModule = await import("../cargo-target-shim.mjs");
|
|
3586
|
+
let broker = null;
|
|
3587
|
+
if (wrapBroker) {
|
|
3588
|
+
// Broker mode (the Mac, decision C): every cargo a test reaches is its own carried request. The gate's own snapshot-to-checkout
|
|
3589
|
+
// map is the only source of a request's primaryCwd; each carried request records its sub-handle in the ownership just before it is sent.
|
|
3590
|
+
const checkoutRoot = fs.realpathSync.native(root);
|
|
3591
|
+
const checkoutDirs = Object.fromEntries(rust.workspaces.map((ws) => [ws.cargoDir, path.join(checkoutRoot, ...(ws.cargoDir === "." ? [] : ws.cargoDir.split("/")))]));
|
|
3592
|
+
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
3593
|
+
broker = { checkoutDirs, section: brokerSection(wrapBroker) };
|
|
3594
|
+
}
|
|
3595
|
+
// Its own base directory: the wrapper's directory is named by its identity, and a unit stage's outer wrapper over the same
|
|
3596
|
+
// workspaces has the same identity, so a shared base would let this run's cleanup delete the wrapper the next stage runs under.
|
|
3597
|
+
const shimBase = fs.mkdtempSync(path.join(os.tmpdir(), "rk-gate-shim-"));
|
|
3598
|
+
cargoShimDir = shimBase;
|
|
3599
|
+
cargoShim = shimModule.installCargoShim({ base: shimBase, snapshotRoot: wrapRoot, targetDirs, broker, realCargo: realCargo ?? null, platform: o.platform });
|
|
3600
|
+
cargoShim.scanEnv = shimModule.shimScanEnv(o.env, cargoShim);
|
|
3601
|
+
} catch (error) { o.err(`${tag} BLOCKED: the runtime cargo wrapper cannot be installed (${error.message}); nothing compiled`); return 1; }
|
|
3602
|
+
o.out(`${tag} runtime cargo wrapper ${cargoShim.shim} (identity ${cargoShim.identity.slice(0, 16)}, ${cargoShim.mode}) -> ${cargoShim.mode === "broker" ? "the RustRight broker, one carried request per call" : realCargo}: each cargo invocation ${cargoShim.mode === "broker" ? "is carried with its own receipt, verified by the gate," : "binds its workspace's checkout target directory"} or is refused`);
|
|
3603
|
+
}
|
|
3604
|
+
return 0;
|
|
3605
|
+
};
|
|
3248
3606
|
if (snapshotB) {
|
|
3249
3607
|
const sc = await import("../snapshot-check.mjs");
|
|
3250
3608
|
try {
|
|
@@ -3281,45 +3639,14 @@ export async function main(argv, io = {}) {
|
|
|
3281
3639
|
} catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SNAPSHOT_BROKER"}: ${error.message}; nothing compiled`); return 1; }
|
|
3282
3640
|
o.out(`${tag} broker snapshot compile: pins ${snapshotBroker.pins.keyId.slice(0, 12)} from ${snapshotBroker.pinsSource}, ${snapshotBroker.declaredInputs.length} declared input(s) read from the snapshot, ownership ${snapshotBroker.handle}`);
|
|
3283
3641
|
}
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
try { resolved = resolveTargetDirectory({ spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, manifest, env: o.env, checkoutRoot: root, snapshotRoot: snapshotB, workspace: ws.cargoDir }); }
|
|
3293
|
-
catch (error) { o.err(`${tag} BLOCKED: ${error.message}; nothing compiled`); return 1; }
|
|
3294
|
-
targetDirs[ws.cargoDir] = resolved.dir; outputResolution[ws.cargoDir] = resolved.inputs;
|
|
3295
|
-
o.out(`${tag} output directory for workspace ${ws.cargoDir}: ${targetDirs[ws.cargoDir]} (the checkout's)`);
|
|
3296
|
-
}
|
|
3297
|
-
// CARGO_HOME, sccache & temp are inherited as absolute values, never snapshot-relative (GATE-CONTRACT "Output directory & cache"):
|
|
3298
|
-
// a relative one would be resolved inside the snapshot, a second cache or output location. Refused before anything compiles.
|
|
3299
|
-
const relativeEnv = SNAPSHOT_ABSOLUTE_ENV.filter((name) => typeof o.env[name] === "string" && o.env[name] !== "" && !path.isAbsolute(o.env[name]));
|
|
3300
|
-
if (relativeEnv.length) { o.err(`${tag} BLOCKED: ${relativeEnv.map((n) => `${n}=${o.env[n]}`).join(", ")} ${relativeEnv.length > 1 ? "are" : "is"} relative; a snapshot compile inherits CARGO_HOME, sccache and temp only as absolute paths, never resolved inside the snapshot; nothing compiled`); return 1; }
|
|
3301
|
-
// The runtime cargo wrapper (item 7): first on PATH and named in CARGO for every scan, it binds each real cargo invocation to its
|
|
3302
|
-
// own workspace's checkout target directory or refuses it (RK_CARGO_*), whatever path reaches cargo. Direct POSIX hosts only (a
|
|
3303
|
-
// broker host never reaches here; a direct Windows host keeps the fail-closed static walk). Removed when the gate ends (cleanup).
|
|
3304
|
-
if (o.platform !== "win32" && rust.workspaces.length) {
|
|
3305
|
-
const realCargo = which("cargo").find((c) => path.isAbsolute(c));
|
|
3306
|
-
if (!realCargo && !snapshotBroker) { o.err(`${tag} BLOCKED: no cargo on PATH to wrap for the snapshot compile; nothing compiled`); return 1; }
|
|
3307
|
-
try {
|
|
3308
|
-
const shimModule = await import("../cargo-target-shim.mjs");
|
|
3309
|
-
let broker = null;
|
|
3310
|
-
if (snapshotBroker) {
|
|
3311
|
-
// Broker mode (the Mac, decision C): every cargo a test reaches is its own carried request. The gate's own snapshot-to-checkout
|
|
3312
|
-
// map is the only source of a request's primaryCwd; each carried request records its sub-handle in the ownership just before it is sent.
|
|
3313
|
-
const checkoutRoot = fs.realpathSync.native(root);
|
|
3314
|
-
const checkoutDirs = Object.fromEntries(rust.workspaces.map((ws) => [ws.cargoDir, path.join(checkoutRoot, ...(ws.cargoDir === "." ? [] : ws.cargoDir.split("/")))]));
|
|
3315
|
-
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
3316
|
-
broker = { checkoutDirs, section: brokerSection(snapshotBroker) };
|
|
3317
|
-
}
|
|
3318
|
-
cargoShim = shimModule.installCargoShim({ base: os.tmpdir(), snapshotRoot: snapshotB, targetDirs, broker, realCargo: realCargo ?? null, platform: o.platform, onDir: (dir) => { cargoShimDir = dir; } });
|
|
3319
|
-
cargoShim.scanEnv = shimModule.shimScanEnv(o.env, cargoShim);
|
|
3320
|
-
} catch (error) { o.err(`${tag} BLOCKED: the runtime cargo wrapper cannot be installed (${error.message}); nothing compiled`); return 1; }
|
|
3321
|
-
o.out(`${tag} runtime cargo wrapper ${cargoShim.shim} (identity ${cargoShim.identity.slice(0, 16)}, ${cargoShim.mode}) -> ${cargoShim.mode === "broker" ? "the RustRight broker, one carried request per call" : realCargo}: each cargo invocation ${cargoShim.mode === "broker" ? "is carried with its own receipt, verified by the gate," : "binds its workspace's checkout target directory"} or is refused`);
|
|
3322
|
-
}
|
|
3642
|
+
{ const rc = await bindCargoWrapper(snapshotB, snapshotBroker); if (rc) return rc; }
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
// Full class A of a snapshot (the door's check stage): the same wrapper, sentinel and ledger as the compile, installed BEFORE the scans so
|
|
3646
|
+
// the one Class A pass is the one that sees a cargo bypass (rightkit#1328). The compile pass runs class B only.
|
|
3647
|
+
if (!planOnly && fullA && snapshotA && rust.workspaces.length) {
|
|
3648
|
+
const rc = await bindCargoWrapper(snapshotA, executor.kind === "broker" ? discoveryBroker : null);
|
|
3649
|
+
if (rc) return rc;
|
|
3323
3650
|
}
|
|
3324
3651
|
|
|
3325
3652
|
// Class B scans (compile, lint, tests) run in the live checkout, so it must equal HEAD where they read it: an uncommitted fix could
|
|
@@ -3347,8 +3674,15 @@ export async function main(argv, io = {}) {
|
|
|
3347
3674
|
const changed = allFilesMode ? files : [...new Set(text(diff).split(/\r?\n/).map((f) => f.trim()).filter(Boolean))].filter((f) => o.exists(path.join(repo, f)));
|
|
3348
3675
|
if (allFilesMode) base = snapshot ?? "the working tree";
|
|
3349
3676
|
|
|
3350
|
-
let crateFeatures = {}; let packageFeatures = {};
|
|
3351
|
-
try { const gateConfig = JSON.parse(o.readFile(path.join(repo, "ci/gate.config.json"))); crateFeatures = gateConfig.rust ?? {}; packageFeatures = gateConfig.npm ?? {}; } catch { /* no config: default feature sets */ }
|
|
3677
|
+
let crateFeatures = {}; let packageFeatures = {}; let swiftPackages = {};
|
|
3678
|
+
try { const gateConfig = JSON.parse(o.readFile(path.join(repo, "ci/gate.config.json"))); crateFeatures = gateConfig.rust ?? {}; packageFeatures = gateConfig.npm ?? {}; swiftPackages = gateConfig.swift && typeof gateConfig.swift === "object" ? gateConfig.swift : {}; } catch { /* no config: default feature sets */ }
|
|
3679
|
+
// rightkit#1333 (P7): the end-to-end crates of the tree (a non-optional rightkit-qa dependency and a driver module), found from the files alone by the
|
|
3680
|
+
// same reader the G22 check rule uses; their test scans compile with --no-run and their tests are owed to the qualify stage (planScans e2eCrateDirs).
|
|
3681
|
+
let e2eCrateDirs = null;
|
|
3682
|
+
if (rust.units.length) {
|
|
3683
|
+
try { const { e2eCrates } = await import("../app-check.mjs"); e2eCrateDirs = new Set(e2eCrates(repo, files).crates.map((c) => c.dir)); }
|
|
3684
|
+
catch (e) { o.err(`${tag} BLOCKED: cannot tell which crates are end-to-end crates (${e.message}); their tests would run as class B unit tests, so nothing is planned`); return 1; }
|
|
3685
|
+
}
|
|
3352
3686
|
// One lock and one lock scope per Cargo workspace. A workspace whose lock-only change reaches a subset puts only that subset's
|
|
3353
3687
|
// units (by unit name) in scope; every other workspace keeps all of its units in scope (planScans reads the scope only for the
|
|
3354
3688
|
// workspaces whose own root files changed).
|
|
@@ -3402,6 +3736,7 @@ export async function main(argv, io = {}) {
|
|
|
3402
3736
|
// The door's snapshot compile already executes the repository's own code (its tests) in the verified snapshot, guarded like
|
|
3403
3737
|
// full class A, so the docs generator runs there too instead of being deferred (step 4c review).
|
|
3404
3738
|
quietTests: fullA || o.env.RK_GATE_QUIET_TESTS === "local" ? "local" : "ci", packageFeatures, liveExecution: (fullA && (args.includes("--worktree") || Boolean(snapshotA))) || Boolean(snapshotB),
|
|
3739
|
+
e2eCrateDirs, swiftPackages,
|
|
3405
3740
|
rustUnits: rust.units, npmUnits: npm, npmRoots, cargoDir: ".", rustLocks, crateFeatures, crateDependents, externalInputs,
|
|
3406
3741
|
// The broker (or, on a direct host, the gate itself) admits compiles; no separate grant file decides it here.
|
|
3407
3742
|
localCompile: true,
|
|
@@ -3416,7 +3751,7 @@ export async function main(argv, io = {}) {
|
|
|
3416
3751
|
if (closure) {
|
|
3417
3752
|
let kept;
|
|
3418
3753
|
try {
|
|
3419
|
-
kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units });
|
|
3754
|
+
kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units, npmUnits: npm });
|
|
3420
3755
|
plan = closureTools.scopedPlan(fullPlan, kept.scans, closure);
|
|
3421
3756
|
} catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
|
|
3422
3757
|
plan.notes = plan.notes.filter((n) => !n.includes("every Rust unit is in scope"));
|
|
@@ -3428,9 +3763,21 @@ export async function main(argv, io = {}) {
|
|
|
3428
3763
|
const cargoExcluded = kept.dropped.filter((s) => closureTools.isCargoScan(s)).length;
|
|
3429
3764
|
o.out(`${tag} scope ${closure.dirs.join(", ")}: dependency directories ${closure.dependencyDirs.join(", ")}; ${closure.npm.size} npm & ${closure.rust.size} Rust unit(s); Class A ${executing} executing scan(s) (${cargoRun} Cargo), ${classA.length - executing} repository file-only scan(s); ${kept.dropped.length} unrelated executing scan(s) excluded (${cargoExcluded} Cargo); ${plan.scans.length - classA.length} closure Class B scan(s) do not run in this check; install roots ${installRoots.join(", ") || "none"}; scope digest ${plan.scopeDigest}`);
|
|
3430
3765
|
}
|
|
3766
|
+
// What the plan leaves to the qualify stage (end-to-end crates, declared Swift packages): handed to the door, which records it in the compile
|
|
3767
|
+
// receipt and refuses to qualify without a declared run route (rightkit#1332, #1333). Never dropped silently.
|
|
3768
|
+
o.onQualifyOwed?.((plan.qualifyOwed ?? []).filter((x) => x.kind !== "e2e-crate" || plan.scans.some((s) => s.id === x.id)));
|
|
3431
3769
|
const jobs = Math.max(1, Number(o.env.RK_GATE_JOBS ?? 4) || 4);
|
|
3432
3770
|
const testJobs = Math.max(1, Number(o.env.RK_GATE_TEST_JOBS ?? jobs) || jobs);
|
|
3433
|
-
|
|
3771
|
+
// A broker host has no gate-side build count: the broker decides it. What remains is the local limit on class B processes the broker never sees.
|
|
3772
|
+
const brokerHost = executor.kind === "broker";
|
|
3773
|
+
const classBJobs = brokerHost ? LOCAL_CLASS_B_JOBS : Math.max(1, Number(o.env.RK_GATE_CLASS_B_JOBS ?? 3) || 3);
|
|
3774
|
+
// The broker's live state of a request (queued or running), asked through the executor's own client with an asynchronous spawn: a carried scan's run limit does not tick
|
|
3775
|
+
// while the broker holds it queued. No client to ask (none resolves): no reader, and a carried scan's limit simply runs, counting its queue time.
|
|
3776
|
+
// The client is the declared Linux route's, else the managed runtime client, else the executor's own: a broker host whose executor is the `rustright` found on PATH
|
|
3777
|
+
// (`["rustright", ...]`) asks through that one. No client to ask (a shim-only executor with none resolved): no reader, and the scan's limit simply runs.
|
|
3778
|
+
const executorClient = path.basename(String(cargoCommand[0] ?? "")).replace(/\.(exe|cmd|bat)$/i, "") === "rustright" ? (brokerClientOf(o, executor) ?? cargoCommand[0]) : null;
|
|
3779
|
+
const brokerClient = !brokerHost ? null : declaredClient ? "rustright" : managedRuntimeClient(o.env, o.platform, o.exists) ?? executorClient;
|
|
3780
|
+
const brokerLive = brokerClient ? createBrokerLive({ client: brokerClient, env: { ...o.env, ...(executor.env ?? {}) }, spawn: withDeclaredClient(defaultSpawnAsync), platform: o.platform }) : null;
|
|
3434
3781
|
|
|
3435
3782
|
if (planOnly) {
|
|
3436
3783
|
o.out(`${tag} plan: ${plan.scans.length} scan(s) for ${changed.length} changed file(s) vs ${base.slice(0, 12)} (reads the working tree; nothing runs)`);
|
|
@@ -3446,7 +3793,7 @@ export async function main(argv, io = {}) {
|
|
|
3446
3793
|
for (const n of plan.notes) o.out(`${tag} note: ${n}`);
|
|
3447
3794
|
o.out(`${tag} executor: ${executor.kind}${executor.kind === "broker" ? ` (${cargoCommand.join(" ")})` : " (no broker on this host: cargo runs directly, through the same gates)"}`);
|
|
3448
3795
|
const quietCount = plan.scans.filter((s) => s.quiet === true).length;
|
|
3449
|
-
o.out(`${tag} ${plan.scans.length} scan(s) for ${changed.length} changed file(s) vs ${base.slice(0, 12)}; class A scans ${jobs} at a time, class B scans ${classBJobs} component(s) at a time${quietCount ? `, ${quietCount} quiet test scan(s) last under the broker's quiet lease` : ""}`);
|
|
3796
|
+
o.out(`${tag} ${plan.scans.length} scan(s) for ${changed.length} changed file(s) vs ${base.slice(0, 12)}; class A scans ${jobs} at a time, ${brokerHost ? `class B: the broker queues and orders the carried scans of every component, no gate count (local test suites the broker does not see: ${classBJobs} at a time)` : `class B scans ${classBJobs} component(s) at a time`}${quietCount ? `, ${quietCount} quiet test scan(s) last under the broker's quiet lease` : ""}`);
|
|
3450
3797
|
let signalCode = 0;
|
|
3451
3798
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
3452
3799
|
const handler = () => {
|
|
@@ -3520,6 +3867,8 @@ export async function main(argv, io = {}) {
|
|
|
3520
3867
|
if (boundFullA) {
|
|
3521
3868
|
const orphans = orphanedPnpmStores({ root: liveBase, files: live.files });
|
|
3522
3869
|
if (orphans.length) { o.err(`${tag} BLOCKED: orphaned nested pnpm store(s) ${orphans.join(", ")}: left by a standalone install in a workspace member; they can hold stale dependencies and make concurrent vitest runs clear their shared module cache under each other. Move them aside (or reinstall) and run again.`); return 1; }
|
|
3870
|
+
const pinProblem = pnpmPinProblem({ root: liveBase, files: live.files, spawn: o.spawn, readFile: o.readFile });
|
|
3871
|
+
if (pinProblem) { o.err(`${tag} BLOCKED: ${pinProblem.message}; no class A row ran`); return 1; }
|
|
3523
3872
|
try { envAtStart = classAEnvironmentDigest({ root: liveBase, gatePackage: o.gatePackage, files: live.files, spawn: o.spawn, includeCaches: false }); envStartWithCaches = classAEnvironmentDigest({ root: liveBase, gatePackage: o.gatePackage, files: live.files, spawn: o.spawn }); cachesAtStart = toolCacheManifest({ root: liveBase, files: live.files }); }
|
|
3524
3873
|
catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
|
|
3525
3874
|
}
|
|
@@ -3581,11 +3930,15 @@ export async function main(argv, io = {}) {
|
|
|
3581
3930
|
if (enrolledTools.length) o.out(`${tag} scan-tool policy: ${enrolledTools.map((name) => `${name} (${scanTools.sources?.[name] ?? "enrolled"})`).join(", ")}`);
|
|
3582
3931
|
// The direct compile's freshness census (row 7.19): the fingerprint units before the scans, and the fingerprint log of each cargo
|
|
3583
3932
|
// process that compiles (CARGO_LOG on that process only, read from its own standard error). A broker host's census is the broker's.
|
|
3933
|
+
// The door's compile stage runs only after its check stage passed full class A of this same snapshot, once, before the compile and under the
|
|
3934
|
+
// cargo wrapper (rightkit#1328): this pass runs class B alone. Set by the door (io option), never by a command-line flag.
|
|
3935
|
+
const classAEarlier = Boolean(snapshotB && o.classAEarlier === true);
|
|
3936
|
+
if (classAEarlier) o.out(`${tag} class A is not repeated: it ran once before the compile, under the cargo wrapper, on this snapshot`);
|
|
3584
3937
|
const census = snapshotB && !snapshotBroker ? createFreshnessCensus({ targetDirs }) : null;
|
|
3585
3938
|
directTiming.cargoStart = Date.now();
|
|
3586
3939
|
census?.start();
|
|
3587
3940
|
const scanResult = await (o.runScans ?? runScans)(plan, {
|
|
3588
|
-
classes: classAOnly ? ["A"] : ["A", "B"], jobs, testJobs, classBJobs, signal: ac.signal, ...(cache ? { cache } : {}),
|
|
3941
|
+
classes: classAOnly ? ["A"] : classAEarlier ? ["B"] : ["A", "B"], ...(classAEarlier ? { classAEarlier: true } : {}), jobs, testJobs, classBJobs, ...(brokerHost ? { brokerHost: true } : {}), ...(brokerLive ? { brokerLive } : {}), signal: ac.signal, ...(cache ? { cache } : {}),
|
|
3589
3942
|
...(census ? { spawn: census.wrapSpawn(defaultSpawnAsync) } : {}),
|
|
3590
3943
|
// The declared Linux broker route: the scans are planned with the logical `rustright`, which this spawn (and scanSpawn's pass-through below) starts as the
|
|
3591
3944
|
// executor's validated client, never a PATH lookup (withDeclaredClient).
|
|
@@ -3685,7 +4038,21 @@ export async function main(argv, io = {}) {
|
|
|
3685
4038
|
// A broker host's evidence is the broker's own signed receipts (each carried scan's row.snapshotEvidence, verified above); the
|
|
3686
4039
|
// gate writes no receipt that says `executor: direct` for it.
|
|
3687
4040
|
// The output directories this compile's evidence names (GB-13): the broker's signed receipts' `outputDir`, or the direct host's resolved targets.
|
|
3688
|
-
|
|
4041
|
+
// With them, per output directory, the binaries this compile linked there (rustright#209 item 5). The plan has no `cargo build`: a bin is
|
|
4042
|
+
// linked only by a PASSED, not cached `cargo test -p <crate>` scan (test:rust:<unit>...) of a package with a selected integration test, which Cargo
|
|
4043
|
+
// rebuilds or proves fresh for this commit. A unit only checked, linted, documented, deferred to CI or without such a target counts for nothing.
|
|
4044
|
+
const unitByName = new Map(rust.units.map((u) => [u.name, u]));
|
|
4045
|
+
const known = new Set(unitByName.keys());
|
|
4046
|
+
const builtBins = {};
|
|
4047
|
+
const addBins = (dir, unit) => { if (dir && unit?.testLinksBins && unit.bins?.length) builtBins[dir] = [...new Set([...(builtBins[dir] ?? []), ...unit.bins])].sort(); };
|
|
4048
|
+
for (const r of scanResult.scans) {
|
|
4049
|
+
// A row answered from the scan cache compiled nothing in this run: the bytes in the target are not vouched for by it.
|
|
4050
|
+
if (r.status !== "pass" || r.cached === true || !/^test:rust(?:-serial)?:/.test(String(r.id))) continue;
|
|
4051
|
+
const unit = unitByName.get(rustUnitOfScanId(r.id, known));
|
|
4052
|
+
addBins(snapshotBroker ? r.snapshotEvidence?.outputDir : targetDirs?.[unit?.cargoDir ?? "."], unit);
|
|
4053
|
+
}
|
|
4054
|
+
const outputWorkspaces = (snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir) : Object.values(targetDirs ?? {})).filter(Boolean).map((dir) => [dir]);
|
|
4055
|
+
o.onCompileOutputs?.(outputWorkspaces.map(([dir]) => dir), builtBins);
|
|
3689
4056
|
if (snapshotBroker) o.out(`${tag} broker compile: ${scanResult.scans.filter((r) => r.snapshotEvidence).length} carried request(s) with verified signed receipts; no direct receipt on a broker host`);
|
|
3690
4057
|
else {
|
|
3691
4058
|
// The direct host's own receipt (GATE-CONTRACT "Broker interface", row 7.18): the contract's returned-evidence names minus the
|