@rightkit/release 0.4.0 → 0.5.0

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 (50) hide show
  1. package/README.md +59 -7
  2. package/adhoc-entitlements.mjs +8 -0
  3. package/adoption-evidence.mjs +650 -0
  4. package/advisory-exceptions.mjs +371 -0
  5. package/apple-profile-create.mjs +113 -10
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/rightkit-gate.mjs +1079 -0
  8. package/binary-license-notices.mjs +161 -0
  9. package/cargo-license-notices.mjs +111 -0
  10. package/cli/right-release.mjs +39 -0
  11. package/deterministic-scans.mjs +966 -0
  12. package/development-signing.mjs +2 -1
  13. package/host-upload-refusal.mjs +90 -0
  14. package/local-publish.mjs +142 -19
  15. package/local-qualify.mjs +728 -0
  16. package/local-runtime.mjs +319 -0
  17. package/lock-graph.mjs +51 -0
  18. package/managed-cargo.mjs +4 -1
  19. package/ownership-approval.mjs +23 -6
  20. package/ownership-dependency-allowlist.json +2 -0
  21. package/ownership-mandatory-set.json +158 -68
  22. package/ownership-mandatory-set.mjs +82 -6
  23. package/package.json +6 -2
  24. package/pipeline-admission.mjs +375 -0
  25. package/pipeline-config.mjs +100 -0
  26. package/pipeline-generators.json +4 -0
  27. package/pipeline-generators.mjs +137 -0
  28. package/pipeline-receipts.mjs +302 -0
  29. package/pipeline-scan.mjs +285 -0
  30. package/pipeline-tools.mjs +171 -0
  31. package/production-mode.mjs +42 -0
  32. package/publication-gate.mjs +94 -32
  33. package/publish-cargo.mjs +24 -7
  34. package/publish-swift.mjs +12 -3
  35. package/qa-control-scan.mjs +4 -0
  36. package/release-config.mjs +330 -0
  37. package/release-switch-scan.mjs +44 -5
  38. package/release.mjs +14 -4
  39. package/rightkit-ownership.json +57 -0
  40. package/rightkit-sync.mjs +8 -0
  41. package/rightkit-versions.json +191 -131
  42. package/runtime-budget-classes.json +8 -0
  43. package/runtime-budget-crates.mjs +14 -0
  44. package/runtime-budgets.mjs +128 -31
  45. package/scan-digest.mjs +107 -0
  46. package/sign-qa-bundle.mjs +210 -0
  47. package/stage-sidecar.mjs +99 -15
  48. package/swap-helper-bundle.mjs +7 -3
  49. package/workflow-yaml.mjs +177 -0
  50. package/bin/darwin/rightkit-swap.sign.json +0 -6
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ // `right-release license-notices binaries [--config <file>] [--output <file>] [--product-name <name>] [--receipt <path>] [--check]`:
3
+ // writes the notice appendix (one HTML file) for the binaries an app ships that no RightKit catalog row covers (a downloaded yt-dlp, a
4
+ // vendored tool, a command-built helper). Catalog-backed sidecars (ffmpeg, onnxruntime, pdfium, whisper) write their own notices when
5
+ // they are staged; everything else must be declared here, with its licence text, or the build has no right to ship it.
6
+ //
7
+ // legal: { binaryNotices: { output: "legal/THIRD_PARTY_BINARIES.html", productName: "ScrapeRight", receipt: "src-tauri/binaries/stage-sidecar.json",
8
+ // binaries: [{ name: "yt-dlp", version: "2026.03.17" | "self-updating", license: "Unlicense", sourceUrl: "https://github.com/yt-dlp/yt-dlp",
9
+ // files: ["legal/texts/yt-dlp-UNLICENSE.txt"], covers?: ["yt-dlp-helper"] }] } }
10
+ //
11
+ // Every `files` entry is an app-relative text file that must exist; no licence text is ever invented. With a `receipt` (the
12
+ // stage-sidecar receipt, also settable with `--receipt`) every staged file of kind remote, command, cmake or prebuilt must belong to a
13
+ // declared binary - by its entry name, or a name listed in `covers` - or the run fails naming the uncovered ones. The page records the
14
+ // receipt-independent inputs only (declared fields and the sha256 of each text), so the same declaration always produces the same bytes.
15
+ // `--check` writes nothing and exits 1 when the file on disk differs.
16
+ import { createHash } from "node:crypto";
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import path from "node:path";
19
+ import { pathToFileURL } from "node:url";
20
+ import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
21
+ import { escapeHtml } from "./npm-license-notices.mjs";
22
+
23
+ const fail = (message) => { throw new CommandSpecError(message); };
24
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
25
+ const KEYS = ["output", "productName", "receipt", "binaries"];
26
+ const BINARY_KEYS = ["name", "version", "license", "sourceUrl", "files", "covers"];
27
+ /** Receipt kinds with no catalog row behind them. */
28
+ export const NON_CATALOG_KINDS = Object.freeze(["remote", "command", "cmake", "prebuilt"]);
29
+
30
+ /** Resolve and validate `legal.binaryNotices` plus flags. */
31
+ export function resolveBinaryNoticeOptions(config, flags = {}) {
32
+ const declared = config?.legal?.binaryNotices;
33
+ if (!isObject(declared)) fail("legal.binaryNotices must be an object");
34
+ const unknown = Object.keys(declared).filter((key) => !KEYS.includes(key));
35
+ if (unknown.length) fail(`legal.binaryNotices has unknown key(s): ${unknown.join(", ")} (allowed: ${KEYS.join(", ")})`);
36
+ const output = flags.output ?? declared.output;
37
+ if (output === undefined) fail("no output file: set legal.binaryNotices.output or pass --output");
38
+ relativeInside(output, "legal.binaryNotices.output");
39
+ const productName = flags.productName ?? declared.productName;
40
+ if (typeof productName !== "string" || !productName.trim()) fail("no product name: set legal.binaryNotices.productName or pass --product-name");
41
+ const receipt = flags.receipt ?? declared.receipt;
42
+ if (receipt !== undefined) relativeInside(receipt, "legal.binaryNotices.receipt");
43
+ if (!Array.isArray(declared.binaries) || !declared.binaries.length) fail("legal.binaryNotices.binaries must be a non-empty list");
44
+ const names = new Set();
45
+ const binaries = declared.binaries.map((item, index) => {
46
+ const label = `legal.binaryNotices.binaries[${index}]`;
47
+ if (!isObject(item)) fail(`${label} must be an object`);
48
+ const extra = Object.keys(item).filter((key) => !BINARY_KEYS.includes(key));
49
+ if (extra.length) fail(`${label} has unknown key(s): ${extra.join(", ")} (allowed: ${BINARY_KEYS.join(", ")})`);
50
+ for (const key of ["name", "version", "license"]) if (typeof item[key] !== "string" || !item[key].trim()) fail(`${label}.${key} must be a non-empty string`);
51
+ if (typeof item.sourceUrl !== "string" || !/^https:\/\/[^\s/]+\/\S*$/.test(item.sourceUrl)) fail(`${label}.sourceUrl must be an https:// URL of the source or release`);
52
+ if (!Array.isArray(item.files) || !item.files.length) fail(`${label}.files must list at least one licence/notice text file`);
53
+ const files = item.files.map((file, fileIndex) => relativeInside(file, `${label}.files[${fileIndex}]`));
54
+ const covers = item.covers ?? [];
55
+ if (!Array.isArray(covers) || covers.some((name) => typeof name !== "string" || !name.trim())) fail(`${label}.covers must be a list of staged entry names`);
56
+ for (const name of [item.name, ...covers]) {
57
+ if (names.has(name)) fail(`${label}: ${name} is declared twice`);
58
+ names.add(name);
59
+ }
60
+ return { name: item.name, version: item.version, license: item.license, sourceUrl: item.sourceUrl, files, covers: [...covers] };
61
+ });
62
+ return { output, productName, receipt, binaries };
63
+ }
64
+
65
+ /** The names of staged non-catalog entries that no declared binary covers. */
66
+ export function uncoveredBinaries(receipt, binaries) {
67
+ const covered = new Set(binaries.flatMap((binary) => [binary.name, ...(binary.covers ?? [])]));
68
+ const missing = new Set();
69
+ for (const file of receipt?.files ?? []) {
70
+ if (!NON_CATALOG_KINDS.includes(file.kind)) continue;
71
+ const name = file.entryName ?? file.command ?? file.name;
72
+ if (!covered.has(name)) missing.add(name);
73
+ }
74
+ return [...missing].sort();
75
+ }
76
+
77
+ /** Render the appendix. `texts` maps an app-relative file to its text. Returns the HTML. */
78
+ export function renderBinaryNotices({ productName, binaries, texts }) {
79
+ const sections = binaries.map((binary) => {
80
+ const bodies = binary.files.map((file) => `<h3>${escapeHtml(path.posix.basename(file))}</h3><pre>${escapeHtml(texts.get(file))}</pre>`).join("\n");
81
+ const digests = binary.files.map((file) => `${escapeHtml(path.posix.basename(file))} sha256=${createHash("sha256").update(texts.get(file)).digest("hex")}`).join("; ");
82
+ return `<section><h2>${escapeHtml(binary.name)} ${escapeHtml(binary.version)}</h2><p>License: ${escapeHtml(binary.license)}</p><p>Source: ${escapeHtml(binary.sourceUrl)}</p><p>Texts: ${digests}</p>\n${bodies}</section>`;
83
+ });
84
+ return `<!doctype html>
85
+ <html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(productName)} bundled program licenses</title></head><body>
86
+ <h1>${escapeHtml(productName)} bundled program licenses</h1>
87
+ <p>Programs shipped with ${escapeHtml(productName)} that are not Cargo or npm dependencies; generated-by=right-release license-notices binaries</p>
88
+ ${sections.join("\n")}
89
+ </body></html>
90
+ `;
91
+ }
92
+
93
+ /** Generate (or check) the appendix for the app at `appRoot`. File access is injectable. */
94
+ export function generateBinaryNotices({ appRoot, config, flags = {}, check = false, exists = existsSync, readText = (file) => readFileSync(file, "utf8"), writeText = (file, text) => { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, text); } }) {
95
+ const { output, productName, receipt, binaries } = resolveBinaryNoticeOptions(config, flags);
96
+ const texts = new Map();
97
+ for (const binary of binaries) {
98
+ for (const file of binary.files) {
99
+ const absolute = path.join(appRoot, file);
100
+ if (!exists(absolute)) fail(`${binary.name}: licence text ${file} does not exist; no text is invented, add the file`);
101
+ const text = String(readText(absolute)).replace(/\r\n/g, "\n");
102
+ if (!text.trim()) fail(`${binary.name}: licence text ${file} is empty`);
103
+ texts.set(file, text);
104
+ }
105
+ }
106
+ let uncovered = [];
107
+ if (receipt !== undefined) {
108
+ const receiptFile = path.join(appRoot, receipt);
109
+ if (!exists(receiptFile)) fail(`the stage-sidecar receipt ${receipt} does not exist; run right-release stage-sidecar first`);
110
+ let parsed;
111
+ try { parsed = JSON.parse(readText(receiptFile)); } catch (error) { fail(`the stage-sidecar receipt ${receipt} is not JSON (${error.message})`); }
112
+ uncovered = uncoveredBinaries(parsed, binaries);
113
+ if (uncovered.length) fail(`staged program(s) with no declared licence notice: ${uncovered.join(", ")}; declare them in legal.binaryNotices.binaries (name or covers)`);
114
+ }
115
+ const html = renderBinaryNotices({ productName, binaries, texts });
116
+ const target = path.join(appRoot, output);
117
+ if (check) {
118
+ const current = exists(target) ? String(readText(target)).replace(/\r\n/g, "\n") : null;
119
+ return { target, binaryCount: binaries.length, uncovered, upToDate: current === html };
120
+ }
121
+ writeText(target, html);
122
+ return { target, binaryCount: binaries.length, uncovered, upToDate: true };
123
+ }
124
+
125
+ export function parseBinaryNoticeArgs(argv) {
126
+ const opts = { config: "right-release.config.mjs", output: undefined, productName: undefined, receipt: undefined, check: false };
127
+ for (let i = 0; i < argv.length; i += 1) {
128
+ const arg = argv[i];
129
+ if (arg === "--config" || arg === "--output" || arg === "--product-name" || arg === "--receipt") {
130
+ const value = argv[i + 1];
131
+ if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
132
+ opts[arg === "--product-name" ? "productName" : arg.slice(2)] = value;
133
+ i += 1;
134
+ } else if (arg === "--check") opts.check = true;
135
+ else throw new Error(`unknown license-notices argument: ${arg}`);
136
+ }
137
+ return opts;
138
+ }
139
+
140
+ export async function main(argv = process.argv.slice(2)) {
141
+ const opts = parseBinaryNoticeArgs(argv);
142
+ const { config, appRoot } = await loadAppConfig(opts.config);
143
+ const outcome = generateBinaryNotices({ appRoot, config, flags: { output: opts.output, productName: opts.productName, receipt: opts.receipt }, check: opts.check });
144
+ if (opts.check) {
145
+ if (!outcome.upToDate) {
146
+ console.error(`right-release license-notices: ${outcome.target} is out of date; run right-release license-notices binaries`);
147
+ return 1;
148
+ }
149
+ console.log(`${outcome.target} is up to date (${outcome.binaryCount} program notices)`);
150
+ return 0;
151
+ }
152
+ console.log(`wrote ${outcome.binaryCount} program notices to ${outcome.target}`);
153
+ return 0;
154
+ }
155
+
156
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
157
+ main().then((code) => { process.exitCode = code; }, (error) => {
158
+ console.error(`right-release license-notices: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
159
+ process.exitCode = 1;
160
+ });
161
+ }
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // `right-release license-notices cargo [--config <file>] [--output <file>] [--check]`: writes the third-party Cargo license appendix
3
+ // (one file) for an app's Rust dependency closure with `cargo about`, run through the managed cargo client (`runCargoSync`: the
4
+ // RightKit broker on a managed host, never a bare-cargo fallback on a managed Mac).
5
+ //
6
+ // legal: { cargoNotices: { output: "legal/THIRD_PARTY_CARGO_LICENSES.html", template: "legal/about.hbs",
7
+ // config?: "legal/about.toml", manifestPath?: "src-tauri/Cargo.toml", features?: ["a"], allFeatures?: true } }
8
+ //
9
+ // `output`, `template`, `config` and `manifestPath` are app-relative and may not leave the app. The run is `cargo about generate --locked
10
+ // --fail [--config <config>] [--manifest-path <path>] [--all-features | --features a,b] <template>`; cargo-about writes the rendered
11
+ // appendix to stdout, which is normalised to LF and one trailing newline so the same lockfile always produces the same bytes. `--fail`
12
+ // makes an unaccepted license or an unresolved clarification an error instead of a warning. `--check` writes nothing and exits 1 when
13
+ // the file on disk differs.
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import path from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+ import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
18
+ import { runCargoSync } from "./managed-cargo.mjs";
19
+
20
+ const fail = (message) => { throw new CommandSpecError(message); };
21
+ const KEYS = ["output", "template", "config", "manifestPath", "features", "allFeatures"];
22
+
23
+ /** Resolve and validate `legal.cargoNotices` plus flags. */
24
+ export function resolveCargoNoticeOptions(config, flags = {}) {
25
+ const declared = config?.legal?.cargoNotices ?? {};
26
+ if (typeof declared !== "object" || declared === null || Array.isArray(declared)) fail("legal.cargoNotices must be an object");
27
+ const unknown = Object.keys(declared).filter((key) => !KEYS.includes(key));
28
+ if (unknown.length) fail(`legal.cargoNotices has unknown key(s): ${unknown.join(", ")} (allowed: ${KEYS.join(", ")})`);
29
+ const output = flags.output ?? declared.output;
30
+ if (output === undefined) fail("no output file: set legal.cargoNotices.output or pass --output");
31
+ relativeInside(output, "legal.cargoNotices.output");
32
+ if (declared.template === undefined) fail("legal.cargoNotices.template (the cargo-about handlebars template) is required");
33
+ relativeInside(declared.template, "legal.cargoNotices.template");
34
+ for (const key of ["config", "manifestPath"]) if (declared[key] !== undefined) relativeInside(declared[key], `legal.cargoNotices.${key}`);
35
+ const features = declared.features ?? [];
36
+ if (!Array.isArray(features) || features.some((feature) => typeof feature !== "string" || !/^[A-Za-z0-9_/-]+$/.test(feature))) fail("legal.cargoNotices.features must be a list of feature names");
37
+ if (declared.allFeatures !== undefined && typeof declared.allFeatures !== "boolean") fail("legal.cargoNotices.allFeatures must be a boolean");
38
+ if (declared.allFeatures === true && features.length) fail("legal.cargoNotices: give allFeatures or features, not both");
39
+ return { output, template: declared.template, config: declared.config, manifestPath: declared.manifestPath, features, allFeatures: declared.allFeatures === true };
40
+ }
41
+
42
+ /** The `cargo about` argument list (after the managed client adds `cargo`). */
43
+ export function cargoAboutArgs(options) {
44
+ return [
45
+ "about", "generate", "--locked", "--fail",
46
+ ...(options.config ? ["--config", options.config] : []),
47
+ ...(options.manifestPath ? ["--manifest-path", options.manifestPath] : []),
48
+ ...(options.allFeatures ? ["--all-features"] : options.features.length ? ["--features", options.features.join(",")] : []),
49
+ options.template,
50
+ ];
51
+ }
52
+
53
+ const normalise = (text) => `${String(text).replace(/\r\n/g, "\n").replace(/\s+$/, "")}\n`;
54
+
55
+ /** Generate (or check) the Cargo appendix for the app at `appRoot`. `run` is the managed cargo runner (injectable). */
56
+ export function generateCargoNotices({ appRoot, config, flags = {}, check = false, run = runCargoSync, env = process.env, exists = existsSync, readText = (file) => readFileSync(file, "utf8"), writeText = (file, text) => { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, text); } }) {
57
+ const options = resolveCargoNoticeOptions(config, flags);
58
+ for (const key of ["template", "config", "manifestPath"]) {
59
+ if (options[key] !== undefined && !exists(path.join(appRoot, options[key]))) fail(`legal.cargoNotices.${key} ${options[key]} does not exist`);
60
+ }
61
+ const result = run(cargoAboutArgs(options), { cwd: appRoot, env, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
62
+ if (result?.error) fail(`cargo about could not start (${result.error.message}); install cargo-about on the build host`);
63
+ if (result?.status !== 0) fail(`cargo about exited ${result?.status ?? "without status"}${result?.stderr ? `:\n${String(result.stderr).trim()}` : ""}`);
64
+ const text = normalise(result.stdout ?? "");
65
+ if (text.trim() === "") fail("cargo about produced no output");
66
+ const target = path.join(appRoot, options.output);
67
+ if (check) {
68
+ const current = exists(target) ? normalise(readText(target)) : null;
69
+ return { target, bytes: Buffer.byteLength(text), upToDate: current === text };
70
+ }
71
+ writeText(target, text);
72
+ return { target, bytes: Buffer.byteLength(text), upToDate: true };
73
+ }
74
+
75
+ export function parseCargoNoticeArgs(argv) {
76
+ const opts = { config: "right-release.config.mjs", output: undefined, check: false };
77
+ for (let i = 0; i < argv.length; i += 1) {
78
+ const arg = argv[i];
79
+ if (arg === "--config" || arg === "--output") {
80
+ const value = argv[i + 1];
81
+ if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
82
+ opts[arg.slice(2)] = value;
83
+ i += 1;
84
+ } else if (arg === "--check") opts.check = true;
85
+ else throw new Error(`unknown license-notices argument: ${arg}`);
86
+ }
87
+ return opts;
88
+ }
89
+
90
+ export async function main(argv = process.argv.slice(2)) {
91
+ const opts = parseCargoNoticeArgs(argv);
92
+ const { config, appRoot } = await loadAppConfig(opts.config);
93
+ const outcome = generateCargoNotices({ appRoot, config, flags: { output: opts.output }, check: opts.check });
94
+ if (opts.check) {
95
+ if (!outcome.upToDate) {
96
+ console.error(`right-release license-notices: ${outcome.target} is out of date; run right-release license-notices cargo`);
97
+ return 1;
98
+ }
99
+ console.log(`${outcome.target} is up to date`);
100
+ return 0;
101
+ }
102
+ console.log(`wrote ${outcome.target} (${outcome.bytes} bytes)`);
103
+ return 0;
104
+ }
105
+
106
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
107
+ main().then((code) => { process.exitCode = code; }, (error) => {
108
+ console.error(`right-release license-notices: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
109
+ process.exitCode = 1;
110
+ });
111
+ }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { spawn } from "../background-process.mjs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { assertReleaseExecutionAllowed } from "../execution-policy.mjs";
6
+ import { hostUploadRefusalMessage } from "../host-upload-refusal.mjs";
6
7
 
7
8
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
9
  const commands = new Map([
@@ -19,6 +20,7 @@ const commands = new Map([
19
20
  ["sign-windows", "sign-windows.mjs"],
20
21
  ["sign-updater", "sign-updater.mjs"],
21
22
  ["sign-dev-artifact", "sign-dev-artifact.mjs"],
23
+ ["sign-qa-bundle", "sign-qa-bundle.mjs"],
22
24
  ["sign-candidate", "sign-candidate.mjs"],
23
25
  ["engine-locate", "engine-locator.mjs"],
24
26
  ["stage-sidecar", "stage-sidecar-command.mjs"],
@@ -40,6 +42,7 @@ const commands = new Map([
40
42
  ["ownership-approval", "ownership-approval-command.mjs"],
41
43
  ["ownership-report", "ownership-report-command.mjs"],
42
44
  ["ownership-mandatory", "ownership-mandatory-set.mjs"],
45
+ ["adoption-evidence", "adoption-evidence.mjs"],
43
46
  ["swift-package", "swift-package-command.mjs"],
44
47
  ["bundle-scan", "runtime-bundle-scan.mjs"],
45
48
  ["check-release-dependencies", "release-dependencies-command.mjs"],
@@ -64,6 +67,7 @@ try {
64
67
  || first?.startsWith("-")
65
68
  || (first === "addon" && args[1] === "build")
66
69
  || (first === "dmg" && args[1] === "build")
70
+ || first === "qualify"
67
71
  || (first === "publish" && ["cargo", "swift"].includes(args[1]))) {
68
72
  assertReleaseExecutionAllowed({ command: `right-release ${args.join(" ")}`.trim() });
69
73
  }
@@ -102,8 +106,25 @@ if (first === "--version" || first === "-v") {
102
106
  run("dmg-build.mjs", args.slice(1));
103
107
  } else if (first === "apple") {
104
108
  run("apple-profile-renew.mjs", args.slice(1));
109
+ } else if (first === "qualify") {
110
+ // Local qualification (default): both native hosts qualify the crates through the RustRight broker and a candidate is cut.
111
+ // It uploads nothing; the approval commit and publish-reviewed.yml publish the pinned bytes (docs/CI.md "Local qualification").
112
+ run("local-qualify.mjs", args.slice(1));
105
113
  } else if (first === "publish") {
106
114
  const rest = args.slice(1);
115
+ // RightKit crates and @rightkit packages reach a registry from CI only: publish.yml qualification, the approval
116
+ // commit in ci/approved-artifacts.json, then publish-reviewed.yml uploads exactly the approved bytes (docs/CI.md).
117
+ // Every host-side upload path is refused on every host; preflight, --dry-run and --qualify-only still run.
118
+ if (rest[0] === "cargo" || rest[0] === "npm") {
119
+ const uploads = rest[0] === "npm"
120
+ ? rest.includes("--publish") || rest.includes("--local")
121
+ : !rest.includes("--dry-run") && !rest.includes("--qualify-only");
122
+ // Unconditional: a caller-set GITHUB_ACTIONS=true never lifts it (CI uploads use scripts/ci/publish.mjs and reviewed-candidate.mjs).
123
+ if (uploads) {
124
+ console.error(hostUploadRefusalMessage(`right-release publish ${rest[0]}`));
125
+ process.exit(2);
126
+ }
127
+ }
107
128
  if ((rest[0] === "cargo" || rest[0] === "npm") && rest.includes("--local")) {
108
129
  run("local-publish.mjs", rest);
109
130
  } else if (rest[0] === "cargo") {
@@ -136,6 +157,9 @@ if (first === "--version" || first === "-v") {
136
157
  runBinary("python3", [path.join(packageRoot, "generate-dmg-background.py"), ...args.slice(1)]);
137
158
  } else if (first === "build" && args.includes("--web")) {
138
159
  run("web-build.mjs", args.slice(1).filter((arg) => arg !== "--web"));
160
+ } else if (first === "license-notices" && (args[1] === "cargo" || args[1] === "binaries")) {
161
+ // `license-notices npm` stays on the commands map; the Rust and native-binary appendices have their own modules.
162
+ run(args[1] === "cargo" ? "cargo-license-notices.mjs" : "binary-license-notices.mjs", args.slice(2));
139
163
  } else if (commands.has(first)) {
140
164
  run(commands.get(first), args.slice(1));
141
165
  } else if (first.startsWith("-")) {
@@ -217,6 +241,10 @@ Commands:
217
241
  Run the app's declared test suites (config tests.suites / tests.runners) in order; stops at the first failure
218
242
  license-notices npm [--config <file>] [--output <file>] [--product-name <name>] [--check]
219
243
  Write the third-party npm license appendix HTML from pnpm licenses (config legal.npmNotices); --check only compares
244
+ license-notices cargo [--config <file>] [--output <file>] [--check]
245
+ Write the third-party Cargo license appendix HTML through cargo about (config legal.cargoNotices); --check only compares
246
+ license-notices binaries [--config <file>] [--output <file>] [--product-name <name>] [--receipt <path>] [--check]
247
+ Write the notices for shipped programs that are not Cargo or npm dependencies (config legal.binaryNotices); --check only compares
220
248
  toolchain swift --version <x.y.z> --sha256 <pin> [--url <https url>] [--github-env] [--json]
221
249
  Windows: install the pinned swift.org Swift toolchain (sha256 verified before it runs), check swift --version, find the runtime bin; --github-env exports what the installer changed to GITHUB_ENV / GITHUB_PATH
222
250
  dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
@@ -229,6 +257,8 @@ Commands:
229
257
  Fail when a built web dist/ carries a forbidden marker string or mock/fixture chunk name (dev, mock or fixture code in release)
230
258
  sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]
231
259
  Developer ID sign an existing CI dev artifact per file with fixed identifiers; never builds, notarizes or publishes (macOS)
260
+ sign-qa-bundle --app <abs .app> --qa-root <abs dir> [--entitlements <abs plist>] [--out <evidence.json>]
261
+ QA ONLY: ad hoc sign a bundle inside a QA scratch root, nested code first, outer last, never --deep, never the signing agent (macOS); not a release artifact
232
262
  sign-candidate --platform win|mac --artifact <dir> --candidate-receipt <receipt.json> --out <binding.json> [--manifest <sign.json>] [--publisher <subject>] [--dry-run]
233
263
  Sign the exact unsigned candidate bytes (sha256 checked against the candidate receipt) instead of rebuilding; writes a candidate-sha -> signed-sha binding receipt
234
264
  engine-locate --engine cutright|genright [--sha256 <hex>] [--json]
@@ -260,6 +290,13 @@ Commands:
260
290
  Verify on source host; export crate bytes & gate evidence without uploading
261
291
  publish cargo --crate <name> --from-evidence <gate.json>
262
292
  Reuse exact HEAD/artifact proof on upload host; recheck archive hash & registry bytes
293
+ qualify cargo --crates <a,b> [--hosts win,mac] [--id <id>] [--allow-ignored-units <a,b>] [--jobs <1-64>] [--concurrency <1-8>] [--out <dir>] [--no-release] [--with-ci]
294
+ Qualify the crates on BOTH native hosts through the RustRight broker (Dell: Windows, Mac: macOS over ssh), seal one packed
295
+ asset and one receipt per host, attach them to the prerelease candidate-<id> and print the candidate and approval entry.
296
+ Needs a clean checkout with HEAD on origin/main (the other host at the same HEAD). Never uploads to a registry.
297
+ --with-ci also dispatches publish.yml for the same commit when the hosts start and pins that run in the candidate as clean-room proof
298
+ (Linux-required units and runtime owners no local workload covers are planned only with --with-ci, and then need that run to succeed).
299
+ Any repository: owner/name from its origin remote; hosts, approvals file and promotion workflow from .rightkit-gate.json
263
300
  publish swift [--dry-run] [--evidence <file>] [--workspace <dir>]
264
301
  Check main, a clean tree, and the publication evidence, then tag RightKitSwift at its VERSION on origin
265
302
  inventory check [--json] [--warn-only] [--md <file>] [--no-cache]
@@ -288,6 +325,8 @@ Commands:
288
325
  Report cross-app ownership & pending requests
289
326
  ownership-mandatory [--workspace <dir>] [--app <name>]... [--amendment v2.1] [--enforce] [--json]
290
327
  Check each app declares every Table 1 owner at its required version (report by default)
328
+ adoption-evidence --manifest <published-release-manifest.json> [--packets <dir>] [--app <dir> [--adoption <file>] [--target <target>]] [--json]
329
+ Validate the handoff manifest (derived fields, AP set = frozen packets) and, with --app, an app's adoption record (lock pins, closure, receipts)
291
330
  cache status [--json] Inspect the shared local build cache
292
331
  cache prune [--dry-run|--apply] [--json] Plan or apply safe shared-target pruning
293
332
  cache migrate --config <file> [--dry-run|--apply] [--json]