@rightkit/release 0.6.15 → 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +7 -3
  2. package/admission-host.mjs +9 -5
  3. package/app-closure.mjs +9 -3
  4. package/bin/darwin/rightkit-swap +0 -0
  5. package/bin/rightkit-gate.mjs +469 -102
  6. package/bin/rightkit-linux-setup.mjs +15 -0
  7. package/broker-admission.mjs +184 -0
  8. package/broker-wire.mjs +15 -3
  9. package/build-launch.mjs +4 -1
  10. package/build-ownership.mjs +125 -36
  11. package/build-release.mjs +39 -7
  12. package/cargo-contract.mjs +4 -1
  13. package/cargo-guard.mjs +25 -5
  14. package/cargo-target-shim.mjs +56 -7
  15. package/clippy-route.mjs +8 -7
  16. package/crate-graph.mjs +5 -1
  17. package/credential-provider.mjs +174 -0
  18. package/deterministic-scans.mjs +256 -41
  19. package/development.mjs +8 -0
  20. package/disk-policy.mjs +2 -2
  21. package/gate-executor.mjs +5 -3
  22. package/gate-stages.mjs +34 -3
  23. package/gradle-lock.mjs +206 -0
  24. package/heavy-command.mjs +33 -2
  25. package/linux-artifacts.mjs +2 -1
  26. package/linux-setup.mjs +106 -0
  27. package/local-publish.mjs +21 -10
  28. package/local-qualify.mjs +9 -3
  29. package/managed-cargo.mjs +54 -18
  30. package/npm-license-notices.mjs +49 -1
  31. package/ownership-lockfiles.mjs +27 -4
  32. package/ownership.mjs +5 -1
  33. package/package.json +17 -11
  34. package/protected-credentials.mjs +13 -5
  35. package/publication-gate.mjs +9 -4
  36. package/python-lock.mjs +209 -0
  37. package/qa-admission-policy.json +32 -0
  38. package/qa-layout.mjs +229 -0
  39. package/qa-runner.mjs +156 -0
  40. package/release-artifact-path.mjs +29 -3
  41. package/release-config.mjs +1 -1
  42. package/release-qa-command.mjs +58 -8
  43. package/release-qa-policy.mjs +5 -3
  44. package/release-source-identity.mjs +1 -0
  45. package/release.mjs +10 -2
  46. package/rightkit-ownership.json +1465 -79
  47. package/rightkit-registry-resolver.mjs +7 -3
  48. package/rightkit-versions.json +173 -36
  49. package/runtime-budget-classes.json +4 -0
  50. package/runtime-budgets.mjs +5 -1
  51. package/shell-contract.mjs +1 -1
  52. package/snapshot-broker.mjs +107 -9
  53. package/snapshot-check.mjs +2 -0
  54. package/snapshot-request.mjs +20 -3
  55. package/snapshot.mjs +11 -4
  56. package/stage-commands.mjs +2 -1
  57. package/stage-ledger.mjs +15 -0
  58. package/stage-sidecar.mjs +17 -3
  59. package/suite-signing.mjs +40 -4
  60. package/swift-lock.mjs +334 -0
  61. package/test-command.mjs +13 -3
  62. package/tool-build.mjs +156 -0
  63. package/unit-gate.mjs +9 -2
  64. package/windows-identity-plan.mjs +119 -0
  65. package/windows-identity.mjs +136 -0
@@ -6,13 +6,17 @@
6
6
  // right-release qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] (release-qa-audit.mjs; exit 1 when an uploaded build lacks passing QA)
7
7
  // right-release qa policy show | set --app <app> --mode warn|refuse --set-by <name> [--stage source|installed]
8
8
  // | approve-runner --runner <file> --version <v> --platform mac|win --set-by <name> [--note <text>] (release-qa-policy.mjs)
9
+ // right-release qa runner enroll --runner <built rightkit-qa> --version <v> [--platform mac|win] [--rotate] | show (qa-runner.mjs)
9
10
  //
10
11
  // The release config declares the app's QA plan:
11
12
  // qa: { version: "1", config: "rightkit-qa.toml", runner?: "<path to rightkit-qa>",
12
13
  // args?: { source?: [..], installed?: [..] }, // {sealedDir} {releaseId} expand; installed also {installedApp} {installedDir} {qaRoot}
13
14
  // installed?: { installDir?: "<abs dir>" }, // opt in: mount the sealed DMG (mac) / run the sealed NSIS installer (win, needs RIGHT_RELEASE_QA_ALLOW_INSTALL=1 off CI), see release-qa-installed.mjs
