@rightkit/release 0.6.13 → 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 -337
- 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 +14 -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/unit-gate.mjs
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { locateBrokerClient, spawnBrokerClient } from "./broker-client.mjs";
|
|
7
|
+
import { publicationRowProblem } from "./publication-outcomes.mjs";
|
|
6
8
|
import { localBuildGranted } from "./execution-policy.mjs";
|
|
7
9
|
import { readCargoPackage } from "./crate-graph.mjs";
|
|
8
10
|
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
9
11
|
import { endRun, startRun, stagesTo } from "./gate-stages.mjs";
|
|
10
|
-
import {
|
|
12
|
+
import { RIGHTKIT_REPOSITORY, buildDirectOrigin, directStageEvidence, verifyDirectOrigin } from "./direct-origin.mjs";
|
|
13
|
+
import { exclusionsFor } from "./pack-digest.mjs";
|
|
14
|
+
import { ATTEST_STAGE_NONCE_ENV, qualificationNonce } from "./provenance-verify.mjs";
|
|
15
|
+
import { verifyQualificationReceipts } from "./qualification-verify.mjs";
|
|
11
16
|
import { assignJobs, describeCommand, expectedStageCommands } from "./stage-commands.mjs";
|
|
17
|
+
import { resolveManagedRoots } from "./managed-roots.mjs";
|
|
18
|
+
import { controlPlaneProblem } from "./control-plane.mjs";
|
|
12
19
|
|
|
13
20
|
const GH = process.platform === "win32" ? "C:\\Program Files\\GitHub CLI\\gh.exe" : "/opt/homebrew/bin/gh";
|
|
14
21
|
const defaultRun = (cmd, args, options = {}) => nodeSpawnSync(cmd, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, ...options });
|
|
@@ -40,8 +47,10 @@ export function defaultStageCommands({ unit, root, platform, readJson = defaultR
|
|
|
40
47
|
return expectedStageCommands({ kind: unit.kind, unit: unit.name, config: readJson(path.join(root, "ci", "gate.config.json")), manifest, platform });
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
export function readBrokerReceipt(id, { run = defaultRun } = {}) {
|
|
44
|
-
|
|
50
|
+
export function readBrokerReceipt(id, { run = defaultRun, platform = process.platform, readFile, env = process.env, installed } = {}) {
|
|
51
|
+
// The client starts the one way every gate child starts it (broker-client.mjs): on Windows `rustright` is rustright.cmd, which node cannot
|
|
52
|
+
// spawn, so its target .exe is read from the shim and started directly.
|
|
53
|
+
const r = spawnBrokerClient(run, locateBrokerClient({ platform, env, probe: run, ...(installed ? { installed } : {}) }), ["status", id], { encoding: "utf8", timeout: 60_000, windowsHide: true, shell: false }, { platform, readFile });
|
|
45
54
|
if (r.error || r.status !== 0) throw new Error(`the broker cannot report job ${id} (rustright status failed)`);
|
|
46
55
|
let doc;
|
|
47
56
|
try { doc = JSON.parse(r.stdout); } catch { throw new Error(`the broker's status of job ${id} is not JSON`); }
|
|
@@ -50,16 +59,35 @@ export function readBrokerReceipt(id, { run = defaultRun } = {}) {
|
|
|
50
59
|
return { id, provenance: receipt.provenance };
|
|
51
60
|
}
|
|
52
61
|
/** `<bundle>/qualification.json`: the local stages that passed and the one artifact sha256 they bound, for the dispatcher. */
|
|
53
|
-
export function writeQualification({ report, artifact, unit, commit, tree, runtime, executor, write = fs.writeFileSync }) {
|
|
62
|
+
export function writeQualification({ report, artifact, unit, commit, tree, runtime, executor, directOrigin = null, write = fs.writeFileSync }) {
|
|
54
63
|
for (const stage of ["check", "compile", "package", "qualify"]) if (report.stages?.[stage]?.ok !== true) throw new Error(`release needs stage ${stage} ok before it runs`);
|
|
55
64
|
if (!artifact?.artifactFile || !/^[0-9a-f]{64}$/.test(artifact.artifactSha256 ?? "")) throw new Error("release needs the qualified artifact");
|
|
65
|
+
// An npm unit is bound hosted by its content digest (pack-digest.mjs), not by the archive's bytes; for @rightkit/release the local archive
|
|
66
|
+
// must also have held the swap helper and its signing receipt, the two entries that digest leaves out.
|
|
67
|
+
const content = {};
|
|
68
|
+
if (unit.kind === "npm") {
|
|
69
|
+
if (!/^[0-9a-f]{64}$/.test(artifact.contentSha256 ?? "")) throw new Error("release needs the qualified npm archive's content digest");
|
|
70
|
+
content.contentSha256 = artifact.contentSha256;
|
|
71
|
+
const excluded = exclusionsFor(unit.name);
|
|
72
|
+
if (excluded.length) {
|
|
73
|
+
if (!Array.isArray(artifact.contentExcluded) || [...artifact.contentExcluded].sort().join("\n") !== [...excluded].sort().join("\n")) throw new Error(`release needs the qualified ${unit.name} archive to hold ${excluded.join(" and ")}`);
|
|
74
|
+
content.contentExcluded = [...artifact.contentExcluded].sort();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
56
77
|
const receipt = { schema: QUALIFICATION_SCHEMA, unit: { kind: unit.kind, name: unit.name, version: unit.version }, commit, tree, runtime, executor: executor?.kind ?? null,
|
|
57
|
-
stages: Object.fromEntries(["check", "compile", "package", "qualify"].map((s) => [s, { ...report.stages[s] }])), artifactSha256: artifact.artifactSha256, writtenAt: new Date().toISOString() };
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
stages: Object.fromEntries(["check", "compile", "package", "qualify"].map((s) => [s, { ...report.stages[s] }])), artifactSha256: artifact.artifactSha256, ...content, writtenAt: new Date().toISOString() };
|
|
79
|
+
// GB-29: a direct Linux origin has no broker and so no signed receipts; it carries its direct-origin record instead (direct-origin.mjs), which the hosted side re-derives and never trusts
|
|
80
|
+
// by itself. Every other host owes the broker's signed receipts for the compile and package stages.
|
|
81
|
+
if (executor?.kind === "direct") {
|
|
82
|
+
if (!directOrigin) throw new Error("a direct origin's qualification needs its direct-origin record");
|
|
83
|
+
receipt.directOrigin = directOrigin;
|
|
84
|
+
} else {
|
|
85
|
+
// The broker's signed receipts for the compile and package stages are what the hosted admission verifies; without them the
|
|
86
|
+
// booleans above are only this process's say-so.
|
|
87
|
+
for (const stage of ATTESTED_STAGES) {
|
|
88
|
+
const list = report.stages[stage].receipts;
|
|
89
|
+
if (!Array.isArray(list) || list.length < 1 || list.length > MAX_STAGE_RECEIPTS) throw new Error(`release needs 1 to ${MAX_STAGE_RECEIPTS} signed broker receipts of the ${stage} stage (found ${Array.isArray(list) ? list.length : "none"})`);
|
|
90
|
+
}
|
|
63
91
|
}
|
|
64
92
|
if (JSON.stringify(receipt).length > MAX_QUALIFICATION_BYTES) throw new Error("the qualification receipt with its broker receipts exceeds the workflow dispatch input limit");
|
|
65
93
|
const file = path.join(path.dirname(artifact.artifactFile), "qualification.json");
|
|
@@ -67,17 +95,93 @@ export function writeQualification({ report, artifact, unit, commit, tree, runti
|
|
|
67
95
|
return file;
|
|
68
96
|
}
|
|
69
97
|
|
|
98
|
+
/**
|
|
99
|
+
* GB-07 (BROKER-CONTRACT section 16): the coordinator that dispatches and pre-flights a unit is the TRUSTED, INSTALLED one: the running gate's own `scripts/ci/rk-pipeline.mjs`, found from this
|
|
100
|
+
* module's own location (never from the `root` snapshot or the `checkout` the release was asked about, so a modified or planted coordinator there cannot change the candidate's logic) with the
|
|
101
|
+
* digest of its bytes, which the release reports. Throws when the gate has none beside it.
|
|
102
|
+
*/
|
|
103
|
+
export function trustedCoordinator({ moduleUrl = import.meta.url, exists = fs.existsSync, readFile = fs.readFileSync } = {}) {
|
|
104
|
+
const file = path.resolve(path.dirname(fileURLToPath(moduleUrl)), "..", "..", "scripts", "ci", "rk-pipeline.mjs");
|
|
105
|
+
if (!exists(file)) throw new Error(`the installed coordinator ${file} is not beside the running gate; dispatch and pre-flight run only the gate's own installed rk-pipeline (never one under the snapshot or the checkout), so nothing was started`);
|
|
106
|
+
return { file, sha256: createHash("sha256").update(readFile(file)).digest("hex") };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { controlPlaneProblem };
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* GB-07: the in-gate-process entry to the trusted, installed coordinator. The coordinator's own module is imported into THIS (the gate's) process, from the gate's own installed location
|
|
113
|
+
* (trustedCoordinator), and its `runPipeline` runs the canary, pre-flight, dispatch and watch as in-process calls over the explicit root, commit and qualification the release passes: no child
|
|
114
|
+
* process is started from a path under the snapshot or the checkout, and the coordinator derives no root from where it lives. `expectedSha256` is a registered pin (the digest the host enrolled for
|
|
115
|
+
* the installed coordinator); the file is hashed before and again after it is imported, so bytes that change under the gate are refused. A coordinator that sits inside the managed snapshots root
|
|
116
|
+
* or inside the source snapshot is refused (it would be the candidate's own bytes), and so is a module without the entry.
|
|
117
|
+
*/
|
|
118
|
+
export async function loadInGatePipeline({ coordinator = trustedCoordinator(), expectedSha256 = null, snapshotsRoot = null, root = null, pathApi = path, importModule = (url) => import(url), readFile = fs.readFileSync } = {}) {
|
|
119
|
+
const fold = (p) => { const r = pathApi.resolve(String(p)); return pathApi === path.win32 ? r.toLowerCase() : r; };
|
|
120
|
+
const inside = (child, parent) => { const rel = pathApi.relative(fold(parent), fold(child)); return rel === "" || (!rel.startsWith("..") && !pathApi.isAbsolute(rel)); };
|
|
121
|
+
if (!coordinator?.file || !/^[0-9a-f]{64}$/.test(coordinator.sha256 ?? "")) throw new Error("the in-gate coordinator needs its installed file and the sha256 of its bytes; nothing was started");
|
|
122
|
+
if (expectedSha256 !== null && coordinator.sha256 !== expectedSha256) throw new Error(`the installed coordinator ${coordinator.file} is ${coordinator.sha256.slice(0, 12)}, not the pinned ${String(expectedSha256).slice(0, 12)}; the gate runs only the coordinator the host registered, so nothing was started`);
|
|
123
|
+
for (const [where, parent] of [["the managed snapshots root", snapshotsRoot], ["the source snapshot", root]]) {
|
|
124
|
+
if (parent && inside(coordinator.file, parent)) throw new Error(`the coordinator ${coordinator.file} is inside ${where} ${parent}: it would be the candidate's own bytes, never the trusted installed coordinator; nothing was started`);
|
|
125
|
+
}
|
|
126
|
+
const digest = () => createHash("sha256").update(readFile(coordinator.file)).digest("hex");
|
|
127
|
+
if (digest() !== coordinator.sha256) throw new Error(`the installed coordinator ${coordinator.file} changed after its digest was taken; nothing was started`);
|
|
128
|
+
const loaded = await importModule(pathToFileURL(coordinator.file).href);
|
|
129
|
+
if (digest() !== coordinator.sha256) throw new Error(`the installed coordinator ${coordinator.file} changed while it was loaded; nothing was started`);
|
|
130
|
+
if (typeof loaded?.runPipeline !== "function") throw new Error(`the installed coordinator ${coordinator.file} has no in-gate entry (runPipeline); nothing was started`);
|
|
131
|
+
return { runPipeline: loaded.runPipeline, coordinator: { file: coordinator.file, sha256: coordinator.sha256 } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The managed snapshots root of this host (`<data root>/snapshots`), or null when the host's roots cannot be resolved. */
|
|
135
|
+
const managedSnapshotsRoot = ({ env, platform }) => { try { return (platform === "win32" ? path.win32 : path.posix).join(resolveManagedRoots({ env, platform }).dataRoot, "snapshots"); } catch { return null; } };
|
|
136
|
+
|
|
70
137
|
const requireBrokerHost = (executor) => { if (executor?.kind !== "broker") throw new Error("unit release runs only on a broker-managed host (the hosted qualification is dispatched by the RustRight rightkit-dispatch adapter); a direct host has no gate-owned release route, so no upload was started"); };
|
|
71
138
|
|
|
72
|
-
export async function releaseUnit({ unit, root, commit, executor, env, artifact, qualification, platform = process.platform, run = defaultRun, readJson = defaultReadJson, out = console.log, scratch = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch", registryBytes = defaultRegistryBytes }) {
|
|
139
|
+
export async function releaseUnit({ unit, root, checkout = root, commit, tree = null, coordinator = null, pipeline = null, executor, env, artifact, qualification, platform = process.platform, run = defaultRun, readJson = defaultReadJson, out = console.log, scratch = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch", registryBytes = defaultRegistryBytes }) {
|
|
73
140
|
requireBrokerHost(executor);
|
|
141
|
+
// GB-07: the source root and the control-plane checkout are apart, and the coordinator is the gate's own installed one.
|
|
142
|
+
const snapshotsRoot = managedSnapshotsRoot({ env, platform });
|
|
143
|
+
const apart = controlPlaneProblem({ root, checkout, snapshotsRoot, pathApi: platform === "win32" ? path.win32 : path.posix });
|
|
144
|
+
if (apart) throw new Error(`${apart}; nothing was started`);
|
|
145
|
+
// The coordinator runs in this process when the gate loaded its in-gate entry (`pipeline`, loadInGatePipeline: production); otherwise the installed file runs as a child process (the older route).
|
|
146
|
+
const installed = pipeline?.coordinator ?? coordinator ?? trustedCoordinator();
|
|
147
|
+
if (pipeline) {
|
|
148
|
+
if (typeof pipeline.runPipeline !== "function") throw new Error("the in-gate coordinator entry has no runPipeline; nothing was started");
|
|
149
|
+
const api = platform === "win32" ? path.win32 : path.posix;
|
|
150
|
+
const fold = (p) => { const r = api.resolve(String(p)); return platform === "win32" ? r.toLowerCase() : r; };
|
|
151
|
+
const inside = (child, parent) => { const rel = api.relative(fold(parent), fold(child)); return rel === "" || (!rel.startsWith("..") && !api.isAbsolute(rel)); };
|
|
152
|
+
if (snapshotsRoot && inside(installed.file, snapshotsRoot)) throw new Error(`the coordinator ${installed.file} is inside the managed snapshots root ${snapshotsRoot}: only the installed coordinator runs; nothing was started`);
|
|
153
|
+
if (inside(installed.file, root) && checkout !== root) throw new Error(`the coordinator ${installed.file} is inside the source snapshot ${root}: it would be the candidate's own bytes, never the trusted installed coordinator; nothing was started`);
|
|
154
|
+
}
|
|
74
155
|
if (typeof qualification !== "string" || !fs.existsSync(qualification)) throw new Error("release needs the unit qualification receipt (written by the gate after the qualify stage); nothing is dispatched without it");
|
|
75
156
|
const registry = unit.kind === "npm" ? "npm" : "crates";
|
|
76
|
-
|
|
157
|
+
// rk-pipeline (the canary, the preflight rows, the dispatch) makes its broker calls as ROOT and ADAPTER requests, and the broker refuses any
|
|
158
|
+
// such request whose workspace is inside a managed snapshot (service.rs "a root request cannot run inside a managed snapshot"). So it runs from
|
|
159
|
+
// the PRIMARY CHECKOUT (`checkout`; `root` is the snapshot, which stays the source of the bytes that were qualified). rk-preflight proves the
|
|
160
|
+
// commit itself: it refuses a checkout whose tracked files are modified, and every row reads HEAD's committed bytes (resolveRowWorkspace
|
|
161
|
+
// compares each consumer manifest with `git show <sha>`); rk-pipeline dispatch pins HEAD to origin/main. Here HEAD must also be the
|
|
162
|
+
// qualified commit, or the pre-flight and the dispatch would prove another commit than the one that was packaged.
|
|
163
|
+
if (checkout !== root) {
|
|
164
|
+
const head = run("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
165
|
+
if (head.status !== 0 || String(head.stdout ?? "").trim() !== commit) throw new Error(`the primary checkout ${checkout} is not at the qualified commit ${commit.slice(0, 12)} (HEAD ${String(head.stdout ?? "").trim().slice(0, 12) || "unreadable"}); the release runs rk-pipeline from the checkout, so it proves nothing else, and nothing was started`);
|
|
166
|
+
}
|
|
167
|
+
// GB-07: the coordinator judges the primary checkout (the broker refuses a root request whose workspace is inside a managed snapshot), and the candidate's logic is protected as well: HEAD is the
|
|
168
|
+
// qualified commit (above) AND the checkout holds exactly that commit's bytes: no tracked file modified and nothing untracked under scripts/ci or packages/release (an untracked sibling is
|
|
169
|
+
// importable), checked BEFORE the first coordinator step starts. A modified or newer coordinator in the live checkout therefore can never stand in for the candidate's own.
|
|
170
|
+
if (checkout !== root) {
|
|
171
|
+
const dirty = run("git", ["status", "--porcelain", "--", "scripts/ci", "packages/release"], { cwd: checkout, encoding: "utf8" });
|
|
172
|
+
const tracked = run("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: checkout, encoding: "utf8" });
|
|
173
|
+
const changed = [dirty, tracked].map((r) => (r.status === 0 ? String(r.stdout ?? "").trim() : null));
|
|
174
|
+
if (changed.some((c) => c === null)) throw new Error(`the primary checkout ${checkout} cannot be read for modified coordinator files; nothing is dispatched`);
|
|
175
|
+
const lines = [...new Set(changed.flatMap((c) => c.split("\n").filter(Boolean)))];
|
|
176
|
+
if (lines.length) throw new Error(`the primary checkout ${checkout} holds modified or untracked files (${lines.slice(0, 4).map((l) => l.trim()).join("; ")}${lines.length > 4 ? "; ..." : ""}); the coordinator that dispatches ${unit.name} must be exactly the qualified commit ${commit.slice(0, 12)}'s bytes, so nothing is dispatched`);
|
|
177
|
+
}
|
|
77
178
|
const stateFile = platform === "win32" ? "D:\\scratch\\rk-pipeline\\state.json" : "/Volumes/D/scratch/rk-pipeline/state.json";
|
|
78
|
-
const
|
|
179
|
+
const stepRun = pipeline
|
|
180
|
+
? (args) => pipeline.runPipeline({ argv: args, root: checkout, env, platform, stateFile })
|
|
181
|
+
: (args) => run(process.execPath, [installed.file, ...args], { cwd: checkout, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
182
|
+
const step = async (label, args, { accept = [0], acceptWhen = null } = {}) => {
|
|
79
183
|
out(`rightkit-gate: [release] ${label}`);
|
|
80
|
-
const r =
|
|
184
|
+
const r = await stepRun(args);
|
|
81
185
|
const accepted = r.status === 0 || (accept.includes(r.status) && (!acceptWhen || acceptWhen(r)));
|
|
82
186
|
if (!accepted) {
|
|
83
187
|
const said = `${String(r.stdout ?? "")}\n${String(r.stderr ?? "")}`.split("\n").map((l) => l.trim()).filter((l) => l && !l.includes("note:")).slice(-4).join(" | ");
|
|
@@ -85,21 +189,30 @@ export async function releaseUnit({ unit, root, commit, executor, env, artifact,
|
|
|
85
189
|
}
|
|
86
190
|
return r;
|
|
87
191
|
};
|
|
192
|
+
// The candidate's identity is explicit: the commit and tree the gate qualified and the artifact digest it packaged (never read from files under the checkout). A dispatch record for the commit is the
|
|
193
|
+
// call's own only when it names the same tree and the same artifact digest; one that names another, or none, is ambiguous and never adopted.
|
|
194
|
+
const identity = { tree, artifactSha256: artifact?.artifactSha256 ?? null };
|
|
88
195
|
const findDispatch = () => {
|
|
89
196
|
const found = Object.values(readJson(stateFile).dispatches ?? {}).filter((d) => d.unit === unit.name && d.sha === commit && d.registry === registry);
|
|
90
197
|
if (found.length > 1) throw new Error(`${found.length} dispatch records for ${unit.name} at ${commit.slice(0, 12)} in rk-pipeline state; reconcile before any retry`);
|
|
91
|
-
|
|
198
|
+
const [record] = found;
|
|
199
|
+
if (record) {
|
|
200
|
+
if (identity.tree !== null && record.tree === undefined || identity.artifactSha256 !== null && record.artifactSha256 === undefined) throw new Error(`the dispatch record of ${unit.name} at ${commit.slice(0, 12)} names no tree or artifact digest (no identity), so it cannot be reconciled with this candidate; reconcile rk-pipeline state by hand`);
|
|
201
|
+
if (identity.tree !== null && record.tree !== identity.tree) throw new Error(`the dispatch record of ${unit.name} at ${commit.slice(0, 12)} names tree ${String(record.tree).slice(0, 12)}, not the candidate's ${identity.tree.slice(0, 12)}; reconcile rk-pipeline state by hand`);
|
|
202
|
+
if (identity.artifactSha256 !== null && record.artifactSha256 !== identity.artifactSha256) throw new Error(`the dispatch record of ${unit.name} at ${commit.slice(0, 12)} names artifact digest ${String(record.artifactSha256).slice(0, 12)}, not the candidate's ${identity.artifactSha256.slice(0, 12)}; reconcile rk-pipeline state by hand`);
|
|
203
|
+
}
|
|
204
|
+
return record;
|
|
92
205
|
};
|
|
93
206
|
// The pipeline canary (rk-release-canary: the candidate's `right-release doctor` against every app on both hosts, versus the
|
|
94
207
|
// registry's version) runs before the pre-flight. rk-pipeline exits 2 both for "no pipeline canary for this unit" and for any
|
|
95
208
|
// internal error, so 2 is accepted only when it says the former (reviewer, 2026-10-27); a canary error never reads as no canary.
|
|
96
|
-
step(`pipeline canary of ${unit.name} (doctor on every app, both hosts, candidate vs registry)`, ["canary", unit.name],
|
|
209
|
+
await step(`pipeline canary of ${unit.name} (doctor on every app, both hosts, candidate vs registry)`, ["canary", unit.name, "--sha", commit],
|
|
97
210
|
{ accept: [2], acceptWhen: (r) => /no pipeline canary for this unit/.test(`${r.stdout ?? ""}\n${r.stderr ?? ""}`) });
|
|
98
|
-
step(`local pre-flight of ${unit.name} through the broker`, ["preflight", unit.name, "--registry", registry]);
|
|
99
|
-
step(`broker-admitted hosted qualification dispatch of ${unit.name} at ${commit.slice(0, 12)}`, ["dispatch", unit.name, "--registry", registry, "--qualification", qualification]);
|
|
211
|
+
await step(`local pre-flight of ${unit.name} through the broker`, ["preflight", unit.name, "--registry", registry, "--sha", commit]);
|
|
212
|
+
await step(`broker-admitted hosted qualification dispatch of ${unit.name} at ${commit.slice(0, 12)}`, ["dispatch", unit.name, "--registry", registry, "--sha", commit, "--qualification", qualification]);
|
|
100
213
|
const dispatched = findDispatch();
|
|
101
214
|
if (!dispatched?.runId || !dispatched.runAttempt) throw new Error(`no dispatch record with a run for ${unit.name} at ${commit.slice(0, 12)}; nothing is claimed (reconcile rk-pipeline state before any retry)`);
|
|
102
|
-
step(`waiting for publish.yml run ${dispatched.runId}, its publish-direct upload run and the registry`, ["watch", "--wait"]);
|
|
215
|
+
await step(`waiting for publish.yml run ${dispatched.runId}, its publish-direct upload run and the registry`, ["watch", "--wait"]);
|
|
103
216
|
const done = findDispatch();
|
|
104
217
|
if (done?.status !== "published" || done.version !== unit.version || !done.uploadRunId) throw new Error(`${unit.name}: the watch ended without a published record (${done?.status ?? "none"}${done?.conclusion ? `: ${done.conclusion}` : ""}); nothing is claimed`);
|
|
105
218
|
const receipt = readPublicationReceipt({ uploadRunId: done.uploadRunId, runId: done.runId, runAttempt: done.runAttempt, run, env, scratch });
|
|
@@ -116,7 +229,10 @@ export async function releaseUnit({ unit, root, commit, executor, env, artifact,
|
|
|
116
229
|
const rows = (receipt.receipts ?? []).filter((r) => r.unit === unit.name && r.version === unit.version);
|
|
117
230
|
if (rows.length !== 1) throw new Error(`${unit.name}@${unit.version}: the publication receipt holds ${rows.length} row(s) for this unit; exactly one is required`);
|
|
118
231
|
const [row] = rows;
|
|
119
|
-
|
|
232
|
+
// The shared success definition (publication-outcomes.mjs, GB-45): `published` and the uploader's verified reconciliation of an earlier attempt's
|
|
233
|
+
// identical bytes (`published-by-earlier-attempt`) are both success; the registry readback below still has to serve the receipt's own hash.
|
|
234
|
+
const rowProblem = publicationRowProblem(row);
|
|
235
|
+
if (rowProblem) throw new Error(`${unit.name}@${unit.version}: the publication receipt of upload run ${done.uploadRunId} records no verified publication (${row.outcome}): ${rowProblem}`);
|
|
120
236
|
// Read the registry back independently of the upload run: npm's dist.shasum (sha1) and the served tarball's sha256 (the receipt
|
|
121
237
|
// row carries sha256 + sha1, not the sha512 integrity); crates: the sparse index cksum (sha256).
|
|
122
238
|
const served = await registryBytes({ unit, registry, run, env, expected: row, scratch });
|
|
@@ -126,6 +242,7 @@ export async function releaseUnit({ unit, root, commit, executor, env, artifact,
|
|
|
126
242
|
registry, runId: done.runId, runAttempt: done.runAttempt, requestId: done.requestId ?? null, uploadRunId: done.uploadRunId,
|
|
127
243
|
hostedSha256: row.sha256, hostedSha1: row.sha1 ?? null, registryVerified: true,
|
|
128
244
|
localArtifactSha256: artifact?.artifactSha256 ?? null, hostedEqualsLocal: artifact ? artifact.artifactSha256 === row.sha256 : null,
|
|
245
|
+
coordinator: installed,
|
|
129
246
|
};
|
|
130
247
|
}
|
|
131
248
|
|
|
@@ -217,7 +334,7 @@ export function knownRedEntry({ unit, root, platform, toRelease = false, readTex
|
|
|
217
334
|
* ({ start(stage), end(stage) } returning true or false) is the door's ledger verify around each stage (7.08); `envExtra` carries the
|
|
218
335
|
* Cargo output pin. Without `source` every behavior is the checkout route's, unchanged.
|
|
219
336
|
*/
|
|
220
|
-
export async function runUnitGate({ selector, to, root, commonDir, commit, tree, executor, env, platform, check, source = null, stageGuard = null, envExtra = {}, loadOwner = () => import("./local-publish.mjs"), out = console.log, err = console.error, releaser = null, resolve = resolveUnit, writeReport, fetchReceipt = readBrokerReceipt, stageCommands = defaultStageCommands, readKnownRed }) {
|
|
337
|
+
export async function runUnitGate({ selector, to, root, commonDir, commit, tree, executor, env, platform, check, source = null, stageGuard = null, envExtra = {}, loadOwner = () => import("./local-publish.mjs"), out = console.log, err = console.error, releaser = null, loadPipeline = loadInGatePipeline, resolve = resolveUnit, writeReport, fetchReceipt = readBrokerReceipt, stageCommands = defaultStageCommands, readKnownRed, verifyReceipts = verifyQualificationReceipts, selfGit = null }) {
|
|
221
338
|
const stages = stagesTo(to);
|
|
222
339
|
if (!stages) { err(`rightkit-gate: invalid unit stage ${to}`); return 2; }
|
|
223
340
|
let unit;
|
|
@@ -229,7 +346,8 @@ export async function runUnitGate({ selector, to, root, commonDir, commit, tree,
|
|
|
229
346
|
if (red) { err(`rightkit-gate: BLOCKED: ${selector} is listed in ci/known-red.json for ${red.host} (${red.key}: ${red.reason}); a known-red unit is not compiled, packaged, qualified or published. Fix the defect and remove the entry first`); return 1; }
|
|
230
347
|
} catch (error) { err(`rightkit-gate: BLOCKED: ${error.message}`); return 1; }
|
|
231
348
|
}
|
|
232
|
-
|
|
349
|
+
// GB-29: a Linux origin has no managed volume; its scratch is the gate's own state directory (XDG state), never a Mac path.
|
|
350
|
+
const scratch = platform === "win32" ? "D:\\scratch" : platform === "linux" ? path.join(env.XDG_STATE_HOME || path.join(env.HOME || "/tmp", ".local", "state"), "rightkit", "scratch") : "/Volumes/D/scratch";
|
|
233
351
|
const bundle = path.join(scratch, `rk-unit-${randomBytes(12).toString("hex")}`);
|
|
234
352
|
const argv = ["--local", unit.kind === "npm" ? "--package" : "--crate", unit.name, "--workspace", root, "--qualify-only", "--bundle", bundle];
|
|
235
353
|
const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -238,21 +356,44 @@ export async function runUnitGate({ selector, to, root, commonDir, commit, tree,
|
|
|
238
356
|
const report = { schema: "rightkit.unit-gate-report/1", unit, commit, tree, runtime, executor: executor.kind, stages: {} };
|
|
239
357
|
const save = () => { if (writeReport) writeReport(report); };
|
|
240
358
|
let state, owner, artifact, currentStage, checkTier = null;
|
|
241
|
-
// A run
|
|
242
|
-
//
|
|
243
|
-
|
|
359
|
+
// A broker-host run that reaches the qualify stage (--to qualify, or --to release) has its compile and package jobs attested: a pnpm job
|
|
360
|
+
// is sent the stage nonce, a carried Cargo call a nonce derived from it, both bound to this commit, tree, unit and stage, so each job's
|
|
361
|
+
// signed receipt can be verified hosted for exactly that and nothing else. `--to qualify` is the sanctioned no-upload route to that
|
|
362
|
+
// evidence: it writes the qualification and verifies it locally exactly as the hosted admission will, and stops there.
|
|
363
|
+
const attested = stages.includes("qualify") && executor?.kind === "broker";
|
|
364
|
+
// GB-29: a direct Linux origin has no broker to attest its stages; it records its own direct-origin evidence (direct-origin.mjs), verified locally as the hosted side will.
|
|
365
|
+
const directLinux = stages.includes("qualify") && executor?.kind === "direct" && platform === "linux";
|
|
366
|
+
const directStages = {};
|
|
367
|
+
const directQualification = () => {
|
|
368
|
+
const registry = unit.kind === "npm" ? "npm" : "crates";
|
|
369
|
+
const record = buildDirectOrigin({ repository: RIGHTKIT_REPOSITORY, commit, tree, unit, registry, platform, artifact: { artifactSha256: artifact.artifactSha256, contentSha256: artifact.contentSha256 }, stages: directStages });
|
|
370
|
+
verifyDirectOrigin(record, { repository: RIGHTKIT_REPOSITORY, commit, tree, unit, registry, ...(unit.kind === "npm" ? { contentSha256: artifact.contentSha256 } : { artifactSha256: artifact.artifactSha256 }) });
|
|
371
|
+
return writeQualification({ report, artifact, unit, commit, tree, runtime, executor, directOrigin: record });
|
|
372
|
+
};
|
|
244
373
|
const stageEnv = async (stage, action) => {
|
|
245
374
|
const run = startRun({ commonDir, commit, tree, stage });
|
|
246
|
-
const nonce = attested && ATTESTED_STAGES.includes(stage) ? {
|
|
375
|
+
const nonce = attested && ATTESTED_STAGES.includes(stage) ? { [ATTEST_STAGE_NONCE_ENV]: qualificationNonce({ commit, tree, unit: `${unit.kind}:${unit.name}`, stage }) } : {};
|
|
247
376
|
try { return await action({ ...env, ...envExtra, ...nonce, ...run.env }); } finally { endRun(run); }
|
|
248
377
|
};
|
|
378
|
+
// The hosted admission's own verification of the qualification's receipts, run here against the commit before anything is dispatched
|
|
379
|
+
// (or, for --to qualify, before the run reports success): what it would refuse, this refuses first, naming why.
|
|
380
|
+
const gitAt = selfGit ?? ((...args) => {
|
|
381
|
+
const r = defaultRun("git", args, { cwd: source?.checkout ?? root });
|
|
382
|
+
if (r.error || r.status !== 0) throw new Error(`git ${args[0]} failed`);
|
|
383
|
+
return r.stdout;
|
|
384
|
+
});
|
|
385
|
+
const selfCheck = (file) => {
|
|
386
|
+
try { verifyReceipts(fs.readFileSync(file, "utf8"), { sha: commit, git: gitAt }); }
|
|
387
|
+
catch (error) { throw new Error(`the local check of the qualification's signed broker receipts failed; the hosted admission would refuse it, so nothing is dispatched: ${String(error.message).replace(/^publication admission refused: /, "")}`); }
|
|
388
|
+
};
|
|
249
389
|
const receipts = {};
|
|
250
390
|
const collectReceipts = (stage, ids) => {
|
|
251
391
|
if (!attested || !ATTESTED_STAGES.includes(stage)) return;
|
|
252
392
|
// Every job of the stage is fetched; the stage owes a signed receipt for EVERY command it runs (hosted admission requires the same),
|
|
253
393
|
// so a missing one fails here, before any dispatch, naming it. Jobs that are none of the stage's commands are not carried.
|
|
254
394
|
const fetched = [...new Set(ids ?? [])].map((id) => fetchReceipt(id));
|
|
255
|
-
|
|
395
|
+
// A carried cargo command is bound to the directory of the primary checkout it ran in (source.checkout; stage-commands.mjs requestIs).
|
|
396
|
+
const { assigned, missing } = assignJobs(stageCommands({ unit, root, platform })[stage], fetched.map((r) => ({ id: r.id, payload: r.provenance.payload })), { checkout: source?.checkout ?? null });
|
|
256
397
|
if (missing.length) throw new Error(`the ${stage} stage's ${describeCommand(missing[0])} has no broker job receipt (${missing.length} required command(s) missing)`);
|
|
257
398
|
receipts[stage] = fetched.filter((r) => assigned.has(r.id));
|
|
258
399
|
};
|
|
@@ -277,21 +418,50 @@ export async function runUnitGate({ selector, to, root, commonDir, commit, tree,
|
|
|
277
418
|
if (checkTier !== "release" && await check("release") !== 0) throw new Error("release-tier check failed");
|
|
278
419
|
const qualified = await stageEnv(stage, (runEnv) => owner.qualifyLocalUnit(state, { env: runEnv }));
|
|
279
420
|
verifyArtifact(qualified);
|
|
280
|
-
if (!qualified.qualifiedOnly || !qualified.evidencePath || qualified.artifactSha256 !== artifact.artifactSha256) throw new Error("qualification did not bind the packaged artifact");
|
|
421
|
+
if (!qualified.qualifiedOnly || !qualified.evidencePath || qualified.artifactSha256 !== artifact.artifactSha256 || qualified.contentSha256 !== artifact.contentSha256) throw new Error("qualification did not bind the packaged artifact");
|
|
281
422
|
artifact = qualified;
|
|
282
423
|
}
|
|
283
424
|
let released = null;
|
|
284
425
|
if (stage === "release") {
|
|
285
426
|
// The qualification receipt the dispatcher carries to publish.yml: the hosted run is bound to these bytes and stages.
|
|
427
|
+
if (directLinux) {
|
|
428
|
+
// GB-29: the direct-origin qualification is written and verified exactly as the hosted side will. Dispatching it needs the publish workflow's `direct` admission input, which is the
|
|
429
|
+
// reserved publication authorization (GC-T22): nothing is dispatched from a direct host until that input exists.
|
|
430
|
+
const written = directQualification();
|
|
431
|
+
throw new Error(`RK_DIRECT_RELEASE_HELD: the direct-origin qualification ${written} was written and verified; the committed publish workflow admits \`broker\` only, and adding \`direct\` is the reserved publication authorization (GATE-CONTRACT GC-T22, GC-L01), so nothing was dispatched`);
|
|
432
|
+
}
|
|
286
433
|
requireBrokerHost(executor); // a direct host has no broker receipts to carry: refuse for that reason, not for the missing receipts
|
|
287
434
|
const qualification = writeQualification({ report, artifact, unit, commit, tree, runtime, executor });
|
|
288
|
-
|
|
435
|
+
selfCheck(qualification);
|
|
436
|
+
// GB-07: the default release runs the trusted installed coordinator IN this process (loadInGatePipeline: pinned to the gate's own installed bytes, never a file under the snapshot or the checkout);
|
|
437
|
+
// a coordinator that cannot be loaded starts nothing. An injected releaser (a test, or another route) owns its own coordinator.
|
|
438
|
+
released = await stageEnv(stage, async (runEnv) => (releaser ?? releaseUnit)({ unit, root, checkout: source?.checkout ?? root, commit, tree, executor, env: runEnv, artifact, qualification, platform, out,
|
|
439
|
+
...(releaser ? {} : { pipeline: await loadPipeline({ root: source ? root : null, snapshotsRoot: managedSnapshotsRoot({ env: runEnv, platform }), pathApi: platform === "win32" ? path.win32 : path.posix }) }) }));
|
|
289
440
|
}
|
|
290
441
|
if (stageGuard && !(await stageGuard.end(stage))) throw new Error(`RK_CHECK_ERROR: the ${stage} stage's result covers no known bytes (a snapshot byte changed, or a write is outside the ledger)`);
|
|
291
|
-
|
|
442
|
+
if (directLinux && (stage === "compile" || stage === "package")) {
|
|
443
|
+
// The stage guard confirmed the snapshot was the commit's bytes at start and at exit; without one a direct origin has no such evidence and records none.
|
|
444
|
+
if (!stageGuard) throw new Error(`a direct Linux origin needs the snapshot stage guard to evidence the ${stage} stage (RK_DIRECT_ORIGIN_REFUSED)`);
|
|
445
|
+
let command = `${stage} ${unit.kind}:${unit.name}`;
|
|
446
|
+
try { command = (stageCommands({ unit, root, platform })[stage] ?? []).map(describeCommand).join("; ") || command; } catch { /* the stage name alone identifies the command */ }
|
|
447
|
+
directStages[stage] = directStageEvidence({ stage, commit, tree, command, ...(stage === "package" ? { outputSha256: artifact.artifactSha256 } : {}) });
|
|
448
|
+
}
|
|
449
|
+
report.stages[stage] = { ok: true, ...(stage === "package" || stage === "qualify" ? { artifactFile: artifact.artifactFile, artifactSha256: artifact.artifactSha256, ...(artifact.contentSha256 ? { contentSha256: artifact.contentSha256 } : {}) } : {}), ...(receipts[stage] ? { receipts: receipts[stage] } : {}), ...(released ?? {}) };
|
|
292
450
|
save();
|
|
293
451
|
out(`rightkit-gate: [${stage}] ${unit.kind}:${unit.name} passed`);
|
|
294
452
|
}
|
|
453
|
+
if (directLinux && !stages.includes("release")) {
|
|
454
|
+
currentStage = "qualify";
|
|
455
|
+
const qualification = directQualification();
|
|
456
|
+
out(`rightkit-gate: [qualify] ${unit.kind}:${unit.name} direct-origin qualification ${qualification} verified locally; nothing was dispatched`);
|
|
457
|
+
}
|
|
458
|
+
if (attested && !stages.includes("release")) {
|
|
459
|
+
// --to qualify on a broker host: the qualification is written beside the artifact and verified as the hosted run will, and no upload exists.
|
|
460
|
+
currentStage = "qualify";
|
|
461
|
+
const qualification = writeQualification({ report, artifact, unit, commit, tree, runtime, executor });
|
|
462
|
+
selfCheck(qualification);
|
|
463
|
+
out(`rightkit-gate: [qualify] ${unit.kind}:${unit.name} qualification ${qualification} verified locally; nothing was dispatched`);
|
|
464
|
+
}
|
|
295
465
|
return 0;
|
|
296
466
|
} catch (error) {
|
|
297
467
|
if (currentStage) { report.stages[currentStage] = { ok: false }; save(); }
|
package/upload-large.mjs
CHANGED
|
File without changes
|
package/web-build.mjs
CHANGED
|
@@ -75,8 +75,8 @@ export function runWebBuild(steps, { appRoot, env = process.env, dryRun = false,
|
|
|
75
75
|
log(`${dryRun ? "[dry-run] " : ""}${[invocation.cmd, ...invocation.args].join(" ")}`);
|
|
76
76
|
if (dryRun) continue;
|
|
77
77
|
// A web build is a build: through the one door (build-launch.mjs), a broker job on a managed host.
|
|
78
|
-
const target = door(invocation.cmd, invocation.args, { env: invocation.env, cwd: invocation.cwd, exists: existsSync });
|
|
79
|
-
const result = spawnSyncFn(target.command, target.args, { cwd: invocation.cwd, env: invocation.env, stdio: "inherit" });
|
|
78
|
+
const target = door(invocation.cmd, invocation.args, { env: invocation.env, stepEnv: step.env, cwd: invocation.cwd, exists: existsSync });
|
|
79
|
+
const result = spawnSyncFn(target.command, target.args, { cwd: invocation.cwd, env: target.env ? { ...invocation.env, ...target.env } : invocation.env, stdio: "inherit" });
|
|
80
80
|
if (result.error) throw result.error;
|
|
81
81
|
if (result.status !== 0) return result.status ?? 1;
|
|
82
82
|
}
|
package/win-package-recipe.mjs
CHANGED
|
@@ -19,6 +19,9 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { spawnSync } from "./background-process.mjs";
|
|
22
|
+
import { buildLaunch, BUILD_LAUNCH_REFUSED } from "./build-launch.mjs";
|
|
23
|
+
import { doorRefusal } from "./door.mjs";
|
|
24
|
+
import { resolveCommand } from "./development.mjs";
|
|
22
25
|
import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
23
26
|
import { resolveArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
|
|
24
27
|
import { signingReceiptPath } from "./signing-receipts.mjs";
|
|
@@ -160,8 +163,23 @@ function sha256File(file, read) {
|
|
|
160
163
|
export function defaultWinPackageDeps({ appRoot, target, env }) {
|
|
161
164
|
const runSync = (command, args, options) => spawnSync(command, args, options);
|
|
162
165
|
const spawn = (command, args, options) => {
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
// `pnpm` and `node` start as argv arrays under this very node (resolveCommand: the pnpm CLI file, never the pnpm.cmd shim), with no shell: a
|
|
167
|
+
// shell would re-quote `tauri --config <json>` and let an argument inject (L8). Only a command resolved to something else keeps the old
|
|
168
|
+
// rule (a shell for anything that is not an .exe or .com on Windows).
|
|
169
|
+
const direct = command === "pnpm" || command === "node";
|
|
170
|
+
// GB-28 (BR11): a compile-capable step takes the one door (build-launch.mjs) first. On a broker-managed host that is a carried adapter job
|
|
171
|
+
// (or a refusal, before anything starts); on a direct host it is the plain command, resolved to its argv form below. Nothing here reaches
|
|
172
|
+
// the shared toolchain around the door.
|
|
173
|
+
let launch = { command, args };
|
|
174
|
+
if (direct) {
|
|
175
|
+
const stepEnv = options?.env ?? env;
|
|
176
|
+
let planned;
|
|
177
|
+
try { planned = buildLaunch(command, args, { env: stepEnv, cwd: options?.cwd ?? appRoot, exists: existsSync }); }
|
|
178
|
+
catch (error) { if (error?.code === BUILD_LAUNCH_REFUSED) return { error, status: null, signal: null, stdout: "", stderr: String(error.message) }; throw error; }
|
|
179
|
+
launch = planned.command === command ? { ...resolveCommand(command, args, { root: options?.cwd ?? appRoot, env: stepEnv }), ...(planned.env ? { env: planned.env } : {}) } : planned;
|
|
180
|
+
}
|
|
181
|
+
const shell = !direct && process.platform === "win32" && !/\.(?:exe|com)$/i.test(command);
|
|
182
|
+
return runSync(launch.command, launch.args, { ...options, ...(launch.env ? { env: { ...(options?.env ?? env), ...launch.env } } : {}), shell, windowsHide: true, stdio: "inherit" });
|
|
165
183
|
};
|
|
166
184
|
const receiptFor = (phase) => signingReceiptPath({ appRoot, target, platform: "win", phase, stateRoot: env.RIGHT_RELEASE_STATE_ROOT });
|
|
167
185
|
return {
|
|
@@ -219,6 +237,12 @@ function locate(declared, { env, fs, split = false }) {
|
|
|
219
237
|
export function runWinPackageRecipe({ appRoot, config = {}, target, deps = {} } = {}) {
|
|
220
238
|
const plan = validateWinPackage({ appRoot, config, target });
|
|
221
239
|
const env = deps.env ?? process.env;
|
|
240
|
+
// GB-28 (GATE one entry): this exported helper is not a door of its own. With its real effects (no injected spawn) it refuses, before any
|
|
241
|
+
// compile, unless a live door run launched it; a dry run and the fakes a test injects start nothing real.
|
|
242
|
+
if (!deps.spawn && deps.dryRun !== true) {
|
|
243
|
+
const refusal = (deps.doorRefusal ?? doorRefusal)({ cwd: plan.appRoot, env, command: "win-package-recipe" });
|
|
244
|
+
if (refusal) throw new WinPackageConfigError([refusal]);
|
|
245
|
+
}
|
|
222
246
|
const base = defaultWinPackageDeps({ appRoot: plan.appRoot, target, env });
|
|
223
247
|
const d = { ...base, ...deps, env, fs: { ...base.fs, ...(deps.fs ?? {}) } };
|
|
224
248
|
const { fs } = d;
|