@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
@@ -115,7 +115,7 @@ function readBatch(root, oids, totalBytes, runner) {
115
115
  return result;
116
116
  }
117
117
 
118
- function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runner = spawnSync, env = gitEnv() } = {}) {
118
+ function acquire(root, { expectedCommit, expectedTree, commit: selectedCommit, requiredPaths = [], runner = spawnSync, env = gitEnv() } = {}) {
119
119
  const text = (args) => runGit(root, args, { encoding: "utf8", runner, env }).trim();
120
120
  const repositoryRoot = path.resolve(text(["rev-parse", "--show-toplevel"]));
121
121
  const pathPrefix = text(["rev-parse", "--show-prefix"]).replaceAll("\\", "/");
@@ -125,9 +125,20 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
125
125
  const origin = originResult?.status === 0 ? String(originResult.stdout).trim() : null;
126
126
  const objectFormat = text(["rev-parse", "--show-object-format"]);
127
127
  if (objectFormat !== "sha1" && objectFormat !== "sha256") throw new Error("committed ownership: unsupported Git object format");
128
- const commit = text(["rev-parse", "--verify", "HEAD^{commit}"]);
129
128
  const digestLength = objectFormat === "sha256" ? 64 : 40;
130
- if (!new RegExp(`^[0-9a-f]{${digestLength}}$`).test(commit) || (expectedCommit && commit !== expectedCommit)) throw new Error("committed ownership: expected commit is not current HEAD");
129
+ // A door source split (release-source-identity.mjs, item 7 row 7.04) names the commit it builds: that commit's immutable objects are the
130
+ // source, never the checkout's HEAD. Only a full object id is accepted (never a ref, a revision expression or an option), and the
131
+ // object database must answer with that same commit. Without `commit` the source is HEAD, exactly as before.
132
+ let commit;
133
+ if (selectedCommit !== undefined) {
134
+ if (!new RegExp(`^[0-9a-f]{${digestLength}}$`).test(String(selectedCommit))) throw new Error("committed ownership: the selected commit is not a full object id");
135
+ commit = text(["rev-parse", "--verify", `${selectedCommit}^{commit}`]);
136
+ if (commit !== selectedCommit) throw new Error("committed ownership: the selected commit resolved to another object");
137
+ if (expectedCommit && commit !== expectedCommit) throw new Error("committed ownership: expected commit is not the selected commit");
138
+ } else {
139
+ commit = text(["rev-parse", "--verify", "HEAD^{commit}"]);
140
+ if (!new RegExp(`^[0-9a-f]{${digestLength}}$`).test(commit) || (expectedCommit && commit !== expectedCommit)) throw new Error("committed ownership: expected commit is not current HEAD");
141
+ }
131
142
  const tree = text(["rev-parse", "--verify", `${commit}^{tree}`]);
132
143
  if (!new RegExp(`^[0-9a-f]{${digestLength}}$`).test(tree) || (expectedTree && tree !== expectedTree)) throw new Error("committed ownership: expected tree does not match commit");
133
144
  const rows = parseTree(runGit(root, ["ls-tree", "-r", "-z", "--full-tree", tree], { encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env }), objectFormat);
@@ -161,9 +172,13 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
161
172
  return Object.freeze({ repositoryRoot, pathPrefix, origin, commit, tree, objectFormat, entries, blobs, coverage });
162
173
  }
163
174
 
175
+ /**
176
+ * The committed source of `root`'s repository. `options.commit` selects the commit by full object id (a door source split reads the
177
+ * selected commit's objects, never the checkout's HEAD); without it the source is HEAD. Blob-hash verification is the same either way.
178
+ */
164
179
  export function acquireCommittedSource(root, options = {}) {
165
- const { expectedCommit, expectedTree, requiredPaths = [] } = options;
166
- try { return createSourceHandle(acquire(path.resolve(root), { expectedCommit, expectedTree, requiredPaths })); }
180
+ const { expectedCommit, expectedTree, requiredPaths = [], commit } = options;
181
+ try { return createSourceHandle(acquire(path.resolve(root), { expectedCommit, expectedTree, requiredPaths, ...(commit !== undefined ? { commit } : {}) })); }
167
182
  catch (error) { if (error instanceof CommittedSourceIncompleteError) throw error; throw new CommittedSourceIncompleteError(error.message, error); }
168
183
  }
169
184
 
@@ -185,6 +200,25 @@ export function committedSourceDataForInternalUse(handle) {
185
200
  return acquiredSources.get(handle) ?? null;
186
201
  }
187
202
 
203
+ /**
204
+ * `{ commit, tree }` of `commit` read from `root`'s object database: the full object id must name a commit, and the tree is that
205
+ * commit's own. It reads no HEAD, no index and no status, so a dirty or moved checkout cannot change the answer. Throws on any failure.
206
+ */
207
+ export function resolveSelectedCommit(root, commit, { runner = spawnSync } = {}) {
208
+ const resolvedRoot = path.resolve(root);
209
+ const text = (args) => runGit(resolvedRoot, args, { encoding: "utf8", timeout: COMMAND_TIMEOUT_MS, runner, env: gitEnv() }).trim();
210
+ const objectFormat = text(["rev-parse", "--show-object-format"]);
211
+ if (objectFormat !== "sha1" && objectFormat !== "sha256") throw new Error("committed ownership: unsupported Git object format");
212
+ const digestLength = objectFormat === "sha256" ? 64 : 40;
213
+ const shape = new RegExp(`^[0-9a-f]{${digestLength}}$`);
214
+ if (!shape.test(String(commit))) throw new Error("committed ownership: the selected commit is not a full object id");
215
+ const resolved = text(["rev-parse", "--verify", `${commit}^{commit}`]);
216
+ if (resolved !== commit) throw new Error("committed ownership: the selected commit resolved to another object");
217
+ const tree = text(["rev-parse", "--verify", `${commit}^{tree}`]);
218
+ if (!shape.test(tree)) throw new Error("committed ownership: the selected commit has no tree");
219
+ return Object.freeze({ commit: resolved, tree, objectFormat });
220
+ }
221
+
188
222
  export function assertCommittedCheckoutClean(root, { expectedCommit, expectedTree, runner = spawnSync } = {}) {
189
223
  if (expectedCommit || expectedTree) {
190
224
  const currentCommit = runGit(path.resolve(root), ["rev-parse", "--verify", "HEAD^{commit}"], { encoding: "utf8", timeout: COMMAND_TIMEOUT_MS, runner, env: gitEnv() }).trim();
@@ -0,0 +1,87 @@
1
+ // One Cargo workspace and lock per RightKit crate (Adrian, 2026-10-29: "I want you to have 1 cargo dot lock per component";
2
+ // rightkit#1285). Every `crates/<x>` is its own workspace; crates that use each other link by path dependency across
3
+ // workspaces. This module reads manifests only (no cargo) and answers which crates exist, which sibling crates each one
4
+ // path-depends on, and which crates a change reaches: the changed crate plus every crate that path-depends on it, transitively.
5
+ // It imports nothing outside node: CI's plan job runs it before any package install, so it reads the few manifest keys it needs
6
+ // (the package name and dependency `path` values) line by line instead of through a TOML library.
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+
10
+ const posix = (p) => p.split(path.sep).join("/");
11
+ const defaults = { readFile: (f) => fs.readFileSync(f, "utf8"), readdir: (d) => fs.readdirSync(d), exists: fs.existsSync };
12
+
13
+ /** [{ name, dir: "crates/<x>", manifest, lock }] for every crates/<x>/Cargo.toml, sorted by dir. */
14
+ export function crateWorkspaces(root, io = {}) {
15
+ const { readFile, readdir, exists } = { ...defaults, ...io };
16
+ const cratesDir = path.join(root, "crates");
17
+ if (!exists(cratesDir)) return [];
18
+ const out = [];
19
+ for (const entry of [...readdir(cratesDir)].map(String).sort()) {
20
+ if (entry.startsWith(".")) continue;
21
+ const manifest = `crates/${entry}/Cargo.toml`;
22
+ if (!exists(path.join(root, manifest))) continue;
23
+ const name = manifestFacts(readFile(path.join(root, manifest))).name;
24
+ if (typeof name !== "string") continue;
25
+ out.push({ name, dir: `crates/${entry}`, manifest, lock: `crates/${entry}/Cargo.lock` });
26
+ }
27
+ return out;
28
+ }
29
+
30
+ const DEPENDENCY_TABLE = /^(?:target\.(?:'[^']*'|"[^"]*"|[^.\]]+)\.)?(?:dev-|build-)?dependencies$/;
31
+ const unquote = (v) => v.replace(/^(["'])(.*)\1$/, "$2");
32
+
33
+ /** { name, paths }: the [package] name and every dependency `path` value (inline tables and `[dependencies.<x>]` sections). */
34
+ export function manifestFacts(text) {
35
+ let section = ""; let name; const paths = [];
36
+ for (const raw of String(text).split(/\r?\n/)) {
37
+ const line = raw.replace(/\s+#.*$/, "").trim();
38
+ if (!line || line.startsWith("#")) continue;
39
+ const header = line.match(/^\[\[?([^\[\]]+)\]\]?$/);
40
+ if (header) { section = header[1].trim(); continue; }
41
+ if (section === "package") { const m = line.match(/^name\s*=\s*("[^"]*"|'[^']*')/); if (m) name = unquote(m[1]); continue; }
42
+ if (DEPENDENCY_TABLE.test(section)) {
43
+ const m = line.match(/^[A-Za-z0-9_.-]+\s*=\s*\{(.*)\}$/);
44
+ const p = m && m[1].match(/(?:^|,)\s*path\s*=\s*("[^"]*"|'[^']*')/);
45
+ if (p) paths.push(unquote(p[1]));
46
+ continue;
47
+ }
48
+ const dotted = section.match(/^(.*)\.([^.]+)$/);
49
+ if (dotted && DEPENDENCY_TABLE.test(dotted[1])) { const m = line.match(/^path\s*=\s*("[^"]*"|'[^']*')/); if (m) paths.push(unquote(m[1])); }
50
+ }
51
+ return { name, paths };
52
+ }
53
+
54
+ /** Map<name, Set<name>>: each crate's direct path dependencies on sibling crates (normal, dev, build, any target). */
55
+ export function pathDependencies(root, io = {}, workspaces = crateWorkspaces(root, io)) {
56
+ const { readFile } = { ...defaults, ...io };
57
+ const byDir = new Map(workspaces.map((w) => [w.dir, w.name]));
58
+ const deps = new Map(workspaces.map((w) => [w.name, new Set()]));
59
+ for (const w of workspaces) {
60
+ for (const dep of manifestFacts(readFile(path.join(root, w.manifest))).paths) {
61
+ const target = byDir.get(posix(path.posix.normalize(path.posix.join(w.dir, posix(dep)))));
62
+ if (target && target !== w.name) deps.get(w.name).add(target);
63
+ }
64
+ }
65
+ return deps;
66
+ }
67
+
68
+ /** Set<name>: every crate that path-depends on any of `names`, transitively; `names` themselves are excluded. */
69
+ export function reverseDependents(names, deps) {
70
+ const reverse = new Map();
71
+ for (const [from, tos] of deps) for (const to of tos) (reverse.get(to) ?? reverse.set(to, new Set()).get(to)).add(from);
72
+ const seen = new Set(); const queue = [...names];
73
+ while (queue.length) {
74
+ for (const dependent of reverse.get(queue.shift()) ?? []) {
75
+ if (!seen.has(dependent)) { seen.add(dependent); queue.push(dependent); }
76
+ }
77
+ }
78
+ for (const n of names) seen.delete(n);
79
+ return seen;
80
+ }
81
+
82
+ /** The crate whose workspace holds `file` (repository-relative, posix), or null. */
83
+ export function crateOfFile(file, workspaces) {
84
+ let best = null;
85
+ for (const w of workspaces) if (file.startsWith(`${w.dir}/`) && (!best || w.dir.length > best.dir.length)) best = w;
86
+ return best?.name ?? null;
87
+ }
@@ -3,6 +3,7 @@ import { spawnSync } from "./background-process.mjs";
3
3
  import { existsSync, rmSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { signMacUpdaterFiles } from "./macos-signing-session.mjs";
6
+ import { ArtifactPathError, loadSplitArtifactContext, resolveArtifactPath, resolveArtifactWritePath } from "./release-artifact-path.mjs";
6
7
 
7
8
  const args = process.argv.slice(2);
8
9
  let app;
@@ -18,8 +19,19 @@ for (let i = 0; i < args.length; i++) {
18
19
  }
19
20
  if (!app || !output) fail("--app and --output are required");
20
21
 
21
- const appPath = path.resolve(cwd, app);
22
- const outputPath = path.resolve(cwd, output);
22
+ // A door split (row 7.04c, R-T2): a target-prefixed `--app` is the bound Cargo target's, an absolute one must lie inside it, and the tarball is
23
+ // written under the snapshot app root (a generated, ignored ledger entry). Outside a split both are resolved against the cwd as always.
24
+ let appPath;
25
+ let outputPath;
26
+ try {
27
+ const artifactPaths = await loadSplitArtifactContext({ cwd });
28
+ const split = artifactPaths.mode === "split";
29
+ appPath = split ? resolveArtifactPath(app, artifactPaths) : path.resolve(cwd, app);
30
+ outputPath = split ? resolveArtifactWritePath(output, artifactPaths) : path.resolve(cwd, output);
31
+ } catch (error) {
32
+ if (error instanceof ArtifactPathError) fail(error.message);
33
+ throw error;
34
+ }
23
35
  if (!dryRun && !existsSync(appPath)) fail(`signed app not found: ${appPath}`);
24
36
 
25
37
  const env = { ...process.env };