@rightkit/release 0.6.12 → 0.6.14
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 +49 -1
- package/adapter-env.mjs +165 -0
- package/android-ndk.mjs +5 -1
- package/android-release.mjs +73 -8
- package/app-check.mjs +22 -8
- package/app-closure.mjs +214 -0
- package/assemble-app-command.mjs +24 -7
- package/atomic-write.mjs +57 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1190 -335
- package/broker-client.mjs +106 -0
- package/broker-exception.mjs +211 -0
- package/broker-handshake.mjs +160 -0
- package/broker-pins.mjs +27 -8
- package/broker-wire.mjs +262 -0
- package/build-launch.mjs +364 -12
- package/build-ownership.mjs +117 -49
- package/build-release.mjs +3 -2
- package/cache-command.mjs +7 -2
- package/candidate-build-receipt.mjs +96 -0
- package/candidate-receipt-command.mjs +29 -0
- package/cargo-contract.mjs +4 -3
- package/cargo-ledger.mjs +1 -1
- package/cargo-license-notices.mjs +2 -2
- package/cargo-shapes.mjs +332 -0
- package/cargo-target-shim.mjs +44 -5
- package/cargo-target.mjs +5 -0
- package/carried-adapter-run.mjs +31 -0
- package/carried-cargo-run.mjs +9 -0
- package/carried-cargo.mjs +237 -0
- package/carrier-census.mjs +124 -0
- package/class-a-launch-guard.mjs +266 -33
- package/cli/right-release.mjs +2 -0
- package/clippy-route.mjs +23 -11
- package/compile-inputs.mjs +9 -12
- package/control-plane.mjs +18 -0
- package/deps-cargo-lock.mjs +208 -0
- package/deps.mjs +5 -0
- package/deterministic-scans.mjs +273 -44
- package/development-signing.mjs +14 -1
- package/development.mjs +109 -24
- package/direct-origin.mjs +107 -0
- package/disk-command.mjs +1 -0
- package/disk-policy.mjs +18 -3
- package/freshness-census.mjs +206 -58
- package/gate-executor.mjs +30 -12
- package/gate-stages.mjs +93 -16
- package/github-release.mjs +2 -1
- package/github-upload-gate.mjs +2 -1
- package/install-tauri-authenticode.mjs +346 -0
- package/ios-dev.mjs +77 -23
- package/ios-package-recipe.mjs +100 -41
- package/linux-artifacts.mjs +17 -2
- package/linux-qualifier.mjs +83 -0
- package/local-publish.mjs +36 -14
- package/local-runtime.mjs +2 -2
- package/lock-identity.mjs +138 -0
- package/lsclean.sh +0 -0
- package/managed-cargo.mjs +151 -44
- package/managed-roots.mjs +130 -12
- package/media-sidecar-catalog.mjs +1 -1
- package/native-adapters.mjs +411 -0
- package/native-helpers.mjs +26 -18
- package/native-tool-planning.mjs +94 -0
- package/ownership-mark.mjs +182 -0
- package/pack-digest.mjs +122 -0
- package/package.json +12 -12
- package/platform-dir.mjs +14 -0
- package/pnpm-adapter-grammar.mjs +226 -0
- package/progress-control.mjs +7 -2
- package/provenance-real-receipt.pins.json +7 -0
- package/provenance-verify.mjs +262 -1
- package/publication-outcomes.mjs +21 -0
- package/publish-update.mjs +2 -1
- package/qa-journeys-prebuilt-run.mjs +18 -0
- package/qa-journeys-prebuilt.mjs +148 -0
- package/qa-snapshot-run.mjs +53 -0
- package/qa-snapshot.mjs +164 -0
- package/qualification-verify.mjs +144 -0
- package/qualify-reuse.mjs +38 -0
- package/recipe-build.mjs +16 -3
- package/release-qa-audit.mjs +2 -2
- package/release-qa-command.mjs +2 -1
- package/release-qa-receipt.mjs +3 -2
- package/release-state.mjs +3 -2
- package/release.mjs +14 -3
- package/rightkit-drift.mjs +27 -5
- package/rightkit-versions.json +15 -10
- package/rustright-names.mjs +32 -0
- package/scan-tool-enrolment.mjs +167 -0
- package/sign-windows.mjs +212 -112
- package/signing-files.mjs +35 -0
- package/signing-identity.mjs +7 -0
- package/snapshot-broker.mjs +528 -57
- package/snapshot-check.mjs +95 -11
- package/snapshot-metadata.mjs +123 -0
- package/snapshot-request.mjs +389 -96
- package/stage-binary.mjs +22 -4
- package/stage-commands.mjs +56 -13
- package/stage-sidecar.mjs +93 -21
- package/suite-signing-policy.mjs +657 -0
- package/suite-signing.mjs +223 -0
- package/swift-scratch.mjs +25 -0
- package/tauri-locked.mjs +25 -0
- package/test-command.mjs +164 -29
- package/unit-gate.mjs +201 -31
- package/upload-large.mjs +0 -0
- package/web-build.mjs +2 -2
- package/win-package-recipe.mjs +26 -2
package/bin/rightkit-gate.mjs
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
// rightkit-gate [--base <ref>] [--plan] [--no-cache]
|
|
29
29
|
// run the gate (default base origin/main); --plan prints the scans and runs none;
|
|
30
30
|
// --no-cache (or RK_GATE_CACHE=0) reruns every scan
|
|
31
|
+
// rightkit-gate --to check|compile|package|qualify|dev|test --route [--platform p] [--commit <rev>]
|
|
32
|
+
// prints one JSON line (schema rightkit-gate.route/1) naming the route this host takes for the
|
|
33
|
+
// stage and the refusal it would meet, and runs nothing: no ownership, snapshot, check, run record
|
|
34
|
+
// or receipt (rightkit#1297; the release canary probes it for every app). Exit 0 answered, 2 not
|
|
31
35
|
// rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>]
|
|
32
36
|
// the deterministic stage: the app check (../app-check.mjs), then every class A scan
|
|
33
37
|
// (lockfiles, cargo-deny/audit, secrets, licences, manifests, workflow lint, syntax) over the
|
|
@@ -64,20 +68,28 @@
|
|
|
64
68
|
// SIGINT/SIGTERM stop scheduling, kill every running scan's process tree and exit non-zero listing the scans not run.
|
|
65
69
|
import fs from "node:fs";
|
|
66
70
|
import { createHash, randomUUID } from "node:crypto";
|
|
67
|
-
import { resolveManagedRoots } from "../managed-roots.mjs";
|
|
71
|
+
import { envNamed, gateRunsRoot, resolveManagedRoots } from "../managed-roots.mjs";
|
|
72
|
+
import { resolveBrokerClient, spawnBrokerClient } from "../broker-client.mjs";
|
|
68
73
|
import os from "node:os";
|
|
69
74
|
import path from "node:path";
|
|
70
75
|
import { FOREIGN_PRE_COMMIT, retireHooksIn } from "../hook-retirement.mjs";
|
|
71
76
|
import { fileURLToPath } from "node:url";
|
|
72
|
-
import { BIOME_CONFIG_FILES, ESLINT_CONFIG_FILES, WORKSPACE_RUST_FILE, ancestorConfigChanged, ancestorDirsOf, canonicalJson, defaultSpawn, defaultSpawnAsync, digestScanSet, fileScanCache, findRustfmt, metadataArgs, planScans, runScans, scanDeferrals, scanFailures } from "../deterministic-scans.mjs";
|
|
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";
|
|
73
78
|
import { changedLockNames, parseLock, reach } from "../lock-graph.mjs";
|
|
74
79
|
import { resolveExecutor } from "../gate-executor.mjs";
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
80
|
+
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
|
+
import { SIGNING_REFUSAL_CODES, suiteSigningRefusals } from "../suite-signing-policy.mjs";
|
|
82
|
+
import { gitListRustFiles } from "../signing-files.mjs";
|
|
83
|
+
import { LINUX_BROKER_ROUTE, directPlatform, linuxBrokerDeclared } from "../managed-cargo.mjs";
|
|
84
|
+
import { bindingOf, compileOutputsRecord, compileReuseProblem, endRun, readReceipt, RECEIPT_SCHEMA, receiptKey, sameBinding, sameSource, sealedFor, stagesTo, startRun, STAGES, STAGE_SOURCES, writeReceipt } from "../gate-stages.mjs";
|
|
77
85
|
import { spawnSync as spawnInherit } from "../background-process.mjs";
|
|
78
86
|
import { readDiskPolicy, scratchRoots } from "../disk-policy.mjs";
|
|
79
87
|
import { buildDirectReceipt, directVerdict, resolveTargetDirectory, verifyDirectReceipt, writeDirectDiagnostics } from "../direct-receipt.mjs";
|
|
80
88
|
import { createFreshnessCensus } from "../freshness-census.mjs";
|
|
89
|
+
import { platformDirOf } from "../platform-dir.mjs";
|
|
90
|
+
import { nativeToolRefusal, nativeToolUses } from "../native-tool-planning.mjs";
|
|
91
|
+
import { BUILD_CONTROL_PROTOCOL, HANDSHAKE_PROBE_ID, RUNTIME_MANIFEST_FILE, mergeCapabilityViews, parseRuntimeIdentity, queryBrokerHandshake } from "../broker-handshake.mjs";
|
|
92
|
+
import net from "node:net";
|
|
81
93
|
|
|
82
94
|
const ZERO = /^0+$/;
|
|
83
95
|
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -91,7 +103,9 @@ export const PARTIAL_EXIT = 3;
|
|
|
91
103
|
/** The platforms a door run can package: desktop (signed, sealed, installed QA, publisher upload) and mobile (signed, sealed, store test track). */
|
|
92
104
|
export const PLATFORMS = Object.freeze(["mac", "win", "ios", "android"]);
|
|
93
105
|
export const MOBILE_PLATFORMS = Object.freeze(new Set(["ios", "android"]));
|
|
94
|
-
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 dev|test [--commit <rev>] [-- <right-release args>] | 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`;
|
|
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`;
|
|
107
|
+
/** The host platform (process.platform) that constructs each platform's artifact; a platform missing here (android) builds on any host. */
|
|
108
|
+
const STAGE_NATIVE_HOST = Object.freeze({ mac: "darwin", ios: "darwin", win: "win32" });
|
|
95
109
|
|
|
96
110
|
// ---- pure helpers (exported for tests) ----------------------------------------------------------------------
|
|
97
111
|
|
|
@@ -232,13 +246,13 @@ function workspaceMetadata({ root, spawn, cargoCommand, dir, realpath }) {
|
|
|
232
246
|
* { present, units: [{ name, dir, publish }], cargoDir, uncovered }.
|
|
233
247
|
*/
|
|
234
248
|
/**
|
|
235
|
-
* Where the
|
|
236
|
-
* (RK_INTERNAL_DRIVE_REFUSED: agents never write to the internal drive), and
|
|
237
|
-
* host with a disk-policy scratch root (/Volumes/D/scratch, D:\scratch)
|
|
238
|
-
* is the fallback where no scratch root exists (CI runners, cloud containers)
|
|
239
|
-
*
|
|
249
|
+
* Where the gate's own temporary files live (cargo-deny's metadata file and config overlay, read by the broker by absolute path). The
|
|
250
|
+
* RustRight broker refuses paths on the system volume (RK_INTERNAL_DRIVE_REFUSED: agents never write to the internal drive), and
|
|
251
|
+
* os.tmpdir() is on it on every Mac and Dell, so on a host with a disk-policy scratch root (/Volumes/D/scratch, D:\scratch) they go
|
|
252
|
+
* there. RK_GATE_TMPDIR overrides; os.tmpdir() is the fallback where no scratch root exists (CI runners, cloud containers). The gate
|
|
253
|
+
* exports nothing and copies no source: this is not a source location (docs/GATE-CONTRACT.md, 2026-10-29: no export or copy).
|
|
240
254
|
*/
|
|
241
|
-
export function
|
|
255
|
+
export function gateTmpRoot({ env = process.env, platform = process.platform, exists = fs.existsSync, policy = null } = {}) {
|
|
242
256
|
if (env.RK_GATE_TMPDIR) return env.RK_GATE_TMPDIR;
|
|
243
257
|
try {
|
|
244
258
|
const roots = scratchRoots(policy ?? readDiskPolicy(), { platform, env });
|
|
@@ -248,64 +262,6 @@ export function exportTmpRoot({ env = process.env, platform = process.platform,
|
|
|
248
262
|
return os.tmpdir();
|
|
249
263
|
}
|
|
250
264
|
|
|
251
|
-
/** The files cargo metadata, deny and audit read, in the repository root and in every workspace that has a manifest. */
|
|
252
|
-
export const CARGO_INPUT_NAMES = Object.freeze(["Cargo.toml", "Cargo.lock", "deny.toml", "rust-toolchain.toml", "rust-toolchain", ".cargo"]);
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* The Cargo inputs of the live checkout `root` that are not blob-identical to `snapshot`'s (a commit or tree): a changed or deleted
|
|
256
|
-
* tracked input, or any untracked file (ignored ones included, cargo reads them) under those paths. Empty means the broker may scan
|
|
257
|
-
* the primary checkout for exactly the snapshot's result. `manifests` are the snapshot's tracked Cargo.toml paths (posix).
|
|
258
|
-
*/
|
|
259
|
-
export function cargoInputsDiffer({ root, snapshot, manifests, spawn = defaultSpawn }) {
|
|
260
|
-
const git = (...a) => spawn("git", a, { cwd: root });
|
|
261
|
-
// Every manifest directory and every ancestor up to the root: cargo reads a .cargo/config.toml or a rust-toolchain file from any of them.
|
|
262
|
-
const dirs = new Set([""]);
|
|
263
|
-
for (const m of manifests) { const parts = path.posix.dirname(m).split("/").filter((x) => x && x !== "."); for (let i = 1; i <= parts.length; i++) dirs.add(parts.slice(0, i).join("/")); }
|
|
264
|
-
const dirList = [...dirs];
|
|
265
|
-
const paths = dirList.flatMap((d) => CARGO_INPUT_NAMES.map((n) => (d ? `${d}/${n}` : n)));
|
|
266
|
-
// Pathspecs go to git in bounded batches: a repository with many crates (RightKit: ~1000 input paths) overflows Windows' 32767-
|
|
267
|
-
// character command line in one argv (ENAMETOOLONG on the Dell, 2026-10-27; G4 was the same for rustfmt). Each batch's results
|
|
268
|
-
// are merged; a failed batch fails the whole listing closed.
|
|
269
|
-
const batches = pathspecBatches(paths);
|
|
270
|
-
const listedAll = batches.map((b) => git("ls-tree", "-r", "-z", snapshot, "--", ...b));
|
|
271
|
-
const badList = listedAll.find((r) => failed(r));
|
|
272
|
-
if (badList) return [`(cannot list ${snapshot}: ${why(badList)})`];
|
|
273
|
-
const snap = new Map();
|
|
274
|
-
for (const listed of listedAll) for (const entry of text(listed).split("\0").filter(Boolean)) { const tab = entry.indexOf("\t"); const [, , id] = entry.slice(0, tab).split(" "); snap.set(entry.slice(tab + 1), id); }
|
|
275
|
-
const differ = new Set();
|
|
276
|
-
for (const [p, id] of snap) {
|
|
277
|
-
const live = path.join(root, ...p.split("/"));
|
|
278
|
-
let liveId = null;
|
|
279
|
-
// Cargo reads the live bytes, so the live bytes are hashed raw (`--no-filters`: no clean filter, eol or text attribute can make
|
|
280
|
-
// two different byte strings look the same). The one accepted difference is an exact CRLF checkout of an LF blob (D13, Dell
|
|
281
|
-
// 2026-10-27: core.autocrlf), which every TOML/toolchain reader treats as the same newline; anything else differs.
|
|
282
|
-
try { if (fs.statSync(live).isFile()) { const h = git("hash-object", "--no-filters", "--", live); liveId = failed(h) ? null : text(h).trim(); } } catch { liveId = null; }
|
|
283
|
-
if (liveId !== id && !isCrlfCheckoutOf({ git, id, live })) differ.add(p);
|
|
284
|
-
}
|
|
285
|
-
// Untracked under the input paths, ignored files included: cargo reads a .cargo/config.toml or a deny.toml whether or not git tracks it.
|
|
286
|
-
for (const batch of batches) {
|
|
287
|
-
const others = git("ls-files", "-z", "--others", "--", ...batch);
|
|
288
|
-
if (failed(others)) differ.add(`(cannot list untracked Cargo inputs: ${why(others)})`); // fail closed: an unlisted input is an unknown input
|
|
289
|
-
else for (const p of text(others).split("\0").filter(Boolean)) if (!snap.has(p)) differ.add(p);
|
|
290
|
-
// A tracked input the snapshot lacks (added in HEAD after the snapshot, say) is live-only too.
|
|
291
|
-
const tracked = git("ls-files", "-z", "--", ...batch);
|
|
292
|
-
if (failed(tracked)) differ.add(`(cannot list tracked Cargo inputs: ${why(tracked)})`);
|
|
293
|
-
else for (const p of text(tracked).split("\0").filter(Boolean)) if (!snap.has(p)) differ.add(p);
|
|
294
|
-
}
|
|
295
|
-
return [...differ].sort();
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** True only when the live file's bytes are exactly blob `id` with every LF written as CRLF (an autocrlf checkout); fails closed. */
|
|
299
|
-
export function isCrlfCheckoutOf({ git, id, live, readFile = fs.readFileSync }) {
|
|
300
|
-
try {
|
|
301
|
-
const blob = git("cat-file", "blob", id);
|
|
302
|
-
if (failed(blob)) return false;
|
|
303
|
-
const committed = text(blob);
|
|
304
|
-
if (committed.includes("\r")) return false;
|
|
305
|
-
return readFile(live, "utf8") === committed.replace(/\n/g, "\r\n");
|
|
306
|
-
} catch { return false; }
|
|
307
|
-
}
|
|
308
|
-
|
|
309
265
|
/**
|
|
310
266
|
* Tool caches inside node_modules that nothing installs: test-result caches (vitest), the vite optimizer and bundled-config temp
|
|
311
267
|
* files, transpile and other tool caches (.cache). Some hold code a later step executes, so the environment a verdict carries binds
|
|
@@ -1111,21 +1067,32 @@ export const QUIET_LEASE_WAIT_DRAIN_MS = 2 * 60 * 1000;
|
|
|
1111
1067
|
* Without `rustright` on PATH, or when the broker refuses, the lane runs without a lease and the gate says so: the tests still run,
|
|
1112
1068
|
* their verdict is simply taken on a possibly busy host. `o.spawn` is the gate's synchronous spawner.
|
|
1113
1069
|
*/
|
|
1114
|
-
export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-gate-quiet-tests", ttlMs: baseTtlMs = QUIET_LEASE_TTL_MS, waitDrainMs = QUIET_LEASE_WAIT_DRAIN_MS, pid = process.pid } = {}) {
|
|
1070
|
+
export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-gate-quiet-tests", ttlMs: baseTtlMs = QUIET_LEASE_TTL_MS, waitDrainMs = QUIET_LEASE_WAIT_DRAIN_MS, pid = process.pid, executor = null } = {}) {
|
|
1071
|
+
// The declared Linux broker route (managed-cargo.mjs): its validated client and endpoint, never a PATH lookup.
|
|
1072
|
+
const linuxRoute = executor?.route === LINUX_BROKER_ROUTE && executor.kind === "broker" ? executor : null;
|
|
1115
1073
|
const which = (cmd) => {
|
|
1116
1074
|
const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]);
|
|
1117
1075
|
return failed(r) ? null : (text(r).split(/\r?\n/).map((l) => l.trim()).find(Boolean) ?? null);
|
|
1118
1076
|
};
|
|
1077
|
+
// The client is started the one way every gate child starts it (broker-client.mjs): on Windows `rustright` is rustright.cmd, which node cannot
|
|
1078
|
+
// spawn, so its target .exe is read from the shim and started directly, without a shell.
|
|
1079
|
+
const clientCall = (client, args) => {
|
|
1080
|
+
const platform = o.platform ?? process.platform;
|
|
1081
|
+
if (linuxRoute) return spawnBrokerClient(o.spawn, linuxRoute.client, args, { env: { ...o.env, ...linuxRoute.env } }, { platform, readFile: o.readFile });
|
|
1082
|
+
return spawnBrokerClient(o.spawn, platform === "win32" ? client : "rustright", args, {}, { platform, readFile: o.readFile });
|
|
1083
|
+
};
|
|
1119
1084
|
return {
|
|
1120
1085
|
// `want.ttlMs` (from the scan runner): the lane's own bound, every quiet scan's timeout plus a margin, so the lease cannot
|
|
1121
1086
|
// expire while a quiet suite still runs (each scan is killed at its timeout first).
|
|
1122
1087
|
acquire: async (want = {}) => {
|
|
1123
1088
|
const ttlMs = Number.isFinite(want.ttlMs) && want.ttlMs > baseTtlMs ? want.ttlMs : baseTtlMs;
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if (
|
|
1089
|
+
if (o.brokerException) { o.out(`${tag} note: ${o.brokerException.note} (${o.brokerException.exceptionId}); quiet test scans run without a broker lease`); return null; }
|
|
1090
|
+
// A direct (Linux) host never takes a broker lease, whatever is on PATH (managed-cargo directPlatform); a declared Linux broker route does.
|
|
1091
|
+
if (directPlatform(o.platform ?? process.platform) && !linuxRoute) { o.out(`${tag} note: direct host (no broker on ${o.platform ?? process.platform}); quiet test scans run without a broker lease`); return null; }
|
|
1092
|
+
const client = linuxRoute ? linuxRoute.client : which("rustright");
|
|
1093
|
+
if (!client) { o.out(`${tag} note: no rustright on PATH; quiet test scans run without a broker lease`); return null; }
|
|
1127
1094
|
const started = Date.now();
|
|
1128
|
-
const r =
|
|
1095
|
+
const r = clientCall(client, ["quiet", "acquire", "--ttl-ms", String(ttlMs), "--reason", reason, "--wait-drain-ms", String(waitDrainMs), "--holder-pid", String(pid), "--json"]);
|
|
1129
1096
|
if (failed(r)) { o.out(`${tag} note: broker quiet lease refused (${(why(r) || `exit ${r?.status}`).slice(0, 200)}); quiet test scans run without a lease`); return null; }
|
|
1130
1097
|
let id = null;
|
|
1131
1098
|
// The CLI prints the broker's QUIET_LEASE event: { type, granted, state, id } when held; a refusal exits non-zero above.
|
|
@@ -1135,7 +1102,7 @@ export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-g
|
|
|
1135
1102
|
return { id, env: { RUSTRIGHT_QUIET_LEASE: id }, at: Date.now() };
|
|
1136
1103
|
},
|
|
1137
1104
|
release: async (id) => {
|
|
1138
|
-
const r =
|
|
1105
|
+
const r = clientCall(linuxRoute ? linuxRoute.client : (which("rustright") ?? "rustright"), ["quiet", "release", String(id)]);
|
|
1139
1106
|
if (failed(r)) o.out(`${tag} note: broker quiet lease ${id} release failed (${(why(r) || `exit ${r?.status}`).slice(0, 200)}); it expires on its own`);
|
|
1140
1107
|
else o.out(`${tag} broker quiet lease ${id} released`);
|
|
1141
1108
|
},
|
|
@@ -1239,19 +1206,106 @@ async function installHook(o, root) {
|
|
|
1239
1206
|
* apps/coderight-tauri), else the repository root. Every right-release stage runs there, the sealed set is read from there, and the
|
|
1240
1207
|
* receipt is keyed to it (receiptKey). Returns { appRoot, appDir } with appDir relative to the repository root, "." at the root.
|
|
1241
1208
|
*/
|
|
1242
|
-
export function appRootOf({ root, cwd, exists = fs.existsSync }) {
|
|
1209
|
+
export function appRootOf({ root, cwd, exists = fs.existsSync, configDirs = null }) {
|
|
1243
1210
|
const inside = cwd && (cwd === root || cwd.startsWith(root + path.sep));
|
|
1244
|
-
|
|
1211
|
+
// With `configDirs` (the directories whose committed tree holds a release config: `committedConfigDirs`) the scope is the SELECTED COMMIT's, read from
|
|
1212
|
+
// its tree alone; the checkout's files are never asked, so a newer, dirty or edited checkout cannot move an older commit's app. Without it (callers that
|
|
1213
|
+
// hold no commit) the live checkout answers, as before. Either way the directory the gate was run from is the explicit identity.
|
|
1214
|
+
const hasConfig = configDirs
|
|
1215
|
+
? (d) => configDirs.has(path.relative(root, d).split(path.sep).join("/") || ".")
|
|
1216
|
+
: (d) => ["right-release.config.json", "right-release.config.mjs"].some((f) => exists(path.join(d, f)));
|
|
1245
1217
|
// The nearest directory at or above cwd (below the root) that carries a release config is the app; a deeper cwd (apps/foo/src) is apps/foo.
|
|
1246
1218
|
let appRoot = root;
|
|
1247
|
-
for (let d = path.resolve(cwd); d.startsWith(root) && d !== root; d = path.dirname(d)) { if (hasConfig(d)) { appRoot = d; break; } }
|
|
1219
|
+
if (inside) for (let d = path.resolve(cwd); d.startsWith(root) && d !== root; d = path.dirname(d)) { if (hasConfig(d)) { appRoot = d; break; } }
|
|
1248
1220
|
return { appRoot, appDir: path.relative(root, appRoot).split(path.sep).join("/") || "." };
|
|
1249
1221
|
}
|
|
1250
1222
|
|
|
1223
|
+
/**
|
|
1224
|
+
* The directories (posix, relative to the repository root, "." for the root) that hold a release config in `commit`'s tree: `{ dirs }`, or `{ refusal }` when the
|
|
1225
|
+
* tree cannot be listed. Read with `git ls-tree` of the selected commit, so the answer is the commit's, whatever the checkout holds.
|
|
1226
|
+
*/
|
|
1227
|
+
export function committedConfigDirs({ git, commit }) {
|
|
1228
|
+
const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
1229
|
+
if (failed(listed)) return { refusal: `cannot list the files of ${String(commit).slice(0, 12)}: ${why(listed)}` };
|
|
1230
|
+
const dirs = new Set(text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).map((f) => path.posix.dirname(f)));
|
|
1231
|
+
return { dirs };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** True when `commit` is RightKit's own repository: its tree holds packages/release/package.json named @rightkit/release (the identity `check` uses for RightKit's own rules). Read from the commit, never the checkout. */
|
|
1235
|
+
export function rightkitIdentityAt(git, commit) {
|
|
1236
|
+
const shown = git("show", `${commit}:${RIGHTKIT_IDENTITY_FILE}`);
|
|
1237
|
+
return !failed(shown) && isRightKitIdentity(text(shown));
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const UNIT_SELECTOR = /^(?:npm:@rightkit\/[a-z0-9-]+|cargo:rightkit-[a-z0-9-]+)$/;
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* The unit list of `--units-file <file>`: JSON `{ "units": [ "cargo:rightkit-x", "npm:@rightkit/y", ... ] }` written for this run (not a
|
|
1244
|
+
* committed file: Adrian, 2026-10-31: "a JSON file that is created at that time"). Every entry is a `--unit` selector, once. Throws a usage error.
|
|
1245
|
+
*/
|
|
1246
|
+
export function readUnitList(readFile, file) {
|
|
1247
|
+
const usage = (what) => Object.assign(new Error(`RK_UNITS_FILE: ${file} ${what}`), { code: "RK_UNITS_FILE" });
|
|
1248
|
+
let parsed;
|
|
1249
|
+
try { parsed = JSON.parse(readFile(file)); } catch (e) { throw usage(`cannot be read as JSON (${e.message})`); }
|
|
1250
|
+
const units = parsed?.units;
|
|
1251
|
+
if (!Array.isArray(units) || !units.length || !units.every((u) => typeof u === "string" && UNIT_SELECTOR.test(u))) throw usage('must be { "units": [ "cargo:rightkit-<name>" | "npm:@rightkit/<name>", ... ] } with at least one unit');
|
|
1252
|
+
const duplicate = units.find((u, i) => units.indexOf(u) !== i);
|
|
1253
|
+
if (duplicate !== undefined) throw usage(`names ${duplicate} more than once`);
|
|
1254
|
+
return units;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* RightKit names its unit on every run (#1325, Adrian 2026-10-31: "the unit has to be mentioned. Okay, so either it is one specific unit, or it is
|
|
1259
|
+
* a JSON file that is created at that time, or all with my exception"). A door run with no `--unit` is accepted in RightKit only with
|
|
1260
|
+
* `--units-file` (each listed unit runs as its own unit run, in order, on the one selected commit, stopping at the first that fails) or
|
|
1261
|
+
* `--all-units-exception <id>` (the whole-repository run, by Adrian's entry in the commit's ci/all-units-exceptions.json). Both flags name RightKit
|
|
1262
|
+
* 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
|
+
* all-units flag and its id are removed from `args`). An unresolvable commit is runStages' own refusal, so it goes on.
|
|
1264
|
+
*/
|
|
1265
|
+
export async function rightkitDoorRule(o, args, root, git, io, { unitsFileFlag, allUnitsFlag, unitList }) {
|
|
1266
|
+
const tag = "rightkit-gate:";
|
|
1267
|
+
const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
|
|
1268
|
+
let pinned;
|
|
1269
|
+
try { pinned = (await import("../snapshot.mjs")).resolveCommit(root, option("--commit") ?? "HEAD"); } catch { return {}; }
|
|
1270
|
+
const inRightKit = rightkitIdentityAt(git, pinned.commit);
|
|
1271
|
+
if (!inRightKit) {
|
|
1272
|
+
if (!unitsFileFlag && !allUnitsFlag) return {};
|
|
1273
|
+
o.err(`${tag} BLOCKED: ${unitsFileFlag ? "--units-file" : "--all-units-exception"} names RightKit units and ${pinned.commit.slice(0, 12)} is not RightKit; every other repository is built whole from the ${CARGO_WORKSPACE_LIST_KEY} list committed in ${CARGO_WORKSPACE_LIST_FILE}, with no unit named; nothing ran`);
|
|
1274
|
+
return { exit: 1 };
|
|
1275
|
+
}
|
|
1276
|
+
if (unitsFileFlag) {
|
|
1277
|
+
const stage = option("--to");
|
|
1278
|
+
const given = args.filter((a, i) => a.startsWith("--") && args[i - 1] !== "--units-file" && a !== "--units-file");
|
|
1279
|
+
if (given.some((a) => a !== "--to" && a !== "--commit")) { o.err(`${tag} a unit list run takes only --to, --units-file and optional --commit (a receipt is written per unit run: run the unit alone with --receipt-out)`); return { exit: 2 }; }
|
|
1280
|
+
for (const [i, selector] of unitList.entries()) {
|
|
1281
|
+
o.out(`${tag} unit list ${i + 1}/${unitList.length}: ${selector} on commit ${pinned.commit.slice(0, 12)}`);
|
|
1282
|
+
const code = await main(["--to", stage, "--unit", selector, "--commit", pinned.commit], io);
|
|
1283
|
+
if (code !== 0) { o.err(`${tag} BLOCKED: unit ${selector} ended with exit ${code}; ${unitList.length - i - 1} unit(s) of the list did not run`); return { exit: code }; }
|
|
1284
|
+
}
|
|
1285
|
+
return { exit: 0 };
|
|
1286
|
+
}
|
|
1287
|
+
if (allUnitsFlag) {
|
|
1288
|
+
const id = option("--all-units-exception");
|
|
1289
|
+
let granted;
|
|
1290
|
+
try {
|
|
1291
|
+
const shown = git("show", `${pinned.commit}:${ALL_UNITS_EXCEPTIONS_FILE}`);
|
|
1292
|
+
granted = authorizeAllUnits({ id, record: failed(shown) ? null : text(shown), now: o.now ? o.now() : Date.now() });
|
|
1293
|
+
} catch (e) {
|
|
1294
|
+
if (!e.code?.startsWith("RK_ALL_UNITS_")) throw e;
|
|
1295
|
+
o.err(`${tag} BLOCKED: ${e.message}; nothing ran`);
|
|
1296
|
+
return { exit: e.code === ALL_UNITS_ERRORS.usage ? 2 : 1 };
|
|
1297
|
+
}
|
|
1298
|
+
args.splice(args.indexOf("--all-units-exception"), 2);
|
|
1299
|
+
o.out(`${tag} ALL UNITS EXCEPTION ${describeAllUnitsException(granted)}`);
|
|
1300
|
+
return {};
|
|
1301
|
+
}
|
|
1302
|
+
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
|
+
return { exit: 1 };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1251
1306
|
async function runStages(o, args, root, git, io) {
|
|
1252
1307
|
const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
|
|
1253
1308
|
const tag = "rightkit-gate:";
|
|
1254
|
-
const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, exists: o.exists });
|
|
1255
1309
|
const to = option("--to");
|
|
1256
1310
|
// dev and test are door stages like the rest: the same commit-only route (select a commit, sync its snapshot under ownership, class A
|
|
1257
1311
|
// 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.
|
|
@@ -1272,25 +1326,51 @@ async function runStages(o, args, root, git, io) {
|
|
|
1272
1326
|
if (mobile && to === "qualify") { o.err(`${tag} --to qualify has no stage for ${platform}: there is no installed-QA lane for a mobile build; qualification is the store's test track after --to release. Run --to package or --to release.`); return 2; }
|
|
1273
1327
|
if (mobile) stages = stages.filter((s) => s !== "qualify");
|
|
1274
1328
|
if (stages.includes("release") && !mobile && !["patch", "update"].includes(tier)) { o.err(`${tag} --to release needs --tier patch|update (chosen explicitly, never defaulted)`); return 2; }
|
|
1329
|
+
// The commit this run is for is selected FIRST (--commit <rev>, default HEAD; the door snapshot's commit inside a door run; HEAD on hosted CI, whose runner
|
|
1330
|
+
// checkout is the commit), and the app scope is read from THAT commit's tree (committedConfigDirs): the checkout's present files never decide it.
|
|
1331
|
+
const hosted = o.env.GITHUB_ACTIONS === "true";
|
|
1332
|
+
let selected = null;
|
|
1333
|
+
if (!o.doorSnapshot && !hosted) {
|
|
1334
|
+
const snapModule = await import("../snapshot.mjs");
|
|
1335
|
+
try { selected = snapModule.resolveCommit(root, option("--commit") ?? "HEAD"); }
|
|
1336
|
+
catch (e) {
|
|
1337
|
+
if (o.routeProbe) return await runRouteProbe(o, args, root, git, { to, platform, stages: devTest ? [] : stages, devTest, appDir: "." }); // the probe names the same refusal in its own words
|
|
1338
|
+
o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
const scopeCommit = o.doorSnapshot ? o.doorSnapshot.commit : selected ? selected.commit : text(git("rev-parse", "HEAD")).trim();
|
|
1342
|
+
const inventory = committedConfigDirs({ git, commit: scopeCommit });
|
|
1343
|
+
if (inventory.refusal) { o.err(`${tag} cannot read the app configuration of the selected commit: ${inventory.refusal}`); return 2; }
|
|
1344
|
+
const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, configDirs: inventory.dirs });
|
|
1345
|
+
// `--route`: which route this stage takes here and whether it refuses, answered without running anything (rightkit#1297).
|
|
1346
|
+
if (o.routeProbe) return await runRouteProbe(o, args, root, git, { to, platform, stages: devTest ? [] : stages, devTest, appDir });
|
|
1275
1347
|
// Commit-only build (GATE-CONTRACT §1): on every host (not hosted CI, whose runner checkout is the commit) the door selects a commit
|
|
1276
1348
|
// (--commit <rev>, default HEAD), takes the host's build ownership, syncs the commit's gate-owned snapshot once, and every stage
|
|
1277
|
-
// below reads that snapshot; the checkout may hold anything and is never a build source.
|
|
1278
|
-
// RustRight snapshot carrier (`deps.snapshotBroker`, item 7 rows 7.20
|
|
1279
|
-
//
|
|
1280
|
-
// and
|
|
1281
|
-
//
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1349
|
+
// below reads that snapshot; the checkout may hold anything and is never a build source. Every non-hosted host takes this route: Mac
|
|
1350
|
+
// and Dell compile it through the RustRight snapshot carrier (`deps.snapshotBroker`, wired by `productionEntryDeps`; item 7 rows 7.20
|
|
1351
|
+
// to 7.24, rightkit#1301), right-release's own children included. Once it is selected any failure refuses; there is no retry on the
|
|
1352
|
+
// checkout, and no flag or environment variable selects another route.
|
|
1353
|
+
// The route and every refusal the door can meet before it takes ownership are `routePlan`'s, the one function the `--route` probe
|
|
1354
|
+
// also answers from (rightkit#1297 L1): the probe and the real run cannot drift, and the door-route parity test compares the two.
|
|
1355
|
+
if (!o.doorSnapshot) {
|
|
1356
|
+
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 ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which: whichRoute, readFile: o.readFile });
|
|
1358
|
+
let decided;
|
|
1359
|
+
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
|
+
catch (e) { o.err(`${tag} ${e.message}`); return 2; }
|
|
1361
|
+
if (decided.refusal) { o.err(`${tag} BLOCKED: ${decided.refusal.code}: ${decided.refusal.message}; nothing ran${decided.route === "snapshot" ? " and the checkout is never built" : ""}`); return 1; }
|
|
1362
|
+
const notPublisher = decided.route === "snapshot" ? releaseRouteRefusal(o, { stages, platform }) : null;
|
|
1363
|
+
if (notPublisher) { o.err(`${tag} BLOCKED: ${notPublisher.code}: ${notPublisher.message}; nothing ran and the checkout is never built`); return 1; }
|
|
1364
|
+
// The declared Linux broker admits snapshots only under its own data root: refused before any ownership or sync when the gate's root is elsewhere.
|
|
1365
|
+
if (decided.route === "snapshot" && !hosted) {
|
|
1366
|
+
const snapSync = await import("../snapshot.mjs");
|
|
1367
|
+
const mismatch = await declaredSnapshotRootRefusal(o, routeExecutor, o.snapshotRoot ?? snapSync.snapshotRoot({ platform: o.platform, env: o.env }));
|
|
1368
|
+
if (mismatch) { o.err(`${tag} BLOCKED: ${mismatch.code}: ${mismatch.message}; nothing ran`); return 1; }
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
if (!o.doorSnapshot && !hosted) {
|
|
1289
1372
|
const snap = await import("../snapshot.mjs");
|
|
1290
1373
|
const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
|
|
1291
|
-
let selected;
|
|
1292
|
-
try { selected = snap.resolveCommit(root, option("--commit") ?? "HEAD"); }
|
|
1293
|
-
catch (e) { o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
|
|
1294
1374
|
const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
|
|
1295
1375
|
let own;
|
|
1296
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 }); }
|
|
@@ -1308,12 +1388,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1308
1388
|
if (r?.released === false && !own.joined) o.err(`${tag} build ownership ${own.handle} kept: ${r.why}`);
|
|
1309
1389
|
}
|
|
1310
1390
|
}
|
|
1311
|
-
if (devTest)
|
|
1312
|
-
// Mac and Dell, until the cutover: the checkout route as it was (class A of the working tree, then right-release on it). Hosted CI's
|
|
1313
|
-
// runner checkout is the commit, so it takes the commit route below without a snapshot.
|
|
1314
|
-
if (!o.doorSnapshot && o.env.GITHUB_ACTIONS !== "true") return await runWorktreeCompile(o, args, root, git, io, to, appRoot);
|
|
1315
|
-
return await runDevTest(o, args, root, git, io, to, appRoot, appDir);
|
|
1316
|
-
}
|
|
1391
|
+
if (devTest) return await runDevTest(o, args, root, git, io, to, appRoot, appDir);
|
|
1317
1392
|
const ds = o.doorSnapshot ?? null;
|
|
1318
1393
|
const commit = ds ? ds.commit : text(git("rev-parse", "HEAD")).trim();
|
|
1319
1394
|
const tree = ds ? ds.tree : text(git("rev-parse", "HEAD^{tree}")).trim();
|
|
@@ -1334,8 +1409,8 @@ async function runStages(o, args, root, git, io) {
|
|
|
1334
1409
|
const shown = git("show", `${commit}:${f}`); if (!failed(shown)) lockFiles[f] = text(shown);
|
|
1335
1410
|
}
|
|
1336
1411
|
const binding = bindingOf({
|
|
1337
|
-
tree, lockFiles, gateFile: fileURLToPath(import.meta.url), executor: executor.kind,
|
|
1338
|
-
toolchain: { classA: { "cargo-deny": version("cargo-deny", ["--version"]), "cargo-audit": version("cargo-audit", ["--version"]), actionlint: version("actionlint", ["-version"]), shellcheck: version("shellcheck", ["--version"]) }, node: process.version, rustc: version(executor.kind === "broker" && executor.cargo
|
|
1412
|
+
tree, lockFiles, gateFile: fileURLToPath(import.meta.url), executor: o.brokerException ? `${executor.kind}:exception:${o.brokerException.exceptionId}` : executor.kind,
|
|
1413
|
+
toolchain: { classA: { "cargo-deny": version("cargo-deny", ["--version"]), "cargo-audit": version("cargo-audit", ["--version"]), actionlint: version("actionlint", ["-version"]), shellcheck: version("shellcheck", ["--version"]) }, node: process.version, rustc: version(executor.kind === "broker" && executor.cargo.length > 1 ? executor.cargo[0] : "rustc", executor.kind === "broker" && executor.cargo.length > 1 ? ["rustc", "-V"] : ["-V"]), pnpm: version("pnpm", ["--version"]) },
|
|
1339
1414
|
});
|
|
1340
1415
|
// Release writes to R2. The write credential lives only in the trusted publisher: a GitHub Actions job in the protected
|
|
1341
1416
|
// publication environment, which sets RIGHTKIT_PUBLISHER=github-environment. Hosts and agents never hold it, so the door refuses the
|
|
@@ -1351,6 +1426,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1351
1426
|
if (receiptIn !== undefined) {
|
|
1352
1427
|
let imported = null;
|
|
1353
1428
|
try { imported = JSON.parse(o.readFile(path.resolve(o.cwd, receiptIn))); } catch { /* refused below */ }
|
|
1429
|
+
if (imported?.brokerException) { o.err(`${tag} BLOCKED: --receipt-in ${receiptIn} was built outside the broker by exception ${imported.brokerException.exceptionId}; it is never taken as a broker-built receipt`); return 1; }
|
|
1354
1430
|
if (!imported || imported.schema !== RECEIPT_SCHEMA || imported.commit !== commit || !sameSource(imported.binding, binding)) {
|
|
1355
1431
|
o.err(`${tag} BLOCKED: --receipt-in ${receiptIn} is not a ${RECEIPT_SCHEMA} receipt for ${commit.slice(0, 12)} with this tree, these lockfiles and this gate`);
|
|
1356
1432
|
return 1;
|
|
@@ -1363,6 +1439,8 @@ async function runStages(o, args, root, git, io) {
|
|
|
1363
1439
|
const receipt = previous && sameBinding(previous.binding, binding) ? previous
|
|
1364
1440
|
: previous && sameSource(previous.binding, binding) ? { ...previous, binding, stages: Object.fromEntries(Object.entries(previous.stages ?? {}).filter(([k]) => k !== "check" && k !== "compile")) }
|
|
1365
1441
|
: { schema: RECEIPT_SCHEMA, commit, app: appDir, binding, stages: {} };
|
|
1442
|
+
// Every run under a broker exception records it in the receipt (broker-exception.mjs): id, Adrian's quote, issue, units and host.
|
|
1443
|
+
if (o.brokerException) receipt.brokerException = o.brokerException;
|
|
1366
1444
|
const receiptOut = option("--receipt-out");
|
|
1367
1445
|
const save = () => {
|
|
1368
1446
|
(o.writeReceipt ?? writeReceipt)({ commonDir, receipt });
|
|
@@ -1374,25 +1452,35 @@ async function runStages(o, args, root, git, io) {
|
|
|
1374
1452
|
// the door run records the split, so the child can prove which snapshot it may read and which checkout holds its outputs
|
|
1375
1453
|
// (source-split.mjs). Anywhere else it runs in the app root of the checkout, which hosted CI's runner holds at the commit.
|
|
1376
1454
|
const sourceOf = ds ? sourceSplitOf({ root, ds, appDir }) : null;
|
|
1377
|
-
const rightRelease = (stage, cliArgs, extraEnv = {}) => {
|
|
1455
|
+
const rightRelease = async (stage, cliArgs, extraEnv = {}) => {
|
|
1456
|
+
// A broker host's child reaches the broker only through this stage's carrier (carried-cargo.mjs): its Cargo calls, and its pnpm, node and QA
|
|
1457
|
+
// steps as adapter jobs with the third-party children's compiles nested under them. Every call is in the gate-owned ledger and is verified
|
|
1458
|
+
// again after the child ends.
|
|
1459
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
1460
|
+
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing ran`); return false; }
|
|
1378
1461
|
const run = startRun({ commonDir, commit, tree, stage, ...(sourceOf ? { source: sourceOf } : {}) });
|
|
1462
|
+
let childOk = false;
|
|
1379
1463
|
try {
|
|
1380
1464
|
o.out(`${tag} [${stage}] right-release ${cliArgs.join(" ")}${sourceOf ? ` (from the snapshot ${ds.path})` : ""}`);
|
|
1381
|
-
const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, ...cliArgs], { cwd: sourceOf ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: { ...o.env, ...run.env,
|
|
1382
|
-
|
|
1465
|
+
const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, ...cliArgs], { cwd: sourceOf ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: carriedEnv(carrier, { ...o.env, ...run.env }, extraEnv) });
|
|
1466
|
+
childOk = r.status === 0;
|
|
1383
1467
|
} finally { endRun(run); }
|
|
1468
|
+
const carriedOk = await carrier.finish({ stage });
|
|
1469
|
+
return childOk && carriedOk;
|
|
1384
1470
|
};
|
|
1385
1471
|
|
|
1386
1472
|
o.out(`${tag} --to ${to} for ${commit.slice(0, 12)}${appDir === "." ? "" : ` (app ${appDir})`}: ${stages.join(" -> ")}; executor ${executor.kind}`);
|
|
1387
1473
|
// A later stage on the sealed bytes: check, compile and package already passed for them (in this receipt) and the bytes on disk are
|
|
1388
1474
|
// the ones they sealed, so they are not run again.
|
|
1389
1475
|
// 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.
|
|
1390
|
-
const sidecarConfigsOf = () => {
|
|
1391
|
-
const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
1392
|
-
return (failed(listedConfigs) ? [] : text(listedConfigs).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)))
|
|
1393
|
-
.filter((f) => { try { return /\bsidecars\b/.test(o.readFile(path.join(ds ? ds.path : root, f))); } catch { return false; } });
|
|
1394
|
-
};
|
|
1476
|
+
const sidecarConfigsOf = () => committedSidecarConfigs({ o, git, commit, appDir, base: ds ? ds.path : root });
|
|
1395
1477
|
const sealedNow = receipt.stages.package?.ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
|
|
1478
|
+
// The protected-receipt verifier pulls in the ownership policy modules (and their dependencies): loaded only for a run that qualifies or releases,
|
|
1479
|
+
// so an installed copy that only checks or compiles never needs them (GB-14).
|
|
1480
|
+
const qualifyReuse = stages.includes("qualify") || stages.includes("release") ? await import("../qualify-reuse.mjs") : null;
|
|
1481
|
+
const installedQaReceiptDigest = (...a) => qualifyReuse.installedQaReceiptDigest(...a);
|
|
1482
|
+
const qualifyReuseProblem = (...a) => qualifyReuse.qualifyReuseProblem(...a);
|
|
1483
|
+
const qualifyRecordProblem = (recorded) => (o.qualifyReuseProblem ?? qualifyReuseProblem)({ recorded, sealedNow, repoRoot: appRoot, platform, ...(o.verifyQa !== undefined ? { verify: o.verifyQa } : (o.env.GITHUB_ACTIONS === "true" && o.env.RIGHTKIT_PUBLISHER === "github-environment" ? { verify: null } : {})) });
|
|
1396
1484
|
const sealedIntact = Boolean(sealedNow && sealedNow.id === receipt.stages.package.sealed && sealedNow.digest === receipt.stages.package.manifestSha256);
|
|
1397
1485
|
// Every deterministic scan (full class A) runs before any compile, for every door stage. The app rules run at the tier the stage
|
|
1398
1486
|
// owes: dev, test and package take the compile-tier rules; qualify and release add the release-only rules (approvals, ratified
|
|
@@ -1400,16 +1488,36 @@ async function runStages(o, args, root, git, io) {
|
|
|
1400
1488
|
// 2026-10-28 change to release tier everywhere was never his decision and blocked HeardRight's dev and test builds).
|
|
1401
1489
|
const earlyTier = ["qualify", "release"].includes(to) ? "release" : "compile";
|
|
1402
1490
|
const tierAtLeast = (have, need) => have === "release" || need === "compile";
|
|
1491
|
+
// GB-12 (GATE 36/44/86, E18): Class A admission runs at every stage boundary a door run crosses; an outer receipt boolean or an intact
|
|
1492
|
+
// sealed set never stands in for it. The deterministic scans reuse through their own complete result-cache key (never this receipt's
|
|
1493
|
+
// coarse binding, which names the entry file only), and the live scans (cargo-deny, cargo-audit, pnpm-audit, pnpm-lockfile,
|
|
1494
|
+
// advisory-exceptions) are never cached, so the day's advisories are re-asked on every reuse. The one exception is the trusted
|
|
1495
|
+
// publisher's upload (`--to release` on a desktop platform): it runs no build environment, only uploads bytes whose earlier stages
|
|
1496
|
+
// (Class A included) another job of the same pipeline recorded in the receipt it imported (--receipt-in).
|
|
1497
|
+
const publisherUploadOnly = to === "release" && !mobile;
|
|
1498
|
+
// GB-15: a nested app's Class A runs its executing scans for that app's dependency closure; an app at the repository root is the repository.
|
|
1499
|
+
const appScope = appDir === "." ? [] : ["--scope", appDir];
|
|
1403
1500
|
for (const stage of stages) {
|
|
1404
|
-
if (stage === "check" && receipt.stages.check?.ok && tierAtLeast(receipt.stages.check.ruleTier ?? "compile", earlyTier)) { o.out(`${tag} [check] already passed (${receipt.stages.check.ruleTier ?? "compile"} tier) for this exact input (commit, tree, lockfiles, gate, toolchain)`); continue; }
|
|
1405
|
-
if (stage === "compile" && receipt.stages.compile?.ok) {
|
|
1406
|
-
|
|
1501
|
+
if (stage === "check" && publisherUploadOnly && receipt.stages.check?.ok && tierAtLeast(receipt.stages.check.ruleTier ?? "compile", earlyTier)) { o.out(`${tag} [check] already passed (${receipt.stages.check.ruleTier ?? "compile"} tier) for this exact input (commit, tree, lockfiles, gate, toolchain)`); continue; }
|
|
1502
|
+
if (stage === "compile" && receipt.stages.compile?.ok) {
|
|
1503
|
+
// GB-13 (GATE E6/E7/E16): a recorded compile stands only while the outputs it recorded are still on disk and the compile environment is
|
|
1504
|
+
// the one it ran in; otherwise the compile runs again, warm (never a cleared target), unless the sealed bytes below carry it.
|
|
1505
|
+
const outputsProblem = (o.compileReuseProblem ?? compileReuseProblem)(receipt.stages.compile, { env: o.env });
|
|
1506
|
+
if (!outputsProblem) { o.out(`${tag} [${stage}] already passed for this exact input (commit, tree, lockfiles, gate, toolchain) and its outputs are intact`); continue; }
|
|
1507
|
+
if (!sealedIntact) o.out(`${tag} [${stage}] the recorded compile no longer stands: ${outputsProblem}; running it again (warm)`);
|
|
1508
|
+
}
|
|
1509
|
+
if (((stage === "check" && publisherUploadOnly) || stage === "compile" || stage === "package") && sealedIntact) {
|
|
1407
1510
|
o.out(`${tag} [${stage}] already passed for the sealed set ${sealedNow.id} (manifest ${sealedNow.digest.slice(0, 12)})`); continue;
|
|
1408
1511
|
}
|
|
1409
1512
|
// The publisher (another runner, often another OS) cannot repeat installed QA; it takes the qualify result recorded for exactly
|
|
1410
1513
|
// these sealed bytes. --to qualify itself always runs it.
|
|
1514
|
+
// GB-14: the record is reused only while the protected installed-QA receipt it names still verifies over the current state (the existing
|
|
1515
|
+
// verifier: policy bundle, floor, required checks, source, artifact hashes); the publisher, which holds no receipt key, is held to the
|
|
1516
|
+
// recorded receipt digest and the manifest digest, and the upload admission verifies the receipt itself.
|
|
1411
1517
|
if (stage === "qualify" && to !== "qualify" && sealedIntact && receipt.stages.qualify?.ok && receipt.stages.qualify.manifestSha256 === sealedNow.digest) {
|
|
1412
|
-
|
|
1518
|
+
const problem = qualifyRecordProblem(receipt.stages.qualify);
|
|
1519
|
+
if (!problem) { o.out(`${tag} [qualify] already passed for the sealed set ${sealedNow.id} (manifest ${sealedNow.digest.slice(0, 12)}; protected QA receipt ${receipt.stages.qualify.qaReceiptDigest.slice(0, 12)} verified)`); continue; }
|
|
1520
|
+
o.out(`${tag} [qualify] the recorded result no longer stands: ${problem}; it runs again`);
|
|
1413
1521
|
}
|
|
1414
1522
|
// The publisher only uploads. It never checks out a build environment, signs or runs installed QA, so every earlier stage must
|
|
1415
1523
|
// already stand in its receipt for exactly the sealed bytes on disk; anything else is refused, never rebuilt or requalified here.
|
|
@@ -1421,7 +1529,7 @@ async function runStages(o, args, root, git, io) {
|
|
|
1421
1529
|
if (ds && !(await openSnapshotStage({ o, ds, root, stage, tag }))) { receipt.stages[stage] = { ok: false, at: new Date().toISOString(), snapshot: { path: ds.path, tree: ds.tree, intactAfter: false }, ownership: ds.own.handle }; save(); o.err(`${tag} --to ${to} stopped before ${stage}: the snapshot is not the commit's bytes plus the ledger; nothing ran`); return 2; }
|
|
1422
1530
|
let ok = false; const at = new Date().toISOString(); let extra = {}; let directReceipt = null;
|
|
1423
1531
|
if (stage === "check") {
|
|
1424
|
-
ok = (await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), "--rule-tier", earlyTier], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))(earlyTier)) === 0;
|
|
1532
|
+
ok = (await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...appScope, "--rule-tier", earlyTier], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))(earlyTier)) === 0;
|
|
1425
1533
|
extra = { ruleTier: earlyTier };
|
|
1426
1534
|
}
|
|
1427
1535
|
else if (stage === "compile") {
|
|
@@ -1431,15 +1539,17 @@ async function runStages(o, args, root, git, io) {
|
|
|
1431
1539
|
ok = true;
|
|
1432
1540
|
// On the snapshot route the sidecars are staged from the snapshot's own config and sources through right-release's source split
|
|
1433
1541
|
// (the staged files and receipt land beside the sources the compile reads; the caches and build trees stay the checkout's), before
|
|
1434
|
-
// `main --all` compiles. A broker host reaches Cargo
|
|
1435
|
-
// have yet (item 7 row 7.24): it is refused with a code, never staged from the checkout.
|
|
1436
|
-
if (ds && sidecarConfigs.length && executor.kind === "broker") { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: [compile] ${sidecarConfigs.join(", ")} stage sidecars through right-release, whose Cargo calls reach the RustRight broker only through the snapshot carrier (item 7 row 7.24), which is not wired; nothing compiled`); ok = false; }
|
|
1542
|
+
// `main --all` compiles. A broker host's stage-sidecar child reaches Cargo through the stage's carrier (carried-cargo.mjs).
|
|
1437
1543
|
// right-release runs in appRoot (nested apps), so the repository-relative config is handed over absolute (Astra, second pass).
|
|
1438
1544
|
// On the snapshot route sidecars are staged ONCE per door run by the ledger bookkeeping (attribution diff, immutable staged entries).
|
|
1439
|
-
if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage });
|
|
1440
|
-
else for (const config of ok ? sidecarConfigs : []) if (!rightRelease(stage, ["stage-sidecar", "--config", path.join(root, config)])) { ok = false; break; }
|
|
1441
|
-
|
|
1545
|
+
if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1546
|
+
else for (const config of ok ? sidecarConfigs : []) if (!await rightRelease(stage, ["stage-sidecar", "--config", path.join(root, config)])) { ok = false; break; }
|
|
1547
|
+
let compiledDirs = [];
|
|
1548
|
+
const reportOutputs = (dirs) => { compiledDirs = [...dirs]; };
|
|
1549
|
+
if (ok) ok = (await (o.runCompile ?? (() => main(ds ? ["--all", "--commit", commit, "--snapshot", ds.path] : ["--all"], { ...io, cwd: root, readStdin: () => "", ...(ds ? { buildOwnership: ds.own, snapshotLedger: ds.ledger?.entries ?? [], onCompileLedger: (entries) => { ds.compileAdmitted = entries; }, onDirectReceipt: (r) => { directReceipt = r; }, onCompileOutputs: reportOutputs } : {}) })))({ reportOutputs })) === 0;
|
|
1442
1550
|
if (directReceipt) extra = { ...extra, direct: directReceipt };
|
|
1551
|
+
// 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 }) };
|
|
1443
1553
|
// The cross-check with `.rightkit/compile-inputs.json`: a declared input that covers ledger entries must carry the digest of those
|
|
1444
1554
|
// very bytes in the compile receipt (the broker digests declared inputs in place in the snapshot, BC:39).
|
|
1445
1555
|
if (ok && ds && directReceipt?.declaredInputDigests) {
|
|
@@ -1454,29 +1564,61 @@ async function runStages(o, args, root, git, io) {
|
|
|
1454
1564
|
// compile was already passed (a separately invoked `--to package` re-stages after the sync, so it never meets a missing externalBin).
|
|
1455
1565
|
// right-release then takes its identity from the selected commit and the verified snapshot (release-source-identity.mjs, row 7.04),
|
|
1456
1566
|
// never the checkout's HEAD, status or branch; its outputs, state and the sealed directory stay in the checkout's `.right-release`.
|
|
1457
|
-
ok = ds ? await stageSidecarsOnce({ o, ds, root, configs: sidecarConfigsOf(), appDir, cli, commonDir, commit, tree, stage, executor }) : true;
|
|
1567
|
+
ok = ds ? await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigsOf(), appDir, cli, commonDir, commit, tree, stage, executor }) : true;
|
|
1458
1568
|
// Source QA (right-release qa --stage source) exists for mac and win only; a mobile package is the signed build alone.
|
|
1459
1569
|
// Row 7.04c (R-T1, 7.09): the snapshot holds no Cargo target. On a direct host the checkout's output directory is resolved here, in the primary
|
|
1460
1570
|
// checkout, and handed to right-release absolute: it binds every manifest's target for this stage, and the sealed manifest must record it back
|
|
1461
1571
|
// (the door never trusts the child's value alone). One workspace is pinned as a whole; several are refused, naming them. A broker host's target
|
|
1462
|
-
// is the carrier's (rows 7.20 to 7.24)
|
|
1572
|
+
// is the carrier's (rows 7.20 to 7.24): the carried calls' signed receipts name it (carried-cargo.mjs), and no CARGO_TARGET_DIR is passed.
|
|
1463
1573
|
let packagePin = { dir: null };
|
|
1464
1574
|
if (ok && ds && executor.kind !== "broker") {
|
|
1465
|
-
packagePin =
|
|
1575
|
+
packagePin = await cargoOutputPinForScope({ o, git, root, commit, executor, snapshot: ds.path, ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
|
|
1466
1576
|
if (packagePin.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: [package] ${packagePin.refusal}; nothing was built`); ok = false; }
|
|
1467
1577
|
else if (packagePin.dir) o.out(`${tag} [package] output directory for the Cargo workspace: ${packagePin.dir} (the checkout's)`);
|
|
1468
1578
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1579
|
+
// 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
|
+
let packageMulti = null;
|
|
1581
|
+
if (ok && packagePin.dirs?.length > 1) {
|
|
1582
|
+
packageMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
|
|
1583
|
+
if (packageMulti.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: [package] ${packageMulti.refusal}; nothing was built`); ok = false; }
|
|
1584
|
+
else if (packageMulti.note) o.out(`${tag} [package] ${packageMulti.note}`);
|
|
1585
|
+
}
|
|
1586
|
+
const packageEnv = packagePin.dir ? { CARGO_TARGET_DIR: packagePin.dir } : (packageMulti?.env ?? {});
|
|
1587
|
+
ok = ok && (mobile || await rightRelease(stage, ["qa", "--stage", "source", "--platform", platform], packageEnv)) && await rightRelease(stage, ["build", "--platform", platform], packageEnv);
|
|
1471
1588
|
const sealed = ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
|
|
1472
1589
|
if (ok && !sealed) { o.err(`${tag} [package] the build reported success but sealed nothing for ${commit.slice(0, 8)} on ${platform}`); ok = false; }
|
|
1473
1590
|
if (sealed) extra = { platform, sealed: sealed.id, manifestSha256: sealed.digest };
|
|
1474
1591
|
// The cross-check: the Cargo targets the sealed manifest records are the directory this door resolved itself. A different one voids the stage.
|
|
1475
|
-
|
|
1592
|
+
try { packageMulti?.cleanup?.(); } catch { /* the wrapper's map is the gate's own temp directory */ }
|
|
1593
|
+
if (ok && sealed && ds && (packagePin.dir || packagePin.dirs?.length > 1)) {
|
|
1594
|
+
let recorded = null;
|
|
1595
|
+
try { recorded = JSON.parse(String(o.readFile(sealed.manifest))).boundTargets ?? null; } catch { recorded = null; }
|
|
1596
|
+
const { crossCheckBoundTargets, SPLIT_TARGET_DRIFT_CODE } = await import("../release-artifact-path.mjs");
|
|
1597
|
+
const drift = crossCheckBoundTargets({ recorded, expected: packagePin.dirs });
|
|
1598
|
+
if (drift.length) {
|
|
1599
|
+
for (const problem of drift.slice(0, 10)) o.err(`${tag} ${SPLIT_TARGET_DRIFT_CODE}: [package] ${problem}`);
|
|
1600
|
+
const closedDrift = await closeSnapshotStage({ o, ds, root, stage, step: stage, tag });
|
|
1601
|
+
receipt.stages[stage] = { ok: false, at, voided: SPLIT_TARGET_DRIFT_CODE, ...extra, snapshot: { path: ds.path, tree: ds.tree, intactAfter: closedDrift.ok }, ownership: ds.own.handle };
|
|
1602
|
+
save();
|
|
1603
|
+
o.err(`${tag} --to ${to} stopped at ${stage}: ${SPLIT_TARGET_DRIFT_CODE}; the sealed set is void and nothing after it ran`);
|
|
1604
|
+
return 2;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
// A broker host's Cargo target is the carrier's: every target the sealed manifest records is a directory a verified, signed receipt of this
|
|
1608
|
+
// stage named (`outputDir`), never one the child alone reported.
|
|
1609
|
+
if (ok && sealed && ds && executor.kind === "broker") {
|
|
1476
1610
|
let recorded = null;
|
|
1477
1611
|
try { recorded = JSON.parse(String(o.readFile(sealed.manifest))).boundTargets ?? null; } catch { recorded = null; }
|
|
1478
1612
|
const { crossCheckBoundTargets, SPLIT_TARGET_DRIFT_CODE } = await import("../release-artifact-path.mjs");
|
|
1479
|
-
|
|
1613
|
+
// The commit's workspaces decide whether a bound target is owed; the receipts compared are THIS package stage's own (rightkit#1301 L1).
|
|
1614
|
+
const owed = Object.keys(await snapshotCargoWorkspaces({ git, ds, root })).length > 0;
|
|
1615
|
+
const want = [...(ds.carriedByStage?.[stage] ?? [])];
|
|
1616
|
+
let drift = [];
|
|
1617
|
+
if (owed) {
|
|
1618
|
+
if (!Array.isArray(recorded) || !recorded.length) drift = ["the sealed manifest records no bound Cargo target; a broker host's package stage binds the output directory its carried build's receipt named"];
|
|
1619
|
+
else if (!want.length) drift = ["no verified receipt of this package stage names an output directory, so the recorded target cannot be checked"];
|
|
1620
|
+
else drift = crossCheckBoundTargets({ recorded, expected: want });
|
|
1621
|
+
}
|
|
1480
1622
|
if (drift.length) {
|
|
1481
1623
|
for (const problem of drift.slice(0, 10)) o.err(`${tag} ${SPLIT_TARGET_DRIFT_CODE}: [package] ${problem}`);
|
|
1482
1624
|
const closedDrift = await closeSnapshotStage({ o, ds, root, stage, step: stage, tag });
|
|
@@ -1497,21 +1639,23 @@ async function runStages(o, args, root, git, io) {
|
|
|
1497
1639
|
// store); the release tier is owed first, as for desktop qualification.
|
|
1498
1640
|
const releaseTier = receipt.stages.check?.ok && receipt.stages.check.ruleTier === "release"
|
|
1499
1641
|
? (o.out(`${tag} [release] the release tier already passed at check for this exact input; not run again`), 0)
|
|
1500
|
-
: await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
|
|
1642
|
+
: 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 } : {}) })))();
|
|
1501
1643
|
if (releaseTier !== 0) { o.err(`${tag} [release] the release tier (every rule and scan) is red for ${commit.slice(0, 12)}; nothing was uploaded`); ok = false; }
|
|
1502
|
-
else ok = rightRelease(stage, ["upload", "--platform", platform, "--release", sealed.sealed, "--apply"]);
|
|
1644
|
+
else ok = await rightRelease(stage, ["upload", "--platform", platform, "--release", sealed.sealed, "--apply"]);
|
|
1503
1645
|
extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, track: platform === "ios" ? "testflight" : "play-internal", ruleTier: "release" };
|
|
1504
1646
|
} else if (stage === "qualify") {
|
|
1505
1647
|
// Qualification owes the release tier: every rule and scan, the ones held out of compile admission included (app-check.mjs TIERS).
|
|
1506
1648
|
const releaseTier = receipt.stages.check?.ok && receipt.stages.check.ruleTier === "release"
|
|
1507
1649
|
? (o.out(`${tag} [qualify] the release tier already passed at check for this exact input; not run again`), 0)
|
|
1508
|
-
: await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
|
|
1650
|
+
: 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 } : {}) })))();
|
|
1509
1651
|
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; }
|
|
1510
|
-
else ok = rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
|
|
1511
|
-
|
|
1652
|
+
else ok = await rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
|
|
1653
|
+
// 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: appRoot, releaseId: sealed.sealed, platform }) } : {}) };
|
|
1512
1655
|
} else {
|
|
1513
1656
|
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; }
|
|
1514
|
-
else
|
|
1657
|
+
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; }
|
|
1658
|
+
else { ok = await rightRelease(stage, ["upload", "--release", sealed.sealed, "--platform", platform, "--tier", tier]); extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, tier }; }
|
|
1515
1659
|
}
|
|
1516
1660
|
}
|
|
1517
1661
|
if (ds) {
|
|
@@ -1550,7 +1694,9 @@ function stageForPublisher(o, args, root, git) {
|
|
|
1550
1694
|
const common = text(git("rev-parse", "--git-common-dir")).trim();
|
|
1551
1695
|
if (!/^[0-9a-f]{40}$/.test(commit) || !common) { o.err(`${tag} cannot read HEAD and the git directory`); return 2; }
|
|
1552
1696
|
const commonDir = path.resolve(root, common);
|
|
1553
|
-
const
|
|
1697
|
+
const inventory = committedConfigDirs({ git, commit });
|
|
1698
|
+
if (inventory.refusal) { o.err(`${tag} cannot read the app configuration of ${commit.slice(0, 12)}: ${inventory.refusal}`); return 2; }
|
|
1699
|
+
const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, configDirs: inventory.dirs });
|
|
1554
1700
|
const receipt = (o.readReceipt ?? readReceipt)({ commonDir, commit, app: appDir });
|
|
1555
1701
|
const pack = receipt?.stages?.package; const qual = receipt?.stages?.qualify;
|
|
1556
1702
|
const sealed = pack?.ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
|
|
@@ -1565,7 +1711,7 @@ function stageForPublisher(o, args, root, git) {
|
|
|
1565
1711
|
const receiptFile = path.join(work, `receipt-${platform}.json`);
|
|
1566
1712
|
fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1567
1713
|
const archive = path.join(work, `sealed-${platform}.tar.gz`);
|
|
1568
|
-
const tar = o.spawn("tar", ["-czf", archive, "-C", appRoot, path.join(".right-release", "sealed", sealed.id, platform)]);
|
|
1714
|
+
const tar = o.spawn("tar", ["-czf", archive, "-C", appRoot, path.join(".right-release", "sealed", sealed.id, platformDirOf(platform))]);
|
|
1569
1715
|
if (failed(tar)) { o.err(`${tag} could not pack ${sealed.id}/${platform}`); return 1; }
|
|
1570
1716
|
const gh = (...a) => o.spawn("gh", a, { cwd: root });
|
|
1571
1717
|
if (failed(gh("release", "view", releaseTag))) {
|
|
@@ -1663,30 +1809,41 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
|
|
|
1663
1809
|
return { ok: true, entries };
|
|
1664
1810
|
}
|
|
1665
1811
|
|
|
1812
|
+
/** 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
|
+
export function committedSidecarConfigs({ o, git, commit, appDir = ".", base }) {
|
|
1814
|
+
const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
1815
|
+
return (failed(listedConfigs) ? [] : text(listedConfigs).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)))
|
|
1816
|
+
.filter((f) => { try { return /\bsidecars\b/.test(o.readFile(path.join(base, f))); } catch { return false; } });
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1666
1819
|
/**
|
|
1667
1820
|
* Sidecars are staged ONCE per door run, after the sync and before the first stage that builds the Tauri crate. The step is
|
|
1668
1821
|
* right-release's own `stage-sidecar --json` run inside the snapshot (the split); this gate then walks the snapshot itself and the
|
|
1669
1822
|
* step's additions must equal the files it reported plus its receipt (anything else is an unattributed write, RK_CHECK_ERROR). The
|
|
1670
1823
|
* staged files become immutable `staged` ledger entries. Returns true when nothing is owed or staging passed.
|
|
1671
1824
|
*/
|
|
1672
|
-
export async function stageSidecarsOnce({ o, ds, root, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
|
|
1825
|
+
export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
|
|
1673
1826
|
if (ds.sidecarsStaged) return true;
|
|
1674
1827
|
ds.sidecarsStaged = true;
|
|
1675
1828
|
if (!configs.length) return true;
|
|
1676
|
-
if (executor?.kind === "broker") { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: [${stage}] ${configs.join(", ")} stage sidecars through right-release, whose Cargo calls reach the RustRight broker only through the snapshot carrier (item 7 row 7.24); nothing was staged`); return false; }
|
|
1677
1829
|
const { attributionProblems, reportedPaths } = await import("../stage-ledger.mjs");
|
|
1678
1830
|
const source = sourceSplitOf({ root, ds, appDir });
|
|
1679
1831
|
const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited; // never set or pass one (refused at the door's start)
|
|
1680
1832
|
for (const config of configs) {
|
|
1681
1833
|
const before = ds.ledger?.entries ?? [];
|
|
1834
|
+
// The stage-sidecar child's Cargo calls (its builds and metadata queries) are carried on a broker host (carried-cargo.mjs).
|
|
1835
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
1836
|
+
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing was staged`); return false; }
|
|
1682
1837
|
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
1683
1838
|
let r;
|
|
1684
1839
|
try {
|
|
1685
1840
|
o.out(`${tag} [${stage}] right-release stage-sidecar --config ${path.join(ds.path, config)} (from the snapshot ${ds.path})`);
|
|
1686
|
-
r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: { ...cleanEnv, ...run.env } });
|
|
1841
|
+
r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
|
|
1687
1842
|
} finally { endRun(run); }
|
|
1843
|
+
const carriedOk = await carrier.finish({ stage });
|
|
1688
1844
|
if (r?.stderr) o.err(String(r.stderr).trimEnd());
|
|
1689
1845
|
if (!r || r.status !== 0) { o.err(`${tag} [${stage}] stage-sidecar for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
|
|
1846
|
+
if (!carriedOk) return false;
|
|
1690
1847
|
let result;
|
|
1691
1848
|
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; }
|
|
1692
1849
|
let walked;
|
|
@@ -1702,44 +1859,116 @@ export async function stageSidecarsOnce({ o, ds, root, configs, appDir, cli, com
|
|
|
1702
1859
|
return true;
|
|
1703
1860
|
}
|
|
1704
1861
|
|
|
1705
|
-
/**
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
const
|
|
1715
|
-
if (
|
|
1716
|
-
|
|
1717
|
-
|
|
1862
|
+
/** Hosted CI scans its committed runner checkout. Raw-tree verification defeats filters or a stale index hiding edits. */
|
|
1863
|
+
async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
|
|
1864
|
+
const head = git("rev-parse", "--verify", "HEAD^{commit}");
|
|
1865
|
+
const dirty = git("status", "--porcelain", "--untracked-files=all");
|
|
1866
|
+
if (failed(head) || text(head).trim() !== commit || failed(dirty) || text(dirty).trim()) throw new Error(`hosted CI checkout is not clean commit ${commit} ${when}`);
|
|
1867
|
+
const { commitOf } = await import("../snapshot-check.mjs");
|
|
1868
|
+
const identity = commitOf(root, commit);
|
|
1869
|
+
if (identity.commit !== commit || identity.tree !== tree) throw new Error(`hosted CI commit identity changed ${when}`);
|
|
1870
|
+
const { snapshotTree } = await import("../snapshot.mjs");
|
|
1871
|
+
const actual = snapshotTree({ checkout: root, dir: root, baseTree: identity.tree });
|
|
1872
|
+
if (actual !== tree) throw new Error(`hosted CI checkout is raw tree ${actual}, not tree ${tree} of commit ${commit} ${when}`);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/** Select output-binding workspaces from committed source before any Cargo metadata process runs. */
|
|
1876
|
+
async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
|
|
1877
|
+
const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
1878
|
+
if (failed(listed)) throw new Error(`cannot list the files of ${commit.slice(0, 12)}: ${why(listed)}`);
|
|
1879
|
+
const files = text(listed).split("\0").filter(Boolean).map(posix);
|
|
1880
|
+
const tracked = (name) => files.filter((f) => f === name || f.endsWith(`/${name}`));
|
|
1881
|
+
const allManifests = tracked("Cargo.toml");
|
|
1882
|
+
if (!scopeDirs?.length) return { manifests: allManifests, hasCargo: allManifests.length > 0 };
|
|
1883
|
+
const fileSet = new Set(files);
|
|
1884
|
+
const readCommitted = (file) => {
|
|
1885
|
+
if (!fileSet.has(file)) throw new Error(`RK_SCOPE_CLOSURE: ${file} is absent from commit ${commit}`);
|
|
1886
|
+
const r = git("show", `${commit}:${file}`);
|
|
1887
|
+
if (failed(r)) throw new Error(`RK_SCOPE_CLOSURE: cannot read ${file} of commit ${commit}: ${why(r)}`);
|
|
1888
|
+
return text(r);
|
|
1889
|
+
};
|
|
1890
|
+
const relative = (file) => posix(path.relative(root, file));
|
|
1891
|
+
const readFile = (file) => readCommitted(relative(file));
|
|
1892
|
+
const exists = (file) => { const rel = relative(file); return fileSet.has(rel) || files.some((f) => f.startsWith(`${rel}/`)); };
|
|
1893
|
+
let fixtures = [];
|
|
1894
|
+
if (fileSet.has(GATE_CONFIG)) {
|
|
1895
|
+
const config = JSON.parse(readCommitted(GATE_CONFIG));
|
|
1896
|
+
if (Array.isArray(config.fixtures)) fixtures = config.fixtures.filter((d) => typeof d === "string");
|
|
1897
|
+
}
|
|
1898
|
+
const tools = await import("../app-closure.mjs");
|
|
1899
|
+
const sourceMetadata = tools.cargoMetadataFromManifests({ manifests: tracked("Cargo.toml").filter((m) => !fixtures.some((d) => m.startsWith(`${d.replace(/\/$/, "")}/`))), readFile: readCommitted });
|
|
1900
|
+
const inventory = discoverRustWorkspaces({ root, tracked, fixtures, readFile, metadata: sourceMetadata });
|
|
1901
|
+
const npm = discoverNpmUnits({ root, tracked, readFile, exists });
|
|
1902
|
+
const closure = tools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: inventory.units, readFile: readCommitted });
|
|
1903
|
+
const workspaces = [...new Set(inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir))].sort();
|
|
1904
|
+
o.out(`rightkit-gate: Cargo output scope ${scopeDirs.join(", ")}: ${workspaces.length} workspace(s) (${workspaces.join(", ") || "none"}); dependency directories ${closure.dependencyDirs.join(", ")}`);
|
|
1905
|
+
return { manifests: workspaces.map((dir) => path.posix.join(dir, "Cargo.toml")), hasCargo: allManifests.length > 0 };
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
async function cargoOutputPinForScope(options) {
|
|
1909
|
+
try { return cargoOutputPin({ ...options, manifests: (await committedCargoManifests(options)).manifests }); }
|
|
1910
|
+
catch (error) { return { refusal: error.message }; }
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/** 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 }) {
|
|
1915
|
+
let selection;
|
|
1916
|
+
try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs }); }
|
|
1917
|
+
catch (error) { return { refusal: error.message }; }
|
|
1918
|
+
const { manifests, hasCargo } = selection;
|
|
1919
|
+
if (!hasCargo) return { env: {}, note: null, cleanup() {} };
|
|
1920
|
+
// A scoped npm stage with no declared Rust dependency still gets an empty runtime map: reaching undeclared Cargo refuses.
|
|
1718
1921
|
let checkoutReal = root; try { checkoutReal = fs.realpathSync.native(root); } catch { /* as given */ }
|
|
1719
|
-
const cargo = executor.cargo ?? ["cargo"];
|
|
1720
1922
|
const targetDirs = {};
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1923
|
+
if (executor.kind === "broker") {
|
|
1924
|
+
// A broker host never asks Cargo anything about the checkout: the broker pins every output directory (the receipt's `outputDir`), so
|
|
1925
|
+
// all this binding needs is which directories of the commit are Cargo workspaces, and the commit's own manifests say so (read with the
|
|
1926
|
+
// object database, never the editable checkout: GATE-CONTRACT "Commit-only build"). The same rule the carried seam applies to a call
|
|
1927
|
+
// (cargo-target-shim.mjs locateWorkspaceByWalk): the nearest enclosing manifest that declares `[workspace]`, else the nearest manifest.
|
|
1928
|
+
const tracked = new Set(manifests);
|
|
1929
|
+
const bodies = new Map();
|
|
1930
|
+
const declaresWorkspace = (m) => {
|
|
1931
|
+
if (!bodies.has(m)) { const r = git("show", `${ds.commit}:${m}`); bodies.set(m, failed(r) ? null : text(r)); }
|
|
1932
|
+
return bodies.get(m);
|
|
1933
|
+
};
|
|
1934
|
+
for (const m of manifests) {
|
|
1935
|
+
let dir = path.posix.dirname(m); let nearest = null; let found = null;
|
|
1936
|
+
for (;;) {
|
|
1937
|
+
const candidate = dir === "." ? "Cargo.toml" : `${dir}/Cargo.toml`;
|
|
1938
|
+
if (tracked.has(candidate)) {
|
|
1939
|
+
nearest ??= dir;
|
|
1940
|
+
const body = declaresWorkspace(candidate);
|
|
1941
|
+
if (body === null) return { refusal: `cannot read ${candidate} of ${ds.commit.slice(0, 12)}` };
|
|
1942
|
+
if (/^\s*\[workspace(?:\.[^\]]*)?\]/m.test(body)) { found = dir; break; }
|
|
1943
|
+
}
|
|
1944
|
+
if (dir === ".") break;
|
|
1945
|
+
dir = path.posix.dirname(dir);
|
|
1946
|
+
}
|
|
1947
|
+
targetDirs[found ?? nearest ?? path.posix.dirname(m)] = null;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
// GB-16/GB-17: a direct host's workspaces and target directories are resolved from the SELECTED COMMIT's snapshot (its committed manifests and `.cargo/config.toml`) and mapped
|
|
1951
|
+
// lexically to the checkout's counterpart, exactly as cargoOutputPin does: the live checkout's manifests are never read, so a directory the newer checkout deleted or moved, or a dirty or
|
|
1952
|
+
// untracked manifest there, changes nothing.
|
|
1953
|
+
if (executor.kind !== "broker") {
|
|
1954
|
+
const resolved = cargoOutputPin({ o, git, root, commit: ds.commit, executor, snapshot: ds.path, manifests });
|
|
1955
|
+
if (resolved.refusal) return { refusal: resolved.refusal };
|
|
1956
|
+
const relOf = (w) => { for (const base of [root, checkoutReal]) { const r = path.relative(base, w); if (!r.startsWith("..") && !path.isAbsolute(r)) return posix(r) || "."; } return posix(w); };
|
|
1957
|
+
for (const [workspaceRoot, target] of resolved.workspaces ?? []) targetDirs[relOf(workspaceRoot)] = target;
|
|
1730
1958
|
}
|
|
1731
1959
|
const workspaces = Object.keys(targetDirs);
|
|
1732
1960
|
if (o.platform === "win32" && executor.kind !== "broker") {
|
|
1961
|
+
if (workspaces.length === 0) return { refusal: "the selected closure has no Cargo workspace; a direct Windows host cannot wrap undeclared Cargo calls" };
|
|
1733
1962
|
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` };
|
|
1734
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() {} };
|
|
1735
1964
|
}
|
|
1736
1965
|
const shimModule = await import("../cargo-target-shim.mjs");
|
|
1737
1966
|
let broker = null; let realCargo = null;
|
|
1738
1967
|
if (executor.kind === "broker") {
|
|
1739
|
-
if (!o.snapshotBroker) return { refusal: "a broker host reaches Cargo from a unit stage only through the snapshot carrier (deps.snapshotBroker), which
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1968
|
+
if (!o.snapshotBroker) return { refusal: "a broker host reaches Cargo from a unit stage only through the snapshot carrier (deps.snapshotBroker), which this gate was started without" };
|
|
1969
|
+
let sb;
|
|
1970
|
+
try { sb = await carrierBroker({ o, root, ds, executor, which }); } catch (error) { return { refusal: `the snapshot carrier cannot be built (${error.code ?? "error"}: ${error.message})` }; }
|
|
1971
|
+
// Installing the carrier map sends nothing: the ownership records each carried request's sub-handle just before it is sent (snapshot-broker.mjs).
|
|
1743
1972
|
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
1744
1973
|
broker = { checkoutDirs: Object.fromEntries(workspaces.map((w) => [w, path.join(checkoutReal, ...(w === "." ? [] : w.split("/")))])), section: (o.brokerSection ?? brokerSection)(sb) };
|
|
1745
1974
|
} else {
|
|
@@ -1751,8 +1980,9 @@ async function bindUnitCargo({ o, git, root, ds, executor, which }) {
|
|
|
1751
1980
|
catch (error) { if (dir) fs.rmSync(dir, { recursive: true, force: true }); return { refusal: `the runtime cargo wrapper cannot be installed (${error.message})` }; }
|
|
1752
1981
|
const scan = shimModule.shimScanEnv(o.env, installed);
|
|
1753
1982
|
const delta = Object.fromEntries(Object.entries(scan).filter(([k, v]) => o.env[k] !== v));
|
|
1983
|
+
const { CARRIED_CARGO_MAP_ENV } = await import("../carried-cargo.mjs");
|
|
1754
1984
|
return {
|
|
1755
|
-
env: delta, 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`,
|
|
1985
|
+
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`,
|
|
1756
1986
|
// 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).
|
|
1757
1987
|
check: async () => { const { readLedger, UNSCOPED } = await import("../cargo-ledger.mjs"); return readLedger(installed.ledgerDir, UNSCOPED).problems ?? []; },
|
|
1758
1988
|
cleanup: () => { fs.rmSync(installed.dir ?? dir, { recursive: true, force: true }); },
|
|
@@ -1785,10 +2015,13 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
1785
2015
|
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); };
|
|
1786
2016
|
const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
1787
2017
|
if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
2018
|
+
{ const decided = routePlan(o, { to: option("--to"), stages: [], devTest: true, executor, capabilities: await brokerCapabilityStateWithHandshake(o, { to: option("--to"), executor }) });
|
|
2019
|
+
if (decided.refusal?.code === "RK_BROKER_CAPABILITY_MISSING") { o.err(`${tag} BLOCKED: ${decided.refusal.code}: ${decided.refusal.message}; nothing ran`); return 1; } }
|
|
1788
2020
|
if (executor.kind === "broker" && !o.snapshotBroker) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: a broker host builds a unit snapshot only through the snapshot carrier; nothing ran`); return 1; }
|
|
1789
2021
|
const common = git("rev-parse", "--git-common-dir");
|
|
1790
2022
|
if (failed(common) || !text(common).trim()) { o.err(`${tag} cannot read the git directory`); return 2; }
|
|
1791
2023
|
const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
|
|
2024
|
+
{ const mismatch = await declaredSnapshotRootRefusal(o, executor, snapshotsAt); if (mismatch) { o.err(`${tag} BLOCKED: ${mismatch.code}: ${mismatch.message}; nothing ran`); return 1; } }
|
|
1792
2025
|
let own;
|
|
1793
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 }); }
|
|
1794
2027
|
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; }
|
|
@@ -1800,7 +2033,9 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
1800
2033
|
o.out(`${tag} snapshot ${synced.path} is commit ${synced.commit} tree ${synced.tree} (written=${synced.written} removed=${synced.removed} unchanged=${synced.unchanged})`);
|
|
1801
2034
|
const ds = { own, path: synced.path, commit: synced.commit, tree: synced.tree, root: snapshotsAt, ledger: { entries: [] } };
|
|
1802
2035
|
// A unit's test or pack that reaches Cargo must never write `target/` into the snapshot (npm units included): see bindUnitCargo.
|
|
1803
|
-
|
|
2036
|
+
// A check starts no unit command: its own Class A path binds read-only Cargo calls. Resolving every unit's compile output
|
|
2037
|
+
// here would run unrelated metadata before the scoped check even begins.
|
|
2038
|
+
const bound = option("--to") === "check" ? { env: {}, cleanup() {} } : await bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs: [path.posix.dirname(manifest)] });
|
|
1804
2039
|
if (bound.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${bound.refusal}; nothing ran`); return 1; }
|
|
1805
2040
|
if (bound.note) o.out(`${tag} ${bound.note}`);
|
|
1806
2041
|
const envExtra = bound.env;
|
|
@@ -1813,14 +2048,15 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
1813
2048
|
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,
|
|
1814
2049
|
resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
|
|
1815
2050
|
...(o.loadUnitOwner ? { loadOwner: o.loadUnitOwner } : {}),
|
|
2051
|
+
...(o.unitGateDeps ?? {}), // test seam: fetchReceipt, stageCommands, verifyReceipts, selfGit
|
|
1816
2052
|
stageGuard: { start: (stage) => openSnapshotStage({ o, ds, root, stage: `unit:${stage}`, tag }), end: async (stage) => {
|
|
1817
2053
|
if (!(await closeSnapshotStage({ o, ds, root, stage: `unit:${stage}`, step: `unit:${stage}`, tag })).ok) return false;
|
|
1818
2054
|
const calls = bound.check ? await bound.check() : [];
|
|
1819
2055
|
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)}`);
|
|
1820
2056
|
return calls.length === 0;
|
|
1821
2057
|
} },
|
|
1822
|
-
check: (tier) => (o.runCheck ?? (() => main(["check", "--commit", ds.commit, "--full-a", "--snapshot", ds.path, "--rule-tier", tier], { ...io, cwd: root, buildOwnership: own })))(tier), out: o.out, err: o.err,
|
|
1823
|
-
...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(report, null, 2)}\n`) } : {}),
|
|
2058
|
+
check: (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), out: o.out, err: o.err,
|
|
2059
|
+
...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
|
|
1824
2060
|
});
|
|
1825
2061
|
} finally { bound.cleanup(); }
|
|
1826
2062
|
} finally {
|
|
@@ -1830,52 +2066,12 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
|
|
|
1830
2066
|
}
|
|
1831
2067
|
|
|
1832
2068
|
/**
|
|
1833
|
-
* Whether the door takes the snapshot route on this host
|
|
1834
|
-
*
|
|
1835
|
-
*
|
|
2069
|
+
* Whether the door takes the snapshot route on this host: every host but hosted CI, whose runner checkout is the commit. Linux
|
|
2070
|
+
* compiles it directly; Mac and Dell compile it through the RustRight snapshot carrier. There is no other route and no flag or
|
|
2071
|
+
* environment variable that selects one (rightkit#1301: the checkout route is deleted).
|
|
1836
2072
|
*/
|
|
1837
2073
|
export function snapshotRouteSelected(o) {
|
|
1838
|
-
return
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
/**
|
|
1842
|
-
* rightkit-gate --to dev|test [-- <right-release dev|test arguments>] on the CHECKOUT route (Mac and Dell until the cutover commit):
|
|
1843
|
-
* class A over the working tree first (nothing compiles while it is red), then `right-release dev|test` runs under a live door run,
|
|
1844
|
-
* the only way it runs at all. The executor is the same as every other stage: the broker where there is one (its own admission
|
|
1845
|
-
* checks again), plain cargo on an unmanaged host, never around a managed host's broker. The cutover commit deletes this function.
|
|
1846
|
-
*/
|
|
1847
|
-
async function runWorktreeCompile(o, args, root, git, io, to, appRoot = root) {
|
|
1848
|
-
const tag = "rightkit-gate:";
|
|
1849
|
-
const separator = args.indexOf("--");
|
|
1850
|
-
const passthrough = separator >= 0 ? args.slice(separator + 1) : [];
|
|
1851
|
-
// `--to dev --platform mac` (the documented form) hands the platform to right-release as `--platform mac`; a platform given after
|
|
1852
|
-
// `--` wins (Pulse, 2026-10-27: the flag before `--` was dropped and right-release refused "requires --platform").
|
|
1853
|
-
const gateArgs = separator >= 0 ? args.slice(0, separator) : args;
|
|
1854
|
-
const platformAt = gateArgs.indexOf("--platform");
|
|
1855
|
-
if (platformAt >= 0 && gateArgs[platformAt + 1] && !passthrough.includes("--platform")) passthrough.unshift("--platform", gateArgs[platformAt + 1]);
|
|
1856
|
-
const commit = text(git("rev-parse", "HEAD")).trim();
|
|
1857
|
-
const tree = text(git("rev-parse", "HEAD^{tree}")).trim();
|
|
1858
|
-
const common = text(git("rev-parse", "--git-common-dir")).trim();
|
|
1859
|
-
if (!/^[0-9a-f]{40}$/.test(commit) || !common) { o.err(`${tag} cannot read HEAD and the git directory`); return 2; }
|
|
1860
|
-
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); };
|
|
1861
|
-
const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
1862
|
-
if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
1863
|
-
o.out(`${tag} --to ${to} on the working tree of ${commit.slice(0, 12)}: check (class A) -> right-release ${to}; executor ${executor.kind}`);
|
|
1864
|
-
// Full class A on the working tree, the checks that execute repository code included, before right-release compiles anything.
|
|
1865
|
-
const checked = await (o.runCheck ?? (() => main(["check", "--worktree", "--full-a"], { ...io, cwd: root })))();
|
|
1866
|
-
if (checked !== 0) { o.err(`${tag} --to ${to} stopped at check: nothing compiled`); return 1; }
|
|
1867
|
-
const commonDir = path.resolve(root, common);
|
|
1868
|
-
const run = startRun({ commonDir, commit, tree, stage: to });
|
|
1869
|
-
try {
|
|
1870
|
-
const cli = fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url));
|
|
1871
|
-
// right-release reads its config from its cwd: the resolved app root (appRootOf), not the directory the command was typed in
|
|
1872
|
-
// (apps/foo/src is apps/foo; Astra, third pass).
|
|
1873
|
-
// The door's own RightKit runs every sub-step: a dev target's `right-release web-build|stage-sidecar` steps resolve `right-release`
|
|
1874
|
-
// on PATH, which found the app's node_modules copy (HeardRight on the Dell, 2026-10-27: the app's 0.6.8 guard refused what the
|
|
1875
|
-
// door's 0.6.9 admitted). An installed gate (node_modules/@rightkit/release/bin) has its package's bin shims two levels up.
|
|
1876
|
-
const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, to, ...passthrough], { cwd: appRoot, env: { ...o.env, ...run.env, ...doorPath(o) } });
|
|
1877
|
-
return r.status === 0 ? 0 : 1;
|
|
1878
|
-
} finally { endRun(run); }
|
|
2074
|
+
return o.env?.GITHUB_ACTIONS !== "true";
|
|
1879
2075
|
}
|
|
1880
2076
|
|
|
1881
2077
|
/** The app's root inside the snapshot: `<snapshot>/<appDir>`, the snapshot itself for an app at the repository root. */
|
|
@@ -1893,43 +2089,430 @@ function sourceSplitOf({ root, ds, appDir }) {
|
|
|
1893
2089
|
/**
|
|
1894
2090
|
* Why the snapshot route cannot start on this host, or null. The route is the executor's: `direct` hosts (Linux always, any host
|
|
1895
2091
|
* with no broker) take it as they are. A broker host compiles the snapshot only through the RustRight snapshot carrier, which the
|
|
1896
|
-
* gate reaches through `deps.snapshotBroker` (
|
|
1897
|
-
*
|
|
1898
|
-
* Neither ever falls back to compiling the checkout.
|
|
2092
|
+
* gate reaches through `deps.snapshotBroker` (production wires it on Mac and Dell: `productionEntryDeps`); a gate started without
|
|
2093
|
+
* it is refused (RK_SNAPSHOT_CARRIER_NOT_WIRED: tests only, never production), and a broker that cannot be reached is refused too
|
|
2094
|
+
* (RK_SNAPSHOT_BROKER_UNAVAILABLE). Neither ever falls back to compiling the checkout.
|
|
1899
2095
|
*/
|
|
1900
|
-
export function snapshotRouteRefusal(o
|
|
2096
|
+
export function snapshotRouteRefusal(o) {
|
|
1901
2097
|
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); };
|
|
1902
2098
|
const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
|
|
1903
2099
|
if (executor.kind === "refuse") return { code: "RK_SNAPSHOT_BROKER_UNAVAILABLE", message: executor.problem };
|
|
1904
|
-
|
|
1905
|
-
if (executor.kind === "broker" && rightRelease) return { code: "RK_SNAPSHOT_CARRIER_NOT_WIRED", message: `right-release dev and test reach Cargo on a ${o.platform} broker host only through the snapshot carrier's per-call wrapper (item 7 row 7.24), which is not wired` };
|
|
1906
|
-
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), which this gate does not carry yet` };
|
|
2100
|
+
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` };
|
|
1907
2101
|
return null;
|
|
1908
2102
|
}
|
|
1909
2103
|
|
|
2104
|
+
/** The refusal codes a route probe can answer with: the codes the stages themselves print (`BLOCKED: <code>: ...`). */
|
|
2105
|
+
export const ROUTE_REFUSAL_CODES = Object.freeze(["RK_TAURI_CONFIG_OVERRIDE", "RK_STAGE_HOST_MISMATCH", "RK_SNAPSHOT_BROKER_UNAVAILABLE", "RK_SNAPSHOT_CARRIER_NOT_WIRED", "RK_BROKER_CAPABILITY_MISSING", "RK_BROKER_UNAVAILABLE",
|
|
2106
|
+
// One signing identity per product (rightkit#1303): the Mac and the Dell refuse an unsigned, ad hoc or re-identified test or dev build.
|
|
2107
|
+
...SIGNING_REFUSAL_CODES]);
|
|
2108
|
+
/** The stages `--route` can answer for (`release` writes to the publisher and has no route of its own on a host). */
|
|
2109
|
+
export const ROUTE_PROBE_STAGES = Object.freeze(["check", "compile", "package", "qualify", "dev", "test"]);
|
|
2110
|
+
export const ROUTE_SCHEMA = "rightkit-gate.route/1";
|
|
2111
|
+
|
|
2112
|
+
/**
|
|
2113
|
+
* The capabilities a broker must advertise for the carried stages (rightkit#1301 B2). RustRight reports them in `rustright doctor --json`
|
|
2114
|
+
* as `.broker.capabilities` (the HANDSHAKE's `capabilities`: `journal-v1`, `snapshot-readonly-v1`, `snapshot-adapter-v1`, ...; constants
|
|
2115
|
+
* in rustright-protocol response.rs). The two required strings ship with RustRight's upcoming change: today's broker (78eda7e) reports no
|
|
2116
|
+
* `.broker.capabilities`, so this release refuses every stage that builds until that broker is installed. `snapshot-adapter-v1`
|
|
2117
|
+
* (rustright#190, rightkit#1301 B10) is the broker's acceptance of a snapshot-bound adapter job (pnpm, node, QA) inside a gate snapshot,
|
|
2118
|
+
* under which a Tauri CLI's compiles run nested: without it every pnpm step of a snapshot stage (an app's dev and package steps, an npm
|
|
2119
|
+
* unit's compile and package) is refused by the broker, so the gate refuses first. `stage-grant-v1` is no longer required: the gate mints
|
|
2120
|
+
* no stage grant (one model: every compile of a snapshot stage is a carried request or nested under one).
|
|
2121
|
+
*/
|
|
2122
|
+
export const REQUIRED_BROKER_CAPABILITIES = Object.freeze(["snapshot-readonly-v1", "snapshot-adapter-v1"]);
|
|
2123
|
+
|
|
2124
|
+
/**
|
|
2125
|
+
* The broker's capabilities, asked without a build through `rustright doctor --json`: `{ ok: true, capabilities }` or `{ ok: false, problem }`.
|
|
2126
|
+
* A missing client, a client that cannot run, a hung (20 s), unparsable or field-less answer is `ok: false`, which the route refuses (fail
|
|
2127
|
+
* closed). A doctor that reports an unhealthy host still prints its JSON: the field decides, not the exit status.
|
|
2128
|
+
*/
|
|
2129
|
+
export function queryBrokerCapabilities(o, { executor = null } = {}) {
|
|
2130
|
+
const client = brokerClientOf(o, executor);
|
|
2131
|
+
if (!client) return { ok: false, problem: "the RustRight client is not on PATH" };
|
|
2132
|
+
const r = spawnBrokerClient(o.spawn, client, ["doctor", "--json"], { env: brokerEnvOf(o, executor), encoding: "utf8", timeout: 20_000, windowsHide: true }, { platform: o.platform ?? process.platform, readFile: o.readFile });
|
|
2133
|
+
if (r?.error || r?.status === null || r?.status === undefined) return { ok: false, problem: `\`rustright doctor --json\` did not finish: ${why(r)}` };
|
|
2134
|
+
let doc;
|
|
2135
|
+
try { doc = JSON.parse(text(r)); } catch { return { ok: false, problem: `\`rustright doctor --json\` printed no JSON${r.status ? ` (exit ${r.status})` : ""}` }; }
|
|
2136
|
+
const caps = doc?.broker?.capabilities;
|
|
2137
|
+
if (!Array.isArray(caps) || !caps.every((c) => typeof c === "string")) return { ok: false, problem: "`rustright doctor --json` reports no `.broker.capabilities` (a broker that predates snapshot-readonly-v1 and snapshot-adapter-v1)" };
|
|
2138
|
+
// The list is the broker's HANDSHAKE.capabilities only when the client received that handshake: an unreachable broker (doctor reports an empty list for it) advertises nothing, which is a
|
|
2139
|
+
// different statement from a broker that advertises few capabilities, so it is a problem naming why, never a list. Each name is advertised once.
|
|
2140
|
+
if (doc.broker.reachable !== true) return { ok: false, problem: `the broker is not reachable, so its HANDSHAKE.capabilities were not received (\`rustright doctor --json\` .broker.reachable is ${JSON.stringify(doc.broker.reachable ?? null)}${typeof doc.broker.error === "string" && doc.broker.error ? `: ${doc.broker.error}` : ""})` };
|
|
2141
|
+
if (new Set(caps).size !== caps.length) return { ok: false, problem: "`rustright doctor --json` lists a capability twice: the broker's HANDSHAKE.capabilities are not read from an answer that repeats itself" };
|
|
2142
|
+
return { ok: true, capabilities: caps };
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* What a route decision needs of the broker: `null` when nothing is asked (not a broker executor, hosted CI, `--to check`, or a gate
|
|
2147
|
+
* whose broker is an injected fixture without a declared capability query), else the answer. The production carrier asks the real
|
|
2148
|
+
* client; a test's injected broker declares its own through `o.brokerCapabilities`.
|
|
2149
|
+
*/
|
|
2150
|
+
export function brokerCapabilityState(o, { to, executor }) {
|
|
2151
|
+
if (executor?.kind !== "broker" || to === "check" || o.env.GITHUB_ACTIONS === "true") return null;
|
|
2152
|
+
if (typeof o.brokerCapabilities === "function") return o.brokerCapabilities(o);
|
|
2153
|
+
return typeof o.snapshotBroker?.create === "function" ? queryBrokerCapabilities(o, { executor }) : null;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/**
|
|
2157
|
+
* GB-51 / GB-62: the capability state a route plans from, with the broker's HANDSHAKE received by the gate itself (broker-handshake.mjs) beside `rustright doctor --json`: both are asked, they must agree,
|
|
2158
|
+
* and a plan sees only names advertised by what answered. Not asked (null) exactly when `brokerCapabilityState` asks nothing; an injected fixture broker that declares its own capabilities
|
|
2159
|
+
* (`o.brokerCapabilities`) keeps them unless it also declares a handshake (`o.brokerHandshake`, a function returning a parsed handshake or `{ ok: false, problem }`).
|
|
2160
|
+
*/
|
|
2161
|
+
export async function brokerCapabilityStateWithHandshake(o, { to, executor }) {
|
|
2162
|
+
const doctor = brokerCapabilityState(o, { to, executor });
|
|
2163
|
+
if (doctor === null) return null;
|
|
2164
|
+
const injected = typeof o.brokerHandshake === "function";
|
|
2165
|
+
if (!injected && typeof o.brokerCapabilities === "function") return doctor;
|
|
2166
|
+
let handshake;
|
|
2167
|
+
try {
|
|
2168
|
+
handshake = injected ? await o.brokerHandshake(o) : await queryBrokerHandshake({ ...o, env: brokerEnvOf(o, executor) }, { client: brokerClientOf(o, executor) });
|
|
2169
|
+
} catch (error) { handshake = { ok: false, problem: `the broker's HANDSHAKE could not be read (${error?.message ?? error})` }; }
|
|
2170
|
+
return mergeCapabilityViews({ doctor, handshake });
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* The refusal of `--to release` on a host that is not the trusted publisher, or null: a release uploads sealed bytes and runs only in the release workflow's
|
|
2175
|
+
* protected publication job (GITHUB_ACTIONS with RIGHTKIT_PUBLISHER=github-environment); a mobile platform's store upload has its own tier-less route. Asked by a real
|
|
2176
|
+
* run before it takes ownership or syncs the snapshot (E2E L3-03); the probe has no release route, so this is not a probe code.
|
|
2177
|
+
*/
|
|
2178
|
+
export function releaseRouteRefusal(o, { stages, platform }) {
|
|
2179
|
+
if (!stages.includes("release") || MOBILE_PLATFORMS.has(platform)) return null;
|
|
2180
|
+
if (o.env.GITHUB_ACTIONS === "true" && o.env.RIGHTKIT_PUBLISHER === "github-environment") return null;
|
|
2181
|
+
return { code: "RK_RELEASE_PUBLISHER_ONLY", message: "--to release runs only in the trusted publisher (the release workflow's protected publication job). Run --to qualify on the native host, then dispatch the release workflow for the commit" };
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/**
|
|
2185
|
+
* Which route this host's door takes for `--to <stage>` and whether it refuses, WITHOUT running anything: the decisions of `runStages`
|
|
2186
|
+
* in the order it makes them. `runStages` takes its own pre-ownership route and refusal from this very function (rightkit#1297 L1), so
|
|
2187
|
+
* the probe and a real run cannot drift (the door-route parity test compares them over the carrier, executor, platform and Tauri
|
|
2188
|
+
* combinations):
|
|
2189
|
+
* 1. hosted CI (GITHUB_ACTIONS) builds the runner's checkout, which is the commit: no snapshot route;
|
|
2190
|
+
* 2. every other host takes the snapshot route (Linux direct; Mac and Dell through the carrier; there is no checkout route);
|
|
2191
|
+
* 3. the snapshot route refuses an inherited TAURI_CONFIG;
|
|
2192
|
+
* 4. then `snapshotRouteRefusal` (skipped for `--to check`, which compiles nothing): an unreachable broker, a gate without its carrier;
|
|
2193
|
+
* 5. then an executor that cannot start (a broker-managed host whose broker is unreachable), on any route.
|
|
2194
|
+
* `executor` is a resolved executor. Returns { route, refusal } with
|
|
2195
|
+
* refusal null or { code, stage, message }. Reads no checkout, takes no ownership, syncs no snapshot and spawns nothing itself.
|
|
2196
|
+
*/
|
|
2197
|
+
export function routePlan(o, { to, stages, devTest, executor, capabilities = null, appConfig = null, stagePlatform = null }) {
|
|
2198
|
+
const refuse = (code, stage, message) => ({ code, stage, message });
|
|
2199
|
+
const hosted = o.env.GITHUB_ACTIONS === "true";
|
|
2200
|
+
const snapshot = !hosted && snapshotRouteSelected(o);
|
|
2201
|
+
const route = hosted ? "hosted" : "snapshot";
|
|
2202
|
+
if (snapshot) {
|
|
2203
|
+
if (o.env.TAURI_CONFIG !== undefined) return { route, refusal: refuse("RK_TAURI_CONFIG_OVERRIDE", to, "TAURI_CONFIG is set in the environment; it would replace the commit's Tauri configuration (externalBin included) for every build below, so a commit-only build refuses it") };
|
|
2204
|
+
// A stage that packages or qualifies a platform's artifact runs on that platform's own host: Mac and Windows artifacts are constructed on their
|
|
2205
|
+
// native platform and never cross-built (GATE-CONTRACT s1 "Native build target"; PLATFORM-MATRIX: packages only on the matching native host).
|
|
2206
|
+
// Refused here, before ownership, a snapshot or any process, so `--route` and a real run say the same thing (E2E L3-03).
|
|
2207
|
+
const hostNeeded = STAGE_NATIVE_HOST[stagePlatform];
|
|
2208
|
+
if (stagePlatform && (stages.includes("package") || stages.includes("qualify")) && hostNeeded && o.platform !== hostNeeded) {
|
|
2209
|
+
return { route, refusal: refuse("RK_STAGE_HOST_MISMATCH", to, `--to ${to} --platform ${stagePlatform} builds the ${stagePlatform} artifact, which is constructed only on a ${hostNeeded === "darwin" ? "Mac" : "Windows"} host (this host is ${o.platform}); a Mac or Windows artifact is never cross-built from another platform, so nothing was started`) };
|
|
2210
|
+
}
|
|
2211
|
+
const refused = to === "check" ? null : snapshotRouteRefusal(o);
|
|
2212
|
+
if (refused) return { route, refusal: refuse(refused.code, to, refused.message) };
|
|
2213
|
+
// The broker must accept what the carried stages ask of it (rightkit#1301 B2): a missing or failing capability query refuses.
|
|
2214
|
+
if (capabilities && executor?.kind === "broker" && to !== "check") {
|
|
2215
|
+
const missing = capabilities.ok ? REQUIRED_BROKER_CAPABILITIES.filter((c) => !capabilities.capabilities.includes(c)) : REQUIRED_BROKER_CAPABILITIES;
|
|
2216
|
+
if (!capabilities.ok || missing.length) return { route, refusal: refuse("RK_BROKER_CAPABILITY_MISSING", to, `the broker ${capabilities.ok ? `does not advertise ${missing.join(", ")}` : `cannot be asked for its capabilities (${capabilities.problem})`} (\`rustright doctor --json\` \`.broker.capabilities\`); this release builds only against a broker that accepts snapshot-bound read-only Cargo and snapshot-bound adapter jobs (rightkit#1301, rustright#188, rustright#190)`) };
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
// GB-51: a declared native build tool with no carried adapter (xcodegen, xcodebuild, Gradle, CMake) refuses here, at planning, naming the missing
|
|
2220
|
+
// capability, instead of after the earlier stages built (the door's launch-time refusal remains the backstop).
|
|
2221
|
+
if (snapshot && executor?.kind === "broker" && appConfig?.config && to !== "check") {
|
|
2222
|
+
const refusalText = nativeToolRefusal(nativeToolUses({ config: appConfig.config, to, devPlatform: appConfig.nativePlatform ?? appConfig.devPlatform ?? null, platform: appConfig.nativePlatform ?? null }), { capabilities, readFile: appConfig.readFile ?? null, listFiles: appConfig.listFiles ?? null });
|
|
2223
|
+
if (refusalText) return { route, refusal: refuse("RK_BROKER_CAPABILITY_MISSING", to, refusalText) };
|
|
2224
|
+
}
|
|
2225
|
+
// One signing identity per product, every build (rightkit#1303): on the Mac and the Dell a test or dev build that would run unsigned, ad hoc or under
|
|
2226
|
+
// another identifier is refused here, from the commit's own config, so `--route` and a real run decide alike. Linux and hosted CI are exempt.
|
|
2227
|
+
if (snapshot && appConfig?.config && (devTest || to === "test" || to === "dev")) {
|
|
2228
|
+
const [first] = suiteSigningRefusals({ config: appConfig.config, platform: o.platform, env: o.env, to, devPlatform: appConfig.devPlatform ?? null, readFile: appConfig.readFile, listFiles: appConfig.listFiles ?? null });
|
|
2229
|
+
if (first) return { route, refusal: refuse(first.code, to, first.message) };
|
|
2230
|
+
}
|
|
2231
|
+
if (executor?.kind === "refuse") return { route, refusal: refuse("RK_BROKER_UNAVAILABLE", to, executor.problem) };
|
|
2232
|
+
return { route, refusal: null };
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/**
|
|
2236
|
+
* The app config of the selected commit for the route's signing refusals, read from the object database (never the checkout): `{ config, readFile }`,
|
|
2237
|
+
* `readFile(rel)` being an app-relative file of the commit (text or null). Null when the commit has no readable JSON config (class A judges that).
|
|
2238
|
+
* The probe covers JSON configs only: a legacy right-release.config.mjs is not evaluated here (that would run app code in the probe), so `--route` is
|
|
2239
|
+
* silent about it. The same policy (suiteSigningRefusals) is enforced at run time on whatever config loaded: test-command.mjs before any suite runs and
|
|
2240
|
+
* development.mjs before a dev target builds, so a .mjs config's unsigned product build is refused there, on the Mac and the Dell.
|
|
2241
|
+
*/
|
|
2242
|
+
export function routeAppConfig({ git, commit, appDir = ".", platform = null }) {
|
|
2243
|
+
if (!commit) return null;
|
|
2244
|
+
const show = (rel) => { const r = git("show", `${commit}:${path.posix.normalize(appDir === "." ? rel : `${appDir}/${rel}`)}`); return failed(r) ? null : text(r); };
|
|
2245
|
+
const raw = show("right-release.config.json");
|
|
2246
|
+
if (raw === null) return null;
|
|
2247
|
+
let config;
|
|
2248
|
+
try { config = JSON.parse(raw); } catch { return null; }
|
|
2249
|
+
const list = (c, rel) => { const r = git("ls-tree", "-r", "--name-only", c, "--", rel); return failed(r) ? null : text(r).split(/\r?\n/).filter(Boolean); };
|
|
2250
|
+
return { config, readFile: show, listFiles: gitListRustFiles(list, commit, appDir), devPlatform: platform === "mac" || platform === "win" ? platform : null, nativePlatform: platform ?? null };
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/** The `--platform` a dev or test run was given: before `--`, or in the arguments after it (a platform after `--` wins, as runDevTest hands it on). */
|
|
2254
|
+
export function devTestPlatform(args) {
|
|
2255
|
+
const at = args.indexOf("--");
|
|
2256
|
+
const find = (list) => { const i = list.lastIndexOf("--platform"); return i < 0 ? null : list[i + 1] ?? null; };
|
|
2257
|
+
return (at >= 0 ? find(args.slice(at + 1)) : null) ?? find(at >= 0 ? args.slice(0, at) : args);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/**
|
|
2261
|
+
* `rightkit-gate --to <stage> --route [--platform p] [--commit rev]`: prints one JSON line (`rightkit-gate.route/1`) naming the route
|
|
2262
|
+
* the stage would take and the refusal it would meet, and runs nothing: no ownership, no snapshot sync, no check, no run record, no
|
|
2263
|
+
* receipt, no right-release. Exit 0 when it answered (a refusal is an answer), 2 when it could not compute one.
|
|
2264
|
+
*/
|
|
2265
|
+
async function runRouteProbe(o, args, root, git, { to, platform, stages, devTest, appDir }) {
|
|
2266
|
+
const tag = "rightkit-gate: --route:";
|
|
2267
|
+
const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
|
|
2268
|
+
const snap = await import("../snapshot.mjs");
|
|
2269
|
+
let selected;
|
|
2270
|
+
try { selected = snap.resolveCommit(root, option("--commit") ?? "HEAD"); }
|
|
2271
|
+
catch (e) { o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
|
|
2272
|
+
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 ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
|
|
2274
|
+
let plan;
|
|
2275
|
+
const capabilities = await brokerCapabilityStateWithHandshake(o, { to, executor });
|
|
2276
|
+
try { plan = routePlan(o, { to, stages, devTest, executor, stagePlatform: platform, capabilities, appConfig: to !== "check" ? routeAppConfig({ git, commit: selected.commit, appDir, platform: devTestPlatform(args) }) : null }); }
|
|
2277
|
+
catch (e) { o.err(`${tag} ${e.message}`); return 2; }
|
|
2278
|
+
// The declared Linux broker's snapshot root: the probe reports the refusal a real run would meet (read-only: one status probe of the broker).
|
|
2279
|
+
if (!plan.refusal && plan.route === "snapshot") {
|
|
2280
|
+
let at = null; try { at = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env }); } catch { at = null; }
|
|
2281
|
+
const mismatch = at ? await declaredSnapshotRootRefusal(o, executor, at) : null;
|
|
2282
|
+
if (mismatch) plan = { ...plan, refusal: { code: mismatch.code, stage: to, message: mismatch.message } };
|
|
2283
|
+
}
|
|
2284
|
+
// Read-only: GIT_OPTIONAL_LOCKS=0, so the probe's `git status` never refreshes the index or contends with a writer (rightkit#1297 L4).
|
|
2285
|
+
const dirty = o.spawn("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: root, env: { ...o.env, GIT_OPTIONAL_LOCKS: "0" } });
|
|
2286
|
+
const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
2287
|
+
o.out(JSON.stringify({
|
|
2288
|
+
schema: ROUTE_SCHEMA, gate: version, to, platform: platform ?? null, host: o.platform, app: appDir, commit: selected.commit,
|
|
2289
|
+
executor: executor.kind, carrier: o.snapshotBroker ? (o.snapshotBroker.kind ?? "injected") : null, route: plan.route,
|
|
2290
|
+
brokerCapabilities: capabilities?.ok ? capabilities.capabilities : null, stages: devTest ? ["check", to] : stages, refusal: plan.refusal, checkoutClean: failed(dirty) ? null : !text(dirty).trim(),
|
|
2291
|
+
}));
|
|
2292
|
+
return 0;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// ---- the carrier of a snapshot stage's right-release children (carried-cargo.mjs; rightkit#1301) --------------------------------------
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* The environment of a carried stage's child: the base, the stage's carrier and the caller's extras, without an output directory.
|
|
2299
|
+
* The broker pins every Cargo output, so the Tauri CLI and every other child must not look for bundles in a directory the environment
|
|
2300
|
+
* names (rightkit#1301 F5): CARGO_TARGET_DIR and CARGO_BUILD_TARGET_DIR are dropped, in any case.
|
|
2301
|
+
*/
|
|
2302
|
+
export function carriedEnv(carrier, base, extra = {}) {
|
|
2303
|
+
if (!carrier.carried) return { ...base, ...carrier.env, ...extra };
|
|
2304
|
+
const strip = (env) => {
|
|
2305
|
+
const out = { ...env };
|
|
2306
|
+
for (const key of Object.keys(out)) {
|
|
2307
|
+
if (/^(CARGO_TARGET_DIR|CARGO_BUILD_TARGET_DIR)$/i.test(key)) delete out[key];
|
|
2308
|
+
}
|
|
2309
|
+
return out;
|
|
2310
|
+
};
|
|
2311
|
+
return { ...strip(base), ...carrier.env, ...strip(extra) };
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// ---- the declared Linux broker's snapshot root --------------------------------------------------------------------------------------------------
|
|
2315
|
+
// The broker admits a snapshot only under `<its data root>/snapshots`. On Linux the gate's snapshot root is `~/.config/rightkit/gate.json` `snapshotRoot`, else
|
|
2316
|
+
// `~/.local/state/rightkit/snapshots` (snapshot.mjs snapshotRoot); a host whose broker keeps its data elsewhere would sync thousands of files, take build ownership and submit,
|
|
2317
|
+
// only to be refused at admission with the ownership left unknown. So the declared route compares the two BEFORE ownership or a sync, and refuses typed.
|
|
2318
|
+
export const LINUX_SNAPSHOT_ROOT_MISMATCH = "RK_LINUX_BROKER_SNAPSHOT_ROOT_MISMATCH";
|
|
2319
|
+
const ANSWERING_ROOTS_TIMEOUT_MS = 10_000;
|
|
2320
|
+
|
|
2321
|
+
/**
|
|
2322
|
+
* What the answering broker says its roots are: the health probe's `STATUS` carries `roots` (`rustright.answering-roots.v1`: dataRoot, controlRoot, ...), after the HANDSHAKE the first event
|
|
2323
|
+
* is. Reads lines until a STATUS with roots (or any other end) and resolves `{ ok: true, dataRoot }` or `{ ok: false, problem }`; never rejects. `connect` is the transport (tests, `net.connect`).
|
|
2324
|
+
*/
|
|
2325
|
+
export function readAnsweringRoots({ endpoint, identity, connect = (target) => net.connect({ path: target }), timeoutMs = ANSWERING_ROOTS_TIMEOUT_MS }) {
|
|
2326
|
+
return new Promise((resolve) => {
|
|
2327
|
+
let settled = false; let conn = null; let timer = null; let buffer = "";
|
|
2328
|
+
const finish = (result) => { if (settled) return; settled = true; clearTimeout(timer); try { conn?.destroy(); } catch { /* gone */ } resolve(result); };
|
|
2329
|
+
const problem = (why) => finish({ ok: false, problem: why });
|
|
2330
|
+
try { conn = connect(endpoint); } catch (e) { problem(`cannot connect to the broker at ${endpoint} (${e?.code ?? e?.message ?? e})`); return; }
|
|
2331
|
+
timer = setTimeout(() => problem(`the broker at ${endpoint} reported no roots within ${timeoutMs} ms`), timeoutMs);
|
|
2332
|
+
conn.on("connect", () => { try { conn.write(`${JSON.stringify({ type: "STATUS", protocol: BUILD_CONTROL_PROTOCOL, runtime: identity, id: HANDSHAKE_PROBE_ID })}\n`); } catch (e) { problem(`cannot send the probe to the broker at ${endpoint} (${e?.message ?? e})`); } });
|
|
2333
|
+
conn.on("data", (chunk) => {
|
|
2334
|
+
buffer += String(chunk);
|
|
2335
|
+
if (buffer.length > (1 << 21)) { problem("the broker's reply is too large"); return; }
|
|
2336
|
+
for (let at = buffer.indexOf("\n"); at >= 0; at = buffer.indexOf("\n")) {
|
|
2337
|
+
const line = buffer.slice(0, at); buffer = buffer.slice(at + 1);
|
|
2338
|
+
let event; try { event = JSON.parse(line); } catch { problem("the broker's reply is not JSON"); return; }
|
|
2339
|
+
if (event?.type === "STATUS" && event.roots && typeof event.roots === "object") {
|
|
2340
|
+
const root = event.roots.dataRoot;
|
|
2341
|
+
if (event.roots.schema !== "rustright.answering-roots.v1" || typeof root !== "string" || !path.isAbsolute(root)) { problem("the broker's STATUS roots are not rustright.answering-roots.v1 with an absolute dataRoot"); return; }
|
|
2342
|
+
finish({ ok: true, dataRoot: root }); return;
|
|
2343
|
+
}
|
|
2344
|
+
if (event?.type !== "HANDSHAKE" && event?.type !== "STATUS") { problem(`the broker answered ${event?.type ?? "something"} to the roots probe`); return; }
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
const closed = () => problem(`the broker at ${endpoint} closed without reporting its roots (a broker that predates the answering roots)`);
|
|
2348
|
+
conn.on("error", (e) => problem(`the broker connection at ${endpoint} failed (${e?.code ?? e?.message ?? e})`));
|
|
2349
|
+
conn.on("close", closed); conn.on("end", closed);
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/** The declared Linux broker's data root: what it reports on the wire (the validated client, its runtime manifest, the declared endpoint), `{ ok, dataRoot }` or `{ ok: false, problem }`. */
|
|
2354
|
+
export async function queryBrokerDataRoot(o, executor) {
|
|
2355
|
+
try {
|
|
2356
|
+
const exe = resolveBrokerClient(executor.client, { platform: o.platform, readFile: o.readFile }).file;
|
|
2357
|
+
let real = exe; try { real = fs.realpathSync.native(exe); } catch { real = exe; }
|
|
2358
|
+
const identity = parseRuntimeIdentity(fs.readFileSync(path.join(path.dirname(real), RUNTIME_MANIFEST_FILE), "utf8"));
|
|
2359
|
+
if (!identity.ok) return { ok: false, problem: identity.problem };
|
|
2360
|
+
return await readAnsweringRoots({ endpoint: executor.socket, identity: identity.identity, ...(o.connectBroker ? { connect: o.connectBroker } : {}) });
|
|
2361
|
+
} catch (e) { return { ok: false, problem: e.message }; }
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/**
|
|
2365
|
+
* null, or `{ code, message }` when the declared Linux broker would refuse this gate's snapshots. Only the declared route is asked (a Linux host with no declared broker stays direct and
|
|
2366
|
+
* has no broker root). The broker's own report decides; when it reports none (a broker that predates it, or an unreadable answer) the DECLARED data root (RUSTRIGHT_DATA_ROOT, else
|
|
2367
|
+
* RIGHTKIT_DATA_ROOT, MANAGED_ROOT or EXTERNAL_ROOT) is the comparison, and the message says it is only a declaration; with neither nothing is known and nothing is refused here.
|
|
2368
|
+
* `snapshotsAt` is the gate's snapshot root.
|
|
2369
|
+
*/
|
|
2370
|
+
export async function declaredSnapshotRootRefusal(o, executor, snapshotsAt) {
|
|
2371
|
+
if (executor?.kind !== "broker" || executor.route !== LINUX_BROKER_ROUTE) return null;
|
|
2372
|
+
const answered = await (o.queryBrokerDataRoot ?? queryBrokerDataRoot)(o, executor);
|
|
2373
|
+
let dataRoot; let basis;
|
|
2374
|
+
if (answered?.ok) { dataRoot = answered.dataRoot; basis = "the broker reports its data root"; }
|
|
2375
|
+
else {
|
|
2376
|
+
dataRoot = envNamed(o.env, "DATA_ROOT") || envNamed(o.env, "MANAGED_ROOT") || envNamed(o.env, "EXTERNAL_ROOT");
|
|
2377
|
+
basis = `the broker did not report its roots (${answered?.problem ?? "no answer"}), so the DECLARED data root from the environment is the comparison (a declaration, not the broker's word)`;
|
|
2378
|
+
}
|
|
2379
|
+
if (!dataRoot) return null;
|
|
2380
|
+
const real = (p) => { try { return fs.realpathSync.native(p); } catch { return path.resolve(p); } };
|
|
2381
|
+
const expected = path.join(dataRoot, "snapshots");
|
|
2382
|
+
if (real(expected) === real(snapshotsAt)) return null;
|
|
2383
|
+
return { code: LINUX_SNAPSHOT_ROOT_MISMATCH, message: `the gate's snapshot root is ${snapshotsAt}, but the declared Linux broker admits snapshots only under ${expected} (${basis}); set "snapshotRoot": ${JSON.stringify(expected)} in ~/.config/rightkit/gate.json (the gate host configuration), then run again. Nothing was synced and no build ownership was taken` };
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/** The RustRight client this executor drives: the declared Linux route's validated client, else the absolute `rustright` on PATH (Mac, Dell). */
|
|
2387
|
+
const brokerClientOf = (o, executor) => (executor?.route === LINUX_BROKER_ROUTE && executor.kind === "broker" ? executor.client : whichIn(o)("rustright").find((c) => path.isAbsolute(c)));
|
|
2388
|
+
/** The environment the client runs in: the gate's, plus the endpoint the executor declared (the declared Linux route's socket). */
|
|
2389
|
+
const brokerEnvOf = (o, executor) => (executor?.route === LINUX_BROKER_ROUTE && executor.env ? { ...o.env, ...executor.env } : o.env);
|
|
2390
|
+
const whichIn = (o) => (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); };
|
|
2391
|
+
|
|
2392
|
+
/**
|
|
2393
|
+
* The gate's snapshot broker for this door run: `o.snapshotBroker` is the production carrier statement (`{ kind, create }`, which
|
|
2394
|
+
* builds the broker once: the host's enrolled pins, the declared inputs read from the snapshot, the build ownership and the RustRight
|
|
2395
|
+
* client) or, in a test, a ready broker object. Built once per door run.
|
|
2396
|
+
*/
|
|
2397
|
+
async function carrierBroker({ o, root, ds, executor = null, which = whichIn(o) }) {
|
|
2398
|
+
if (ds.carrierBroker) return ds.carrierBroker;
|
|
2399
|
+
const carrier = o.snapshotBroker;
|
|
2400
|
+
let sb = carrier;
|
|
2401
|
+
if (typeof carrier?.create === "function") {
|
|
2402
|
+
const client = executor?.route === LINUX_BROKER_ROUTE && executor.kind === "broker" ? executor.client : which("rustright").find((c) => path.isAbsolute(c));
|
|
2403
|
+
const { brokerLeaseState } = await import("../build-ownership.mjs");
|
|
2404
|
+
sb = carrier.create({ lease: brokerLeaseState, env: brokerEnvOf(o, executor), platform: o.platform, checkout: root, commit: { sha: ds.commit, tree: ds.tree }, snapshot: { path: ds.path, expectedTree: ds.tree }, ownership: ds.own, client, gateFile: callingGatePath(), spawn: o.spawn });
|
|
2405
|
+
}
|
|
2406
|
+
ds.carrierBroker = sb;
|
|
2407
|
+
return sb;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
/** The commit's Cargo workspaces in the snapshot, each with its counterpart in the primary checkout: `{ [cargoDir]: checkout path }`, from the files alone. */
|
|
2411
|
+
async function snapshotCargoWorkspaces({ git, ds, root }) {
|
|
2412
|
+
const listed = git("ls-tree", "-r", "--name-only", "-z", ds.commit);
|
|
2413
|
+
if (failed(listed)) throw new Error(`cannot list the files of ${ds.commit.slice(0, 12)}: ${why(listed)}`);
|
|
2414
|
+
const manifests = text(listed).split("\0").filter((f) => f === "Cargo.toml" || f.endsWith("/Cargo.toml"));
|
|
2415
|
+
const { locateWorkspaceByWalk } = await import("../cargo-target-shim.mjs");
|
|
2416
|
+
let checkout = root; try { checkout = fs.realpathSync.native(root); } catch { /* as given */ }
|
|
2417
|
+
const dirs = {};
|
|
2418
|
+
for (const m of manifests) {
|
|
2419
|
+
const file = path.join(ds.path, ...m.split("/"));
|
|
2420
|
+
let rel;
|
|
2421
|
+
try { rel = posix(path.relative(ds.path, path.dirname(locateWorkspaceByWalk(file, path.dirname(file))))) || "."; } catch { rel = posix(path.dirname(m)); }
|
|
2422
|
+
if (rel === ".." || rel.startsWith("../")) rel = posix(path.dirname(m)) || ".";
|
|
2423
|
+
dirs[rel] = path.join(checkout, ...(rel === "." ? [] : rel.split("/")));
|
|
2424
|
+
}
|
|
2425
|
+
return dirs;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/**
|
|
2429
|
+
* The carrier of one snapshot stage's right-release child on a broker host: the stage's workspaces, the broker section, one map and
|
|
2430
|
+
* ledger. Returns `{ env, refusal, finish({ stage }), cleanup }`: `env` is added to the
|
|
2431
|
+
* child's environment; `finish` runs after the child ended, fails on any call that was refused, unverified or unfinished and on any
|
|
2432
|
+
* receipt the gate cannot verify again itself, and removes the carrier. A direct executor, or no snapshot, has nothing to carry.
|
|
2433
|
+
*/
|
|
2434
|
+
async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "rightkit-gate:" }) {
|
|
2435
|
+
const none = { env: {}, carried: false, refusal: null, async finish() { return true; }, cleanup() {} };
|
|
2436
|
+
if (!ds || executor?.kind !== "broker") return none;
|
|
2437
|
+
let base = null;
|
|
2438
|
+
try {
|
|
2439
|
+
if (!git) throw new Error("the gate's git reader is missing");
|
|
2440
|
+
const carried = await import("../carried-cargo.mjs");
|
|
2441
|
+
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
2442
|
+
const sb = await carrierBroker({ o, root, ds, executor });
|
|
2443
|
+
// Installing the carrier map sends nothing, so it records nothing: the child's own uncarried probes (`rustc -vV`, `--version`) never
|
|
2444
|
+
// reach the broker under a snapshot, and each carried request records its sub-handle just before it is sent (snapshot-broker.mjs).
|
|
2445
|
+
const checkoutDirs = await snapshotCargoWorkspaces({ git, ds, root });
|
|
2446
|
+
base = fs.mkdtempSync(path.join(os.tmpdir(), "rk-carrier-"));
|
|
2447
|
+
// The carrier is the map and the ledger on every platform: no PATH wrapper and no stage grant. The child's pnpm, node and QA steps are carried
|
|
2448
|
+
// adapter jobs and its Cargo calls carried requests; whatever the Tauri CLI compiles runs nested under the adapter job that started it, and the
|
|
2449
|
+
// broker's signed list of those children is what the gate verifies and reads the bundle root from (carried-cargo.mjs).
|
|
2450
|
+
const installed = (o.installCarrier ?? carried.installCarrier)({ base, snapshotRoot: ds.path, checkoutDirs, section: (o.brokerSection ?? brokerSection)(sb), known: Object.fromEntries(ds.carriedOutputs ?? []), platform: o.platform, env: o.env });
|
|
2451
|
+
o.out(`${tag} [${stage}] carrier (${installed.mode}): ${Object.keys(checkoutDirs).length} workspace(s); each right-release cargo call and each pnpm, node or QA step is one carried request with its own signed receipt, and a compile a step's child makes is nested under it`);
|
|
2452
|
+
let finished = false;
|
|
2453
|
+
const cleanup = () => { try { installed.cleanup?.(); } catch { /* removed below */ } fs.rmSync(base, { recursive: true, force: true }); };
|
|
2454
|
+
return {
|
|
2455
|
+
env: installed.env, carried: true, refusal: null, cleanup,
|
|
2456
|
+
async finish({ stage: at = stage } = {}) {
|
|
2457
|
+
if (finished) return true;
|
|
2458
|
+
finished = true;
|
|
2459
|
+
let problems;
|
|
2460
|
+
try {
|
|
2461
|
+
problems = await installed.problems(sb);
|
|
2462
|
+
// The output directories the stage's verified receipts name: the package stage's sealed manifest must record only these.
|
|
2463
|
+
for (const dir of installed.outputDirs?.() ?? []) ((ds.carriedByStage ??= {})[at] ??= new Set()).add(dir);
|
|
2464
|
+
// And by workspace, for the next stage's children: a receipt already names the output directory, so none asks Cargo for it.
|
|
2465
|
+
for (const [root, dir] of installed.outputsByWorkspace?.() ?? []) (ds.carriedOutputs ??= new Map()).set(root, dir);
|
|
2466
|
+
} catch (error) { problems = [`the cargo ledger cannot be read (${error.message})`]; } finally { cleanup(); }
|
|
2467
|
+
for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${at}] a broker call the right-release child made did not complete: ${typeof p === "string" ? p : JSON.stringify(p)}`);
|
|
2468
|
+
return problems.length === 0;
|
|
2469
|
+
},
|
|
2470
|
+
};
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
if (base) fs.rmSync(base, { recursive: true, force: true });
|
|
2473
|
+
return { ...none, refusal: `${error.code ?? "RK_CARGO_CARRIER"}: the stage's cargo carrier cannot be installed (${error.message})` };
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
1910
2477
|
/**
|
|
1911
2478
|
* The checkout's Cargo output directory for the snapshot of `commit`: `{ dir }` (null when the commit has no Cargo manifest) or
|
|
1912
|
-
* `{ refusal }`. Each
|
|
1913
|
-
* `target_directory` (which honours CARGO_TARGET_DIR and
|
|
1914
|
-
*
|
|
2479
|
+
* `{ refusal }`. Each Cargo.toml the COMMIT tracks (git ls-tree of the selected commit, never the checkout's files) is asked, in the
|
|
2480
|
+
* commit's SNAPSHOT with the stage's environment, for its `target_directory` (which honours CARGO_TARGET_DIR and the committed
|
|
2481
|
+
* `.cargo/config.toml` build.target-dir) and its workspace root. A path the snapshot holds is mapped lexically to the same
|
|
2482
|
+
* snapshot-relative path under the checkout (the output directory is the checkout's, GATE-CONTRACT "Output directory & cache"), so a
|
|
2483
|
+
* directory the newer checkout deleted or moved, or a dirty or untracked manifest there, changes nothing: the checkout is not read. A
|
|
2484
|
+
* path outside the snapshot (an absolute CARGO_TARGET_DIR) stands as reported. Optional manifests restrict discovery to a resolved
|
|
2485
|
+
* committed closure. Several workspaces retain independent output directories for the runtime wrapper; they never share one target.
|
|
1915
2486
|
*/
|
|
1916
|
-
function cargoOutputPin({ o, git, root, commit, executor }) {
|
|
2487
|
+
export function cargoOutputPin({ o, git, root, commit, executor, snapshot, manifests: selectedManifests }) {
|
|
1917
2488
|
const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
1918
2489
|
if (failed(listed)) return { refusal: `cannot list the files of ${commit.slice(0, 12)}: ${why(listed)}` };
|
|
1919
|
-
const
|
|
2490
|
+
const trackedManifests = text(listed).split("\0").filter((f) => f === "Cargo.toml" || f.endsWith("/Cargo.toml"));
|
|
2491
|
+
if (selectedManifests && selectedManifests.some((m) => !trackedManifests.includes(m))) return { refusal: "the scoped Cargo output map names a manifest outside the selected commit" };
|
|
2492
|
+
const manifests = selectedManifests ?? trackedManifests;
|
|
2493
|
+
if (manifests.length && (typeof snapshot !== "string" || !path.isAbsolute(snapshot))) return { refusal: "the output directory is resolved in the commit's snapshot and none was given" };
|
|
1920
2494
|
const cargo = executor.cargo ?? ["cargo"];
|
|
2495
|
+
// The snapshot as the tool reports it may be a realpath of the spelling the gate holds (a symlinked temp root): both spellings map.
|
|
2496
|
+
const snapshots = [path.resolve(snapshot ?? "/")];
|
|
2497
|
+
try { snapshots.push(fs.realpathSync.native(snapshot)); } catch { /* an absent snapshot fails at the metadata call */ }
|
|
2498
|
+
const toCheckout = (p) => {
|
|
2499
|
+
for (const base of snapshots) { const rel = path.relative(base, p); if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) return path.join(root, rel); }
|
|
2500
|
+
return p;
|
|
2501
|
+
};
|
|
1921
2502
|
const workspaces = new Map();
|
|
1922
2503
|
for (const m of manifests) {
|
|
1923
|
-
const file = path.join(
|
|
1924
|
-
if (!o.exists(file)) return { refusal: `the checkout has no ${m}, so its Cargo output directory cannot be resolved` };
|
|
2504
|
+
const file = path.join(snapshot, ...m.split("/"));
|
|
1925
2505
|
const r = o.spawn(cargo[0], [...cargo.slice(1), "metadata", "--offline", "--format-version", "1", "--no-deps", "--manifest-path", file], { cwd: path.dirname(file), env: o.env });
|
|
1926
2506
|
let doc = null;
|
|
1927
2507
|
try { doc = failed(r) ? null : JSON.parse(text(r)); } catch { doc = null; }
|
|
1928
|
-
if (!doc || typeof doc.target_directory !== "string" || !path.isAbsolute(doc.target_directory) || typeof doc.workspace_root !== "string") return { refusal: `cargo metadata in the
|
|
1929
|
-
workspaces.set(path.resolve(doc.workspace_root), path.resolve(doc.target_directory));
|
|
2508
|
+
if (!doc || typeof doc.target_directory !== "string" || !path.isAbsolute(doc.target_directory) || typeof doc.workspace_root !== "string") return { refusal: `cargo metadata in the snapshot reported no absolute target_directory for ${m}${failed(r) ? ` (${why(r)})` : ""}` };
|
|
2509
|
+
workspaces.set(path.resolve(toCheckout(path.resolve(doc.workspace_root))), path.resolve(toCheckout(path.resolve(doc.target_directory))));
|
|
1930
2510
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
2511
|
+
// GB-17: several Cargo workspaces are independent components with their own target caches (GATE-CONTRACT, mixed workspaces never share a target): the per-workspace mapping
|
|
2512
|
+
// (workspace root of the checkout -> its own target directory, all resolved from the commit's snapshot) is returned, never one shared directory and no refusal. The caller binds
|
|
2513
|
+
// each of right-release's and its children's Cargo calls to its own workspace through the per-call wrapper (bindUnitCargo).
|
|
2514
|
+
if (workspaces.size > 1) return { dir: null, workspaces, dirs: [...workspaces.values()] };
|
|
2515
|
+
return { dir: workspaces.size ? [...workspaces.values()][0] : null, workspaces, dirs: [...workspaces.values()] };
|
|
1933
2516
|
}
|
|
1934
2517
|
|
|
1935
2518
|
/**
|
|
@@ -1963,10 +2546,9 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
|
|
|
1963
2546
|
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); };
|
|
1964
2547
|
const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
|
|
1965
2548
|
if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
1966
|
-
if (ds && executor.kind === "broker") { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: right-release ${to} reaches Cargo on a broker host only through the snapshot carrier (item 7 row 7.24), which is not wired; nothing ran and the checkout is never built`); return 1; }
|
|
1967
2549
|
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}` : ""}`);
|
|
1968
2550
|
// Full class A of the commit before right-release compiles anything: of the snapshot, the checks that execute repository code included.
|
|
1969
|
-
const checked = await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : [])], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
|
|
2551
|
+
const checked = await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...(appDir === "." ? [] : ["--scope", appDir])], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
|
|
1970
2552
|
if (checked !== 0) { o.err(`${tag} --to ${to} stopped at check: nothing compiled`); return 1; }
|
|
1971
2553
|
// The snapshot is exactly tree + dependencies + the ledger before right-release starts; what it wrote may remain only as ledger
|
|
1972
2554
|
// entries (a generated `gen/schemas`, a dist), recorded by this gate's own walk (stage-ledger.mjs, decision 2 Q1).
|
|
@@ -1974,11 +2556,30 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
|
|
|
1974
2556
|
// Cargo would resolve `target/` (or a relative build.target-dir) against the snapshot: its output directory is resolved in the checkout
|
|
1975
2557
|
// and passed absolute, so the warm outputs stay the checkout's and the snapshot holds no target/ (docs/GATE-CONTRACT.md "Output
|
|
1976
2558
|
// directory & cache"). One Cargo workspace is pinned as a whole; several would share one directory (refused, naming them).
|
|
1977
|
-
|
|
2559
|
+
// A broker host's output is the broker's (the receipts' `outputDir`); only a direct host pins the checkout's target directory.
|
|
2560
|
+
const pin = ds && executor.kind !== "broker" ? await cargoOutputPinForScope({ o, git, root, commit, executor, snapshot: ds.path, ...(appDir === "." ? {} : { scopeDirs: [appDir] }) }) : { dir: null };
|
|
1978
2561
|
if (pin.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${pin.refusal}; nothing compiled`); return 1; }
|
|
1979
2562
|
if (pin.dir) o.out(`${tag} output directory for the Cargo workspace: ${pin.dir} (the checkout's)`);
|
|
2563
|
+
// GB-17: several workspaces, each with its own target (never one shared directory): the per-call wrapper binds every Cargo call to its workspace's target, from the snapshot's mapping.
|
|
2564
|
+
let pinMulti = null;
|
|
2565
|
+
if (pin.dirs?.length > 1) {
|
|
2566
|
+
pinMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
|
|
2567
|
+
if (pinMulti.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${pinMulti.refusal}; nothing compiled`); return 1; }
|
|
2568
|
+
if (pinMulti.note) o.out(`${tag} ${pinMulti.note}`);
|
|
2569
|
+
}
|
|
1980
2570
|
const commonDir = path.resolve(root, common);
|
|
1981
2571
|
const source = ds ? sourceSplitOf({ root, ds, appDir }) : null;
|
|
2572
|
+
// GB-58 (GATE dev/test exact stage; E19): the selected app's declared sidecars are staged ONCE under this snapshot's ownership before the dependent
|
|
2573
|
+
// build, through the same planner and ledger as the compile and package stages (a Tauri crate with externalBin refuses to build without them). The
|
|
2574
|
+
// app's own prepare step may stage them too; the ledger's attribution check, not an environment marker, is what keeps that single and exact.
|
|
2575
|
+
if (ds) {
|
|
2576
|
+
const sidecarConfigs = committedSidecarConfigs({ o, git, commit, appDir, base: ds.path });
|
|
2577
|
+
const staged = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli: fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url)), commonDir, commit, tree, stage: to, executor, tag });
|
|
2578
|
+
if (!staged) { o.err(`${tag} --to ${to} stopped: the declared sidecars could not be staged; nothing compiled`); return 1; }
|
|
2579
|
+
}
|
|
2580
|
+
// right-release dev and test, and the Tauri CLI and every other child they start, reach Cargo only through the stage's carrier on a broker host.
|
|
2581
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage: to, tag });
|
|
2582
|
+
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing compiled`); return 1; }
|
|
1982
2583
|
const run = startRun({ commonDir, commit, tree, stage: to, ...(source ? { source } : {}) });
|
|
1983
2584
|
try {
|
|
1984
2585
|
const cli = fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url));
|
|
@@ -1987,10 +2588,11 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
|
|
|
1987
2588
|
// The door's own RightKit runs every sub-step: a dev target's `right-release web-build|stage-sidecar` steps resolve `right-release`
|
|
1988
2589
|
// on PATH, which found the app's node_modules copy (HeardRight on the Dell, 2026-10-27: the app's 0.6.8 guard refused what the
|
|
1989
2590
|
// door's 0.6.9 admitted). An installed gate (node_modules/@rightkit/release/bin) has its package's bin shims two levels up.
|
|
1990
|
-
const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, to, ...passthrough], { cwd: source ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: { ...o.env, ...run.env, ...doorPath(o), ...(pin.dir ? { CARGO_TARGET_DIR: pin.dir } : {}) } });
|
|
2591
|
+
const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, to, ...passthrough], { cwd: source ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: carriedEnv(carrier, { ...o.env, ...run.env }, { ...doorPath({ ...o, env: { ...o.env, ...carrier.env } }), ...(pin.dir ? { CARGO_TARGET_DIR: pin.dir } : (pinMulti?.env ?? {})) }) });
|
|
2592
|
+
const carriedOk = await carrier.finish({ stage: to });
|
|
1991
2593
|
if (ds && !(await closeSnapshotStage({ o, ds, root, stage: to, step: to, tag })).ok) { o.err(`${tag} --to ${to} stopped: the result covers no known bytes`); return 2; }
|
|
1992
|
-
return r.status === 0 ? 0 : 1;
|
|
1993
|
-
} finally { endRun(run); }
|
|
2594
|
+
return r.status === 0 && carriedOk ? 0 : 1;
|
|
2595
|
+
} finally { endRun(run); carrier.cleanup(); try { pinMulti?.cleanup?.(); } catch { /* the wrapper map is the gate's own temp directory */ } }
|
|
1994
2596
|
}
|
|
1995
2597
|
|
|
1996
2598
|
/** A direct (Linux) executor's environment (PATH without RustRight shim directories) for every child this run starts. */
|
|
@@ -2016,6 +2618,32 @@ export function doorPath(o, gateFile = o.gateFile ?? fileURLToPath(import.meta.u
|
|
|
2016
2618
|
* readStdin, out, err, cwd, env, platform, abort (an AbortController), runScans and findRustfmt (the scan runner and the
|
|
2017
2619
|
* lookup of a usable rustfmt, which the deferred Rust gate needs).
|
|
2018
2620
|
*/
|
|
2621
|
+
/** The gate flags a `--to <stage>` invocation takes (before a `--` separator, after which the arguments belong to right-release), and whether each takes a value. */
|
|
2622
|
+
const STAGE_VALUE_FLAGS = Object.freeze(["--to", "--platform", "--tier", "--commit", "--receipt-in", "--receipt-out", "--unit", "--units-file", "--all-units-exception"]);
|
|
2623
|
+
const STAGE_BOOLEAN_FLAGS = Object.freeze(["--route"]);
|
|
2624
|
+
|
|
2625
|
+
/**
|
|
2626
|
+
* Why `args` (a `--to <stage>` invocation) is not a well-formed request, or null. Every value flag needs a value that is not another flag and may be
|
|
2627
|
+
* given once; `--flag=value`, unknown flags and stray words are refused. The arguments after a `--` separator are right-release's and are not read.
|
|
2628
|
+
*/
|
|
2629
|
+
export function stageArgsProblem(args) {
|
|
2630
|
+
const at = args.indexOf("--");
|
|
2631
|
+
const gate = at < 0 ? args : args.slice(0, at);
|
|
2632
|
+
const seen = new Set();
|
|
2633
|
+
for (let i = 0; i < gate.length; i += 1) {
|
|
2634
|
+
const a = gate[i];
|
|
2635
|
+
if (STAGE_BOOLEAN_FLAGS.includes(a)) continue;
|
|
2636
|
+
if (STAGE_VALUE_FLAGS.includes(a)) {
|
|
2637
|
+
const value = gate[i + 1];
|
|
2638
|
+
if (value === undefined || value.startsWith("--")) return a === "--to" ? `--to needs a stage (${[...STAGES, "dev", "test"].join("|")})` : `${a} needs a value`;
|
|
2639
|
+
if (seen.has(a)) return `${a} was given twice`;
|
|
2640
|
+
seen.add(a); i += 1; continue;
|
|
2641
|
+
}
|
|
2642
|
+
return a.startsWith("-") ? `unknown option ${JSON.stringify(a)} (a value follows its flag as the next word: \`--commit <rev>\`, not \`--commit=<rev>\`)` : `unexpected argument ${JSON.stringify(a)}`;
|
|
2643
|
+
}
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2019
2647
|
export async function main(argv, io = {}) {
|
|
2020
2648
|
const o = {
|
|
2021
2649
|
spawn: defaultSpawn, exists: fs.existsSync, readFile: (f) => fs.readFileSync(f, "utf8"), writeFile: fs.writeFileSync, mkdir: fs.mkdirSync, chmod: fs.chmodSync, readdir: fs.readdirSync,
|
|
@@ -2028,6 +2656,8 @@ export async function main(argv, io = {}) {
|
|
|
2028
2656
|
const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
|
|
2029
2657
|
// `clippy`: the Clippy route (../clippy-route.mjs; docs/GATE-CONTRACT.md §1 "Clippy route & batching"). Dispatched before the global
|
|
2030
2658
|
// flags below because everything after `clippy` is cargo's own argv, verbatim (`-v`, `-h` and `--version` are cargo's).
|
|
2659
|
+
// `candidate-receipt` (GB-73): the candidate build receipt the RustRight installer takes in place of a clean primary checkout.
|
|
2660
|
+
if (args[0] === "candidate-receipt") return (await import("../candidate-receipt-command.mjs")).candidateReceiptCommand(o, args.slice(1));
|
|
2031
2661
|
if (args[0] === "clippy") return (await import("../clippy-route.mjs")).clippyCommand(o, args.slice(1), { main, io, absoluteEnv: SNAPSHOT_ABSOLUTE_ENV });
|
|
2032
2662
|
if (args.includes("--version") || args.includes("-v")) {
|
|
2033
2663
|
o.out(JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
@@ -2052,6 +2682,42 @@ export async function main(argv, io = {}) {
|
|
|
2052
2682
|
// class A scan (lockfiles, cargo-deny, cargo-audit, secrets, licences, manifests, workflow lint, syntax...) over the whole tree of the
|
|
2053
2683
|
// commit, never a diff. Nothing compiles. The RustRight broker runs exactly this before it admits any compile. `--list`,
|
|
2054
2684
|
// `--print-baseline` and `--only` stay app-check-only views.
|
|
2685
|
+
// `check --commit <sha>` without `--snapshot` reads the gate's one warm snapshot of that commit, selected the way
|
|
2686
|
+
// every `--to` stage selects it: host-wide build ownership first, then one sync of the commit into the gate-owned source directory, then the
|
|
2687
|
+
// 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
|
|
2688
|
+
// 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.
|
|
2689
|
+
// `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
|
|
2690
|
+
// `--worktree` that reads the live checkout after a momentary cleanliness check. Only hosted CI keeps the in-place full check.
|
|
2691
|
+
const commitCheck = sub === "check"
|
|
2692
|
+
&& !["--snapshot", "--worktree", "--plan", "--list", "--print-baseline", "--only"].some((f) => args.includes(f)) && o.classA !== false && snapshotRouteSelected(o);
|
|
2693
|
+
if (commitCheck) {
|
|
2694
|
+
const sc = await import("../snapshot-check.mjs");
|
|
2695
|
+
const snap = await import("../snapshot.mjs");
|
|
2696
|
+
const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
|
|
2697
|
+
const at = option("--repo") ? path.resolve(o.cwd, option("--repo")) : o.cwd;
|
|
2698
|
+
const top0 = o.spawn("git", ["rev-parse", "--show-toplevel"], { cwd: at });
|
|
2699
|
+
if (failed(top0) || !text(top0).trim()) { o.err("rightkit-gate: not inside a git repository"); return 2; }
|
|
2700
|
+
let own = o.buildOwnership ?? null; let ownHere = false;
|
|
2701
|
+
try {
|
|
2702
|
+
const checkout = fs.realpathSync.native(path.resolve(text(top0).trim()));
|
|
2703
|
+
const selected = sc.commitOf(checkout, option("--commit") ?? "HEAD", o.spawn);
|
|
2704
|
+
const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
|
|
2705
|
+
if (!own) {
|
|
2706
|
+
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; }
|
|
2707
|
+
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; }
|
|
2708
|
+
o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
|
|
2709
|
+
}
|
|
2710
|
+
let synced;
|
|
2711
|
+
try { synced = (o.syncSnapshot ?? snap.syncSnapshot)({ checkout, rev: selected.commit, root: snapshotsAt }); }
|
|
2712
|
+
catch (e) { o.err(`rightkit-gate: ${e.code === "error" ? sc.CHECK_ERROR : "BLOCKED"}: snapshot ${e.message}; nothing ran`); return e.code === "error" ? 2 : 1; }
|
|
2713
|
+
o.out(`rightkit-gate: snapshot ${synced.path} is commit ${synced.commit} tree ${synced.tree} (written=${synced.written} removed=${synced.removed} unchanged=${synced.unchanged})`);
|
|
2714
|
+
const next = [...argv]; const ci = next.indexOf("--commit");
|
|
2715
|
+
if (ci >= 0) next.splice(ci, 2, "--commit", synced.commit); else next.push("--commit", synced.commit);
|
|
2716
|
+
next.push("--snapshot", synced.path);
|
|
2717
|
+
return await main(next, { ...io, buildOwnership: own });
|
|
2718
|
+
} catch (e) { if (e instanceof sc.CheckError) { o.err(`rightkit-gate: ${sc.CHECK_ERROR}: ${e.message}`); return 2; } throw e; }
|
|
2719
|
+
finally { if (ownHere) { const r = own.release(); if (r?.released === false && !own.joined) o.err(`rightkit-gate: build ownership ${own.handle} kept: ${r.why}`); } }
|
|
2720
|
+
}
|
|
2055
2721
|
if ((sub === "check" || (!sub && args.includes("--all"))) && args.includes("--snapshot") && !o.buildOwnership) {
|
|
2056
2722
|
// Host-wide build ownership (../build-ownership.mjs; GATE-CONTRACT §1): held from before the first snapshot read through the
|
|
2057
2723
|
// end of the check, so no other request can sync the snapshot while class A runs in it. A nested gate whose
|
|
@@ -2091,9 +2757,11 @@ export async function main(argv, io = {}) {
|
|
|
2091
2757
|
// and package compile on the compile-tier rules, qualify and release name the release tier (rightkit#1296).
|
|
2092
2758
|
const fullAdmission = args.includes("--full-a") && !args.includes("--list") && !args.includes("--print-baseline") && !args.includes("--only");
|
|
2093
2759
|
const stripSnapshot = (list) => { const i = list.indexOf("--snapshot"); return i < 0 ? list : [...list.slice(0, i), ...list.slice(i + 2)]; };
|
|
2094
|
-
const
|
|
2760
|
+
const scopeForward = args.flatMap((a, i) => a === "--scope" ? [a, args[i + 1]] : []);
|
|
2761
|
+
const withoutScope = args.slice(1).filter((a, i, list) => a !== "--scope" && list[i - 1] !== "--scope");
|
|
2762
|
+
const appArgs = stripSnapshot(withoutScope.filter((a) => a !== "--full-a"));
|
|
2095
2763
|
if (fullAdmission && !appArgs.includes("--rule-tier")) appArgs.push("--rule-tier", requestedTier);
|
|
2096
|
-
const appCheck = await (o.appCheck ?? (await import("../app-check.mjs")).main)(appArgs, { out: o.out, err: o.err, label: "check", cwd: o.cwd });
|
|
2764
|
+
const appCheck = await (o.appCheck ?? (await import("../app-check.mjs")).main)(appArgs, { out: o.out, err: o.err, label: "check", cwd: o.cwd, ...(snapshotDir && !args.includes("--worktree") ? { snapshot: snapshotDir } : {}) });
|
|
2097
2765
|
if (args.includes("--list") || args.includes("--print-baseline") || args.includes("--only") || o.classA === false) {
|
|
2098
2766
|
// A view of the app rules, never a class A verdict: say so, so no caller can take its exit 0 for a full check.
|
|
2099
2767
|
// A green partial view exits PARTIAL_EXIT (3), never 0, so a caller that admits only exit 0 rejects it by status alone.
|
|
@@ -2109,7 +2777,7 @@ export async function main(argv, io = {}) {
|
|
|
2109
2777
|
const checkCwd = repoArg ? path.resolve(o.cwd, repoArg) : o.cwd;
|
|
2110
2778
|
// The app rules' tier is the stage's (rightkit#1296); class A's deterministic scans are the same at either tier.
|
|
2111
2779
|
const ruleTier = requestedTier;
|
|
2112
|
-
const classA = await main(["--class-a", "--rule-tier", ruleTier, ...(args.includes("--full-a") ? ["--full-a"] : []), ...(args.includes("--worktree") ? ["--worktree"] : ["--commit", snapshotCommit?.commit ?? option("--commit") ?? "HEAD"]), ...(snapshotDir ? ["--snapshot", snapshotDir] : [])], { ...io, cwd: checkCwd });
|
|
2780
|
+
const classA = await main(["--class-a", "--rule-tier", ruleTier, ...(args.includes("--full-a") ? ["--full-a"] : []), ...(args.includes("--worktree") ? ["--worktree"] : ["--commit", snapshotCommit?.commit ?? option("--commit") ?? "HEAD"]), ...(snapshotDir ? ["--snapshot", snapshotDir] : []), ...scopeForward], { ...io, cwd: checkCwd });
|
|
2113
2781
|
// RightKit checking itself: its own rules (line endings, consumer recipes, npm surface, version copies and the version feed, dispatcher
|
|
2114
2782
|
// pins, consumer locks, generated docs, temp paths: scripts/preflight.mjs) and its exports' import safety are class A for the RightKit repository.
|
|
2115
2783
|
let selfRules = 0;
|
|
@@ -2123,7 +2791,7 @@ export async function main(argv, io = {}) {
|
|
|
2123
2791
|
// In snapshot mode RightKit's own scripts are the snapshot's (the commit's code), never the live checkout's.
|
|
2124
2792
|
const selfRoot = snapshotDir ?? root;
|
|
2125
2793
|
const rightkitRepository = root && (() => {
|
|
2126
|
-
try { return
|
|
2794
|
+
try { return isRightKitIdentity(o.readFile(path.join(selfRoot, ...RIGHTKIT_IDENTITY_FILE.split("/")))); }
|
|
2127
2795
|
catch { return false; }
|
|
2128
2796
|
})();
|
|
2129
2797
|
if (rightkitRepository && (!o.exists(path.join(selfRoot, "scripts", "preflight.mjs")) || !o.exists(path.join(selfRoot, "scripts", "ci", "exports-import-effects.mjs")))) {
|
|
@@ -2137,7 +2805,7 @@ export async function main(argv, io = {}) {
|
|
|
2137
2805
|
const r = launch(process.execPath, [path.join(snapshotDir, "scripts", "preflight.mjs"), "--repo", root, "--commit", snapshotCommit.commit], { cwd: root, env: o.env });
|
|
2138
2806
|
selfRules = r.status === 0 ? 0 : 1;
|
|
2139
2807
|
const effects = path.join(snapshotDir, "scripts", "ci", "exports-import-effects.mjs");
|
|
2140
|
-
if (selfRules === 0 && o.exists(effects)) {
|
|
2808
|
+
if (selfRules === 0 && o.exists(effects) && scopeForward.length === 0) {
|
|
2141
2809
|
const common = o.spawn("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root });
|
|
2142
2810
|
const report = path.join(failed(common) ? path.join(root, ".git") : text(common).trim(), "rightkit-gate", "import-effects", `snapshot-${snapshotCommit.commit}.json`);
|
|
2143
2811
|
const e = launch(process.execPath, [effects, "--root", snapshotDir, "--no-export", "--report", report], { cwd: snapshotDir, env: o.env });
|
|
@@ -2206,6 +2874,7 @@ export async function main(argv, io = {}) {
|
|
|
2206
2874
|
// directory. A direct host only, until the broker's snapshot carrier lands (step 4d).
|
|
2207
2875
|
const snapshotB = !classAOnly && args.includes("--all") ? (option("--snapshot") ?? null) : null;
|
|
2208
2876
|
if (snapshotB && !option("--commit")) { o.err(`${tag} --all --snapshot <dir> needs --commit <sha>`); return 2; }
|
|
2877
|
+
let hostedCommitA = null;
|
|
2209
2878
|
if (fullA && !args.includes("--worktree") && !snapshotA) {
|
|
2210
2879
|
// A commit is checked in full only where its bytes are the live checkout: it is HEAD and nothing tracked or untracked differs
|
|
2211
2880
|
// (the unit and app stages already refuse a dirty tree). Then the working tree is that commit, and the checks run there.
|
|
@@ -2214,16 +2883,32 @@ export async function main(argv, io = {}) {
|
|
|
2214
2883
|
const dirty = at("status", "--porcelain", "--untracked-files=all");
|
|
2215
2884
|
if (failed(want) || failed(head) || text(want).trim() !== text(head).trim()) { o.err("rightkit-gate: --full-a checks a commit only when it is HEAD of this checkout: the repository's checks run on the live tree"); return 2; }
|
|
2216
2885
|
if (failed(dirty) || text(dirty).trim()) { o.err("rightkit-gate: --full-a on a commit needs a clean checkout (the live tree must be exactly that commit); commit or remove the changes"); return 2; }
|
|
2217
|
-
|
|
2218
|
-
|
|
2886
|
+
if (!snapshotRouteSelected(o)) {
|
|
2887
|
+
const tree = at("rev-parse", "--verify", `${text(want).trim()}^{tree}`);
|
|
2888
|
+
if (failed(tree) || !text(tree).trim()) { o.err("rightkit-gate: RK_CHECK_ERROR: cannot identify hosted CI commit tree"); return 2; }
|
|
2889
|
+
hostedCommitA = { commit: text(want).trim(), tree: text(tree).trim() };
|
|
2890
|
+
const i = args.indexOf("--commit"); if (i >= 0) args.splice(i, 2, "--commit", hostedCommitA.commit); else args.push("--commit", hostedCommitA.commit);
|
|
2891
|
+
} else {
|
|
2892
|
+
const i = args.indexOf("--commit"); if (i >= 0) args.splice(i, 2);
|
|
2893
|
+
args.push("--worktree");
|
|
2894
|
+
}
|
|
2219
2895
|
}
|
|
2220
2896
|
const classACommit = classAOnly && !args.includes("--worktree") ? (option("--commit") ?? "HEAD") : null;
|
|
2897
|
+
const scopeDirs = args.flatMap((a, i) => a === "--scope" ? [args[i + 1]] : []);
|
|
2898
|
+
if (scopeDirs.length) {
|
|
2899
|
+
const bad = scopeDirs.find((d) => typeof d !== "string" || !d || d.startsWith("--") || path.isAbsolute(d) || /^[A-Za-z]:/.test(d) || /(^|[\\/])\.\.([\\/]|$)/.test(d));
|
|
2900
|
+
if (!fullA || (!snapshotA && !hostedCommitA) || 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; }
|
|
2901
|
+
}
|
|
2221
2902
|
if (args.includes("--base") && (!option("--base") || option("--base").startsWith("--"))) { o.err("rightkit-gate: --base needs a ref"); return 2; }
|
|
2222
2903
|
|
|
2223
2904
|
const top = o.spawn("git", ["rev-parse", "--show-toplevel"], { cwd: o.cwd });
|
|
2224
2905
|
if (failed(top) || !text(top).trim()) { o.err("rightkit-gate: not inside a git repository"); return 2; }
|
|
2225
2906
|
const root = path.resolve(text(top).trim());
|
|
2226
2907
|
const git = (...a) => o.spawn("git", a, { cwd: root });
|
|
2908
|
+
if (hostedCommitA) {
|
|
2909
|
+
try { await verifyCommittedCheckout({ git, root, ...hostedCommitA, when: "before class A discovery" }); }
|
|
2910
|
+
catch (error) { o.err(`rightkit-gate: RK_CHECK_ERROR: ${error.message}; nothing scanned`); return 2; }
|
|
2911
|
+
}
|
|
2227
2912
|
if (sub === "install-hook") return await installHook(o, root);
|
|
2228
2913
|
// `env-digest`: only the environment identity a full class A verdict depends on (classAEnvironmentDigest), no scans. The broker
|
|
2229
2914
|
// recomputes it when a queued compile starts and re-runs the check if it changed since the verdict.
|
|
@@ -2253,16 +2938,81 @@ export async function main(argv, io = {}) {
|
|
|
2253
2938
|
// `--unit` is a gate flag only before a `--` separator: after it the arguments belong to right-release (an app's `--to dev -- …`).
|
|
2254
2939
|
const separator = args.indexOf("--");
|
|
2255
2940
|
const unitFlag = args.includes("--unit") && (separator < 0 || args.indexOf("--unit") < separator);
|
|
2941
|
+
// RightKit names its unit on every run (rightkitDoorRule): `--unit <selector>`, `--units-file <json written for this run>` or
|
|
2942
|
+
// `--all-units-exception <id>`; exactly one. The unit list is read here, before anything runs, so a bad file is a usage error (exit 2).
|
|
2943
|
+
const gateFlag = (name) => args.includes(name) && (separator < 0 || args.indexOf(name) < separator);
|
|
2944
|
+
const unitsFileFlag = gateFlag("--units-file"); const allUnitsFlag = gateFlag("--all-units-exception");
|
|
2945
|
+
let unitList = null;
|
|
2946
|
+
if (unitsFileFlag || allUnitsFlag) {
|
|
2947
|
+
const flag = unitsFileFlag ? "--units-file" : "--all-units-exception";
|
|
2948
|
+
if (option("--to") === undefined) { o.err(`rightkit-gate: ${flag} applies to a --to stage\n${USAGE}`); return 2; }
|
|
2949
|
+
if ([unitFlag, unitsFileFlag, allUnitsFlag].filter(Boolean).length > 1) { o.err("rightkit-gate: name the unit once: --unit, --units-file or --all-units-exception, not several"); return 2; }
|
|
2950
|
+
if (!option(flag) || option(flag).startsWith("--")) { o.err(`rightkit-gate: ${flag} needs a value\n${USAGE}`); return 2; }
|
|
2951
|
+
if (unitsFileFlag) {
|
|
2952
|
+
try { unitList = readUnitList((f) => o.readFile(path.resolve(o.cwd, f)), option("--units-file")); }
|
|
2953
|
+
catch (e) { o.err(`rightkit-gate: ${e.message}`); return 2; }
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
// `--no-broker <exception-id>`: the one documented exception to the mandatory broker on Mac and Dell (broker-exception.mjs). The record is
|
|
2957
|
+
// read from the selected commit, never the working tree; every refusal is typed and nothing runs. The run then takes the same stages
|
|
2958
|
+
// with the direct executor, and prints and records the exception.
|
|
2959
|
+
const noBrokerAt = args.indexOf("--no-broker");
|
|
2960
|
+
if (noBrokerAt >= 0 && (separator < 0 || noBrokerAt < separator)) {
|
|
2961
|
+
const brokerId = args[noBrokerAt + 1];
|
|
2962
|
+
if (option("--to") === undefined) { o.err(`rightkit-gate: ${BROKER_EXCEPTION_ERRORS.usage}: --no-broker <exception-id> applies to a --to stage\n${USAGE}`); return 2; }
|
|
2963
|
+
args.splice(noBrokerAt, typeof brokerId === "string" && !brokerId.startsWith("--") ? 2 : 1);
|
|
2964
|
+
let granted;
|
|
2965
|
+
try {
|
|
2966
|
+
let record = null; let inRightKit = true;
|
|
2967
|
+
if (typeof brokerId === "string" && !brokerId.startsWith("--")) {
|
|
2968
|
+
const pinned = (await import("../snapshot.mjs")).resolveCommit(root, option("--commit") ?? "HEAD");
|
|
2969
|
+
const shown = git("show", `${pinned.commit}:${BROKER_EXCEPTIONS_FILE}`);
|
|
2970
|
+
record = failed(shown) ? null : text(shown);
|
|
2971
|
+
// The repository identity of the commit decides what an exception must name: RightKit's units, or (every other repository, built whole) nothing.
|
|
2972
|
+
inRightKit = rightkitIdentityAt(git, pinned.commit);
|
|
2973
|
+
}
|
|
2974
|
+
// A unit list run covers every listed unit; a run that names no unit (the whole repository) selects every unit.
|
|
2975
|
+
const selected = unitFlag ? unitsOfSelector(option("--unit")) : unitList ? unitList.flatMap((u) => unitsOfSelector(u)) : unitsOfSelector(undefined);
|
|
2976
|
+
granted = authorizeNoBroker({ id: brokerId, record, platform: o.platform, units: selected, now: o.now ? o.now() : Date.now(), inRightKit });
|
|
2977
|
+
} catch (e) {
|
|
2978
|
+
if (!e.code?.startsWith("RK_BROKER_EXCEPTION_")) { o.err(`rightkit-gate: cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
|
|
2979
|
+
o.err(`rightkit-gate: BLOCKED: ${e.message}; nothing ran`); return e.code === BROKER_EXCEPTION_ERRORS.usage ? 2 : 1;
|
|
2980
|
+
}
|
|
2981
|
+
const brokerResolver = o.resolveExecutor ?? resolveExecutor;
|
|
2982
|
+
o.resolveExecutor = (a) => brokerResolver({ ...a, forceDirect: true });
|
|
2983
|
+
o.brokerException = granted;
|
|
2984
|
+
io = { ...io, resolveExecutor: o.resolveExecutor, brokerException: granted };
|
|
2985
|
+
o.out(`rightkit-gate: BROKER EXCEPTION ${describeBrokerException(granted)}`);
|
|
2986
|
+
}
|
|
2987
|
+
// The door's stage arguments are read strictly (E2E L3-02): a flag with no value, `--flag=value`, an unknown flag, a repeated value flag or a stray
|
|
2988
|
+
// word is refused before git, ownership or any process, never ignored (`--commit=<old sha>` used to build HEAD, a trailing `--to` ran the default gate).
|
|
2989
|
+
// A unit run (`--unit`) validates its own, narrower vocabulary below ("unit stages take only ...").
|
|
2990
|
+
if (!sub && !unitFlag && args.some((a) => a === "--to" || a.startsWith("--to="))) {
|
|
2991
|
+
const problem = stageArgsProblem(args);
|
|
2992
|
+
if (problem) { o.err(`rightkit-gate: ${problem}\n${USAGE}`); return 2; }
|
|
2993
|
+
}
|
|
2994
|
+
// `--route` is a gate flag only before a `--` separator (after it the arguments belong to right-release). It answers which route a
|
|
2995
|
+
// stage takes and runs nothing, so it takes no unit, receipt or release stage (rightkit#1297).
|
|
2996
|
+
const gateArgs = separator < 0 ? args : args.slice(0, separator);
|
|
2997
|
+
if (gateArgs.includes("--route")) {
|
|
2998
|
+
const routeTo = option("--to");
|
|
2999
|
+
const clash = ["--unit", "--units-file", "--all-units-exception", "--receipt-in", "--receipt-out"].filter((f) => gateArgs.includes(f));
|
|
3000
|
+
if (routeTo === undefined) { o.err(`rightkit-gate: --route needs --to ${ROUTE_PROBE_STAGES.join("|")}\n${USAGE}`); return 2; }
|
|
3001
|
+
if (routeTo === "release") { o.err("rightkit-gate: --route answers for --to check|compile|package|qualify|dev|test; release uploads and has no route of its own"); return 2; }
|
|
3002
|
+
if (clash.length) { o.err(`rightkit-gate: --route runs nothing and takes no ${clash.join(" or ")}`); return 2; }
|
|
3003
|
+
o.routeProbe = true;
|
|
3004
|
+
}
|
|
2256
3005
|
if (option("--to") !== undefined && unitFlag) {
|
|
2257
3006
|
const selector = option("--unit");
|
|
2258
3007
|
const validArgs = args.length % 2 === 0 && args.every((a, i) => i % 2 ? !a.startsWith("--") : ["--to", "--unit", "--receipt-out", "--commit"].includes(a)) && ["--to", "--unit", "--receipt-out", "--commit"].every((flag) => args.filter((a) => a === flag).length <= 1);
|
|
2259
3008
|
if (!validArgs || !selector) {
|
|
2260
3009
|
o.err("rightkit-gate: unit stages take only --to, --unit and optional --receipt-out and --commit"); return 2;
|
|
2261
3010
|
}
|
|
2262
|
-
// Row 7.06a: on
|
|
2263
|
-
if (!o.doorSnapshot &&
|
|
2264
|
-
|
|
2265
|
-
|
|
3011
|
+
// Row 7.06a: on every host but hosted CI a unit stage builds the selected commit's snapshot (Linux direct, Mac and Dell through the carrier).
|
|
3012
|
+
if (!o.doorSnapshot && snapshotRouteSelected(o)) return await runUnitSnapshot(o, args, root, git, io, option);
|
|
3013
|
+
// Hosted CI only: the runner's checkout is the commit.
|
|
3014
|
+
if (option("--commit") !== undefined) { o.err("rightkit-gate: BLOCKED: --commit selects a commit for the snapshot route; hosted CI builds the runner's checkout, which is the commit"); return 2; }
|
|
3015
|
+
const head = git("rev-parse", "HEAD"), treeResult = git("rev-parse", "HEAD^{tree}"), common = git("rev-parse", "--git-common-dir"), dirty = git("status", "--porcelain", "--untracked-files=all");
|
|
2266
3016
|
const commit = text(head).trim(), tree = text(treeResult).trim();
|
|
2267
3017
|
if (failed(head) || failed(treeResult) || failed(common) || !text(common).trim() || !/^[0-9a-f]{40}$/.test(commit) || !/^[0-9a-f]{40}$/.test(tree) || failed(dirty) || text(dirty).trim()) { o.err("rightkit-gate: BLOCKED: unit stages need clean committed HEAD and tree"); return 1; }
|
|
2268
3018
|
const match = /^(?:npm:@rightkit\/([a-z0-9-]+)|cargo:(rightkit-[a-z0-9-]+))$/.exec(selector);
|
|
@@ -2276,12 +3026,22 @@ export async function main(argv, io = {}) {
|
|
|
2276
3026
|
// node_modules does not carry; the app paths of this gate must keep loading there (app-check.test: the installed copy).
|
|
2277
3027
|
const { runUnitGate } = await import("../unit-gate.mjs");
|
|
2278
3028
|
return await runUnitGate({ selector, to: option("--to"), root, commonDir: path.resolve(root, text(common).trim()), commit, tree, executor, env: o.env, platform: o.platform,
|
|
2279
|
-
check: (tier) =>
|
|
3029
|
+
check: async (tier) => {
|
|
3030
|
+
await verifyCommittedCheckout({ git, root, commit, tree, when: "before unit:check" });
|
|
3031
|
+
const result = await main(["check", "--commit", commit, "--full-a", "--rule-tier", tier, "--scope", path.posix.dirname(manifest)], { ...io, cwd: root });
|
|
3032
|
+
await verifyCommittedCheckout({ git, root, commit, tree, when: "after unit:check" });
|
|
3033
|
+
return result;
|
|
3034
|
+
}, out: o.out, err: o.err,
|
|
2280
3035
|
...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(report, null, 2)}\n`) } : {}),
|
|
2281
3036
|
});
|
|
2282
3037
|
}
|
|
2283
3038
|
if (unitFlag) { o.err("rightkit-gate: --unit requires --to"); return 2; }
|
|
2284
|
-
if (option("--to") !== undefined)
|
|
3039
|
+
if (option("--to") !== undefined) {
|
|
3040
|
+
// A door run that names no unit: RightKit refuses it unless Adrian's exception or a unit list names what runs (rightkitDoorRule).
|
|
3041
|
+
const door = await rightkitDoorRule(o, args, root, git, io, { unitsFileFlag, allUnitsFlag, unitList });
|
|
3042
|
+
if (door.exit !== undefined) return door.exit;
|
|
3043
|
+
return await runStages(o, args, root, git, io);
|
|
3044
|
+
}
|
|
2285
3045
|
if (sub === "stage") return stageForPublisher(o, args, root, git);
|
|
2286
3046
|
|
|
2287
3047
|
const planOnly = args.includes("--plan");
|
|
@@ -2323,6 +3083,16 @@ export async function main(argv, io = {}) {
|
|
|
2323
3083
|
// A broker-managed host whose broker is unreachable refuses the moment a Cargo manifest is in the snapshot (below): a repository
|
|
2324
3084
|
// 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.
|
|
2325
3085
|
const cargoCommand = executor.cargo ?? ["cargo"];
|
|
3086
|
+
// The declared Linux broker route's cargo is `[<absolute validated client>, "cargo"]`, which the scan planner (planScans: `["rustright","cargo"]` or
|
|
3087
|
+
// `["cargo"]`) refuses, and a PATH `rustright` is never the client. So the scans are PLANNED with the logical `["rustright","cargo"]` and every spawn the
|
|
3088
|
+
// gate hands the scans, the discovery of the snapshot's workspaces and the tool probes resolves that one name to the executor's validated client,
|
|
3089
|
+
// with the endpoint the executor declared (`withDeclaredClient`); nothing starts a `rustright` found on PATH. The carried requests
|
|
3090
|
+
// (snapshotCargoSpawn's carrySpawn, the snapshot carrier) name the client by absolute path already. Every other route plans `cargoCommand` itself.
|
|
3091
|
+
const declaredClient = executor.kind === "broker" && executor.route === LINUX_BROKER_ROUTE && typeof executor.client === "string" ? executor.client : null;
|
|
3092
|
+
const planCargoCommand = declaredClient ? ["rustright", ...cargoCommand.slice(1)] : cargoCommand;
|
|
3093
|
+
const withDeclaredClient = (spawn) => (declaredClient
|
|
3094
|
+
? (command, args, opts = {}) => (command === "rustright" ? spawn(declaredClient, args, { ...opts, env: { ...(opts.env ?? o.env), ...(executor.env ?? {}) } }) : spawn(command, args, opts))
|
|
3095
|
+
: spawn);
|
|
2326
3096
|
// A broker host (Mac, Dell) compiles a commit snapshot through the RustRight snapshot carrier: `deps.snapshotBroker`, built in the
|
|
2327
3097
|
// snapshot-compile region below (item 7 rows 7.20 to 7.24). A broker-managed host whose broker cannot be reached is refused with
|
|
2328
3098
|
// the executor's own problem, never compiled around.
|
|
@@ -2339,11 +3109,9 @@ export async function main(argv, io = {}) {
|
|
|
2339
3109
|
}
|
|
2340
3110
|
|
|
2341
3111
|
const ac = o.abort ?? new AbortController();
|
|
2342
|
-
let exportRoot = null;
|
|
2343
3112
|
let snapshotGitConfig = null;
|
|
2344
3113
|
let cargoShimDir = null;
|
|
2345
3114
|
const cleanup = () => {
|
|
2346
|
-
if (exportRoot) fs.rmSync(exportRoot, { recursive: true, force: true });
|
|
2347
3115
|
if (cargoShimDir) fs.rmSync(cargoShimDir, { recursive: true, force: true });
|
|
2348
3116
|
if (snapshotGitConfig) { fs.rmSync(snapshotGitConfig, { force: true }); try { fs.rmdirSync(path.dirname(snapshotGitConfig)); } catch { /* not empty: leave it, the next exact sync prunes it */ } }
|
|
2349
3117
|
};
|
|
@@ -2351,6 +3119,14 @@ export async function main(argv, io = {}) {
|
|
|
2351
3119
|
try {
|
|
2352
3120
|
let repo = root;
|
|
2353
3121
|
let snapshotBCommit = null;
|
|
3122
|
+
// On a broker host the gate's snapshot is the broker's managed one, where RustRight refuses a root request ("a root request cannot run inside a
|
|
3123
|
+
// managed snapshot", rightkit#1311): the gate's own Cargo reads there (unit discovery, workspace members) are carried read-only requests,
|
|
3124
|
+
// with the broker's signed receipt verified (snapshot-metadata.mjs). The checkout, git and a direct host keep the plain spawn.
|
|
3125
|
+
let snapshotSpawn = withDeclaredClient((c, a, opts) => o.spawn(c, a, opts));
|
|
3126
|
+
let discoveryBroker = null;
|
|
3127
|
+
// On a broker host, the spawn every Class A Cargo scan of the snapshot runs through: a Cargo call inside the snapshot is a carried read-only
|
|
3128
|
+
// request of one of the exact admitted shapes (snapshot-metadata.mjs), anything else passes through (node, git, the client's probe).
|
|
3129
|
+
let scanSpawn = null;
|
|
2354
3130
|
if (snapshot && snapshotB) {
|
|
2355
3131
|
// Direct host: no broker grant to satisfy, so no config-only .git; the snapshot is scanned and compiled in place.
|
|
2356
3132
|
repo = snapshotB;
|
|
@@ -2368,27 +3144,14 @@ export async function main(argv, io = {}) {
|
|
|
2368
3144
|
o.writeFile(snapshotGitConfig, `[remote "origin"]\n\turl = ${originUrl}\n`);
|
|
2369
3145
|
}
|
|
2370
3146
|
} else if (snapshot) {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
//
|
|
2374
|
-
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
// (publish run 40282215043: "cannot create directory at ...\\repo\\.github").
|
|
2380
|
-
fs.mkdirSync(repo, { recursive: true });
|
|
2381
|
-
const x = failed(made) ? made : o.spawn("git", ["checkout-index", "--all", "--force", `--prefix=${repo.split(path.sep).join("/")}/`], { cwd: root, env: indexEnv });
|
|
2382
|
-
if (failed(made) || failed(x)) { o.err(`${tag} cannot export ${snapshot}: ${why(made) || why(x)}`); return 2; }
|
|
2383
|
-
// rustright binds the tracked .rightkit-local-build.json grant to the origin URL in .git/config (rustright-policy local_build.rs),
|
|
2384
|
-
// and the export has no .git, so cargo metadata in it was refused on every broker host and no push passed the hook (rightkit#1215).
|
|
2385
|
-
// A config-only .git carries exactly the source checkout's origin and is not a repository to git (no HEAD, no objects), so nothing
|
|
2386
|
-
// can run git against the export. No origin (a checkout without a remote): nothing is written and rustright decides as before.
|
|
2387
|
-
const origin = o.spawn("git", ["config", "--get", "remote.origin.url"], { cwd: root });
|
|
2388
|
-
const originUrl = failed(origin) ? "" : text(origin).trim();
|
|
2389
|
-
if (originUrl) {
|
|
2390
|
-
o.mkdir(path.join(repo, ".git"), { recursive: true });
|
|
2391
|
-
o.writeFile(path.join(repo, ".git", "config"), `[remote "origin"]\n\turl = ${originUrl}\n`);
|
|
3147
|
+
// No export, no copy of the source (docs/GATE-CONTRACT.md, 2026-10-29): a host that has the gate's warm snapshot reads the commit
|
|
3148
|
+
// through it (`check --commit` selects it above, `--to` stages hand it over). What reaches here is a run whose checkout IS the
|
|
3149
|
+
// commit: hosted CI's runner checkout, or HEAD for the legacy gate run. It is scanned in place, only after proving it is that commit.
|
|
3150
|
+
if (classAOnly && snapshotRouteSelected(o)) { o.err(`${tag} BLOCKED: class A of a commit reads the gate's warm snapshot of it, never the checkout; run \`rightkit-gate check --commit <sha>\` (it selects the snapshot) or pass \`--snapshot <dir>\``); return 2; }
|
|
3151
|
+
const want = git("rev-parse", "--verify", `${snapshot}^{commit}`); const head = git("rev-parse", "--verify", "HEAD^{commit}");
|
|
3152
|
+
if (snapshot !== "HEAD" && (failed(want) || failed(head) || text(want).trim() !== text(head).trim())) {
|
|
3153
|
+
o.err(`${tag} BLOCKED: ${snapshot} is not HEAD of this checkout, and this run has no gate-owned snapshot of it: only the commit the checkout already is can be scanned in place (hosted CI's runner checkout); check another commit with \`check --commit <sha>\` on a host with the gate's warm snapshot`);
|
|
3154
|
+
return 2;
|
|
2392
3155
|
}
|
|
2393
3156
|
}
|
|
2394
3157
|
// Every file of the snapshot, once: units, manifests, lockfiles and the changed set are all read from what is scanned, never from
|
|
@@ -2399,7 +3162,7 @@ export async function main(argv, io = {}) {
|
|
|
2399
3162
|
const tracked = (name) => files.filter((f) => f === name || f.endsWith(`/${name}`));
|
|
2400
3163
|
const readSnap = (f) => o.readFile(f);
|
|
2401
3164
|
|
|
2402
|
-
let rust; let npm; let npmRoots = []; let manifests = [];
|
|
3165
|
+
let rust; let npm; let npmRoots = []; let installRoots = []; let manifests = []; let closure = null; let closureTools = null;
|
|
2403
3166
|
try {
|
|
2404
3167
|
manifests = tracked("Cargo.toml");
|
|
2405
3168
|
if (executor.kind === "refuse" && manifests.length) { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
@@ -2413,11 +3176,63 @@ export async function main(argv, io = {}) {
|
|
|
2413
3176
|
const r = git("ls-files", "-z", "--others", "--exclude-standard", "--", ":(glob)**/Cargo.toml");
|
|
2414
3177
|
return failed(r) ? [] : text(r).split("\0").filter(Boolean).map(posix);
|
|
2415
3178
|
};
|
|
2416
|
-
|
|
3179
|
+
if (executor.kind === "broker" && snapshot && (snapshotA || snapshotB) && manifests.length) {
|
|
3180
|
+
const { snapshotCargoSpawn } = await import("../snapshot-metadata.mjs");
|
|
3181
|
+
const { createSnapshotBroker } = await import("../snapshot-broker.mjs");
|
|
3182
|
+
const snapshotCheck = await import("../snapshot-check.mjs");
|
|
3183
|
+
const snapshotCommitOf = () => snapshotCheck.commitOf(fs.realpathSync.native(root), String(snapshot), o.spawn);
|
|
3184
|
+
// The broker is built before anything is read: a missing enrollment, ownership or client refuses here, naming its code (as the compile does).
|
|
3185
|
+
try {
|
|
3186
|
+
if (o.snapshotBroker && typeof o.snapshotBroker.create !== "function") discoveryBroker = o.snapshotBroker; // a fixture broker (tests)
|
|
3187
|
+
else {
|
|
3188
|
+
const sc = snapshotCommitOf();
|
|
3189
|
+
discoveryBroker = createSnapshotBroker({ env: o.env, platform: o.platform, checkout: root, commit: { sha: sc.commit, tree: sc.tree }, snapshot: { path: repo, expectedTree: sc.tree }, ownership: o.buildOwnership, client: executor.route === LINUX_BROKER_ROUTE ? executor.client : which("rustright").find((c) => path.isAbsolute(c)), gateFile: callingGatePath(), spawn: o.spawn });
|
|
3190
|
+
}
|
|
3191
|
+
} catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SNAPSHOT_BROKER"}: ${error.message}; nothing compiled`); return 1; }
|
|
3192
|
+
snapshotSpawn = snapshotCargoSpawn({ spawn: withDeclaredClient((c, a, opts) => o.spawn(c, a, opts)), cargoCommand: planCargoCommand, snapshotRoot: repo, env: o.env, err: o.err, broker: () => discoveryBroker });
|
|
3193
|
+
scanSpawn = snapshotCargoSpawn({ spawn: withDeclaredClient(defaultSpawnAsync), carrySpawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand: planCargoCommand, snapshotRoot: repo, env: o.env, err: o.err, broker: () => discoveryBroker });
|
|
3194
|
+
}
|
|
2417
3195
|
npm = discoverNpmUnits({ root: repo, tracked, readFile: readSnap, exists: o.exists });
|
|
2418
3196
|
npmRoots = discoverPnpmRoots({ root: repo, tracked, exists: o.exists });
|
|
3197
|
+
let metadata = null;
|
|
3198
|
+
if (scopeDirs.length) {
|
|
3199
|
+
closureTools = await import("../app-closure.mjs");
|
|
3200
|
+
const readCommitted = (file) => {
|
|
3201
|
+
if (!files.includes(file)) throw new Error(`${file} is absent from the committed tree`);
|
|
3202
|
+
return readSnap(path.join(repo, file));
|
|
3203
|
+
};
|
|
3204
|
+
const sourceMetadata = closureTools.cargoMetadataFromManifests({ manifests: manifests.filter((m) => !fixtures.some((d) => m.startsWith(`${d.replace(/\/$/, "")}/`))), readFile: readCommitted });
|
|
3205
|
+
const inventory = discoverRustWorkspaces({ root: repo, tracked, fixtures, readFile: readSnap, metadata: sourceMetadata });
|
|
3206
|
+
closure = closureTools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: inventory.units, readFile: readCommitted });
|
|
3207
|
+
const selectedWorkspaces = new Set(inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir));
|
|
3208
|
+
metadata = (dir) => {
|
|
3209
|
+
const source = sourceMetadata(dir);
|
|
3210
|
+
return selectedWorkspaces.has(source.cargoDir) ? workspaceMetadata({ root: repo, spawn: snapshotSpawn, cargoCommand: planCargoCommand, dir, realpath: fs.realpathSync }) : source;
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
rust = discoverRustWorkspaces({ root: repo, spawn: snapshotSpawn, cargoCommand: planCargoCommand, tracked, untracked, fixtures, readFile: readSnap, ...(metadata ? { metadata } : {}) });
|
|
3214
|
+
if (closure) {
|
|
3215
|
+
// Confirm membership discovered by Cargo; its closure must resolve without unrelated execution.
|
|
3216
|
+
closure = closureTools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: rust.units, readFile: (file) => { if (!files.includes(file)) throw new Error(`${file} is absent from the committed tree`); return readSnap(path.join(repo, file)); } });
|
|
3217
|
+
installRoots = [...new Set(npm.filter((u) => closure.npm.has(u.name)).map((u) => u.root ?? "."))].sort();
|
|
3218
|
+
} else installRoots = npmRoots;
|
|
2419
3219
|
} catch (e) { o.err(`${tag} BLOCKED: cannot discover the repository's units: ${e.message}`); return 1; }
|
|
2420
3220
|
for (const k of rust.skipped) o.out(`${tag} note: ${k.manifest} is skipped, not scanned: ${k.why}`);
|
|
3221
|
+
// Every repository but RightKit commits one fixed build definition that lists every Cargo workspace; a run builds all of them and nothing is
|
|
3222
|
+
// chosen per run (#1325, Adrian 2026-10-31). The workspaces this commit holds and the committed list must be the same set, missing or extra,
|
|
3223
|
+
// or nothing runs. RightKit is judged per unit instead (rightkitDoorRule). `--plan` builds nothing, so it only says what a run would meet.
|
|
3224
|
+
{
|
|
3225
|
+
let rightkitHere = false;
|
|
3226
|
+
if (files.includes(RIGHTKIT_IDENTITY_FILE)) { try { rightkitHere = isRightKitIdentity(readSnap(path.join(repo, ...RIGHTKIT_IDENTITY_FILE.split("/")))); } catch { rightkitHere = false; } }
|
|
3227
|
+
if (!rightkitHere) {
|
|
3228
|
+
try { checkCargoWorkspaceList({ discovered: rust.workspaces.map((w) => w.cargoDir), record: files.includes(CARGO_WORKSPACE_LIST_FILE) ? readSnap(path.join(repo, CARGO_WORKSPACE_LIST_FILE)) : null }); }
|
|
3229
|
+
catch (e) {
|
|
3230
|
+
if (!e.code?.startsWith("RK_CARGO_WORKSPACE_LIST_")) throw e;
|
|
3231
|
+
if (args.includes("--plan")) o.out(`${tag} note: a run would be refused: ${e.message}`);
|
|
3232
|
+
else { o.err(`${tag} BLOCKED: ${e.message}; nothing ran`); return 1; }
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
2421
3236
|
|
|
2422
3237
|
// The snapshot compile: verified as the commit's tree before anything runs, dependencies installed into it offline from the
|
|
2423
3238
|
// checkout's store, and every Cargo workspace's output pinned to the target directory cargo resolves for it IN THE CHECKOUT
|
|
@@ -2449,7 +3264,7 @@ export async function main(argv, io = {}) {
|
|
|
2449
3264
|
directDeclaredNotes = judged.findings.map((f) => `declared compile input: ${f}`);
|
|
2450
3265
|
}
|
|
2451
3266
|
o.out(`${tag} snapshot ${snapshotB} is commit ${snapshotBCommit.commit} tree ${snapshotBCommit.tree} (verified before the compile)`);
|
|
2452
|
-
for (const r of sc.installSnapshotDependencies({ checkout, snapshot: snapshotB, roots:
|
|
3267
|
+
for (const r of sc.installSnapshotDependencies({ checkout, snapshot: snapshotB, ...snapshotBCommit, ...(o.snapshotLedger !== undefined ? { ledger: o.snapshotLedger } : {}), roots: installRoots, ...(closure ? { packageDirs: npm.filter((u) => closure.npm.has(u.name)).map((u) => u.dir) } : {}), env: o.env, spawn: o.spawn })) o.out(`${tag} ${sc.describeInstall(r)}`);
|
|
2453
3268
|
} catch (error) { o.err(`${tag} ${error.code === "RK_CHECK_ERROR" ? "RK_CHECK_ERROR" : "BLOCKED"}: ${error.message}`); return error.code === "RK_CHECK_ERROR" ? 2 : 1; }
|
|
2454
3269
|
// A broker host (item 7 rows 7.20 to 7.24): the compile goes through the RustRight snapshot carrier. The broker pins the output
|
|
2455
3270
|
// directory (its managed generation target), so no checkout target is resolved here; the gate's broker is built once, with the
|
|
@@ -2458,8 +3273,8 @@ export async function main(argv, io = {}) {
|
|
|
2458
3273
|
if (executor.kind === "broker" && manifests.length) {
|
|
2459
3274
|
try {
|
|
2460
3275
|
const { createSnapshotBroker } = await import("../snapshot-broker.mjs");
|
|
2461
|
-
const client = which("rustright").find((c) => path.isAbsolute(c));
|
|
2462
|
-
snapshotBroker = createSnapshotBroker({ env: o.env, platform: o.platform, checkout: root, commit: { sha: snapshotBCommit.commit, tree: snapshotBCommit.tree }, snapshot: { path: snapshotB, expectedTree: snapshotBCommit.tree }, ownership: o.buildOwnership, client, gateFile: callingGatePath(), spawn: o.spawn });
|
|
3276
|
+
const client = executor.route === LINUX_BROKER_ROUTE ? executor.client : which("rustright").find((c) => path.isAbsolute(c));
|
|
3277
|
+
snapshotBroker = discoveryBroker ?? createSnapshotBroker({ env: o.env, platform: o.platform, checkout: root, commit: { sha: snapshotBCommit.commit, tree: snapshotBCommit.tree }, snapshot: { path: snapshotB, expectedTree: snapshotBCommit.tree }, ownership: o.buildOwnership, client, gateFile: callingGatePath(), spawn: o.spawn });
|
|
2463
3278
|
} catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SNAPSHOT_BROKER"}: ${error.message}; nothing compiled`); return 1; }
|
|
2464
3279
|
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}`);
|
|
2465
3280
|
}
|
|
@@ -2491,11 +3306,9 @@ export async function main(argv, io = {}) {
|
|
|
2491
3306
|
let broker = null;
|
|
2492
3307
|
if (snapshotBroker) {
|
|
2493
3308
|
// Broker mode (the Mac, decision C): every cargo a test reaches is its own carried request. The gate's own snapshot-to-checkout
|
|
2494
|
-
// map is the only source of a request's primaryCwd
|
|
3309
|
+
// 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.
|
|
2495
3310
|
const checkoutRoot = fs.realpathSync.native(root);
|
|
2496
3311
|
const checkoutDirs = Object.fromEntries(rust.workspaces.map((ws) => [ws.cargoDir, path.join(checkoutRoot, ...(ws.cargoDir === "." ? [] : ws.cargoDir.split("/")))]));
|
|
2497
|
-
const marked = snapshotBroker.markBrokerSubmit();
|
|
2498
|
-
if (marked?.broker?.submitted !== true || marked.handle !== snapshotBroker.handle) throw new Error(`the build ownership did not record the broker submit under ${snapshotBroker.handle}`);
|
|
2499
3312
|
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
2500
3313
|
broker = { checkoutDirs, section: brokerSection(snapshotBroker) };
|
|
2501
3314
|
}
|
|
@@ -2515,7 +3328,7 @@ export async function main(argv, io = {}) {
|
|
|
2515
3328
|
// (its directory is an input above, which holds its Cargo.toml, Cargo.lock, deny.toml, .cargo and .rightkit).
|
|
2516
3329
|
...(rust.workspaces.some((w) => w.cargoDir === ".") ? ["Cargo.toml", "Cargo.lock", "deny.toml"] : []),
|
|
2517
3330
|
".cargo", "rust-toolchain.toml", "rust-toolchain", "clippy.toml", "rustfmt.toml", ...npmRootFiles, ...npmRoots.filter((r) => r !== ".").flatMap((r) => npmRootFiles.map((f) => `${r}/${f}`))])];
|
|
2518
|
-
// Batched
|
|
3331
|
+
// Batched (pathspecBatches): one argv with every input path overflows Windows' command line in a large repository.
|
|
2519
3332
|
const dirtyRuns = pathspecBatches(inputs).map((batch) => git("status", "--porcelain", "--untracked-files=all", "--", ...batch));
|
|
2520
3333
|
const dirtyFailed = dirtyRuns.find((r) => failed(r));
|
|
2521
3334
|
const dirtyFiles = dirtyFailed ? [`git status failed: ${why(dirtyFailed)}`] : dirtyRuns.flatMap((r) => text(r).split(/\r?\n/)).filter(Boolean).filter((l) => !l.includes("node_modules"));
|
|
@@ -2552,38 +3365,12 @@ export async function main(argv, io = {}) {
|
|
|
2552
3365
|
}
|
|
2553
3366
|
const rustScope = restricted ? scopeNames : null;
|
|
2554
3367
|
|
|
2555
|
-
// A broker host:
|
|
2556
|
-
// (
|
|
2557
|
-
//
|
|
2558
|
-
//
|
|
3368
|
+
// A broker host: the class A Cargo scans of the snapshot (metadata, fetch, cargo-deny, cargo-audit) are carried read-only requests that run IN
|
|
3369
|
+
// THE SNAPSHOT (scanSpawn above), so an uncommitted edit in the checkout is ignored, never refused (docs/GATE-CONTRACT.md: no dirty-tree
|
|
3370
|
+
// condition; class A reads the snapshot). The checkout is only where the RustRight client starts (it refuses to start anywhere else) and
|
|
3371
|
+
// where class B's carried compiles name their working directory; `cargoLiveRoot` is that directory, never a source.
|
|
2559
3372
|
let cargoLiveRoot = null;
|
|
2560
|
-
if (
|
|
2561
|
-
const differing = cargoInputsDiffer({ root, snapshot, manifests, spawn: (c, a, opts) => o.spawn(c, a, opts) });
|
|
2562
|
-
if (differing.length) {
|
|
2563
|
-
const resolved = git("rev-parse", "--verify", String(snapshot));
|
|
2564
|
-
o.err(`${tag} BLOCKED: the broker scans Cargo inputs in the primary checkout, and these differ from ${(failed(resolved) ? String(snapshot) : text(resolved).trim()).slice(0, 12)}: ${differing.join(", ")}; commit or restore them, then run the check again`);
|
|
2565
|
-
return 1;
|
|
2566
|
-
}
|
|
2567
|
-
// A snapshot check's cargo scans then read the checkout's WORKSPACE, which can hold more than its tracked inputs: an untracked
|
|
2568
|
-
// member, or a stray directory a members glob (crates/*) matches. Cargo's own member list there must equal the commit's (from
|
|
2569
|
-
// the snapshot), or the verdict would not be the commit's (Oracle review of 79c4427b). A checkout cargo cannot read refuses too.
|
|
2570
|
-
if (snapshotA) {
|
|
2571
|
-
const memberDiff = [];
|
|
2572
|
-
const membersOf = (base, dir) => workspaceMetadata({ root: base, spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, dir, realpath: fs.realpathSync }).members.map((m) => m.manifest).sort();
|
|
2573
|
-
for (const ws of rust.workspaces) {
|
|
2574
|
-
let inCommit; let inCheckout;
|
|
2575
|
-
try { inCommit = membersOf(repo, ws.cargoDir); } catch (e) { memberDiff.push(`${ws.cargoDir} (commit): ${e.message}`); continue; }
|
|
2576
|
-
try { inCheckout = membersOf(root, ws.cargoDir); } catch (e) { memberDiff.push(`${ws.cargoDir} (checkout): ${e.message}`); continue; }
|
|
2577
|
-
const extra = inCheckout.filter((m) => !inCommit.includes(m)); const missing = inCommit.filter((m) => !inCheckout.includes(m));
|
|
2578
|
-
if (extra.length || missing.length) memberDiff.push(`${ws.cargoDir}: checkout-only ${extra.join(", ") || "none"}; commit-only ${missing.join(", ") || "none"}`);
|
|
2579
|
-
}
|
|
2580
|
-
if (memberDiff.length) {
|
|
2581
|
-
o.err(`${tag} BLOCKED: the broker scans Cargo in the primary checkout, and cargo's workspace there is not the commit's: ${memberDiff.join("; ")}; remove or commit what differs, then run the check again`);
|
|
2582
|
-
return 1;
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
cargoLiveRoot = root;
|
|
2586
|
-
}
|
|
3373
|
+
if (executor.kind === "broker" && manifests.length && snapshot && (snapshotA || snapshotB)) cargoLiveRoot = root;
|
|
2587
3374
|
// One Cargo workspace and lock per crate (rightkit#1285): the reach between crates is the manifests' path dependencies, read from the
|
|
2588
3375
|
// gated snapshot. A crate that is selected selects its transitive dependents (checked, not re-linted); a vendored source it patches
|
|
2589
3376
|
// in selects the crate. Neither is derivable from a single lock, so a layout that cannot read them is refused, never planned narrower.
|
|
@@ -2600,14 +3387,11 @@ export async function main(argv, io = {}) {
|
|
|
2600
3387
|
externalInputs = vendorOwners(crateUnits, (u) => o.readFile(path.join(repo, u.dir, "Cargo.toml")));
|
|
2601
3388
|
} catch (e) { o.err(`${tag} BLOCKED: cannot read the path dependencies between the crate workspaces (${e.message}); their reverse dependents would go unchecked, so nothing is planned`); return 1; }
|
|
2602
3389
|
}
|
|
2603
|
-
// A broker snapshot compile: its cargo scans are carried requests that name the commit's snapshot and run from the primary checkout
|
|
2604
|
-
// (rustright refuses cargo anywhere else); the checkout is the request's working directory, never its source.
|
|
2605
|
-
if (snapshotBroker && manifests.length) cargoLiveRoot = root;
|
|
2606
3390
|
let fullPlan;
|
|
2607
3391
|
fullPlan = planScans({
|
|
2608
|
-
// Class A of a commit or tree reads only that snapshot: every scan, cargo's included, runs in the
|
|
3392
|
+
// Class A of a commit or tree reads only that snapshot: every scan, cargo's included, runs in the snapshot, never in the live
|
|
2609
3393
|
// checkout (which may hold other bytes). Compiles (class B) use the live checkout, whose tree the dirty check pinned to HEAD.
|
|
2610
|
-
changed, contentRoot: repo, liveRoot: (classAOnly && snapshot) || snapshotB ? repo : root, trustedRoot: snapshotA ?? snapshotB ?? root, includeTests: true, rustScope, cargoCommand, targetDirs, cargoShim: Boolean(cargoShim),
|
|
3394
|
+
changed, contentRoot: repo, liveRoot: (classAOnly && snapshot) || snapshotB ? repo : root, trustedRoot: snapshotA ?? snapshotB ?? root, includeTests: true, rustScope, cargoCommand: planCargoCommand, targetDirs, cargoShim: Boolean(cargoShim),
|
|
2611
3395
|
tier: fullA ? "release" : (option("--rule-tier") ?? "compile"), cargoLiveRoot,
|
|
2612
3396
|
// The quiet tests (port, pipe, desktop and wall-clock bound) are proven by the CI shards, one crate per runner, unless
|
|
2613
3397
|
// RK_GATE_QUIET_TESTS=local asks for the serial quiet lane here (no CI, or a CI outage).
|
|
@@ -2626,6 +3410,21 @@ export async function main(argv, io = {}) {
|
|
|
2626
3410
|
// broker is unreachable runs neither; the gate stops rather than skip them.
|
|
2627
3411
|
if (executor.kind === "refuse" && fullPlan.scans.some((x) => x.class === "B" || (x.kind === "test" && (!classAOnly || fullA)))) { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
|
|
2628
3412
|
let plan = fullPlan;
|
|
3413
|
+
if (closure) {
|
|
3414
|
+
let kept;
|
|
3415
|
+
try {
|
|
3416
|
+
kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units });
|
|
3417
|
+
plan = closureTools.scopedPlan(fullPlan, kept.scans, closure);
|
|
3418
|
+
} catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
|
|
3419
|
+
plan.notes = plan.notes.filter((n) => !n.includes("every Rust unit is in scope"));
|
|
3420
|
+
const classA = plan.scans.filter((s) => s.class === "A");
|
|
3421
|
+
// What actually executes: tests, executing rows and every Cargo scan (metadata, package list, deny, audit run cargo, through the broker
|
|
3422
|
+
// on a broker host), never only the rows that carry `executes`. The Cargo counts are named so a scoped run shows how much cargo it starts.
|
|
3423
|
+
const executing = classA.filter((s) => closureTools.isExecutingScan(s)).length;
|
|
3424
|
+
const cargoRun = classA.filter((s) => closureTools.isCargoScan(s)).length;
|
|
3425
|
+
const cargoExcluded = kept.dropped.filter((s) => closureTools.isCargoScan(s)).length;
|
|
3426
|
+
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}`);
|
|
3427
|
+
}
|
|
2629
3428
|
const jobs = Math.max(1, Number(o.env.RK_GATE_JOBS ?? 4) || 4);
|
|
2630
3429
|
const testJobs = Math.max(1, Number(o.env.RK_GATE_TEST_JOBS ?? jobs) || jobs);
|
|
2631
3430
|
const classBJobs = Math.max(1, Number(o.env.RK_GATE_CLASS_B_JOBS ?? 3) || 3);
|
|
@@ -2664,7 +3463,8 @@ export async function main(argv, io = {}) {
|
|
|
2664
3463
|
// Full class A of a snapshot binds like the live tree does: its bytes are the commit's (blob ids from the object database), its
|
|
2665
3464
|
// environment includes the dependencies installed into it below.
|
|
2666
3465
|
const snapFullA = fullA && Boolean(snapshotA);
|
|
2667
|
-
const
|
|
3466
|
+
const hostedFullA = fullA && Boolean(hostedCommitA);
|
|
3467
|
+
const boundFullA = liveFullA || snapFullA || hostedFullA;
|
|
2668
3468
|
const liveBase = snapFullA ? snapshotA : root;
|
|
2669
3469
|
// The live bytes full class A judges: every tracked and untracked (not ignored) file, hashed raw. Computed whether or not the
|
|
2670
3470
|
// cache is used; a failure to compute it blocks (the verdict must name the bytes it covers).
|
|
@@ -2687,6 +3487,12 @@ export async function main(argv, io = {}) {
|
|
|
2687
3487
|
if (liveFullA) {
|
|
2688
3488
|
try { live = liveTree(); } catch (error) { o.err(`${tag} BLOCKED: cannot bind full class A to the working tree's bytes (${error.message})`); return 1; }
|
|
2689
3489
|
}
|
|
3490
|
+
if (hostedFullA) {
|
|
3491
|
+
try {
|
|
3492
|
+
await verifyCommittedCheckout({ git, root, ...hostedCommitA, when: "before class A" });
|
|
3493
|
+
live = (await import("../snapshot-check.mjs")).commitFiles(root, hostedCommitA.commit, o.spawn);
|
|
3494
|
+
} catch (error) { o.err(`${tag} RK_CHECK_ERROR: ${error.message}; nothing scanned`); return 2; }
|
|
3495
|
+
}
|
|
2690
3496
|
let snapshotCommitId = null;
|
|
2691
3497
|
if (snapFullA) {
|
|
2692
3498
|
const sc = await import("../snapshot-check.mjs");
|
|
@@ -2698,7 +3504,7 @@ export async function main(argv, io = {}) {
|
|
|
2698
3504
|
live = sc.commitFiles(checkout, ct.commit, o.spawn);
|
|
2699
3505
|
// Dependencies for the checks that execute repository code: installed into the snapshot from the commit's lockfile and the
|
|
2700
3506
|
// primary checkout's existing store (coordinator decision A, rightkit#1257). A store miss stops here, naming the package.
|
|
2701
|
-
const installed = sc.installSnapshotDependencies({ checkout, snapshot: snapshotA, roots:
|
|
3507
|
+
const installed = sc.installSnapshotDependencies({ checkout, snapshot: snapshotA, ...ct, roots: installRoots, ...(closure ? { packageDirs: npm.filter((u) => closure.npm.has(u.name)).map((u) => u.dir) } : {}), env: o.env, spawn: o.spawn });
|
|
2702
3508
|
for (const r of installed) o.out(`${tag} ${sc.describeInstall(r)}`);
|
|
2703
3509
|
} catch (error) { o.err(`${tag} ${error.code === "RK_CHECK_ERROR" ? "RK_CHECK_ERROR" : "BLOCKED"}: ${error.message}`); return error.code === "RK_CHECK_ERROR" ? 2 : 1; }
|
|
2704
3510
|
}
|
|
@@ -2727,7 +3533,8 @@ export async function main(argv, io = {}) {
|
|
|
2727
3533
|
// The verdict also depends on what is not in the tree: the gate's own code and advisory data, the node and pnpm that run
|
|
2728
3534
|
// the checks, the installed dependencies (pnpm's install state) and the platform. Any change there is a cache miss.
|
|
2729
3535
|
const environment = classAEnvironmentDigest({ root: liveBase, gatePackage: o.gatePackage, files, spawn: o.spawn });
|
|
2730
|
-
|
|
3536
|
+
const digest = buildCacheDigest({ objectId, headTree: digestOf(files), rustUnits: rust.units, npmUnits: npm, cargoDir: ".", rustLocks, crateFeatures, listFiles, readBlob });
|
|
3537
|
+
cache = { ...store, digest: (scan) => { const key = digest(scan); return key === null ? null : `${key}:${environment}:${plan.scopeDigest ?? "repository"}:${plan.scanSetDigest}`; } };
|
|
2731
3538
|
} catch { cache = null; }
|
|
2732
3539
|
}
|
|
2733
3540
|
if (!classAOnly && !args.includes("--no-cache") && o.env.RK_GATE_CACHE !== "0") {
|
|
@@ -2757,6 +3564,18 @@ export async function main(argv, io = {}) {
|
|
|
2757
3564
|
}
|
|
2758
3565
|
} catch { cache = null; }
|
|
2759
3566
|
}
|
|
3567
|
+
// The scan-tool policy (RA-18 / L9-03): the analyzers class A accepts (shellcheck, actionlint, cargo-deny, cargo-audit) run only from an enrolled
|
|
3568
|
+
// executable. It comes from this host's enrolment record under the managed data root, else (RightKit's own checkout only) from the RUNNING
|
|
3569
|
+
// gate's revision of ci/rk-pipeline.json `tools` (scan-tool-enrolment.mjs), never the checked repository, the snapshot or an environment variable.
|
|
3570
|
+
// An unreadable or malformed record refuses the run: a scan tool is never judged against a policy that could not be read.
|
|
3571
|
+
let scanTools;
|
|
3572
|
+
try {
|
|
3573
|
+
scanTools = await (o.loadScanToolPolicy ?? (async (q) => (await import("../scan-tool-enrolment.mjs")).loadScanToolPolicy(q)))({
|
|
3574
|
+
dataRoot: resolveManagedRoots({ env: o.env, platform: o.platform }).dataRoot, platform: o.platform, gateFile: callingGatePath(), checkout: root, fromPipeline: scanToolPolicyFromPipeline, // reads the gate's own repository with real git, never the run's (possibly injected) spawn
|
|
3575
|
+
});
|
|
3576
|
+
} catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SCAN_TOOLS"}: ${error.message}; no scan ran`); return 1; }
|
|
3577
|
+
const enrolledTools = Object.keys(scanTools?.policy ?? {});
|
|
3578
|
+
if (enrolledTools.length) o.out(`${tag} scan-tool policy: ${enrolledTools.map((name) => `${name} (${scanTools.sources?.[name] ?? "enrolled"})`).join(", ")}`);
|
|
2760
3579
|
// The direct compile's freshness census (row 7.19): the fingerprint units before the scans, and the fingerprint log of each cargo
|
|
2761
3580
|
// process that compiles (CARGO_LOG on that process only, read from its own standard error). A broker host's census is the broker's.
|
|
2762
3581
|
const census = snapshotB && !snapshotBroker ? createFreshnessCensus({ targetDirs }) : null;
|
|
@@ -2765,6 +3584,20 @@ export async function main(argv, io = {}) {
|
|
|
2765
3584
|
const scanResult = await (o.runScans ?? runScans)(plan, {
|
|
2766
3585
|
classes: classAOnly ? ["A"] : ["A", "B"], jobs, testJobs, classBJobs, signal: ac.signal, ...(cache ? { cache } : {}),
|
|
2767
3586
|
...(census ? { spawn: census.wrapSpawn(defaultSpawnAsync) } : {}),
|
|
3587
|
+
// 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
|
|
3588
|
+
// executor's validated client, never a PATH lookup (withDeclaredClient).
|
|
3589
|
+
...(declaredClient && !scanSpawn ? { spawn: withDeclaredClient(census ? census.wrapSpawn(defaultSpawnAsync) : defaultSpawnAsync) } : {}),
|
|
3590
|
+
toolPolicy: scanTools?.policy ?? {},
|
|
3591
|
+
// The class A cargo reads (deny, audit, metadata, fetch) are carried requests the broker executes (scanSpawn): cargo-deny and cargo-audit are then the
|
|
3592
|
+
// broker's registered tools, in the check stage too (which has no snapshotBroker). Only when the reads really go to the broker.
|
|
3593
|
+
...(executor.kind === "broker" && scanSpawn ? { cargoReadsCarried: true } : {}),
|
|
3594
|
+
// A broker host: every Cargo call a class A scan makes inside the snapshot is a carried read-only request; the gate's own temporary files
|
|
3595
|
+
// (cargo-deny's metadata and config) live in a gate-owned directory the broker can read, outside the checkout.
|
|
3596
|
+
...(scanSpawn ? { spawn: scanSpawn, tmpRoot: gateTmpRoot({ env: o.env, platform: o.platform, exists: o.exists }) } : {}),
|
|
3597
|
+
// A host with no broker runs its Class A Cargo scans itself, in the snapshot: cargo would write target/.rustc_info.json beside each manifest
|
|
3598
|
+
// it reads and the snapshot would no longer be the commit's tree. Each such scan gets an output directory of its own under the gate's
|
|
3599
|
+
// scratch root, removed when it ends (deterministic-scans.mjs cargoReadTargetDir). A broker host carries these reads and the broker owns the output.
|
|
3600
|
+
...(!scanSpawn ? { cargoReadTargetRoot: gateTmpRoot({ env: o.env, platform: o.platform, exists: o.exists }) } : {}),
|
|
2768
3601
|
// The runtime cargo wrapper is first on PATH and named in CARGO for every scan of a snapshot compile; CARGO (a path named by the
|
|
2769
3602
|
// wrapper's identity) is in every scan's cache key, so no result cached without the wrapper or with another map is reused.
|
|
2770
3603
|
...(cargoShim ? { env: cargoShim.scanEnv, ...(cargoShim.sentinel ? { cargoSentinel: cargoShim.sentinel } : {}), cargoLedger: { dir: cargoShim.ledgerDir } } : {}),
|
|
@@ -2775,14 +3608,14 @@ export async function main(argv, io = {}) {
|
|
|
2775
3608
|
// `executes`) run in the full gate (--all, the push hook, the door's compile stage), after the content checks and before any compile.
|
|
2776
3609
|
...(classAOnly && !fullA ? { exclude: (s) => s.executes === true } : {}),
|
|
2777
3610
|
// A snapshot check names its commit; it never reads the checkout's HEAD.
|
|
2778
|
-
quietLane: quietLeaseLane(o, { tag, reason: `rightkit-gate-quiet-tests@${snapshotCommitId ? snapshotCommitId.commit.slice(0, 12) : snapshotBCommit ? snapshotBCommit.commit.slice(0, 12) : text(git("rev-parse", "--short", "HEAD")).trim()}` }),
|
|
3611
|
+
quietLane: quietLeaseLane(o, { tag, executor, reason: `rightkit-gate-quiet-tests@${snapshotCommitId ? snapshotCommitId.commit.slice(0, 12) : snapshotBCommit ? snapshotBCommit.commit.slice(0, 12) : text(git("rev-parse", "--short", "HEAD")).trim()}` }),
|
|
2779
3612
|
// On a broker-managed host the quiet suites need the lease or they are refused (never run unleashed).
|
|
2780
3613
|
requireQuietLease: executor.kind === "broker",
|
|
2781
3614
|
// Failing scans' complete output: never inside the repository, on the host's scratch root (RIGHTKIT_SCRATCH_ROOT, else the
|
|
2782
3615
|
// managed data root's runs/), in a folder unique to this run so evidence is never overwritten.
|
|
2783
3616
|
scanLogDir: (() => {
|
|
2784
3617
|
try {
|
|
2785
|
-
const base =
|
|
3618
|
+
const base = gateRunsRoot({ env: o.env, platform: o.platform });
|
|
2786
3619
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2787
3620
|
return path.join(base, "rightkit-gate-scan-logs", `${path.basename(root)}-${stamp}-${process.pid}`);
|
|
2788
3621
|
} catch { return null; }
|
|
@@ -2807,6 +3640,10 @@ export async function main(argv, io = {}) {
|
|
|
2807
3640
|
// in (gate, tools, installed dependencies, tool caches), unchanged too. A verdict whose binding moved covers nothing: a green one is
|
|
2808
3641
|
// refused and a red one names no reusable deterministic cause (rightkit#1257). Returns { env, notes } or { prefix, problem, exit }.
|
|
2809
3642
|
const judgedBinding = async () => {
|
|
3643
|
+
if (hostedFullA) {
|
|
3644
|
+
try { await verifyCommittedCheckout({ git, root, ...hostedCommitA, when: "after class A" }); }
|
|
3645
|
+
catch (error) { return { prefix: "RK_CHECK_ERROR", exit: 2, problem: `${error.message}; the verdict covers no known bytes` }; }
|
|
3646
|
+
}
|
|
2810
3647
|
if (liveFullA) {
|
|
2811
3648
|
let digestNow = null;
|
|
2812
3649
|
try { digestNow = liveTree().digest; } catch { digestNow = null; }
|
|
@@ -2844,6 +3681,8 @@ export async function main(argv, io = {}) {
|
|
|
2844
3681
|
o.out(`${tag} snapshot ${snapshotB} is still tree ${snapshotBCommit.tree} after the compile`);
|
|
2845
3682
|
// A broker host's evidence is the broker's own signed receipts (each carried scan's row.snapshotEvidence, verified above); the
|
|
2846
3683
|
// gate writes no receipt that says `executor: direct` for it.
|
|
3684
|
+
// The output directories this compile's evidence names (GB-13): the broker's signed receipts' `outputDir`, or the direct host's resolved targets.
|
|
3685
|
+
o.onCompileOutputs?.(snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir).filter(Boolean) : Object.values(targetDirs ?? {}).filter(Boolean));
|
|
2847
3686
|
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`);
|
|
2848
3687
|
else {
|
|
2849
3688
|
// The direct host's own receipt (GATE-CONTRACT "Broker interface", row 7.18): the contract's returned-evidence names minus the
|
|
@@ -2854,7 +3693,7 @@ export async function main(argv, io = {}) {
|
|
|
2854
3693
|
const requestId = randomUUID();
|
|
2855
3694
|
const exitCode = failures.length ? 1 : 0;
|
|
2856
3695
|
const diagnosticsDir = (() => {
|
|
2857
|
-
try { return path.join(
|
|
3696
|
+
try { return path.join(gateRunsRoot({ env: o.env, platform: o.platform }), "rightkit-gate-direct-diagnostics"); } catch { return path.join(os.tmpdir(), "rightkit-gate-direct-diagnostics"); }
|
|
2858
3697
|
})();
|
|
2859
3698
|
let directReceipt;
|
|
2860
3699
|
try {
|
|
@@ -2888,7 +3727,7 @@ export async function main(argv, io = {}) {
|
|
|
2888
3727
|
if (binding.exit === 2) return 2;
|
|
2889
3728
|
} else {
|
|
2890
3729
|
for (const n of binding.notes) o.out(`${tag} ${n}`);
|
|
2891
|
-
const marker = (await import("../snapshot-check.mjs")).deterministicMarker(scanResult, binding.env);
|
|
3730
|
+
const marker = (await import("../snapshot-check.mjs")).deterministicMarker(scanResult, plan.scopeDigest ? createHash("sha256").update(`${binding.env}:${plan.scopeDigest}:${plan.scanSetDigest}`).digest("hex") : binding.env);
|
|
2892
3731
|
if (marker) o.out(marker);
|
|
2893
3732
|
}
|
|
2894
3733
|
}
|
|
@@ -2905,12 +3744,12 @@ export async function main(argv, io = {}) {
|
|
|
2905
3744
|
// raw bytes>\n" lines (git hash-object --no-filters), so the broker can recompute it over its snapshot without this gate.
|
|
2906
3745
|
if (live) {
|
|
2907
3746
|
// A snapshot holds only the commit's files: every one is tracked.
|
|
2908
|
-
const trackedList = snapFullA ? null : git("ls-files", "-z", "--cached");
|
|
3747
|
+
const trackedList = snapFullA || hostedFullA ? null : git("ls-files", "-z", "--cached");
|
|
2909
3748
|
if (trackedList && failed(trackedList)) { o.err(`${tag} BLOCKED: cannot list tracked files to name the checked bytes`); return 1; }
|
|
2910
|
-
const tracked = new Set(snapFullA ? live.files : text(trackedList).split("\0").filter(Boolean).map(posix));
|
|
2911
|
-
o.out(`${tag} class A bytes tracked=${live.digestOf(live.files.filter((f) => tracked.has(f)))} all=${live.digest}${envStartWithCaches ? ` env-at-start=${envStartWithCaches}` : ""} env=${binding.env}`);
|
|
3749
|
+
const tracked = new Set(snapFullA || hostedFullA ? live.files : text(trackedList).split("\0").filter(Boolean).map(posix));
|
|
3750
|
+
o.out(`${tag} class A bytes tracked=${live.digestOf(live.files.filter((f) => tracked.has(f)))} all=${live.digest}${envStartWithCaches ? ` env-at-start=${envStartWithCaches}` : ""} env=${plan.scopeDigest ? createHash("sha256").update(`${binding.env}:${plan.scopeDigest}:${plan.scanSetDigest}`).digest("hex") : binding.env}${plan.scopeDigest ? ` scope=${plan.scopeDigest} scans=${plan.scanSetDigest}` : ""}`);
|
|
2912
3751
|
}
|
|
2913
|
-
o.out(`${tag} class A full pass: ${scanResult.scans.length} deterministic check(s), the ones that execute repository code included, over ${changed.length} file(s) of ${base}`);
|
|
3752
|
+
o.out(`${tag} class A ${plan.scopeDigest ? "scoped" : "full"} pass: ${scanResult.scans.length} deterministic check(s), the ones that execute repository code included, over ${changed.length} file(s) of ${base}`);
|
|
2914
3753
|
return 0;
|
|
2915
3754
|
}
|
|
2916
3755
|
if (classAOnly) {
|
|
@@ -2932,16 +3771,32 @@ export async function main(argv, io = {}) {
|
|
|
2932
3771
|
// The inherited locations a snapshot compile requires as absolute paths (GATE-CONTRACT "Output directory & cache").
|
|
2933
3772
|
const SNAPSHOT_ABSOLUTE_ENV = ["CARGO_HOME", "RUSTUP_HOME", "SCCACHE_DIR", "TMPDIR", "TMP", "TEMP"];
|
|
2934
3773
|
|
|
3774
|
+
/**
|
|
3775
|
+
* What the production entry (`node rightkit-gate.mjs ...`) hands `main` as its injected dependencies, and what `--route` reports: the
|
|
3776
|
+
* snapshot carrier on Mac and Dell (rightkit#1301: right-release's own Cargo calls, the Tauri CLI's and cargo-about's are carried, so
|
|
3777
|
+
* every stage compiles the commit's snapshot through the broker); nothing on Linux, which compiles direct with no broker. Tests inject
|
|
3778
|
+
* their own carrier through `main`; the contract test (door-route.test.mjs) runs the real entry. `platform` is the host whose deps are
|
|
3779
|
+
* asked for (the real entry passes none).
|
|
3780
|
+
*/
|
|
3781
|
+
export async function productionEntryDeps({ platform = process.platform, env = process.env } = {}) {
|
|
3782
|
+
// A direct Linux host carries nothing. A Linux host that DECLARED the broker route (managed-cargo.mjs linuxBrokerDeclared) gets the same carrier as the
|
|
3783
|
+
// Mac and the Dell, valid declaration or not: an invalid one is refused by the executor (kind "refuse"), never left to compile direct without a carrier.
|
|
3784
|
+
if (directPlatform(platform) && !linuxBrokerDeclared(env, platform)) return {};
|
|
3785
|
+
const { PRODUCTION_SNAPSHOT_CARRIER } = await import("../snapshot-broker.mjs");
|
|
3786
|
+
return { snapshotBroker: PRODUCTION_SNAPSHOT_CARRIER };
|
|
3787
|
+
}
|
|
3788
|
+
|
|
2935
3789
|
const invokedDirectly = (() => {
|
|
2936
3790
|
try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; }
|
|
2937
3791
|
})();
|
|
2938
3792
|
/** This gate's own resolved entry file: the broker admits a compile only when the request names the pinned trusted gate by this
|
|
2939
|
-
* path (rustright reads it from
|
|
3793
|
+
* path (rustright reads it from RUSTRIGHT_CALLING_GATE; a missing or different path is refused). */
|
|
2940
3794
|
export const callingGatePath = () => fs.realpathSync(fileURLToPath(import.meta.url));
|
|
2941
3795
|
if (invokedDirectly) {
|
|
2942
3796
|
// Every child this run starts that can reach rustright (scans, unit stages, right-release dev/test, their builds) inherits it.
|
|
2943
|
-
process.env.
|
|
2944
|
-
|
|
2945
|
-
//
|
|
2946
|
-
|
|
3797
|
+
process.env.RUSTRIGHT_CALLING_GATE = callingGatePath();
|
|
3798
|
+
delete process.env.RIGHTKIT_CALLING_GATE; // one spelling, never both
|
|
3799
|
+
// Production carries the snapshot carrier on Mac and Dell (rightkit#1301): every child that reaches Cargo is carried, so every stage
|
|
3800
|
+
// compiles the commit's snapshot. Linux takes the snapshot route direct, with no carrier. Tests inject theirs.
|
|
3801
|
+
productionEntryDeps().then((deps) => main(process.argv.slice(2), deps)).then((code) => process.exit(code), (e) => { console.error(`rightkit-gate: ${e?.stack ?? e}`); process.exit(2); });
|
|
2947
3802
|
}
|