14
15
  // journeys?: { manifest?: "journeys.toml", receipt?: "<path to a rightkit.journeys.receipt/1>", args?: { source?: [..], installed?: [..] } },
16
+ // layout?: { binary: "<bin target of the QA shell>", beside?: [..], build?: [<tests.suites names>] }, // source stage: run against the gate-built QA shell in the layout an install has, see qa-layout.mjs
15
17
  // checks: { "smoke.launch": ["app.launch"], journey: ["journey.*"] } } // floor id -> evidence check names
18
+ // `qa.version` that is a rightkit-qa version (0.10.0) names the runner: the managed, digest-recorded one enrolled by `right-release qa runner enroll`
19
+ // (qa-runner.mjs), never a PATH binary. A legacy plan version ("1") with no `qa.runner` still takes the first `rightkit-qa` on PATH.
16
20
  // Every id in release-qa-floor.json must be mapped; skipped/failed/missing required checks fail the receipt.
17
21
  // `qa.journeys` maps the floor's `journey` id to "every release-critical journey required on this platform passed":
18
22
  // `rightkit-qa journeys run` is run (or `receipt` is read), the receipt is validated through `rightkit-qa journeys
@@ -37,6 +41,11 @@ import {
37
41
  qaReceiptPath, QA_STAGES, sealQaReceipt, sourceIdentity, validateAppQaConfig, writeQaReceipt,
38
42
  } from "./release-qa-receipt.mjs";
39
43
  import { platformDirOf } from "./platform-dir.mjs";
44
+ import { isRunnerVersion, main as runnerMain, resolveManagedQaRunner } from "./qa-runner.mjs";
45
+ import { compiledDirsFromEnv, QA_APP_BINARY_ENV, stageQaLayout, validateQaLayout } from "./qa-layout.mjs";
46
+ import { gateRunsRoot, resolveManagedRoots } from "./managed-roots.mjs";
47
+ import { planTests, runSuites } from "./test-command.mjs";
48
+ import { QA_SIGNED_FILES_ENV } from "./suite-signing-policy.mjs";
40
49
 
41
50
  const sha256File = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
42
51
 
