@rightkit/release 0.6.33 → 0.6.34

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/carried-cargo.mjs CHANGED
@@ -17,10 +17,13 @@
17
17
  // map file through `RIGHTKIT_CARRIED_CARGO_MAP` and reads the ledger after the child ends: a refused, unverified or unfinished call fails
18
18
  // the stage, and every verified call, nested children included, is verified again from the broker's receipt.
19
19
  import fs from "node:fs";
20
+ import os from "node:os";
20
21
  import path from "node:path";
21
22
  import { spawnSync } from "./background-process.mjs";
22
23
  import { appendLedger, readLedger, SCAN_TOKEN_ENV, UNSCOPED } from "./cargo-ledger.mjs";
23
- import { SHIM_MAP_SCHEMA, locateWorkspaceByWalk } from "./cargo-target-shim.mjs";
24
+ import { SHIM_MAP_SCHEMA, locateWorkspaceByWalk, readOnlyCargo } from "./cargo-target-shim.mjs";
25
+ import { directCargoPath } from "./managed-cargo.mjs";
26
+ import { carriedScanRefusal } from "./snapshot-metadata.mjs";
24
27
  import { BROKER_REFUSAL_STATUS, carryBrokerAdapter, carryBrokerCargo, verifyBrokerLedger } from "./snapshot-broker.mjs";
25
28
  import { splitCarriedArgs } from "./snapshot-request.mjs";
26
29
 
@@ -39,6 +42,15 @@ export function carriedCargoMapOf(env = process.env) {
39
42
  } catch (error) { return { error: `the carried cargo map ${file} cannot be read (${error.message})` }; }
40
43
  }
41
44
 
