@rightkit/release 0.6.23 → 0.6.25
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 +11 -35
- package/admission-host.mjs +5 -2
- package/android-dev.mjs +2 -1
- package/app-check.mjs +1 -2
- package/app-closure.mjs +3 -2
- package/assembled-app.mjs +11 -3
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +83 -26
- package/build-helper-trace.mjs +264 -0
- package/build-helpers-command.mjs +184 -0
- package/build-launch.mjs +11 -75
- package/build-release.mjs +10 -11
- package/cargo-profile-contract.mjs +19 -29
- package/cargo-target-shim.mjs +31 -0
- package/cli/right-release.mjs +5 -7
- package/create-mac-updater.mjs +4 -0
- package/cross-targets.mjs +94 -0
- package/deterministic-scans.mjs +310 -20
- package/development.mjs +8 -8
- package/freshness-census.mjs +1 -1
- package/gate-stages.mjs +1 -1
- package/install-assembled-app.mjs +1 -1
- package/install-tauri-authenticode.mjs +3 -3
- package/local-publish.mjs +27 -5
- package/mac-package-recipe.mjs +55 -2
- package/managed-roots.mjs +12 -4
- package/package.json +1 -5
- package/pipeline-admission.mjs +1 -1
- package/pipeline-receipts.mjs +302 -302
- package/provenance-verify.mjs +3 -1
- package/qa-build-features.mjs +11 -2
- package/release-qa-audit.mjs +9 -27
- package/release-qa-command.mjs +23 -64
- package/release-qa-fixtures.mjs +9 -5
- package/release-qa-floor.json +0 -1
- package/release-qa-policy.mjs +9 -10
- package/release-qa-receipt.mjs +6 -44
- package/release-source-identity.mjs +1 -12
- package/release-switch-registry.json +4 -1
- package/rightkit-registry-resolver.mjs +35 -3
- package/rightkit-versions.json +2 -2
- package/sign-windows.mjs +1 -1
- package/snapshot-broker.mjs +33 -8
- package/snapshot-metadata.mjs +32 -0
- package/snapshot-request.mjs +6 -3
- package/stage-sidecar.mjs +30 -12
- package/suite-signing-policy.mjs +8 -58
- package/templates/cargo-profiles.toml +6 -25
- package/test-command.mjs +40 -140
- package/updater-shipped-key.mjs +26 -0
- package/web-build.mjs +29 -4
- package/qa-journeys-prebuilt-run.mjs +0 -18
- package/qa-journeys-prebuilt.mjs +0 -148
- package/qa-layout.mjs +0 -229
- package/qa-snapshot-run.mjs +0 -53
- package/qa-snapshot.mjs +0 -164
- package/sign-qa-bundle.mjs +0 -210
- package/suite-signing.mjs +0 -259
package/test-command.mjs
CHANGED
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
// // Expands to journeys-1, journeys-2, ...: the listed journeys run `chunk` at a time in separate processes.
|
|
14
14
|
// { name: "journeys", node: "scripts/qa-browser-functional.mjs", args: ["--keep-going"],
|
|
15
15
|
// journeys: { file: "scripts/qa-e2e-journeys.json", key: "journeys", chunk: 12, flag: "--journey" } },
|
|
16
|
-
// // A sign step (suite-signing.mjs, rightkit#1303): signs what an EARLIER suite built, in-process, with the product's one identity, then
|
|
17
|
-
// // sets RIGHTKIT_QA_APP_BINARY to the signed copy of the main binary for the suites after it. It is never carried and has no cmd or node.
|
|
18
|
-
// { name: "sign-app", sign: { files: [{ from: "build-app", path: "aarch64-apple-darwin/debug/heardright" }] } },
|
|
19
16
|
// ],
|
|
20
17
|
// }
|
|
21
18
|
//
|
|
@@ -29,14 +26,10 @@ import { buildLaunch } from "./build-launch.mjs";
|
|
|
29
26
|
import path from "node:path";
|
|
30
27
|
import { pathToFileURL } from "node:url";
|
|
31
28
|
import { spawn } from "./background-process.mjs";
|
|
32
|
-
import { carriedCargoMapOf, carriedLedgerIds, carriedOutputDirsSince } from "./carried-cargo.mjs";
|
|
33
|
-
import { GATE_TARGET_DIRS_ENV } from "./qa-layout.mjs";
|
|
34
29
|
import { CommandSpecError, loadAppConfig, resolveInvocation, validateStep } from "./command-spec.mjs";
|
|
35
30
|
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
36
31
|
import { fsListRustFiles } from "./signing-files.mjs";
|
|
37
|
-
import {
|
|
38
|
-
import { SIGN_STEP_KEYS, QA_APP_BINARY_ENV, QA_SIGNING_EVIDENCE_ENV, QA_SIGNED_FILES_ENV, SuiteSigningError, assertPlanSigningPolicy, assertSuiteSigningPolicy, validateSignSpec } from "./suite-signing-policy.mjs";
|
|
39
|
-
import { runSignStep } from "./suite-signing.mjs";
|
|
32
|
+
import { SuiteSigningError, assertPlanSigningPolicy, assertSuiteSigningPolicy } from "./suite-signing-policy.mjs";
|
|
40
33
|
|
|
41
34
|
const fail = (message) => { throw new CommandSpecError(message); };
|
|
42
35
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -122,18 +115,6 @@ export function planTests(config, { suites = [], runner, passthrough = [], appRo
|
|
|
122
115
|
const declared = new Set();
|
|
123
116
|
tests.suites.forEach((entry, index) => {
|
|
124
117
|
const label = `tests.suites[${index}]`;
|
|
125
|
-
// A sign step is data, not a command: `{ name, sign: { files: [{ from, path }] } }`, judged by the policy and run in-process (runSuites).
|
|
126
|
-
if (isObject(entry) && entry.sign !== undefined) {
|
|
127
|
-
const extra = Object.keys(entry).filter((key) => !SIGN_STEP_KEYS.includes(key));
|
|
128
|
-
if (extra.length) throw new SuiteSigningError("SIGN_STEP_SHAPE", `${label} is a sign step and has unknown key(s): ${extra.join(", ")} (allowed: ${SIGN_STEP_KEYS.join(", ")}); it has no cmd, node, args or env`);
|
|
129
|
-
if (typeof entry.name !== "string" || !NAME.test(entry.name)) fail(`${label}.name must be set (letters, digits, . _ -)`);
|
|
130
|
-
if (seen.has(entry.name) || declared.has(entry.name)) fail(`${label}: duplicate suite name ${entry.name}`);
|
|
131
|
-
const sign = validateSignSpec(entry.sign, `${label}.sign`, declared);
|
|
132
|
-
seen.add(entry.name); declared.add(entry.name);
|
|
133
|
-
// A sign step needs the suites whose outputs it signs (GB-44): if one of them failed or was skipped, there is nothing to sign.
|
|
134
|
-
planned.push({ name: entry.name, group: entry.name, sign, step: null, extraArgs: [], requires: [...new Set(sign.files.map((file) => file.from))] });
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
118
|
validateStep(entry, label, ["name", "runner", "journeys", "requires"]);
|
|
138
119
|
// `requires` (GB-44): the earlier suites (by name or group) whose success this suite's inputs depend on; it is skipped, with the reason recorded,
|
|
139
120
|
// when one of them failed or was skipped, while every independent suite still runs.
|
|
@@ -161,26 +142,11 @@ export function planTests(config, { suites = [], runner, passthrough = [], appRo
|
|
|
161
142
|
}
|
|
162
143
|
if (passthrough.length) {
|
|
163
144
|
if (selected.length !== 1) fail("arguments after -- need exactly one selected suite (or use --runner)");
|
|
164
|
-
if (selected[0].sign) fail("arguments after -- cannot go to a sign step");
|
|
165
145
|
selected = [{ ...selected[0], extraArgs: [...passthrough] }];
|
|
166
146
|
}
|
|
167
147
|
return selected;
|
|
168
148
|
}
|
|
169
149
|
|
|
170
|
-
/** The refusal code of a planned `rightkit-qa journeys run` suite (GB-79). */
|
|
171
|
-
/** RIGHTKIT_GATE_TARGET_DIRS: the absolute directories the gate pinned; none when unset or malformed. */
|
|
172
|
-
function directOutputDirs(env) {
|
|
173
|
-
try { const dirs = JSON.parse(env[GATE_TARGET_DIRS_ENV] ?? "[]"); return Array.isArray(dirs) ? dirs.filter((d) => typeof d === "string" && path.isAbsolute(d)) : []; } catch { return []; }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export const QA_JOURNEYS_UNSUPPORTED = "RK_QA_JOURNEYS_PREBUILT_UNSUPPORTED";
|
|
177
|
-
/** True when a suite step is `rightkit-qa journeys run ...` (by command or by node script name, extra args included). */
|
|
178
|
-
export function qaJourneysRunStep(step, extraArgs = []) {
|
|
179
|
-
const words = [step.cmd ?? step.node, ...(step.args ?? []), ...extraArgs].filter((w) => typeof w === "string").map((w) => w.replaceAll("\\", "/"));
|
|
180
|
-
const at = words.findIndex((w) => /(^|\/)rightkit-qa(\.exe)?$/.test(w));
|
|
181
|
-
return at >= 0 && words[at + 1] === "journeys" && words[at + 2] === "run";
|
|
182
|
-
}
|
|
183
|
-
|
|
184
150
|
export function eventPrefix(config) {
|
|
185
151
|
const prefix = config?.tests?.eventPrefix ?? "test";
|
|
186
152
|
if (typeof prefix !== "string" || !PREFIX.test(prefix)) fail("tests.eventPrefix must be lower-case letters, digits and underscores");
|
|
@@ -191,12 +157,8 @@ export function eventPrefix(config) {
|
|
|
191
157
|
* Run the planned suites in order, collecting every reachable failure (GB-44): a failed suite does not stop an independent one, a suite that requires a
|
|
192
158
|
* failed or skipped one is skipped with its reason, and the run ends non-zero. `spawnFn` and `log` are injectable.
|
|
193
159
|
* Resolves `{ exitCode, results: [{ suite, exitCode, durationMs, skipped?, reason?, error? }] }`; a signal (cancellation) rejects.
|
|
194
|
-
* A `sign` step (suite-signing.mjs) runs IN THIS PROCESS, never through `door` (it is no command and never a broker job); it signs what an earlier
|
|
195
|
-
* suite built, named by that suite's verified carried receipts, and only a success sets RIGHTKIT_QA_APP_BINARY for the suites after it.
|
|
196
|
-
* On a direct host (no carrier) a suite's outputs are the Cargo output directories the gate pinned for the stage (RIGHTKIT_GATE_TARGET_DIRS).
|
|
197
|
-
* `within({ outputs, env })`, when given, runs once every suite passed and before the signed copies are removed; its result is returned as `within`.
|
|
198
160
|
*/
|
|
199
|
-
export async function runSuites(plan, { appRoot, prefix = "test", env = process.env, spawnFn = spawn, log = console.log, errLog = console.error, door = buildLaunch, config = null, platform = process.platform
|
|
161
|
+
export async function runSuites(plan, { appRoot, prefix = "test", env = process.env, spawnFn = spawn, log = console.log, errLog = console.error, door = buildLaunch, config = null, platform = process.platform } = {}) {
|
|
200
162
|
const results = [];
|
|
201
163
|
// One signing policy, enforced here whatever the config's format (the route probe reads JSON configs only; a legacy .mjs config is judged now): an
|
|
202
164
|
// unsigned or ad hoc product build, or an identifier override, is refused before any suite runs (rightkit#1303). Mac and Dell only.
|
|
@@ -211,111 +173,49 @@ export async function runSuites(plan, { appRoot, prefix = "test", env = process.
|
|
|
211
173
|
errLog(`right-release test: [${error.code}] ${error.message}`);
|
|
212
174
|
return { exitCode: 1, results };
|
|
213
175
|
}
|
|
214
|
-
// GB-79 (GATE test stage/E1/E6/E11): `rightkit-qa journeys run` spawns its own broker build, which a gate-carried snapshot stage cannot allow. When the installed rightkit-qa
|
|
215
|
-
// advertises `journeys-prebuilt-input` the door carries the build itself, signs the products and hands QA the verified listing and attestation (qa-journeys-prebuilt.mjs).
|
|
216
|
-
// Without the capability it is a PLANNING failure naming it, before any suite runs; nothing is silently skipped and an empty journey set is never green. A step that names
|
|
217
|
-
// its own listing, attestation or plan flag is refused too (an input the gate did not verify). Only a gate-carried snapshot stage (a carrier map in the environment) has the
|
|
218
|
-
// one-build rule; a direct host starts it as it always did.
|
|
219
|
-
const journeys = snapshotStage ? plan.filter((item) => !item.sign && item.step && qaJourneysRunStep(item.step, item.extraArgs)) : [];
|
|
220
|
-
const unsupported = [];
|
|
221
|
-
for (const item of journeys) {
|
|
222
|
-
const argv = [item.step.cmd ?? item.step.node, ...(item.step.args ?? []), ...(item.extraArgs ?? [])].filter((w) => typeof w === "string");
|
|
223
|
-
const at = argv.findIndex((w) => /(^|[\\/])rightkit-qa(\.exe)?$/.test(w));
|
|
224
|
-
const own = journeysRunRefusal(argv.slice(at + 1));
|
|
225
|
-
if (own) { errLog(`right-release test: [${QA_JOURNEYS_UNSUPPORTED}] ${item.name}: ${own}. Nothing ran`); unsupported.push(item); continue; }
|
|
226
|
-
if (!qaCapability(argv[at], item.step)) { errLog(`right-release test: [${QA_JOURNEYS_UNSUPPORTED}] ${journeysCapabilityRefusal(item.name)}`); unsupported.push(item); }
|
|
227
|
-
}
|
|
228
|
-
if (unsupported.length) return { exitCode: 1, results };
|
|
229
|
-
// The environment later suites get: the caller's, plus RIGHTKIT_QA_APP_BINARY and RIGHTKIT_QA_SIGNING_EVIDENCE once a sign step succeeded.
|
|
230
|
-
let suiteEnv = env;
|
|
231
|
-
// The verified carried receipts' output directories of each suite that passed, by name and by group (a sign step reads them).
|
|
232
|
-
const outputs = new Map();
|
|
233
|
-
const signedDirs = [];
|
|
234
176
|
// GB-44 (BR29): every reachable failure is collected. A failed or errored suite does not stop its independent siblings; only a suite that
|
|
235
|
-
// REQUIRES a failed or skipped one (
|
|
236
|
-
// `requires`) is skipped, with the reason recorded. Cancellation (a signal) still stops the whole run, and nothing is signed or launched from a failed build.
|
|
177
|
+
// REQUIRES a failed or skipped one (its explicit `requires`) is skipped, with the reason recorded. Cancellation (a signal) still stops the whole run.
|
|
237
178
|
const status = new Map(); // suite name and group -> "passed" | "failed" | "skipped"
|
|
238
|
-
const
|
|
239
|
-
// What a suite waits for. A sign step waits for the suites it signs. Any other suite positioned after a sign step is by default a CONSUMER of its
|
|
240
|
-
// signed output (a QA runner, a launcher: nothing in its command line shows what its script reads), so it waits for every earlier sign step too;
|
|
241
|
-
// a suite that declares `requires` (even an empty list) states its own dependencies instead and so keeps running for its diagnostics.
|
|
242
|
-
// Fail closed: nothing that may consume signed bytes runs after a sign step that failed or was skipped.
|
|
243
|
-
const waitsFor = (item) => (item.sign || Array.isArray(item.requires) ? (item.requires ?? []) : signsSoFar);
|
|
179
|
+
const waitsFor = (item) => item.requires ?? [];
|
|
244
180
|
const record = (item, outcome) => { for (const key of new Set([item.name, item.group])) status.set(key, outcome); };
|
|
245
181
|
const firstFailure = () => results.find((r) => r.exitCode !== 0 && r.skipped !== true)?.exitCode ?? (results.some((r) => r.skipped) ? 1 : 0);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
log(JSON.stringify({ event: `${prefix}_suite_signed`, suite: item.name, appBinary: signed.appBinary, signing: signed.evidence }));
|
|
270
|
-
} catch (error) {
|
|
271
|
-
if (!(error instanceof SuiteSigningError)) throw error;
|
|
272
|
-
errLog(`right-release test: ${item.name}: [${error.code}] ${error.message}`);
|
|
273
|
-
exitCode = 1;
|
|
274
|
-
}
|
|
275
|
-
const durationMs = Date.now() - started;
|
|
276
|
-
log(JSON.stringify({ event: `${prefix}_suite_completed`, suite: item.name, exitCode, durationMs }));
|
|
277
|
-
results.push({ suite: item.name, exitCode, durationMs });
|
|
278
|
-
record(item, exitCode === 0 ? "passed" : "failed");
|
|
279
|
-
signsSoFar.push(item.name);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
const invocation = resolveInvocation(item.step, { appRoot, baseEnv: suiteEnv, extraArgs: item.extraArgs });
|
|
283
|
-
const before = carriedLedgerIds(env);
|
|
284
|
-
let exitCode; let failure = null;
|
|
285
|
-
try {
|
|
286
|
-
exitCode = await new Promise((resolve, reject) => {
|
|
287
|
-
// A test suite goes through the one door (build-launch.mjs): a broker job on a managed host, which builds it under the app's claim.
|
|
288
|
-
const target = door(invocation.cmd, invocation.args, { env: invocation.env, stepEnv: item.step.env, cwd: invocation.cwd, exists: existsSync });
|
|
289
|
-
const child = spawnFn(target.command, target.args, { cwd: invocation.cwd, env: target.env ? { ...invocation.env, ...target.env } : invocation.env, stdio: "inherit" });
|
|
290
|
-
child.once("error", (error) => reject(Object.assign(error, { spawnFailure: true })));
|
|
291
|
-
child.once("exit", (code, signal) => {
|
|
292
|
-
if (signal) reject(Object.assign(new Error(`${item.name} stopped by ${signal}`), { cancelled: true }));
|
|
293
|
-
else resolve(code ?? 1);
|
|
294
|
-
});
|
|
182
|
+
for (const item of plan) {
|
|
183
|
+
const unmet = waitsFor(item).filter((name) => status.has(name) && status.get(name) !== "passed");
|
|
184
|
+
if (unmet.length) {
|
|
185
|
+
const reason = `${[...new Set(unmet)].map((n) => JSON.stringify(n)).join(", ")} did not pass, and ${item.name} needs what it produces`;
|
|
186
|
+
errLog(`right-release test: ${item.name}: skipped: ${reason}`);
|
|
187
|
+
log(JSON.stringify({ event: `${prefix}_suite_skipped`, suite: item.name, reason }));
|
|
188
|
+
results.push({ suite: item.name, exitCode: null, skipped: true, reason, durationMs: 0 });
|
|
189
|
+
record(item, "skipped");
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
log(JSON.stringify({ event: `${prefix}_suite_started`, suite: item.name }));
|
|
193
|
+
const started = Date.now();
|
|
194
|
+
const invocation = resolveInvocation(item.step, { appRoot, baseEnv: env, extraArgs: item.extraArgs });
|
|
195
|
+
let exitCode; let failure = null;
|
|
196
|
+
try {
|
|
197
|
+
exitCode = await new Promise((resolve, reject) => {
|
|
198
|
+
// A test suite goes through the one door (build-launch.mjs): a broker job on a managed host, which builds it under the app's claim.
|
|
199
|
+
const target = door(invocation.cmd, invocation.args, { env: invocation.env, stepEnv: item.step.env, cwd: invocation.cwd, exists: existsSync });
|
|
200
|
+
const child = spawnFn(target.command, target.args, { cwd: invocation.cwd, env: target.env ? { ...invocation.env, ...target.env } : invocation.env, stdio: "inherit" });
|
|
201
|
+
child.once("error", (error) => reject(Object.assign(error, { spawnFailure: true })));
|
|
202
|
+
child.once("exit", (code, signal) => {
|
|
203
|
+
if (signal) reject(Object.assign(new Error(`${item.name} stopped by ${signal}`), { cancelled: true }));
|
|
204
|
+
else resolve(code ?? 1);
|
|
295
205
|
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
const durationMs = Date.now() - started;
|
|
303
|
-
// Only a suite that passed hands its receipts to a sign step.
|
|
304
|
-
if (exitCode === 0) {
|
|
305
|
-
const dirs = carriedCargoMapOf(env)?.map ? carriedOutputDirsSince(env, before) : directOutputDirs(env);
|
|
306
|
-
for (const key of new Set([item.name, item.group])) outputs.set(key, [...new Set([...(outputs.get(key) ?? []), ...dirs])]);
|
|
307
|
-
}
|
|
308
|
-
log(JSON.stringify({ event: `${prefix}_suite_completed`, suite: item.name, exitCode, durationMs }));
|
|
309
|
-
results.push({ suite: item.name, exitCode, durationMs, ...(failure ? { error: String(failure.message ?? failure) } : {}) });
|
|
310
|
-
record(item, exitCode === 0 ? "passed" : "failed");
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// A suite that cannot start (a refused launch, a spawn error) is a failed suite; a signal is a cancellation and ends the run.
|
|
209
|
+
if (error?.cancelled) throw error;
|
|
210
|
+
failure = error; exitCode = 1;
|
|
211
|
+
errLog(`right-release test: ${item.name}: ${error?.code ? `[${error.code}] ` : ""}${error?.message ?? error}`);
|
|
311
212
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// The signed copies live only as long as the run that signed them.
|
|
317
|
-
for (const dir of signedDirs) { try { rmSync(dir, { recursive: true, force: true }); } catch { /* the managed root is pruned */ } }
|
|
213
|
+
const durationMs = Date.now() - started;
|
|
214
|
+
log(JSON.stringify({ event: `${prefix}_suite_completed`, suite: item.name, exitCode, durationMs }));
|
|
215
|
+
results.push({ suite: item.name, exitCode, durationMs, ...(failure ? { error: String(failure.message ?? failure) } : {}) });
|
|
216
|
+
record(item, exitCode === 0 ? "passed" : "failed");
|
|
318
217
|
}
|
|
218
|
+
return { exitCode: results.some((r) => r.skipped !== true && r.exitCode !== 0) || results.some((r) => r.skipped) ? firstFailure() : 0, results };
|
|
319
219
|
}
|
|
320
220
|
|
|
321
221
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -332,9 +232,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
332
232
|
}
|
|
333
233
|
const plan = planTests(config, { suites: opts.suites, runner: opts.runner, passthrough: opts.passthrough, appRoot });
|
|
334
234
|
if (opts.list) {
|
|
335
|
-
const view = plan.map((item) => (
|
|
235
|
+
const view = plan.map((item) => ({ suite: item.name, ...resolveInvocation(item.step, { appRoot, baseEnv: {}, extraArgs: item.extraArgs }), env: undefined }));
|
|
336
236
|
if (opts.json) console.log(JSON.stringify(view, null, 2));
|
|
337
|
-
else for (const item of view) console.log(
|
|
237
|
+
else for (const item of view) console.log(`${item.suite}: ${[item.cmd, ...item.args].join(" ")}`);
|
|
338
238
|
return 0;
|
|
339
239
|
}
|
|
340
240
|
const { exitCode } = await runSuites(plan, { appRoot, prefix: eventPrefix(config), config });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// rightkit#1443 (hrmac, 2026-11-02): the pre-gate R2 build verified the updater signature against the public key the app SHIPS (its
|
|
2
|
+
// tauri.conf plugins.updater.pubkey, the key every installed copy checks an update with); the gate only checked a requested key id.
|
|
3
|
+
// After the updater is signed, its `.sig` must verify (minisign, github-updater-release.mjs assertUpdaterSignature) over the archive's
|
|
4
|
+
// bytes with the shipped key, or the build stops: an update signed by any other key would be refused by every installed app.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { assertUpdaterSignature } from "./github-updater-release.mjs";
|
|
8
|
+
|
|
9
|
+
/** The app's shipped updater public key: plugins.updater.pubkey of the first tauri.conf.json found (src-tauri/, then the app root). */
|
|
10
|
+
export function shippedUpdaterPublicKey(appRoot, { readFile = (f) => fs.readFileSync(f, "utf8"), exists = fs.existsSync } = {}) {
|
|
11
|
+
for (const rel of [["src-tauri", "tauri.conf.json"], ["tauri.conf.json"]]) {
|
|
12
|
+
const file = path.join(appRoot, ...rel);
|
|
13
|
+
if (!exists(file)) continue;
|
|
14
|
+
const key = JSON.parse(readFile(file))?.plugins?.updater?.pubkey;
|
|
15
|
+
if (typeof key === "string" && key.trim()) return { key: key.trim(), file };
|
|
16
|
+
return { key: null, file };
|
|
17
|
+
}
|
|
18
|
+
return { key: null, file: null };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Throws unless `<file>.sig` verifies over `file`'s bytes with the app's shipped updater key. */
|
|
22
|
+
export function assertUpdaterMatchesShippedKey({ appRoot, file, readFile = (f) => fs.readFileSync(f), readText = (f) => fs.readFileSync(f, "utf8"), exists = fs.existsSync }) {
|
|
23
|
+
const shipped = shippedUpdaterPublicKey(appRoot, { readFile: readText, exists });
|
|
24
|
+
if (!shipped.key) throw new Error(`the app ships no updater public key (plugins.updater.pubkey in ${shipped.file ?? "src-tauri/tauri.conf.json"}), so an updater artifact cannot be verified; nothing is shipped`);
|
|
25
|
+
assertUpdaterSignature(readFile(file), readText(`${file}.sig`), shipped.key, `updater ${path.basename(file)} against the shipped key in ${shipped.file}`);
|
|
26
|
+
}
|
package/web-build.mjs
CHANGED
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
20
|
import { spawnSync } from "./background-process.mjs";
|
|
21
|
-
import { existsSync } from "node:fs";
|
|
22
|
-
import { buildLaunch } from "./build-launch.mjs";
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
22
|
import { CommandSpecError, loadAppConfig, resolveInvocation, validateStep } from "./command-spec.mjs";
|
|
24
23
|
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
25
24
|
import { requireDoorRun } from "./gate-stages.mjs";
|
|
@@ -61,20 +60,46 @@ export function webBuildDoorRefusal({ appRoot, env = process.env, spawnSyncFn =
|
|
|
61
60
|
return requireDoor({ env, commonDir: context.commonDir, commit: context.commit, command: "right-release web-build" });
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Adrian, 2026-11-02 ("Yes send them", relayed by the broker owner and checked): tsc runs once, in Class A only, and vite (any web bundling)
|
|
65
|
+
* runs outside the broker. A `tsc` step that emits nothing (`--noEmit`, or `compilerOptions.noEmit` in the tsconfig it reads) only type-checks,
|
|
66
|
+
* which Class A already did for this commit, so it is skipped here. A tsc step that emits files still runs. An unreadable tsconfig keeps the step.
|
|
67
|
+
*/
|
|
68
|
+
export function typecheckOnlyTsc(invocation, { readFile = (f) => readFileSync(f, "utf8") } = {}) {
|
|
69
|
+
const script = String(invocation.args?.find((a) => /(^|[\\/])typescript[\\/]bin[\\/]tsc$/.test(String(a))) ?? "");
|
|
70
|
+
if (!script) return false;
|
|
71
|
+
const args = invocation.args.slice(invocation.args.indexOf(script) + 1).map(String);
|
|
72
|
+
if (args.includes("--noEmit")) return true;
|
|
73
|
+
if (args.some((a) => a === "--build" || a === "-b")) return false;
|
|
74
|
+
const p = args.indexOf("-p") >= 0 ? args.indexOf("-p") : args.indexOf("--project");
|
|
75
|
+
let file = p >= 0 ? args[p + 1] : "tsconfig.json";
|
|
76
|
+
if (!file) return false;
|
|
77
|
+
file = path.resolve(invocation.cwd ?? ".", file);
|
|
78
|
+
if (!/\.json$/i.test(file)) file = path.join(file, "tsconfig.json");
|
|
79
|
+
try {
|
|
80
|
+
const text = readFile(file).replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"])\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1");
|
|
81
|
+
return JSON.parse(text)?.compilerOptions?.noEmit === true;
|
|
82
|
+
} catch { return false; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The direct launch of a web-build step: never through the broker (Adrian, 2026-11-02: vite and web bundling run outside the broker). */
|
|
86
|
+
export const directWebLaunch = (cmd, args) => ({ command: cmd, args });
|
|
87
|
+
|
|
64
88
|
function doorError(refusal) { const error = new Error(refusal); error.code = "RK_DOOR"; return error; }
|
|
65
89
|
|
|
66
90
|
/**
|
|
67
91
|
* Run the steps in order under a live door run; `spawnSyncFn`, `requireDoor` and `log` are injectable. Returns the exit code. The
|
|
68
92
|
* door is checked here as well as in main: this is the only place a step is spawned.
|
|
69
93
|
*/
|
|
70
|
-
export function runWebBuild(steps, { appRoot, env = process.env, dryRun = false, spawnSyncFn = spawnSync, requireDoor = requireDoorRun, log = console.log, door =
|
|
94
|
+
export function runWebBuild(steps, { appRoot, env = process.env, dryRun = false, spawnSyncFn = spawnSync, requireDoor = requireDoorRun, log = console.log, door = directWebLaunch, readFile } = {}) {
|
|
71
95
|
const refusal = webBuildDoorRefusal({ appRoot, env, spawnSyncFn, requireDoor });
|
|
72
96
|
if (refusal) throw doorError(refusal);
|
|
73
97
|
for (const step of steps) {
|
|
74
98
|
const invocation = resolveInvocation(step, { appRoot, baseEnv: env });
|
|
99
|
+
if (typecheckOnlyTsc(invocation, readFile ? { readFile } : {})) { log(`skipped (type-check only; Class A ran tsc for this commit): ${[invocation.cmd, ...invocation.args].join(" ")}`); continue; }
|
|
75
100
|
log(`${dryRun ? "[dry-run] " : ""}${[invocation.cmd, ...invocation.args].join(" ")}`);
|
|
76
101
|
if (dryRun) continue;
|
|
77
|
-
//
|
|
102
|
+
// The web build runs directly, never as a broker job: it is cheap and not cargo (Adrian, 2026-11-02).
|
|
78
103
|
const target = door(invocation.cmd, invocation.args, { env: invocation.env, stepEnv: step.env, cwd: invocation.cwd, exists: existsSync });
|
|
79
104
|
const result = spawnSyncFn(target.command, target.args, { cwd: invocation.cwd, env: target.env ? { ...invocation.env, ...target.env } : invocation.env, stdio: "inherit" });
|
|
80
105
|
if (result.error) throw result.error;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// `node qa-journeys-prebuilt-run.mjs <rightkit-qa> -- journeys run <args>`: one `rightkit-qa journeys run` step of a snapshot stage (GB-79, capability
|
|
3
|
-
// `journeys-prebuilt-input`) as the gate's build-once route (qa-journeys-prebuilt.mjs): carry the build, sign the products, hand QA the verified input.
|
|
4
|
-
// build-launch.mjs hands such a step to this entry instead of refusing it.
|
|
5
|
-
import { carriedCargoMapOf } from "./carried-cargo.mjs";
|
|
6
|
-
import { runJourneysPrebuilt } from "./qa-journeys-prebuilt.mjs";
|
|
7
|
-
|
|
8
|
-
const stop = (message, status = 101) => { process.stderr.write(`rightkit-gate: ${message}\n`); process.exit(status); };
|
|
9
|
-
const [qa, separator, ...args] = process.argv.slice(2);
|
|
10
|
-
if (!qa || separator !== "--") stop("RK_QA_JOURNEYS_USAGE: usage: qa-journeys-prebuilt-run.mjs <rightkit-qa> -- journeys run <args>");
|
|
11
|
-
const carried = carriedCargoMapOf(process.env);
|
|
12
|
-
if (!carried?.map) stop(`RK_QA_JOURNEYS_CARRIER_MISSING: ${carried?.error ?? "no carried map in the environment"}; a snapshot stage's build is never run around its carrier`);
|
|
13
|
-
try {
|
|
14
|
-
const { status } = runJourneysPrebuilt({ qa, args, map: carried.map });
|
|
15
|
-
process.exit(status);
|
|
16
|
-
} catch (error) {
|
|
17
|
-
stop(String(error.message ?? error).startsWith("RK_") ? error.message : `${error.code ?? "RK_QA_JOURNEYS_FAILED"}: ${error.message}`, Number.isInteger(error.status) ? error.status : 101);
|
|
18
|
-
}
|
package/qa-journeys-prebuilt.mjs
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// GB-79 (GATE test stage E1/E6/E11): `rightkit-qa journeys run` on a snapshot stage, as the gate's own build-once route (capability `journeys-prebuilt-input`).
|
|
2
|
-
//
|
|
3
|
-
// `journeys run` spawns its own `rustright cargo ... --no-run`, which a gate-carried stage cannot allow: a snapshot stage compiles once, in the carrier, and QA runs
|
|
4
|
-
// an already-built, already-signed product outside the broker. So the gate takes the build out of QA's hands:
|
|
5
|
-
// 1. ask QA what it would build, without building (`journeys run ... --print-build-plan`: the declared build command and each artifact role's marker policy);
|
|
6
|
-
// 2. carry that build (`cargo test ... --no-run --message-format=json`) through the carrier: one signed, ledgered broker request, its receipt verified;
|
|
7
|
-
// 3. have QA name the executable each role resolves to in the carried listing (`--print-build-plan --from-cargo-json <listing>`), and check each lies inside the
|
|
8
|
-
// receipt's verified output directory;
|
|
9
|
-
// 4. sign every product (a role whose marker is required) with the product's one identity (suite-signing.mjs runSignStep), outside the broker, from the receipt's
|
|
10
|
-
// output directory; a product that cannot be signed refuses and nothing launches;
|
|
11
|
-
// 5. write the attestation (`rightkit.journeys.prebuilt-input/1`: the broker request id and digest, the listing's sha256, the carried command, the sha256 of
|
|
12
|
-
// every executable QA will launch) and run QA directly with `--from-cargo-json <listing> --prebuilt-attestation <file>`, the signing evidence in its environment.
|
|
13
|
-
// QA verifies all of it again (crates/rightkit-qa/src/journeys/prebuilt.rs build_from_prebuilt) and never compiles. An empty journey set is never green: the plan
|
|
14
|
-
// step fails when nothing is selected, QA's own verdict needs executed journeys, and an empty listing is refused on both sides.
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
16
|
-
import fs from "node:fs";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
import crypto from "node:crypto";
|
|
19
|
-
import { spawnSync } from "./background-process.mjs";
|
|
20
|
-
import { runCarriedCargo } from "./carried-cargo.mjs";
|
|
21
|
-
import { isInside, realish } from "./qa-snapshot.mjs";
|
|
22
|
-
import { runSignStep } from "./suite-signing.mjs";
|
|
23
|
-
import { QA_APP_BINARY_ENV, QA_SIGNED_FILES_ENV, QA_SIGNING_EVIDENCE_ENV } from "./suite-signing-policy.mjs";
|
|
24
|
-
|
|
25
|
-
export const JOURNEYS_PREBUILT_CAPABILITY = "journeys-prebuilt-input";
|
|
26
|
-
export const PREBUILT_SCHEMA = "rightkit.journeys.prebuilt-input/1";
|
|
27
|
-
export const BUILD_PLAN_SCHEMA = "rightkit.journeys.build-plan/1";
|
|
28
|
-
/** The suite name the sign step reads the carried receipt's output directory under. */
|
|
29
|
-
export const JOURNEYS_BUILD_SUITE = "journeys-build";
|
|
30
|
-
/** The exact key set of the attestation (QA reads it with unknown fields denied). */
|
|
31
|
-
export const ATTESTATION_KEYS = Object.freeze(["schema", "request_id", "request_sha256", "listing_sha256", "command", "outputs"]);
|
|
32
|
-
|
|
33
|
-
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
34
|
-
const words = (args) => args.map(String);
|
|
35
|
-
const OWN_FLAGS = ["--from-cargo-json", "--prebuilt-attestation", "--print-build-plan"];
|
|
36
|
-
const flagOf = (w) => { const at = w.indexOf("="); return at > 0 && w.startsWith("--") ? w.slice(0, at) : w; };
|
|
37
|
-
|
|
38
|
-
/** True when this QA argv is `journeys run ...`. */
|
|
39
|
-
export const isJourneysRun = (args) => { const w = words(args); return w[0] === "journeys" && w[1] === "run"; };
|
|
40
|
-
|
|
41
|
-
/** Why a `journeys run` argv cannot be carried, or null: the listing and attestation are the gate's to write, never the step's (an input the gate did not verify). */
|
|
42
|
-
export function journeysRunRefusal(args) {
|
|
43
|
-
const own = words(args).filter((w) => w.startsWith("-")).map(flagOf);
|
|
44
|
-
const named = OWN_FLAGS.find((flag) => own.includes(flag));
|
|
45
|
-
return named ? `${named} is the gate's own: a journeys step names no listing, attestation or plan flag (the gate carries the build and hands QA the verified input)` : null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Whether the installed `rightkit-qa` implements the prebuilt journey input: its `journeys run --help` names the capability. A binary that cannot be asked, or does
|
|
50
|
-
* not name it, has no such mode.
|
|
51
|
-
*/
|
|
52
|
-
export function qaPrebuiltCapability(qa, { spawn = spawnSync, env = process.env } = {}) {
|
|
53
|
-
try {
|
|
54
|
-
const r = spawn(qa, ["journeys", "run", "--help"], { encoding: "utf8", env, windowsHide: true, timeout: 20000 });
|
|
55
|
-
return r?.status === 0 && String(r.stdout ?? "").includes(JOURNEYS_PREBUILT_CAPABILITY);
|
|
56
|
-
} catch { return false; }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** The refusal text of a planned journeys step on a snapshot stage whose QA lacks the capability, naming it. */
|
|
60
|
-
export const journeysCapabilityRefusal = (name) => `${name}: \`rightkit-qa journeys run\` needs capability "${JOURNEYS_PREBUILT_CAPABILITY}" and the installed rightkit-qa does not advertise it (\`rightkit-qa journeys run --help\`): without it QA would compile through the broker itself, and a gate-carried stage must build once, sign, then run prebuilt bytes. Nothing ran`;
|
|
61
|
-
|
|
62
|
-
const fail = (code, message, status = 101) => Object.assign(new Error(`${code}: ${message}`), { code, status });
|
|
63
|
-
|
|
64
|
-
function parsePlan(result, what) {
|
|
65
|
-
if (result?.error) throw fail("RK_QA_JOURNEYS_PLAN", `${what}: rightkit-qa did not start (${result.error.message})`);
|
|
66
|
-
if (result?.status !== 0) throw fail("RK_QA_JOURNEYS_PLAN", `${what}: rightkit-qa exited ${result?.status}: ${String(result?.stderr ?? "").trim().slice(0, 600) || "no message"}; nothing was built`);
|
|
67
|
-
let plan;
|
|
68
|
-
try { plan = JSON.parse(String(result.stdout).trim().split(/\r?\n/).filter(Boolean).pop() ?? ""); } catch { throw fail("RK_QA_JOURNEYS_PLAN", `${what}: rightkit-qa printed no build plan`); }
|
|
69
|
-
if (plan?.schema !== BUILD_PLAN_SCHEMA || !Array.isArray(plan.command) || !Array.isArray(plan.artifacts) || typeof plan.cwd !== "string") throw fail("RK_QA_JOURNEYS_PLAN", `${what}: the build plan is not a ${BUILD_PLAN_SCHEMA} document`);
|
|
70
|
-
return plan;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The gate's build-once run of one `journeys run` step. `args` is QA's argv (`journeys run ...`); `map` is the carrier map. Resolves `{ status }` (QA's own exit status)
|
|
75
|
-
* or throws an Error with a typed `code` (nothing ran, or QA was never started). Every effect is injectable: `spawn` (QA), `carry` (the carrier), `signStep`, `loadConfig`.
|
|
76
|
-
*/
|
|
77
|
-
export function runJourneysPrebuilt({ qa, args, cwd = process.cwd(), env = process.env, map, platform = process.platform, spawn = spawnSync, carry = runCarriedCargo, signStep = runSignStep, signDeps = {}, loadConfig = defaultConfig, log = (m) => process.stderr.write(`${m}\n`) }) {
|
|
78
|
-
const given = words(args);
|
|
79
|
-
if (!isJourneysRun(given)) throw fail("RK_QA_JOURNEYS_USAGE", `rightkit-qa ${given.join(" ")} is not a journeys run step`);
|
|
80
|
-
const own = journeysRunRefusal(given);
|
|
81
|
-
if (own) throw fail("RK_QA_JOURNEYS_FLAG", own);
|
|
82
|
-
const section = map.broker ?? {};
|
|
83
|
-
const snapshot = section.snapshot?.path;
|
|
84
|
-
const checkouts = [section.checkout, env.RK_GATE_CHECKOUT].filter((c) => typeof c === "string" && c);
|
|
85
|
-
const run = (extra, what) => spawn(qa, [...given, ...extra], { cwd, env, encoding: "utf8", windowsHide: true, maxBuffer: 1 << 26 });
|
|
86
|
-
// 1. what QA would build (nothing is created or built).
|
|
87
|
-
const plan = parsePlan(run(["--print-build-plan"]), "the build plan");
|
|
88
|
-
if (plan.command[0] !== "rustright" || plan.command[1] !== "cargo" || plan.command.length < 3) throw fail("RK_QA_JOURNEYS_PLAN", `the declared build ${JSON.stringify(plan.command)} is not a \`rustright cargo ...\` command; a snapshot stage carries Cargo builds only`);
|
|
89
|
-
if (plan.artifacts.length === 0) throw fail("RK_QA_JOURNEYS_EMPTY", "the journeys build declares no artifact: an empty journey set is never green");
|
|
90
|
-
if (!snapshot || !isInside(plan.cwd, snapshot) || checkouts.some((c) => isInside(plan.cwd, c))) throw fail("RK_QA_JOURNEYS_PLACE", `the journeys build directory ${plan.cwd} is not inside the commit's snapshot${snapshot ? ` (${snapshot})` : ""}`);
|
|
91
|
-
// 2. the carried build.
|
|
92
|
-
const dir = path.join(path.dirname(map.ledgerDir), "qa-listings");
|
|
93
|
-
if (isInside(dir, snapshot) || checkouts.some((c) => isInside(dir, c))) throw fail("RK_QA_JOURNEYS_PLACE", `the listing directory ${dir} is inside the snapshot or the primary checkout`);
|
|
94
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
95
|
-
const stem = path.join(realish(dir), `${process.pid}-${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`);
|
|
96
|
-
const listingFile = `${stem}.jsonl`; const attestationFile = `${stem}.attestation.json`;
|
|
97
|
-
const cargoWords = plan.command.slice(2);
|
|
98
|
-
const built = carry(cargoWords, { cwd: plan.cwd, env, stdio: "capture" }, { map });
|
|
99
|
-
if (built.stderr) process.stderr.write(built.stderr);
|
|
100
|
-
if (built.error) throw fail("RK_QA_JOURNEYS_BUILD", built.error.message);
|
|
101
|
-
if (built.status !== 0 || built.refusal || built.outputDir === undefined || !built.requestId || !/^[0-9a-f]{64}$/.test(built.requestDigest ?? "")) {
|
|
102
|
-
throw Object.assign(fail("RK_QA_JOURNEYS_BUILD", `the carried build \`cargo ${cargoWords.join(" ")}\` did not complete with a verified receipt (exit ${built.status}${built.refusal ? `, ${built.refusal.code}` : ""}); QA did not run`), { status: built.status || 101 });
|
|
103
|
-
}
|
|
104
|
-
let signedDir = null;
|
|
105
|
-
try {
|
|
106
|
-
const listing = String(built.stdout ?? "");
|
|
107
|
-
fs.writeFileSync(listingFile, listing, { mode: 0o600 });
|
|
108
|
-
// 3. the executable of each role, as QA resolves it from this listing; each must be a file the receipt's output directory holds.
|
|
109
|
-
const resolved = parsePlan(run(["--print-build-plan", "--from-cargo-json", listingFile]), "the role to executable mapping");
|
|
110
|
-
const roles = resolved.artifacts;
|
|
111
|
-
const outputs = [];
|
|
112
|
-
for (const role of roles) {
|
|
113
|
-
if (typeof role.executable !== "string" || !role.executable) throw fail("RK_QA_JOURNEYS_PLAN", `role ${role.role} resolves to no executable in the carried listing`);
|
|
114
|
-
const real = realish(role.executable);
|
|
115
|
-
if (!isInside(real, built.outputDir)) throw fail("RK_QA_JOURNEYS_PLACE", `role ${role.role}: ${role.executable} is not inside the receipt's output directory ${built.outputDir}`);
|
|
116
|
-
let data;
|
|
117
|
-
try { if (!fs.statSync(real).isFile()) throw new Error("not a file"); data = fs.readFileSync(real); } catch (e) { throw fail("RK_QA_JOURNEYS_PLACE", `role ${role.role}: ${role.executable} cannot be read (${e.message})`); }
|
|
118
|
-
outputs.push({ role: role.role, marker: role.marker, path: role.executable, real, sha256: sha256(data) });
|
|
119
|
-
}
|
|
120
|
-
if (outputs.length === 0) throw fail("RK_QA_JOURNEYS_EMPTY", "the carried build resolved no executable: an empty journey set is never green");
|
|
121
|
-
// 4. sign every product before anything launches.
|
|
122
|
-
let signEnv = {};
|
|
123
|
-
const products = outputs.filter((o) => o.marker === "required");
|
|
124
|
-
if (products.length) {
|
|
125
|
-
let signed;
|
|
126
|
-
try {
|
|
127
|
-
const files = products.map((o) => ({ from: JOURNEYS_BUILD_SUITE, path: path.relative(realish(built.outputDir), o.real).split(path.sep).join("/") }));
|
|
128
|
-
signed = signStep({ name: "journeys-sign", sign: { files }, appRoot: cwd, config: loadConfig(cwd), env, platform, outputDirsOf: (suite) => (suite === JOURNEYS_BUILD_SUITE ? [built.outputDir] : null), deps: signDeps, log });
|
|
129
|
-
} catch (e) { throw fail(e.code ?? "RK_QA_JOURNEYS_UNSIGNED", `a product of the journeys build could not be signed: ${e.message}; an unsigned product is never launched, and QA did not run`); }
|
|
130
|
-
signedDir = signed.signedDir;
|
|
131
|
-
signEnv = { [QA_SIGNING_EVIDENCE_ENV]: signed.evidenceFile, [QA_SIGNED_FILES_ENV]: JSON.stringify(signed.signedFiles ?? {}), ...(signed.appBinary ? { [QA_APP_BINARY_ENV]: signed.appBinary } : {}) };
|
|
132
|
-
}
|
|
133
|
-
// 5. the attestation, then QA directly against the verified input.
|
|
134
|
-
const attestation = { schema: PREBUILT_SCHEMA, request_id: built.requestId, request_sha256: built.requestDigest, listing_sha256: sha256(Buffer.from(listing)), command: [...plan.command.slice(1)], outputs: outputs.map((o) => ({ path: o.path, sha256: o.sha256 })) };
|
|
135
|
-
fs.writeFileSync(attestationFile, JSON.stringify(attestation), { mode: 0o600 });
|
|
136
|
-
const ran = spawn(qa, [...given, "--from-cargo-json", listingFile, "--prebuilt-attestation", attestationFile], { cwd, env: { ...env, ...signEnv }, stdio: "inherit", windowsHide: true });
|
|
137
|
-
if (ran.error) throw fail("RK_QA_JOURNEYS_RUN", ran.error.message);
|
|
138
|
-
return { status: ran.status ?? 1, attestation };
|
|
139
|
-
} finally {
|
|
140
|
-
for (const f of [listingFile, attestationFile]) { try { fs.rmSync(f, { force: true }); } catch { /* pruned with the managed directory */ } }
|
|
141
|
-
if (signedDir) { try { fs.rmSync(signedDir, { recursive: true, force: true }); } catch { /* the managed root is pruned */ } }
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** The app's right-release config JSON beside the step's directory (the sign step derives the product identity from it); null when there is none. */
|
|
146
|
-
function defaultConfig(cwd) {
|
|
147
|
-
try { return JSON.parse(fs.readFileSync(path.join(cwd, "right-release.config.json"), "utf8")); } catch { return null; }
|
|
148
|
-
}
|