@@ -119,6 +128,8 @@ export async function runQa({ stage, platform, releaseIdArg, configPath, carryTo
119
128
  const { floor, digest: floorDigest } = loadQaFloor(deps.floorDir);
120
129
  const problems = validateAppQaConfig(config.qa, floor, stage);
121
130
  if (problems.length) throw new Error(`release config QA plan does not meet the protected QA floor:\n- ${problems.join("\n- ")}`);
131
+ const layoutProblems = config.qa.layout === undefined ? [] : validateQaLayout(config.qa.layout);
132
+ if (layoutProblems.length) throw new Error(`release config qa.layout is invalid:\n- ${layoutProblems.join("\n- ")}`);
122
133
 
123
134
  let releaseId = releaseIdArg;
124
135
  let sealed = null;
@@ -145,8 +156,17 @@ export async function runQa({ stage, platform, releaseIdArg, configPath, carryTo
145
156
  if (journeysDeclared.receipt) assertInsideSource(release, path.resolve(appRoot, String(journeysDeclared.receipt)), "qa.journeys.receipt");
146
157
  }
147
158
  if (config.qa.runner && !deps.runnerFile) assertInsideSource(release, path.resolve(appRoot, config.qa.runner), "qa.runner");
148
- const runnerFile = deps.runnerFile ?? resolveRunner({ qa: config.qa, appRoot });
159
+ // A runner the config names is read from the source; else the managed runner of the version `qa.version` names (qa-runner.mjs); else, for a legacy plan version only, PATH.
160
+ let managedRunner = null;
161
+ let runnerFile = deps.runnerFile;
162
+ if (!runnerFile) {
163
+ if (!config.qa.runner && isRunnerVersion(config.qa.version)) {
164
+ managedRunner = (deps.resolveManagedRunner ?? resolveManagedQaRunner)({ dataRoot: resolveManagedRoots({ env: deps.env ?? process.env }).dataRoot, platform, version: String(config.qa.version) });
165
+ runnerFile = managedRunner.file;
166
+ } else runnerFile = resolveRunner({ qa: config.qa, appRoot });
167
+ }
149
168
  const runnerSha = sha256File(runnerFile);
169
+ if (managedRunner && managedRunner.sha256 !== runnerSha) throw new Error(`the managed rightkit-qa ${managedRunner.version} changed between its verification and its digest; refusing to seal`);
150
170
  const journeysConfig = config.qa.journeys ?? null;
151
171
  const qaConfig = path.resolve(appRoot, config.qa.config);
152
172
  if (!existsSync(qaConfig)) throw new Error(`qa.config does not exist: ${qaConfig}`);
@@ -161,25 +181,50 @@ export async function runQa({ stage, platform, releaseIdArg, configPath, carryTo
161
181
  });
162
182
  }
163
183
  let run, journeysRun = null;
184
+ let layoutStage = null;
164
185
  try {
186
+ // Source stage with a declared layout: the QA shell and its layout are staged in a run-scoped directory from the gate's compile outputs (qa-layout.mjs); the snapshot is never written.
187
+ if (stage === "source" && config.qa.layout?.build) {
188
+ // The QA shell is built here, by the app's own build (and sign) suites, never by the release compile (qa-layout.mjs `build`).
189
+ const env = deps.env ?? process.env;
190
+ const plan = planTests(config, { suites: config.qa.layout.build, appRoot });
191
+ const stageFrom = ({ outputs, env: suiteEnv }) => {
192
+ const dirs = [...new Set(plan.filter((item) => !item.sign).flatMap((item) => outputs.get(item.name) ?? []))];
193
+ if (!dirs.length) throw new Error(`qa.layout.build: ${plan.map((item) => item.name).join(", ")} passed but named no output directory (no carried receipt, and no RIGHTKIT_GATE_TARGET_DIRS from the gate)`);
194
+ const signed = plan.some((item) => item.sign) ? JSON.parse(suiteEnv[QA_SIGNED_FILES_ENV] ?? "{}") : null;
195
+ return (deps.stageLayout ?? stageQaLayout)({ layout: config.qa.layout, snapshotAppRoot: appRoot, qaConfigFile: qaConfig, runsRoot: deps.runsRoot ?? gateRunsRoot({ env }), dirs, platform, signed });
196
+ };
197
+ const built = await (deps.runSuites ?? runSuites)(plan, { appRoot, prefix: "qa_build", env, config, log: (line) => console.error(line), errLog: (line) => console.error(line), within: stageFrom });
198
+ if (built.exitCode !== 0) throw new Error(`qa.layout.build: the QA shell build failed (${built.results.filter((r) => r.exitCode !== 0).map((r) => r.suite).join(", ")}); no QA ran`);
199
+ layoutStage = built.within;
200
+ } else if (stage === "source" && config.qa.layout) {
201
+ const dirs = deps.compiledDirs ?? compiledDirsFromEnv(deps.env ?? process.env);
202
+ if (!dirs) throw new Error("qa.layout needs the gate's compile outputs (RIGHT_RELEASE_QA_COMPILED_DIRS names none): run source QA through `rightkit-gate --to package`, after its compile");
203
+ layoutStage = (deps.stageLayout ?? stageQaLayout)({ layout: config.qa.layout, snapshotAppRoot: appRoot, qaConfigFile: qaConfig, runsRoot: deps.runsRoot ?? gateRunsRoot({ env: deps.env ?? process.env }), dirs, platform });
204
+ }
165
205
  const expand = (value) => String(value).replaceAll("{sealedDir}", sealed?.sealedDir ?? "").replaceAll("{releaseId}", releaseId)
166
206
  .replaceAll("{installedApp}", installedQa?.installedApp ?? "").replaceAll("{installedDir}", installedQa?.installedDir ?? "").replaceAll("{qaRoot}", installedQa?.qaRoot ?? "");
167
207
  const installedDirs = installedDirNames(config, deps.env ?? process.env);
168
208
  const runEnv = {
169
209
  ...process.env, RIGHT_RELEASE_QA_STAGE: stage, RIGHT_RELEASE_QA_COMMIT: source.commit, RIGHT_RELEASE_QA_RELEASE_ID: releaseId,
170
210
  ...(sealed ? { RIGHT_RELEASE_QA_SEALED_DIR: sealed.sealedDir } : {}), ...(installedDirs.length ? { RIGHTKIT_QA_INSTALLED_DIRS: installedDirs.join(";") } : {}), ...(installedQa?.env ?? {}),
211
+ ...(layoutStage ? { [QA_APP_BINARY_ENV]: layoutStage.binary } : {}),
171
212
  };
172
- const args = ["run", "--config", qaConfig, ...((config.qa.args?.[stage] ?? []).map(expand))];
213
+ const runCwd = layoutStage?.appRoot ?? appRoot;
214
+ const args = ["run", "--config", layoutStage?.configFile ?? qaConfig, ...((config.qa.args?.[stage] ?? []).map(expand))];
173
215
  run = (deps.spawnRunner ?? spawnSync)(runnerFile, args, {
174
- cwd: appRoot, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024,
216
+ cwd: runCwd, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024,
175
217
  env: runEnv,
176
218
  });
177
219
  // Journeys lane: the one release proof (ADR-0001 item 10). A receipt named in the config is read as is.
178
220
  if (journeysConfig && !journeysConfig.receipt) {
179
- journeysRun = (deps.spawnJourneys ?? deps.spawnRunner ?? spawnSync)(runnerFile, ["journeys", "run", "--root", appRoot, ...((journeysConfig.args?.[stage] ?? []).map(expand))], {
180
- cwd: appRoot, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024, env: runEnv,
221
+ journeysRun = (deps.spawnJourneys ?? deps.spawnRunner ?? spawnSync)(runnerFile, ["journeys", "run", "--root", runCwd, ...((journeysConfig.args?.[stage] ?? []).map(expand))], {
222
+ cwd: runCwd, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024, env: runEnv,
181
223
  });
182
224
  }
225
+ } catch (error) {
226
+ layoutStage?.cleanup();
227
+ throw error;
183
228
  } finally {
184
229
  installedQa?.cleanup();
185
230
  }
@@ -207,19 +252,23 @@ export async function runQa({ stage, platform, releaseIdArg, configPath, carryTo
207
252
  required = required.map((item) => (item.id === floor.journeys.floorId ? journeyRequiredItem({ floorId: item.id, validation }) : item));
208
253
  }
209
254
  const executionFinishedAt = new Date();
255
+ // The runner's outputs (evidence, journeys receipt) have been read; the run-scoped layout is not evidence and goes.
256
+ const layoutEvidence = layoutStage?.evidence ?? null;
257
+ layoutStage?.cleanup();
210
258
  // A passing receipt needs the runner to exit cleanly AND an evidence bundle that itself passed.
211
259
  if (run.status !== 0 && evidence) evidence = { ...evidence, status: "failed" };
212
- const receipt = sealQaReceipt(buildQaReceipt({
260
+ // The layout evidence rides beside the standard body (release-qa-receipt.mjs is a policy-bundle entrypoint and stays untouched); the seal covers it.
261
+ const receipt = sealQaReceipt({ ...buildQaReceipt({
213
262
  stage, app: config.app, platform, releaseId, source, closure: dependencyClosure(repoRoot, source.commit, gitRunner),
214
263
  releaseConfigSha256: sha256File(configPath), qa: config.qa, floor, floorDigest,
215
264
  policyBundleDigest: deps.policyBundleDigest ?? currentPolicyBundleDigest(deps.floorDir),
216
- runner: { basename: path.basename(runnerFile).replace(/\.exe$/i, ""), path: runnerFile, sha256: runnerSha },
265
+ runner: { basename: path.basename(runnerFile).replace(/\.exe$/i, ""), path: runnerFile, sha256: runnerSha, ...(managedRunner ? { version: managedRunner.version, source: "managed" } : {}) },
217
266
  evidence, evidenceSha256, required, artifacts, sourceReceiptDigest, journeys,
218
267
  execution: {
219
268
  startedAt: executionStartedAt.toISOString(), finishedAt: executionFinishedAt.toISOString(),
220
269
  runnerPids: [run.pid, journeysRun?.pid].filter((pid) => Number.isInteger(pid) && pid > 0), sealerPid: process.pid,
221
270
  },
222
- }), key);
271
+ }), ...(layoutEvidence ? { layout: layoutEvidence } : {}) }, key);
223
272
  const file = qaReceiptPath({ repoRoot, releaseId, platform, stage });
224
273
  writeQaReceipt(file, receipt);
225
274
  // #183: copy the sealed receipt next to the candidate handoff so the signing job (a different runner) can read it.
@@ -239,6 +288,7 @@ function usage(code) {
239
288
  export async function main(args) {
240
289
  if (args[0] === "audit") process.exit(auditMain(args.slice(1)));
241
290
  if (args[0] === "policy") process.exit(policyMain(args.slice(1), { program: "right-release qa policy" }));
291
+ if (args[0] === "runner") process.exit(runnerMain(args.slice(1), { program: "right-release qa runner" }));
242
292
  let stage = "", platform = process.platform === "win32" ? "win" : "mac", releaseId = "", configName = defaultReleaseConfigFile(), carryTo = null;
243
293
  for (let index = 0; index < args.length; index += 1) {
244
294
  const arg = args[index];
@@ -278,10 +278,12 @@ export function approveRunner({
278
278
  if (!runner || !String(runner).trim()) throw new QaPolicyError("runner is required");
279
279
  if (!nonEmpty(version)) throw new QaPolicyError("version is required");
280
280
  if (note !== undefined && typeof note !== "string") throw new QaPolicyError("note must be a string");
281
- let published;
282
- try { published = JSON.parse(readFileSync(versionsFile, "utf8")).registry?.cargo?.["rightkit-qa"]; }
281
+ // A runner is the published version or the one RightKit has staged for its next publication (`stagedCargo`): an app's `qa.version` may name the staged one
282
+ // while the gate-built binary is what runs (qa-runner.mjs); admission is still by this sha256, never by the version string.
283
+ let published; let staged;
284
+ try { const feed = JSON.parse(readFileSync(versionsFile, "utf8")); published = feed.registry?.cargo?.["rightkit-qa"]; staged = feed.stagedCargo?.["rightkit-qa"]; }
283
285
  catch (error) { throw new QaPolicyError(`the published rightkit-qa version cannot be read (${versionsFile}): ${error.message}`); }
284
- if (published !== version) throw new QaPolicyError(`rightkit-qa ${version} is not the published version (rightkit-versions.json registry.cargo has ${published ?? "no rightkit-qa"}); only the published runner can be approved`);
286
+ if (published !== version && staged !== version) throw new QaPolicyError(`rightkit-qa ${version} is not the published version (rightkit-versions.json registry.cargo has ${published ?? "no rightkit-qa"}); only the published runner can be approved`);
285
287
  // The file that is digested is the file that is run: an absolute path, never a PATH lookup, and re-digested after.
286
288
  const file = path.resolve(String(runner));
287
289
  const sha256 = sha256Of(readRunnerBytes(file));
@@ -76,6 +76,7 @@ export function resolveReleaseSource({ cwd = process.cwd(), env = process.env, g
76
76
  let detail = "";
77
77
  try { repoRoot = plainGit(git, cwd, ["rev-parse", "--show-toplevel"]).trim(); } catch (error) { detail = ` (${error.message})`; }
78
78
  if (!repoRoot) throw new ReleaseSourceError(`${cwd} is not inside a Git repository${detail}`, NO_REPOSITORY_CODE);
79
+ repoRoot = path.normalize(repoRoot); // git prints C:/dir on Windows; every other path here is in the platform's own spelling
79
80
  const commit = plainGit(git, repoRoot, ["rev-parse", "HEAD"]).trim();
80
81
  const tree = plainGit(git, repoRoot, ["rev-parse", `${commit}^{tree}`]).trim();
81
82
  if (!OBJECT_ID.test(commit) || !OBJECT_ID.test(tree)) throw new Error("the checkout's HEAD is not a commit");
package/release.mjs CHANGED
@@ -15,7 +15,7 @@ import { assertQaAdoption, assertQaBackdoorContract } from "./qa-contract.mjs";
15
15
  import { assertLegalReleaseContract } from "./legal-contract.mjs";
16
16
  import { DECLARED_UNRELATED_POLICY, declaredUnrelatedRefusal, releaseArtifactPaths } from "./hosted-workflow-policy.mjs";
17
17
  import { assertNativeIosPrepareHasManifest, assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
18
- import { resolveReleaseSource, sourceRootOf } from "./release-source-identity.mjs";
18
+ import { gitRootOf, isSnapshotSource, resolveReleaseSource, sourceRootOf } from "./release-source-identity.mjs";
19
19
  import { ArtifactPathError, assertSplitCommandPaths, releaseCommandSteps, resolveArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
20
20
  import { declaredCargoManifest, hasDefaultTauriManifest, resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
21
21
  import {
@@ -622,7 +622,15 @@ if (problems.length || prebuildFindings.length) {
622
622
  process.exit(1);
623
623
  }
624
624
 
625
- const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
625
+ // The lock is an output, so a door split keeps it in the checkout's app directory: the snapshot is the commit's bytes, and a lock directory left
626
+ // there fails the door's after-stage verify (HeardRight Windows package, 2026-11-01: ".cache/right-release is an empty directory the commit does not have").
627
+ const lockRoot = (() => {
628
+ if (!workerSource || !isSnapshotSource(workerSource)) return root;
629
+ const relative = path.relative(sourceRootOf(workerSource), root);
630
+ if (relative.startsWith("..") || path.isAbsolute(relative)) fail(`the release config directory ${root} is not inside the door snapshot ${sourceRootOf(workerSource)}; the release lock has no checkout counterpart`);
631
+ return path.join(gitRootOf(workerSource), relative);
632
+ })();
633
+ const releaseLock = acquireReleaseLock(lockRoot, config.app ?? path.basename(root), opts.platform, opts.tier);
626
634
  const mode = "package";
627
635
  const command = target.package;
628
636
  if (!command && opts.platform !== "android" && opts.platform !== "ios" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }), { expanded: true })) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);