45
+ /** The broker's registered cargo (managed-cargo.mjs directCargoPath), else PATH's without the RustRight shims; null when none exists. */
46
+ function directCargoBin(env, platform = process.platform) {
47
+ const pathKey = platform === "win32" ? (Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "Path") : "PATH";
48
+ const d = directCargoPath({ env: { ...env, PATH: env[pathKey] }, platform, gateWrappers: true });
49
+ const names = platform === "win32" ? ["cargo.exe"] : ["cargo"];
50
+ for (const dir of String(d.path ?? "").split(platform === "win32" ? ";" : ":").filter(Boolean)) for (const n of names) { const c = path.join(dir, n); if (fs.existsSync(c)) return c; }
51
+ return null;
52
+ }
53
+
42
54
  const refused = (code, message) => {
43
55
  const text = `rightkit-gate: ${code}: ${message}`;
44
56
  return { status: BROKER_REFUSAL_STATUS, signal: null, stdout: "", stderr: `${text}\n`, error: null, refusal: { code, message } };
@@ -73,6 +85,30 @@ export function runCarriedCargo(args, options = {}, { map, spawn = spawnSync, er
73
85
  } catch (error) { return refuse("RK_CARGO_UNBOUND_WORKSPACE", `\`cargo ${words.join(" ")}\` from ${from}: its Cargo workspace cannot be resolved (${error.message}); a snapshot stage's Cargo calls run in the commit's workspaces`); }
74
86
  if (!Object.hasOwn(map.workspaces, workspace)) return refuse("RK_CARGO_UNBOUND_WORKSPACE", `\`cargo ${words.join(" ")}\` builds the Cargo workspace ${workspace}, which is not one this snapshot stage resolved (${Object.keys(map.workspaces).join(", ") || "none"})`);
75
87
  const capture = options.stdio === "inherit" ? "inherit" : "capture";
88
+ // GATE-CONTRACT GC-A11: a call that compiles nothing (metadata, fmt --check, deny, audit, package --list) runs directly with the broker's
89
+ // registered cargo into its own scratch target, never carried: the broker refuses non-compiling requests (rustright a2972f0), which
90
+ // stopped every unit stage at its `cargo fmt -p <crate> --check` step on the Mac and Dell (2026-11-03). Recorded like the cargo
91
+ // wrapper's direct calls (cargo-target-shim.mjs), so the stage's ledger check sees it ran.
92
+ // Only the exact non-writing shapes the cargo wrapper accepts run directly (carriedScanRefusal: `fmt -p <unit> --check`, metadata with
93
+ // --locked/--frozen, `deny ... check`, `audit --db <dir> --no-fetch`), and only when the first word is the subcommand itself (no
94
+ // `+toolchain` or global option in front). Anything else still carries, and the broker refuses it: a direct call never writes the
95
+ // snapshot (no `fmt` without --check, no lock rewrite, no `deny init`, no `audit fix`).
96
+ const directShape = /^[a-z]/.test(words[0] ?? "") && readOnlyCargo(words) && carriedScanRefusal(words) === null
97
+ && (words[0] !== "package" || (words.includes("--list") && (words.includes("--locked") || words.includes("--frozen"))));
98
+ if (directShape) {
99
+ const realCargo = directCargoBin(env);
100
+ if (!realCargo) return refuse("RK_CARGO_DIRECT_MISSING", `\`cargo ${words.join(" ")}\` compiles nothing and runs directly (GC-A11), but no cargo was found on the broker's toolchain or PATH`);
101
+ let scratch;
102
+ try { scratch = fs.mkdtempSync(path.join(os.tmpdir(), "rk-carried-read-")); }
103
+ catch (error) { return refuse("RK_CARGO_DIRECT_SCRATCH", `cannot create a scratch target for \`cargo ${words.join(" ")}\` (${error.message})`); }
104
+ const { CARGO_TARGET_DIR: _t, CARGO_BUILD_TARGET_DIR: _t2, ...rest } = env; void _t; void _t2;
105
+ let r;
106
+ try { r = spawn(realCargo, words, { cwd: from, env: { ...rest, CARGO: realCargo, CARGO_TARGET_DIR: scratch }, stdio: capture === "inherit" ? "inherit" : "pipe", encoding: "utf8", maxBuffer: options.maxBuffer ?? 1 << 28 }); }
107
+ finally { try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* temporary */ } }
108
+ if (r.error) return refuse("RK_CARGO_DIRECT_FAILED", `${realCargo} did not start (${r.error.message})`);
109
+ try { record({ ...base, status: "ran", direct: true, exit: r.status ?? null }); } catch { /* the result is returned either way */ }
110
+ return { status: r.status ?? 1, signal: r.signal ?? null, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: null };
111
+ }
76
112
  const result = carryBrokerCargo({ words, cwd: from, env, section: map.broker, workspace, checkoutWorkspace: map.workspaces[workspace], record: (entry) => record({ ...entry, id: base.id }), spawn, err, stdio: capture });
77
113
  return { status: result.status, signal: null, stdout: result.stdout, stderr: result.stderr, error: null, ...(result.outcome?.status === "verified" ? { outputDir: result.outcome.outputDir, requestId: result.outcome.requestId, requestDigest: result.outcome.requestDigest } : {}), ...(result.outcome?.status === "refused" ? { refusal: { code: result.outcome.code, message: result.outcome.message } } : {}) };
78
114
  }
