@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.
- package/README.md +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- 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", [
|
|
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) =>
|
|
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
|
-
|
|
898
|
-
if (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
return
|
|
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 =
|
|
1045
|
-
|
|
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:
|
|
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) =>
|
|
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")
|
|
1145
|
-
else if (pm === "npm")
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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}`); }
|