@rightkit/release 0.6.8 → 0.6.11

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 (96) hide show
  1. package/README.md +30 -12
  2. package/admission-files.mjs +4 -1
  3. package/admission.mjs +2 -2
  4. package/advisory-exceptions.mjs +41 -5
  5. package/android-play-upload.mjs +16 -15
  6. package/app-check.mjs +105 -27
  7. package/assemble-app-command.mjs +151 -0
  8. package/assembled-app.mjs +445 -0
  9. package/bin/darwin/rightkit-swap +0 -0
  10. package/bin/darwin/rightkit-swap.sign.json +6 -0
  11. package/bin/rightkit-gate.mjs +1608 -345
  12. package/broker-pins.mjs +152 -0
  13. package/build-launch.mjs +88 -0
  14. package/build-ownership.mjs +1231 -0
  15. package/build-release.mjs +82 -28
  16. package/cargo-contract.mjs +3 -1
  17. package/cargo-ledger.mjs +62 -0
  18. package/cargo-target-shim.mjs +238 -0
  19. package/class-a-launch-guard.mjs +126 -0
  20. package/cli/right-release.mjs +31 -5
  21. package/clippy-equivalence.mjs +202 -0
  22. package/clippy-route.mjs +543 -0
  23. package/codesign-shim.mjs +11 -1
  24. package/committed-source.mjs +39 -5
  25. package/crate-workspaces.mjs +87 -0
  26. package/create-mac-updater.mjs +14 -2
  27. package/deterministic-scans.mjs +997 -71
  28. package/development-signing.mjs +4 -1
  29. package/development.mjs +120 -21
  30. package/direct-receipt.mjs +135 -0
  31. package/door-policy.mjs +71 -6
  32. package/door.mjs +6 -6
  33. package/freshness-census.mjs +257 -0
  34. package/gate-executor.mjs +14 -2
  35. package/gate-stages.mjs +91 -15
  36. package/heavy-command.mjs +3 -2
  37. package/hook-retirement.mjs +65 -0
  38. package/init-command.mjs +1 -1
  39. package/install-app.mjs +11 -2
  40. package/install-assembled-app.mjs +423 -0
  41. package/inventory-check.mjs +11 -8
  42. package/ios-dev.mjs +70 -3
  43. package/ios-upload.mjs +20 -15
  44. package/linux-artifacts.mjs +93 -0
  45. package/local-publish.mjs +224 -53
  46. package/local-qualify.mjs +5 -3
  47. package/local-runtime.mjs +5 -4
  48. package/mac-package-recipe.mjs +4 -1
  49. package/managed-cargo.mjs +47 -5
  50. package/managed-root-guard.mjs +34 -0
  51. package/mirror-root-artifact.mjs +18 -5
  52. package/native-helpers.mjs +355 -0
  53. package/ownership-class-rules.mjs +1 -1
  54. package/ownership-evidence.mjs +37 -13
  55. package/ownership-hooks.mjs +20 -59
  56. package/ownership-lockfiles.mjs +14 -1
  57. package/ownership-policy-bundle.mjs +3 -2
  58. package/ownership-staged.mjs +2 -1
  59. package/ownership.mjs +24 -0
  60. package/package-recipe-targets.mjs +84 -10
  61. package/package.json +4 -3
  62. package/pipeline-admission.mjs +4 -4
  63. package/pipeline-receipts.mjs +302 -302
  64. package/preflight.mjs +27 -4
  65. package/project-type.mjs +2 -2
  66. package/provenance-verify.mjs +115 -0
  67. package/publish-cargo.mjs +12 -9
  68. package/qa-admission-policy.json +10 -1
  69. package/recipe-build.mjs +106 -14
  70. package/release-artifact-path.mjs +341 -0
  71. package/release-config.mjs +1 -1
  72. package/release-invocation.mjs +8 -1
  73. package/release-qa-command.mjs +25 -9
  74. package/release-source-brand.mjs +17 -0
  75. package/release-source-identity.mjs +171 -0
  76. package/release.mjs +59 -22
  77. package/rightkit-versions.json +73 -47
  78. package/runtime-budgets.mjs +6 -2
  79. package/scan-digest.mjs +71 -5
  80. package/sign-updater.mjs +6 -3
  81. package/snapshot-broker.mjs +172 -0
  82. package/snapshot-check.mjs +312 -0
  83. package/snapshot-git.mjs +166 -0
  84. package/snapshot-request.mjs +363 -0
  85. package/snapshot-root-config.mjs +76 -0
  86. package/snapshot.mjs +969 -0
  87. package/source-split.mjs +73 -0
  88. package/stage-commands.mjs +60 -0
  89. package/stage-ledger.mjs +204 -0
  90. package/stage-sidecar-command.mjs +5 -1
  91. package/stage-sidecar.mjs +157 -27
  92. package/swap-helper-bundle.mjs +5 -2
  93. package/test-command.mjs +17 -4
  94. package/unit-gate.mjs +301 -0
  95. package/web-build.mjs +40 -6
  96. package/win-package-recipe.mjs +20 -7