@@ -183,7 +183,8 @@ if (first === "--version" || first === "-v") {
183
183
  run("local-qualify.mjs", args.slice(1));
184
184
  } else if (first === "publish-local") {
185
185
  // The local route (rightkit#1471): Class A on the exact pushed commit, then the Keychain token publishes that commit's bytes.
186
- run("publish-local-npm.mjs", args.slice(1));
186
+ if (args[1] === "cargo") run("publish-local-cargo.mjs", args.slice(2));
187
+ else run("publish-local-npm.mjs", args.slice(1));
187
188
  } else if (first === "publish") {
188
189
  const rest = args.slice(1);
189
190
  // RightKit crates and @rightkit packages reach a registry from CI only: publish.yml qualification, the approval
@@ -355,6 +356,10 @@ Commands:
355
356
  release Compatibility alias for build
356
357
  publish Compatibility alias for upload
357
358
  check-release-dependencies [--platform mac|win|linux | --all-targets] Check app-forbidden release crates
359
+ publish-local cargo --crate rightkit-<name> --commit <sha> --compile-report <file>
360
+ Needs the unit-gate report of a green --to compile of that commit
361
+ (either host); packages and publishes --no-verify from the commit,
362
+ verifies the crates.io index checksum
358
363
  publish-local npm --package @rightkit/<name> --commit <sha>
359
364
  Mac: Class A on the pushed commit, pack it, publish with the
360
365
  Keychain token (npm-rightkit-publish), verify the registry shasum
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.33",
3
+ "version": "0.6.34",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ // `right-release publish-local cargo --crate <name> --commit <sha> --compile-report <file>`: the local crates route (Adrian, 2026-11-03,
3
+ // rightkit#1471: "the npm and crates are both local ... feel free to push but no ci"). The crates.io token is cargo's own, read by cargo's
4
+ // credential provider from the OS store (Mac: `cargo:macos-keychain`); this command never reads it.
5
+ //
6
+ // It publishes exactly one commit's crate, and only when:
7
+ // 1. the commit is on origin/main;
8
+ // 2. a unit-gate report (`rightkit-gate --to compile --unit cargo:<name> --commit <sha> --receipt-out <file>`, schema
9
+ // rightkit.unit-gate-report/1) from either broker host names this crate and version, this exact commit and its tree, with the
10
+ // check and compile stages ok: the gate's Class A and its broker compile of that commit are the verification;
11
+ // 3. the version is not on crates.io yet.
12
+ // The commit's `crates/` folder is exported (git archive) to a temporary folder; `cargo package` and `cargo publish` run there with
13
+ // `--no-verify --locked` and compile nothing, so they run with the broker's own cargo directly, as the gate's Class A cargo does
14
+ // (GATE-CONTRACT GC-A11; RustRight serves only a repository's primary checkout). The crates.io index checksum must then equal the
15
+ // sha256 of the packaged .crate.
16
+ import crypto from "node:crypto";
17
+ import { spawnSync } from "node:child_process";
18
+ import fs from "node:fs";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { directCargoPath } from "./managed-cargo.mjs";
23
+
24
+ export const UNIT_REPORT_SCHEMA = "rightkit.unit-gate-report/1";
25
+
26
+ function fail(message) { throw new Error(message); }
27
+
28
+ export function parseArgs(argv) {
29
+ const out = { crate: null, commit: null, report: null, repo: process.cwd() };
30
+ for (let i = 0; i < argv.length; i += 1) {
31
+ if (argv[i] === "--crate") out.crate = argv[++i];
32
+ else if (argv[i] === "--commit") out.commit = argv[++i];
33
+ else if (argv[i] === "--compile-report") out.report = path.resolve(argv[++i]);
34
+ else if (argv[i] === "--repo") out.repo = path.resolve(argv[++i]);
35
+ else fail(`unknown option ${argv[i]}`);
36
+ }
37
+ if (!/^rightkit-[a-z0-9-]+$/.test(out.crate ?? "")) fail("--crate rightkit-<name> is required");
38
+ if (!/^[0-9a-f]{7,40}$/.test(out.commit ?? "")) fail("--commit <sha> is required");
39
+ if (!out.report) fail("--compile-report <file> is required: the unit-gate report of `rightkit-gate --to compile --unit cargo:<name> --commit <sha> --receipt-out <file>`");
40
+ return out;
41
+ }
42
+
43
+ /** The reasons a unit-gate report does not prove this crate, version and commit compiled; empty when it does. */
44
+ export function compileReportProblems(report, { name, version, commit, tree }) {
45
+ const problems = [];
46
+ if (report?.schema !== UNIT_REPORT_SCHEMA) problems.push(`schema is ${JSON.stringify(report?.schema)}, not ${UNIT_REPORT_SCHEMA}`);
47
+ if (report?.unit?.kind !== "cargo" || report?.unit?.name !== name) problems.push(`unit is ${JSON.stringify(report?.unit)}, not cargo:${name}`);
48
+ if (report?.unit?.version !== version) problems.push(`unit version ${report?.unit?.version} != ${version}`);
49
+ if (report?.executor !== "broker") problems.push(`executor is ${JSON.stringify(report?.executor)}, not a broker host's compile`);
50
+ if (report?.commit !== commit) problems.push(`commit ${report?.commit} != ${commit}`);
51
+ if (report?.tree !== tree) problems.push(`tree ${report?.tree} != ${tree}`);
52
+ for (const stage of ["check", "compile"]) if (report?.stages?.[stage]?.ok !== true) problems.push(`stage ${stage} is not ok`);
53
+ return problems;
54
+ }
55
+
56
+ /** crates.io sparse index path for a crate name (https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files). */
57
+ export function indexPath(name) {
58
+ const n = name.toLowerCase();
59
+ if (n.length <= 2) return `${n.length}/${n}`;
60
+ if (n.length === 3) return `3/${n[0]}/${n}`;
61
+ return `${n.slice(0, 2)}/${n.slice(2, 4)}/${n}`;
62
+ }
63
+
64
+ async function indexEntry(name, version, fetchImpl = fetch) {
65
+ const res = await fetchImpl(`https://index.crates.io/${indexPath(name)}`, { headers: { "cache-control": "no-cache" } });
66
+ if (res.status === 404) return null;
67
+ if (!res.ok) fail(`crates.io index answered ${res.status} for ${name}`);
68
+ for (const line of (await res.text()).split("\n")) {
69
+ if (!line.trim()) continue;
70
+ const entry = JSON.parse(line);
71
+ if (entry.vers === version) return entry;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ export async function publishLocalCargo(argv, { run = spawnSync, log = console.log, fetchImpl = fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), env = process.env, platform = process.platform } = {}) {
77
+ const o = parseArgs(argv);
78
+ const git = (args) => {
79
+ const r = run("git", args, { cwd: o.repo, encoding: "utf8", maxBuffer: 1 << 26 });
80
+ if (r.status !== 0) fail(`git ${args.join(" ")} failed: ${String(r.stderr ?? "").trim()}`);
81
+ return String(r.stdout ?? "").trim();
82
+ };
83
+ const commit = git(["rev-parse", "--verify", `${o.commit}^{commit}`]);
84
+ git(["fetch", "-q", "origin", "main"]);
85
+ if (run("git", ["merge-base", "--is-ancestor", commit, "origin/main"], { cwd: o.repo }).status !== 0) fail(`${commit} is not on origin/main: push it first ([skip ci])`);
86
+ const dir = `crates/${o.crate}`;
87
+ const manifest = git(["show", `${commit}:${dir}/Cargo.toml`]);
88
+ const name = /^name\s*=\s*"([^"]+)"/m.exec(manifest)?.[1];
89
+ const version = /^version\s*=\s*"([^"]+)"/m.exec(manifest)?.[1];
90
+ if (name !== o.crate || !version) fail(`${dir}/Cargo.toml at ${commit} does not declare package ${o.crate} with a version`);
91
+ if (await indexEntry(name, version, fetchImpl)) fail(`${name} ${version} is already on crates.io; bump the version`);
92
+
93
+ const tree = git(["rev-parse", `${commit}^{tree}`]);
94
+ let report;
95
+ try { report = JSON.parse(fs.readFileSync(o.report, "utf8")); } catch (e) { fail(`cannot read the compile report ${o.report}: ${e.message}`); }
96
+ const problems = compileReportProblems(report, { name, version, commit, tree });
97
+ if (problems.length) fail(`the compile report does not prove cargo:${name} ${version} at ${commit}: ${problems.join("; ")}`);
98
+ log(`publish-local: compile report ok for cargo:${name} ${version} at ${commit.slice(0, 12)} (executor ${report.executor ?? "unknown"})`);
99
+ // The broker's cargo, run directly: package and publish --no-verify compile nothing (GC-A11).
100
+ const pathKey = platform === "win32" ? (Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "Path") : "PATH";
101
+ const direct = directCargoPath({ env: { ...env, PATH: env[pathKey] }, platform, gateWrappers: true });
102
+ const cargoEnv = { ...env, [pathKey]: direct.path };
103
+
104
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), "rk-publish-local-cargo-"));
105
+ try {
106
+ // The whole crates/ folder: cargo reads each path dependency's manifest while it packages.
107
+ const archive = run("git", ["archive", "--format=tar", commit, "crates"], { cwd: o.repo, maxBuffer: 1 << 30 });
108
+ if (archive.status !== 0) fail(`git archive ${commit} crates failed`);
109
+ if (run("tar", ["-x", "-C", work], { input: archive.stdout }).status !== 0) fail("unpacking the commit's crate folder failed");
110
+ const manifestPath = path.join(work, dir, "Cargo.toml");
111
+ const targetDir = path.join(work, "package-target");
112
+ const pkg = run("cargo", ["package", "--no-verify", "--locked", "--manifest-path", manifestPath, "--target-dir", targetDir], { encoding: "utf8", env: cargoEnv });
113
+ if (pkg.status !== 0) fail(`cargo package failed: ${String(pkg.stderr ?? "").trim().split(/\r?\n/).slice(-3).join(" | ")}`);
114
+ const crateFile = path.join(targetDir, "package", `${name}-${version}.crate`);
115
+ const sha256 = crypto.createHash("sha256").update(fs.readFileSync(crateFile)).digest("hex");
116
+ log(`publish-local: packaged ${name}-${version}.crate sha256 ${sha256}`);
117
+
118
+ const pub = run("cargo", ["publish", "--no-verify", "--locked", "--manifest-path", manifestPath, "--target-dir", targetDir], { encoding: "utf8", env: cargoEnv });
119
+ if (pub.status !== 0) fail(`cargo publish failed: ${String(pub.stderr ?? "").trim().split(/\r?\n/).slice(-4).join(" | ")}`);
120
+
121
+ let entry = null;
122
+ for (let i = 0; i < 20 && !entry; i += 1) { entry = await indexEntry(name, version, fetchImpl); if (!entry) await sleep(15000); }
123
+ if (!entry) fail(`${name} ${version} is not in the crates.io index after 5 minutes`);
124
+ if (entry.cksum !== sha256) fail(`crates.io index checksum ${entry.cksum} != packaged ${sha256} for ${name} ${version}`);
125
+ log(`publish-local: ${name} ${version} published from ${commit.slice(0, 12)}; crates.io checksum ${entry.cksum} matches`);
126
+ return { name, version, commit, sha256 };
127
+ } finally {
128
+ fs.rmSync(work, { recursive: true, force: true });
129
+ }
130
+ }
131
+
132
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
133
+ publishLocalCargo(process.argv.slice(2)).catch((e) => { console.error(`right-release publish-local cargo: ${e.message}`); process.exit(1); });
134
+ }
@@ -49,7 +49,7 @@
49
49
  "@rightkit/qa": "0.6.2",
50
50
  "@rightkit/qa-chrome": "0.1.12",
51
51
  "@rightkit/react-shell": "0.7.2",
52
- "@rightkit/release": "0.6.33",
52
+ "@rightkit/release": "0.6.34",
53
53
  "@rightkit/settings-ui": "0.7.0",
54
54
  "@rightkit/social-ui": "0.1.0",
55
55
  "@rightkit/system-ui": "0.1.0",