@@ -0,0 +1,171 @@
1
+ // Release source identity (item 7 row 7.04; signed/item7-sage-decisions-2.md Q2; docs/GATE-CONTRACT.md "Commit-only build").
2
+ //
3
+ // Every right-release command that builds, qualifies or ships a door stage asks one question before it trusts anything it reads: which
4
+ // source is this, and what proves it? There are exactly two answers.
5
+ //
6
+ // checkout RK_GATE_CHECKOUT is not set (hosted CI, no door, or the Mac/Dell route before the cutover). The source is the checkout's
7
+ // HEAD, proven the way it always was: the primary worktree, a branch-attached HEAD and a clean status (R1). Nothing here
8
+ // changes for that route.
9
+ // snapshot RK_GATE_CHECKOUT is set: this process is a child of a live door run (source-split.mjs `readSourceSplit`). The identity is
10
+ // the SELECTED COMMIT's immutable objects plus the gate's start and end verification of the snapshot under held ownership.
11
+ // It is never the checkout's HEAD, status or branch (GC:24 "no dirty-tree condition", GC:156 item 1, GC:182 E1): the
12
+ // checkout may be dirty, ahead, behind or detached.
13
+ //
14
+ // A process that names a split it cannot prove (no live run, an ended run, another checkout, a cwd outside the snapshot) is refused
15
+ // (R2); it never falls back to the checkout checks, because that would build bytes the identity does not describe.
16
+ //
17
+ // `resolveReleaseSource` is the single owner. It returns a frozen record that only it can mint (release-source-brand.mjs), so the
18
+ // evidence, QA and upload code can refuse a hand-built one. The record's fields are frozen by the decision record:
19
+ // { mode: "snapshot", commit, tree, checkout, checkoutAppRoot, snapshot, appRoot, commonDir }
20
+ // { mode: "checkout", commit, tree, repoRoot, appRoot }
21
+ import path from "node:path";
22
+ import { spawnSync } from "./background-process.mjs";
23
+ import { resolveSelectedCommit } from "./committed-source.mjs";
24
+ import { canonicalRepositoryIdentity } from "./ownership-approval.mjs";
25
+ import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
26
+ import { dependencyClosure } from "./release-qa-receipt.mjs";
27
+ import { brandReleaseSource, isReleaseSource } from "./release-source-brand.mjs";
28
+ import { assertCleanSource } from "./source-gate.mjs";
29
+ import { SOURCE_SPLIT_CODE, SourceSplitError, inSnapshot, readSourceSplit } from "./source-split.mjs";
30
+
31
+ export const RELEASE_SOURCE_IDENTITY_CODE = "RK_SOURCE_IDENTITY";
32
+ export const NO_REPOSITORY_CODE = "RK_SOURCE_NO_REPOSITORY";
33
+
34
+ /** A release source whose identity does not hold (a commit or tree that is not the selected one, a file outside the snapshot). */
35
+ export class ReleaseSourceError extends Error {
36
+ constructor(message, code = RELEASE_SOURCE_IDENTITY_CODE) { super(message); this.name = "ReleaseSourceError"; this.code = code; }
37
+ }
38
+
39
+ // What each resolved source was resolved with, so `assertReleaseSource` can prove the same facts again (a run that has ended since is no
40
+ // split any longer) without the caller threading the context through.
41
+ const contexts = new WeakMap();
42
+
43
+ const OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
44
+
45
+ function plainGit(git, cwd, args) {
46
+ const result = git("git", args, { cwd, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
47
+ if (result?.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${String(result?.stderr ?? "").trim() || "no output"}`);
48
+ return String(result.stdout);
49
+ }
50
+
51
+ /**
52
+ * The source this process runs under. `git` runs git (a spawnSync-shaped function; tests inject a recorder). `readFile` and `alive` are
53
+ * passed to the door-run reader (tests only). Throws SourceSplitError (R2, R3) when a split is named but cannot be proven; throws
54
+ * an Error outside a git repository when no split is named.
55
+ */
56
+ export function resolveReleaseSource({ cwd = process.cwd(), env = process.env, git = spawnSync, readFile, alive } = {}) {
57
+ const context = Object.freeze({ cwd, env, git, ...(readFile ? { readFile } : {}), ...(alive ? { alive } : {}) });
58
+ // 1. A live door run under the named checkout, the same checkout, and a cwd inside the run's snapshot. Throws when anything is off.
59
+ const split = readSourceSplit({ env, cwd, run: git, ...(readFile ? { readFile } : {}), ...(alive ? { alive } : {}) });
60
+ if (split) {
61
+ // 2. The tree is re-derived from the object database. It is never taken from the run file alone (R3).
62
+ let objects;
63
+ try { objects = resolveSelectedCommit(split.checkout, split.commit, { runner: git }); }
64
+ catch (error) { throw new SourceSplitError(`the door run's commit ${split.commit.slice(0, 12)} cannot be read from the object database of ${split.checkout}: ${error.message}`); }
65
+ if (objects.commit !== split.commit) throw new SourceSplitError(`the object database of ${split.checkout} answers ${objects.commit} for the door run's commit ${split.commit}`);
66
+ if (objects.tree !== split.tree) throw new SourceSplitError(`the door run records tree ${split.tree.slice(0, 12)} for commit ${split.commit.slice(0, 12)}, but the object database says ${objects.tree.slice(0, 12)} (R3)`);
67
+ const source = brandReleaseSource(Object.freeze({
68
+ mode: "snapshot", commit: objects.commit, tree: objects.tree, checkout: split.checkout, checkoutAppRoot: split.checkoutAppRoot,
69
+ snapshot: split.snapshot, appRoot: split.appRoot, commonDir: split.commonDir,
70
+ }));
71
+ contexts.set(source, context);
72
+ return source;
73
+ }
74
+ // The checkout route: HEAD of the repository at cwd, read exactly as each command always read it.
75
+ let repoRoot = "";
76
+ let detail = "";
77
+ try { repoRoot = plainGit(git, cwd, ["rev-parse", "--show-toplevel"]).trim(); } catch (error) { detail = ` (${error.message})`; }
78
+ if (!repoRoot) throw new ReleaseSourceError(`${cwd} is not inside a Git repository${detail}`, NO_REPOSITORY_CODE);
79
+ const commit = plainGit(git, repoRoot, ["rev-parse", "HEAD"]).trim();
80
+ const tree = plainGit(git, repoRoot, ["rev-parse", `${commit}^{tree}`]).trim();
81
+ if (!OBJECT_ID.test(commit) || !OBJECT_ID.test(tree)) throw new Error("the checkout's HEAD is not a commit");
82
+ const source = brandReleaseSource(Object.freeze({ mode: "checkout", commit, tree, repoRoot, appRoot: path.resolve(cwd) }));
83
+ contexts.set(source, context);
84
+ return source;
85
+ }
86
+
87
+ /** The record `resolveReleaseSource` minted, or a refusal: a hand-built look-alike never carries an identity. */
88
+ export function requireReleaseSource(source) {
89
+ if (!isReleaseSource(source)) throw new SourceSplitError("the release source was not resolved by resolveReleaseSource; a hand-built source carries no identity");
90
+ return source;
91
+ }
92
+
93
+ export const isSnapshotSource = (source) => requireReleaseSource(source).mode === "snapshot";
94
+ /** The root every git object read and every output, state, lock and sealed directory belongs to: the checkout (`.right-release` lives there). */
95
+ export const gitRootOf = (source) => (requireReleaseSource(source).mode === "snapshot" ? source.checkout : source.repoRoot);
96
+ /** The root every source, config, input and QA file is read from: the snapshot in a split, else the checkout. */
97
+ export const sourceRootOf = (source) => (requireReleaseSource(source).mode === "snapshot" ? source.snapshot : source.repoRoot);
98
+
99
+ /**
100
+ * Proves the source's identity holds now, for the command `commandId`.
101
+ * checkout R1, byte for byte as before: the primary worktree (branch-attached HEAD) and a clean status. `skipPrimaryCheckout` and
102
+ * `skipCleanCheck` are the two existing seams (a test double; a phase resume that reads no unrelated source).
103
+ * snapshot The door run is proven again (live, this checkout, this cwd, the tree from the object database), then the checkout must
104
+ * still be the primary worktree. No status, no HEAD and no branch is read: a dirty or moved checkout is irrelevant.
105
+ * Writes into the snapshot are decided by the door's start and end verification, not here.
106
+ * Returns the source.
107
+ */
108
+ export function assertReleaseSource(source, { commandId = "release", env, git, skipPrimaryCheckout = false, skipCleanCheck = false } = {}) {
109
+ requireReleaseSource(source);
110
+ const context = contexts.get(source);
111
+ const runGit = git ?? context.git;
112
+ const environment = env ?? context.env;
113
+ if (source.mode === "checkout") {
114
+ if (!skipPrimaryCheckout) assertPrimaryReleaseCheckout(source.repoRoot, environment);
115
+ if (!skipCleanCheck) assertCleanSource({ status: plainGit(runGit, source.repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]), commandId });
116
+ return source;
117
+ }
118
+ const again = resolveReleaseSource({ ...context, env: environment, git: runGit });
119
+ if (again.mode !== "snapshot") throw new SourceSplitError("the door run's source split is gone; nothing may continue from a snapshot that is no longer proven");
120
+ for (const key of ["commit", "tree", "checkout", "snapshot", "appRoot", "checkoutAppRoot"]) {
121
+ if (again[key] !== source[key]) throw new SourceSplitError(`the door run's source.${key} changed while ${commandId} ran`);
122
+ }
123
+ // The primary worktree identity stays (GC:31); only the branch-attached refusal, which reads HEAD, is skipped (GC:156).
124
+ if (!skipPrimaryCheckout) assertPrimaryReleaseCheckout(source.checkout, environment, { split: true });
125
+ return source;
126
+ }
127
+
128
+ /**
129
+ * R5: in a split, a config, build input, QA plan or test file must resolve inside the snapshot (symlinks resolved). In the checkout
130
+ * route there is no containment to prove and this does nothing.
131
+ */
132
+ export function assertInsideSource(source, file, label = "a source file") {
133
+ if (!isSnapshotSource(source)) return;
134
+ if (typeof file !== "string" || !file) throw new ReleaseSourceError(`${label} has no path; a split build reads only the snapshot of ${source.commit.slice(0, 12)}`);
135
+ if (!inSnapshot({ snapshot: source.snapshot }, path.resolve(file))) {
136
+ throw new ReleaseSourceError(`${label} ${file} is outside the snapshot ${source.snapshot} of ${source.commit.slice(0, 12)}; a split build reads the commit's snapshot and never the checkout (R5)`);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * R4 (and R8): a record that claims a source (a sealed manifest, ownership evidence, a resumed build's state) is for the selected
142
+ * commit and, when it names a tree, that commit's tree. Another commit or tree is refused, never reinterpreted.
143
+ */
144
+ export function assertBindsSource(source, record, label = "the record") {
145
+ requireReleaseSource(source);
146
+ if (!record || typeof record !== "object") throw new ReleaseSourceError(`${label} is missing, so it cannot be bound to commit ${source.commit.slice(0, 12)}`);
147
+ if (record.commit !== source.commit) throw new ReleaseSourceError(`${label} is for commit ${String(record.commit ?? "(none)").slice(0, 12)}, not the selected commit ${source.commit.slice(0, 12)} (R4)`);
148
+ if (record.tree !== undefined && record.tree !== source.tree) throw new ReleaseSourceError(`${label} is for tree ${String(record.tree).slice(0, 12)}, not the selected commit's tree ${source.tree.slice(0, 12)} (R4)`);
149
+ }
150
+
151
+ /**
152
+ * The `{ commit, tree, repositoryIdentity }` a source QA receipt binds, for a snapshot source: the selected commit and tree, and the
153
+ * repository identity from the checkout's origin. Reads no HEAD. (The checkout route keeps release-qa-receipt.mjs `sourceIdentity`.)
154
+ */
155
+ export function snapshotSourceIdentity(source, git = spawnSync) {
156
+ if (!isSnapshotSource(source)) throw new ReleaseSourceError("snapshotSourceIdentity is for a door source split");
157
+ const origin = plainGit(git, source.checkout, ["remote", "get-url", "origin"]).trim();
158
+ return { commit: source.commit, tree: source.tree, repositoryIdentity: canonicalRepositoryIdentity(origin) };
159
+ }
160
+
161
+ /**
162
+ * The `{ source, closure }` options `admitSourceQa` / `assertSourceQaAdmission` / `assertInstalledQaAdmission` take so that the receipt they
163
+ * verify is compared with the SELECTED commit (its tree, its repository identity, its lockfile closure read from its objects) instead of the
164
+ * checkout's HEAD. `{}` for the checkout route: those functions then read HEAD exactly as before. Spread it into their options.
165
+ */
166
+ export function qaAdmissionSourceOptions(source, git = spawnSync) {
167
+ if (!isSnapshotSource(source)) return {};
168
+ return { source: snapshotSourceIdentity(source, git), closure: dependencyClosure(source.checkout, source.commit, git) };
169
+ }
170
+
171
+ export { SOURCE_SPLIT_CODE };
package/release.mjs CHANGED
@@ -3,6 +3,7 @@ import { expandPackageRecipeTarget } from "./package-recipe-targets.mjs";
3
3
  import { access, readFile, readdir } from "node:fs/promises";
4
4
  import { createHash } from "node:crypto";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { buildLaunch } from "./build-launch.mjs";
6
7
  import { createRequire } from "node:module";
7
8
  import path from "node:path";
8
9
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -13,6 +14,8 @@ import { assertQaAdoption, assertQaBackdoorContract } from "./qa-contract.mjs";
13
14
  import { assertLegalReleaseContract } from "./legal-contract.mjs";
14
15
  import { DECLARED_UNRELATED_POLICY, declaredUnrelatedRefusal, releaseArtifactPaths } from "./hosted-workflow-policy.mjs";
15
16
  import { assertNativeIosPrepareHasManifest, assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
17
+ import { resolveReleaseSource, sourceRootOf } from "./release-source-identity.mjs";
18
+ import { ArtifactPathError, assertSplitCommandPaths, releaseCommandSteps, resolveArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
16
19
  import { declaredCargoManifest, hasDefaultTauriManifest, resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
17
20
  import {
18
21
  canonicalPackageIdentity,
@@ -274,10 +277,17 @@ const nativeFinalizer = resolveNativeFinalizerConfig(nativeAssembly);
274
277
  // state root and admission record; a snapshot from anywhere else is refused. The standalone doctor resolves live.
275
278
  const rightKitSnapshotFile = process.env[SNAPSHOT_ENV] || null;
276
279
  let rightKitResolution = null; // null when the registry resolution failed (recorded as the `rightkit-registry` finding)
280
+ // The worker of a door split (row 7.04): its cwd is the snapshot, which has no git directory. Its commit and source root are the selected
281
+ // commit's and the snapshot's (release-source-identity.mjs); the checkout route reads them from git as before.
282
+ let workerSource = null;
283
+ let artifactPathContext = null; // the declared-artifact resolver's context (artifactContext, row 7.04c)
284
+ if (!opts.doctor && process.env.RK_GATE_CHECKOUT) {
285
+ try { workerSource = resolveReleaseSource({ cwd: process.cwd() }); } catch (error) { fail(`${error.code ?? "RK_SOURCE_IDENTITY"}: ${error.message}`); }
286
+ }
277
287
  try {
278
288
  const lockInventory = () => {
279
289
  try {
280
- const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
290
+ const repoRoot = workerSource ? sourceRootOf(workerSource) : git(root, ["rev-parse", "--show-toplevel"]);
281
291
  let canonicalTarget = config.targets?.[opts.platform];
282
292
  try { canonicalTarget = expandPackageRecipeTarget(canonicalTarget, { platform: opts.platform, appRoot: root, config }); } catch { /* reported later */ }
283
293
  const inputs = resolveReleaseBuildInputs({ repoRoot, appRoot: root, configPath, config, target: canonicalTarget });
@@ -291,7 +301,7 @@ try {
291
301
  if (!rightKitSnapshotFile) throw new RightKitRegistryError("no-admission", "no admitted RightKit registry snapshot was handed to the release worker; run it through `right-release build`, which admits the release");
292
302
  const stateRoot = process.env.RIGHT_RELEASE_STATE_ROOT;
293
303
  if (!stateRoot) throw new RightKitRegistryError("no-admission", "RIGHT_RELEASE_STATE_ROOT is not set; the admitted RightKit registry snapshot cannot be located");
294
- const commit = git(root, ["rev-parse", "HEAD"]);
304
+ const commit = workerSource ? workerSource.commit : git(root, ["rev-parse", "HEAD"]);
295
305
  boundSnapshot = loadAdmittedSnapshot(stateRoot, { releaseId: `${config.app}-${config.version}-${commit.slice(0, 8)}`, commit, platform: opts.platform, snapshotFile: rightKitSnapshotFile });
296
306
  }
297
307
  rightKitResolution = await prepareRightKitResolution({
@@ -347,6 +357,12 @@ await check("target", [], () => {
347
357
  if (!expanded) throw new Error(`${config.app ?? "app"} has no ${opts.platform} release target`);
348
358
  target = expanded;
349
359
  });
360
+ // Row 7.04c (R-T1, R-T4): in a door split each Cargo manifest's target is bound now, once, before any step runs, and no step the app owns may name
361
+ // a target-relative path (the snapshot has no target directory). Checkout mode checks nothing here.
362
+ await check("split-artifact-paths", ["target"], () => {
363
+ if (!workerSource) return;
364
+ assertSplitCommandPaths(releaseCommandSteps({ target, config, root }), artifactContext());
365
+ });
350
366
  const legalCheck = await check("legal-contract", [], () => (config.legal
351
367
  ? assertLegalReleaseContract(root, config.legal, config.app ?? path.basename(root), opts.platform)
352
368
  : null));
@@ -608,7 +624,7 @@ if (problems.length || prebuildFindings.length) {
608
624
  const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
609
625
  const mode = "package";
610
626
  const command = target.package;
611
- if (!command && opts.platform !== "android" && opts.platform !== "ios" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
627
+ 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`);
612
628
  if (opts.upload && !target.publish && !target.upload) {
613
629
  fail(`${config.app ?? "app"} has no ${opts.platform} upload config`);
614
630
  }
@@ -635,7 +651,7 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
635
651
  // writes the App Installer route and the seal record. Never a second signer.
636
652
  await runCommand(target.prePackage, root);
637
653
  // Raw payload scan BEFORE the package is packed (a packed MSIX is only readable through its ZIP directory).
638
- assertNoQaControlBinaries("msix staged payload", [path.resolve(root, target.msix.stageDir)], { requireBinary: true, prePack: true });
654
+ assertNoQaControlBinaries("msix staged payload", [resolveDeclaredPath(root, target.msix.stageDir)], { requireBinary: true, prePack: true });
639
655
  const options = msixBuildOptions({ root, target, app: config.app, dryRun: opts.dryRun, receipts: {
640
656
  pe: windowsReceiptPath(root, "raw-exe"), msix: windowsReceiptPath(root, "installer"),
641
657
  dir: path.dirname(windowsReceiptPath(root, "raw-exe")) } });
@@ -644,7 +660,7 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
644
660
  // Second layer: read the finished package through its ZIP central directory (stored and deflate entries).
645
661
  if (!built.dryRun) assertNoQaControlBinaries("msix package", [options.out], { requireBinary: true });
646
662
  } else if (opts.platform === "win") {
647
- const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
663
+ const rawFiles = target.sign.prePackageFiles.map((p) => resolveDeclaredPath(root, p));
648
664
  const sidecarPlan = await prepareMediaSidecar(root);
649
665
  const externalPlan = await prepareExternalBin(root);
650
666
  await runCommand(target.prePackage, root);
@@ -893,15 +909,25 @@ function assertNoWebBundleMocksInFrontend(phase, { required }) {
893
909
  } catch (error) { fail(error.message); }
894
910
  }
895
911
 
912
+ // Row 7.04c (R-T2): one resolver for every declared artifact path. Checkout mode is the historical rule, byte for byte (the app root, then
913
+ // RIGHT_RELEASE_ARTIFACT_DIR); a door split maps a target-prefixed path to its bound Cargo target and never reads the checkout.
914
+ function artifactContext() {
915
+ artifactPathContext ??= splitArtifactContext({
916
+ cwd: root, env: process.env, nativeLayout, config, target: config.targets?.[opts.platform],
917
+ ...(workerSource?.mode === "snapshot" ? { snapshotRoot: workerSource.snapshot } : {}),
918
+ });
919
+ return artifactPathContext;
920
+ }
921
+
896
922
  function resolveReleaseArtifactPath(root, declared) {
897
- const local = path.resolve(root, declared);
898
- if (existsSync(local)) return local;
899
- const managed = process.env.RIGHT_RELEASE_ARTIFACT_DIR;
900
- if (managed) {
901
- const candidate = path.join(path.resolve(managed), path.basename(declared));
902
- if (existsSync(candidate)) return candidate;
903
- }
904
- return local;
923
+ try { return resolveArtifactPath(declared, artifactContext()); }
924
+ catch (error) { if (error instanceof ArtifactPathError) fail(error.message); throw error; }
925
+ }
926
+
927
+ /** A declared path that never fell back to the managed artifact directory (a staged payload, a raw EXE): the app root, or in a split the same resolver. */
928
+ function resolveDeclaredPath(root, declared) {
929
+ const context = artifactContext();
930
+ return context.mode === "split" ? resolveReleaseArtifactPath(root, declared) : path.resolve(root, declared);
905
931
  }
906
932
 
907
933
  function fail(message) {
@@ -1041,11 +1067,13 @@ function nativeSignedArtifacts(files, root) {
1041
1067
  return values.map((value) => {
1042
1068
  const declared = typeof value === "string" ? value : value?.path ?? value?.file;
1043
1069
  if (!declared) fail("native finalizer signed artifact path is missing");
1044
- const absolute = path.resolve(root, declared);
1045
- if (opts.dryRun) return { path: path.relative(root, absolute).replaceAll("\\", "/") };
1070
+ const absolute = resolveDeclaredPath(root, declared);
1071
+ // A split's bound target lies outside the app: its absolute path is the evidence there.
1072
+ const evidencePath = () => { const relative = path.relative(root, absolute).replaceAll("\\", "/"); return relative.startsWith("..") ? absolute.replaceAll("\\", "/") : relative; };
1073
+ if (opts.dryRun) return { path: evidencePath() };
1046
1074
  if (!existsSync(absolute)) fail(`native finalizer signed artifact missing: ${absolute}`);
1047
1075
  return {
1048
- path: path.relative(root, absolute).replaceAll("\\", "/"),
1076
+ path: evidencePath(),
1049
1077
  sha256: createHash("sha256").update(readFileSync(absolute)).digest("hex"),
1050
1078
  sizeBytes: readFileSync(absolute).length,
1051
1079
  };
@@ -1079,7 +1107,7 @@ function nativeArchiveCandidates({ root, target, nativeAssembly }) {
1079
1107
  ...(target.installer?.artifacts ?? []).map((artifact) => artifact.file),
1080
1108
  ...(target.updater?.artifacts ?? []).map((artifact) => artifact.file),
1081
1109
  ].filter((value) => typeof value === "string" && value.trim());
1082
- return [...new Set(declared.map((value) => path.resolve(root, value)))];
1110
+ return [...new Set(declared.map((value) => resolveDeclaredPath(root, value)))];
1083
1111
  }
1084
1112
 
1085
1113
  function nativeFinalizerOutputPath({ finalizer, target, root }) {
@@ -1141,14 +1169,16 @@ function sweepStaleRustArtifacts() {
1141
1169
  }
1142
1170
 
1143
1171
  async function runInstall(pm, cwd) {
1144
- if (pm === "pnpm") await run("pnpm", ["install", "--frozen-lockfile"], cwd, {}, { install: true });
1145
- else if (pm === "npm") await run("npm", ["ci"], cwd, {}, { install: true });
1172
+ if (pm === "pnpm") { const t = buildLaunch("pnpm", ["install", "--frozen-lockfile"], { cwd, exists: existsSync }); await run(t.command, t.args, cwd, t.env ?? {}, { install: true }); }
1173
+ else if (pm === "npm") { const t = buildLaunch("npm", ["ci"], { cwd, exists: existsSync }); await run(t.command, t.args, cwd, t.env ?? {}, { install: true }); }
1146
1174
  else fail(`unsupported package manager: ${pm}`);
1147
1175
  }
1148
1176
 
1149
1177
  async function runPackageScript(pm, script, cwd) {
1150
1178
  if (script === false) return;
1151
- await run(pm, ["run", script], cwd);
1179
+ // A package script (a check or a web build) goes through the one door (build-launch.mjs): a broker job on a managed host.
1180
+ const target = buildLaunch(pm, ["run", script], { cwd, exists: existsSync });
1181
+ await run(target.command, target.args, cwd, target.env ?? {});
1152
1182
  }
1153
1183
 
1154
1184
  // A hook may be one command object or an ordered array of them (e.g. create-mac-updater then lsclean).
@@ -1162,9 +1192,16 @@ async function runCommands(value, root, options) {
1162
1192
  for (const command of commandList(value)) await runCommand(command, root, options);
1163
1193
  }
1164
1194
 
1195
+ // Every configured hook (prePackage, the package command and hook, sidecar stages, gradle, the finalizer, postSign, publish) is
1196
+ // arbitrary code that can build, so it goes through the one door (build-launch.mjs): this package's own right-release steps run as
1197
+ // they are (their builds come back through the door), node, pnpm, cargo and rightkit-qa are broker jobs, and anything else is refused
1198
+ // on a broker-managed host (rightkit#1241).
1165
1199
  async function runCommand(command, root, { env = {}, captureStdout = false } = {}) {
1166
1200
  const cwd = path.resolve(root, command.cwd ?? ".");
1167
- return run(command.cmd, command.args ?? [], cwd, { ...(await commandEnv(command, root)), ...env }, {
1201
+ const commandEnvironment = { ...(await commandEnv(command, root)), ...env };
1202
+ const target = buildLaunch(command.cmd, command.args ?? [], { env: { ...process.env, ...commandEnvironment }, cwd, exists: existsSync });
1203
+ // A direct (Linux) launch carries PATH without RustRight shim directories (build-launch `env`).
1204
+ return run(target.command, target.args, cwd, { ...commandEnvironment, ...(target.env ?? {}) }, {
1168
1205
  ...command,
1169
1206
  captureStdout,
1170
1207
  timeoutMs: commandTimeoutMs(command),
@@ -1896,7 +1933,7 @@ async function verifyWindowsEmbedding({ root, target, files, markers }) {
1896
1933
  console.log("dry-run: verify installer embeds the signed raw EXE");
1897
1934
  return;
1898
1935
  }
1899
- const rawFile = path.resolve(root, target.sign.prePackageFiles[0]);
1936
+ const rawFile = resolveDeclaredPath(root, target.sign.prePackageFiles[0]);
1900
1937
  const rawReceiptPath = windowsReceiptPath(root, "raw-exe");
1901
1938
  let rawReceipt;
1902
1939
  try { rawReceipt = JSON.parse(readFileSync(rawReceiptPath, "utf8")); } catch { fail(`missing raw EXE signing receipt: ${rawReceiptPath}`); }