@rightkit/release 0.6.28 → 0.6.30
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/build-helper-trace.mjs +45 -45
- package/build-helpers-command.mjs +115 -46
- package/cli/right-release.mjs +6 -0
- package/deterministic-scans.mjs +2 -1
- package/package.json +1 -1
- package/publication-gate.mjs +1 -1
- package/publish-local-npm.mjs +104 -0
- package/rightkit-versions.json +2 -2
package/build-helper-trace.mjs
CHANGED
|
@@ -75,12 +75,12 @@ export function readHostList(file, { readFile = (f) => fs.readFileSync(f, "utf8"
|
|
|
75
75
|
|
|
76
76
|
/** Record one trace verdict in the list (atomic write). */
|
|
77
77
|
/** Tracers whose verdicts count: each follows the build's whole process tree. fs_usage (name-filtered) verdicts recorded before do not count. */
|
|
78
|
-
export const TRUSTED_TRACERS = Object.freeze(["eslogger", "
|
|
79
|
-
export const TRACER_OF = Object.freeze({ darwin: "eslogger",
|
|
78
|
+
export const TRUSTED_TRACERS = Object.freeze(["eslogger", "etw"]);
|
|
79
|
+
export const TRACER_OF = Object.freeze({ darwin: "eslogger", win32: "etw" });
|
|
80
80
|
|
|
81
81
|
export function recordTrace(file, unit, verdict, { now = () => new Date().toISOString(), fsApi = fs, tracer = TRACER_OF[process.platform] } = {}) {
|
|
82
82
|
const list = readHostList(file, { readFile: (f) => fsApi.readFileSync(f, "utf8"), exists: (f) => fsApi.existsSync(f) });
|
|
83
|
-
list.units[unit.key] = { crate: unit.crate, version: unit.version, checksum: unit.checksum, features: unit.features, target: unit.target, toolchain: unit.toolchain, role: unit.role, verdict: verdict.ok ? "pass" : "fail", outside: verdict.outside ?? [], tracer, tracedAt: now() };
|
|
83
|
+
list.units[unit.key] = { crate: unit.crate, version: unit.version, checksum: unit.checksum, features: unit.features, target: unit.target, toolchain: unit.toolchain, role: unit.role, verdict: verdict.ok ? "pass" : verdict.unbuildable ? "unbuildable" : "fail", outside: verdict.outside ?? [], tracer, tracedAt: now() };
|
|
84
84
|
fsApi.mkdirSync(path.dirname(file), { recursive: true });
|
|
85
85
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
86
86
|
fsApi.writeFileSync(tmp, `${JSON.stringify(list, null, 2)}\n`);
|
|
@@ -105,6 +105,8 @@ export function checkBuildHelpers(units, list) {
|
|
|
105
105
|
for (const u of units) {
|
|
106
106
|
const entry = byKey.get(u.key);
|
|
107
107
|
if (!entry || !u.checksum || !TRUSTED_TRACERS.includes(entry.tracer)) untraced.push(u);
|
|
108
|
+
// Unbuildable on this host for its target: the gate builds nothing that runs it here, so it is neither refused nor traced again.
|
|
109
|
+
else if (entry.verdict === "unbuildable") continue;
|
|
108
110
|
else if (entry.verdict !== "pass") failed.push({ unit: u, outside: entry.outside ?? [] });
|
|
109
111
|
}
|
|
110
112
|
return { ok: untraced.length === 0 && failed.length === 0, untraced, failed };
|
|
@@ -157,31 +159,6 @@ export function parseFsUsageWrites(text) {
|
|
|
157
159
|
return [...out];
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
/**
|
|
161
|
-
* The written paths of Linux `strace -f -e trace=<file calls>` output: successful calls that create, write, rename, link or remove a file.
|
|
162
|
-
* An open counts when its flags include O_WRONLY, O_RDWR, O_CREAT, O_TRUNC or O_APPEND; every other listed call mutates by definition.
|
|
163
|
-
* A failed call (`= -1`) wrote nothing. Every quoted path of a mutating call is taken (rename names both); a relative path resolves
|
|
164
|
-
* against `cwd` (the traced cargo's directory; a build script that changes directory is resolved approximately, a recorded limit).
|
|
165
|
-
*/
|
|
166
|
-
export function parseStraceWrites(text, { cwd = "/" } = {}) {
|
|
167
|
-
const OPEN = /\b(open|openat|openat2)\((?:[^"]*?)"((?:[^"\\]|\\.)*)"[^)]*?\b(O_[A-Z_|]+)/;
|
|
168
|
-
const MUTATING = /\b(creat|rename|renameat|renameat2|unlink|unlinkat|mkdir|mkdirat|rmdir|link|linkat|symlink|symlinkat|truncate|chmod|fchmodat|chown|fchownat|lchown|utimensat|utimes)\(/;
|
|
169
|
-
const out = new Set();
|
|
170
|
-
for (const line of String(text).split(/\r?\n/)) {
|
|
171
|
-
if (/=\s*-1\s/.test(line) || /<unfinished \.\.\.>/.test(line)) continue;
|
|
172
|
-
// With `strace -y`, a directory fd prints its real path (`AT_FDCWD</abs/cwd>`, `5</abs/dir>`): a relative path resolves against that,
|
|
173
|
-
// never against a guessed folder (rustc writes `full.rmeta` relative to its own working directory).
|
|
174
|
-
const dirOf = new Map([...line.matchAll(/(?:AT_FDCWD|\d+)<([^>]+)>,\s*"((?:[^"\\]|\\.)*)"/g)].map((m) => [m[2], m[1]]));
|
|
175
|
-
const resolveIt = (p) => (path.posix.isAbsolute(p) ? p : path.posix.resolve(dirOf.get(p) ?? cwd, p));
|
|
176
|
-
let paths = [];
|
|
177
|
-
const open = OPEN.exec(line);
|
|
178
|
-
if (open) { if (/O_WRONLY|O_RDWR|O_CREAT|O_TRUNC|O_APPEND/.test(open[3])) paths = [open[2]]; }
|
|
179
|
-
else if (MUTATING.test(line)) paths = [...line.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
|
|
180
|
-
for (const p of paths) if (p) out.add(resolveIt(p));
|
|
181
|
-
}
|
|
182
|
-
return [...out];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
162
|
/** The repository's committed list (`.rightkit/build-helper-trace.json`, same schema): reviewed passes every host and CI reads. */
|
|
186
163
|
export const repoListPath = (workspaceRoot) => path.join(workspaceRoot, ".rightkit", "build-helper-trace.json");
|
|
187
164
|
|
|
@@ -270,13 +247,6 @@ export function parseEsloggerWrites(text, { rootPid }) {
|
|
|
270
247
|
return [...out];
|
|
271
248
|
}
|
|
272
249
|
|
|
273
|
-
/** The strace -ff output files (`<prefix>.<pid>`, one per process: no call is ever split across lines). */
|
|
274
|
-
export function readStraceFiles(prefix, { fsApi = fs } = {}) {
|
|
275
|
-
const dir = path.dirname(prefix); const base = path.basename(prefix);
|
|
276
|
-
return fsApi.readdirSync(dir).filter((n) => n.startsWith(`${base}.`)).map((n) => fsApi.readFileSync(path.join(dir, n), "utf8")).join("\n");
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
250
|
/** Calls `fn(record)` for every record of `file` separated by `sep`, reading 8 MiB at a time (a whole-run trace can pass Node's string limit). */
|
|
281
251
|
export function forEachRecord(file, sep, fn, { fsApi = fs } = {}) {
|
|
282
252
|
const fd = fsApi.openSync(file, "r"); const buf = Buffer.alloc(8 << 20); let rest = "";
|
|
@@ -324,27 +294,57 @@ export function esloggerWritesByRoot(file, roots) {
|
|
|
324
294
|
}
|
|
325
295
|
|
|
326
296
|
/** tracerpt XML from `file`, streamed by `</Event>`: Map(root pid -> written paths) for every pid in `roots`, kernel paths mapped by `devices`. */
|
|
327
|
-
export function etwWritesByRoot(file, roots, devices = {}) {
|
|
297
|
+
export function etwWritesByRoot(file, roots, devices = {}, { toolchainDirs = [] } = {}) {
|
|
328
298
|
const field = (ev, name) => new RegExp(`<Data Name="${name}">([^<]*)</Data>`).exec(ev)?.[1];
|
|
329
299
|
const id = (ev) => Number(/<EventID>(\d+)<\/EventID>/.exec(ev)?.[1]);
|
|
330
300
|
const pidOf = (ev) => Number(/ProcessID="(\d+)"/.exec(ev)?.[1]);
|
|
331
301
|
const provider = (ev) => /Provider Name="([^"]+)"/.exec(ev)?.[1] ?? "";
|
|
332
|
-
|
|
302
|
+
// Windows reuses a process id within seconds, so a pid belongs to a build only for one lifetime: from its ProcessStart (1) to its
|
|
303
|
+
// ProcessStop (2), in recording order. A build's root is the start of a root pid whose parent is the build runner: the parent most of
|
|
304
|
+
// the root pids were started by (a pid-only match took PowerShell, Pulse and sccache processes that later reused a build's pid).
|
|
305
|
+
const parentsOfRoots = new Map();
|
|
333
306
|
forEachRecord(file, "</Event>", (ev) => {
|
|
334
|
-
if (provider(ev)
|
|
307
|
+
if (provider(ev) !== "Microsoft-Windows-Kernel-Process" || id(ev) !== 1) return;
|
|
308
|
+
const pid = Number(field(ev, "ProcessID")); if (!roots.has(pid)) return;
|
|
309
|
+
const ppid = Number(field(ev, "ParentProcessID")); parentsOfRoots.set(ppid, (parentsOfRoots.get(ppid) ?? 0) + 1);
|
|
335
310
|
});
|
|
336
|
-
const
|
|
311
|
+
const runner = [...parentsOfRoots].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
312
|
+
const out = new Map([...roots].map((r) => [r, new Set()]));
|
|
313
|
+
const live = new Map();
|
|
337
314
|
const toDos = (p) => { if (!p) return null; for (const [dev, letter] of Object.entries(devices)) if (p.toLowerCase().startsWith(`${dev.toLowerCase()}\\`)) return letter + p.slice(dev.length); return p; };
|
|
315
|
+
// FileKey -> path for files open on the host. Every kept string is a copy: a regex match is a slice of its 8 MiB read chunk and keeps the
|
|
316
|
+
// whole chunk alive (holding names that way ran the Dell out of heap).
|
|
317
|
+
const own = (s) => (s == null ? s : Buffer.from(s, "utf8").toString("utf8"));
|
|
338
318
|
const names = new Map();
|
|
339
319
|
forEachRecord(file, "</Event>", (ev) => {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
320
|
+
const prov = provider(ev); const n = id(ev);
|
|
321
|
+
if (prov === "Microsoft-Windows-Kernel-Process") {
|
|
322
|
+
const pid = Number(field(ev, "ProcessID"));
|
|
323
|
+
if (n === 1) {
|
|
324
|
+
const ppid = Number(field(ev, "ParentProcessID"));
|
|
325
|
+
// The MSVC linker starts its telemetry helper (vctip.exe, a .NET program) after a link: it writes the user profile and .NET's
|
|
326
|
+
// caches, belongs to Microsoft's toolchain rather than to any crate, and is left out of the build's tree with everything it starts.
|
|
327
|
+
// Only Microsoft's own copy, by path prefix (Program Files\Microsoft Visual Studio): a copy a build script places elsewhere stays in
|
|
328
|
+
// the tree, and placing one under Program Files is itself an outside write the trace fails.
|
|
329
|
+
const imagePath = String(toDos(field(ev, "ImageName")) ?? "").toLowerCase();
|
|
330
|
+
const msvcOwned = imagePath.endsWith("\\vctip.exe") && toolchainDirs.some((d) => imagePath.startsWith(`${String(d).toLowerCase().replace(/[\\/]+$/, "")}\\`));
|
|
331
|
+
if (msvcOwned) live.delete(pid);
|
|
332
|
+
else if (roots.has(pid) && ppid === runner) live.set(pid, pid);
|
|
333
|
+
else if (live.has(ppid)) live.set(pid, live.get(ppid));
|
|
334
|
+
else live.delete(pid);
|
|
335
|
+
} else if (n === 2) live.delete(pid);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (prov !== "Microsoft-Windows-Kernel-File") return;
|
|
339
|
+
// A write names its file by FileKey, which NameCreate (10) maps to the path until NameDelete (11), from whatever process logs them. The
|
|
340
|
+
// session records no close, and FileObject addresses are reused, so mapping a write by FileObject named files the build only read.
|
|
341
|
+
if (n === 10) { names.set(own(field(ev, "FileKey")), own(field(ev, "FileName"))); return; }
|
|
342
|
+
if (n === 11) { names.delete(field(ev, "FileKey")); return; }
|
|
343
|
+
const root = live.get(pidOf(ev)); if (root === undefined) return;
|
|
344
|
+
const add = (p) => { const d = toDos(p); if (d) out.get(root).add(own(d)); };
|
|
345
345
|
if (n === 12) { const disposition = (Number(field(ev, "CreateOptions") ?? 0) >>> 24) & 0xff; if (disposition !== 1) add(field(ev, "FileName")); }
|
|
346
346
|
else if (n === 30) add(field(ev, "FileName"));
|
|
347
|
-
else if (n === 16) add(names.get(
|
|
347
|
+
else if (n === 16) add(names.get(field(ev, "FileKey")));
|
|
348
348
|
else if (n === 26 || n === 27) add(field(ev, "FilePath") ?? field(ev, "FileName"));
|
|
349
349
|
});
|
|
350
350
|
return new Map([...out].map(([r, s]) => [r, [...s]]));
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `right-release build-helpers check [--manifest-path <Cargo.toml>]` (Class A, rightkit#1450) and
|
|
3
|
-
// `right-release build-helpers trace [--manifest-path <Cargo.toml>]` (the one-time trace: eslogger on macOS (sudo + Full Disk Access),
|
|
3
|
+
// `right-release build-helpers trace [--manifest-path <Cargo.toml>]` (the one-time trace: eslogger on macOS (sudo + Full Disk Access), ETW on Windows (Administrator)).
|
|
4
|
+
// Only broker hosts (Mac, Dell) vet build helpers (Adrian, 2026-11-03: "linux is the wrong target and it has to be only where we have brokers"):
|
|
5
|
+
// a Linux host or CI runner is a throwaway container, so check and trace do nothing there.
|
|
4
6
|
//
|
|
5
7
|
// check: reads the workspace's resolved graph (`cargo metadata --format-version 1 --locked`, a non-compiling call run directly: GATE-CONTRACT
|
|
6
8
|
// GC-A11), its Cargo.lock, the host triple and toolchain (`rustc -vV`), and the host list, and refuses (exit 1) when any build-helper unit is
|
|
7
9
|
// untraced or traced as writing outside its build directories, naming each.
|
|
8
10
|
// trace: for each untraced unit, builds the workspace's dependency alone (`cargo build -p <crate>@<version> --locked`) under the host's tracer, into a
|
|
9
11
|
// scratch target, judges every write against OUT_DIR / target / CARGO_HOME / temp, and records the verdict in the host list.
|
|
10
|
-
//
|
|
12
|
+
// Units are listed per target the host compiles: its own and the two cross-check targets (GC-A14), each from `cargo metadata --filter-platform`.
|
|
13
|
+
// A unit that does not build on this host for its target is recorded once as unbuildable and skipped (Adrian, 2026-11-03: "if it's not compiling
|
|
14
|
+
// on the OS, and doesn't generate output ... why not skip it?"): the gate's own scan cannot build it there either.
|
|
11
15
|
import { spawn, spawnSync } from "node:child_process";
|
|
12
16
|
import fs from "node:fs";
|
|
13
17
|
import os from "node:os";
|
|
14
18
|
import path from "node:path";
|
|
15
19
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import { buildHelperRefusal, buildHelperUnits, checkBuildHelpers, hostListPath, esloggerWritesByRoot, etwWritesByRoot, judgeTrace, mergeLists,
|
|
20
|
+
import { buildHelperRefusal, buildHelperUnits, checkBuildHelpers, hostListPath, esloggerWritesByRoot, etwWritesByRoot, judgeTrace, mergeLists, readHostList, recordTrace, repoListPath } from "./build-helper-trace.mjs";
|
|
21
|
+
import { crossCompileEnv, crossTargetsFor } from "./cross-targets.mjs";
|
|
17
22
|
import { resolveManagedRoots } from "./managed-roots.mjs";
|
|
18
23
|
import { directCargoPath } from "./managed-cargo.mjs";
|
|
19
24
|
|
|
@@ -43,13 +48,19 @@ const start = (i) => new Promise((resolve) => {
|
|
|
43
48
|
const worker = async () => { while (next < jobs.length) { const i = next++; await start(i); } };
|
|
44
49
|
Promise.all(Array.from({ length: Math.min(limit, jobs.length) }, worker)).then(() => fs.writeFileSync(process.argv[2], JSON.stringify(results)));`;
|
|
45
50
|
|
|
46
|
-
export function inventory({ manifestPath, run = spawnSync, readFile = (f) => fs.readFileSync(f, "utf8") }) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
export function inventory({ manifestPath, run = spawnSync, readFile = (f) => fs.readFileSync(f, "utf8"), platform = process.platform }) {
|
|
52
|
+
const host = hostFacts(run);
|
|
53
|
+
// The host's own target plus the two targets every crate is cross-checked for: each target's resolved graph and features.
|
|
54
|
+
const targets = [host.target, ...crossTargetsFor({}, platform)];
|
|
55
|
+
let metadata = null; const units = [];
|
|
56
|
+
for (const target of targets) {
|
|
57
|
+
const args = ["metadata", "--format-version", "1", "--locked", "--filter-platform", target, ...(manifestPath ? ["--manifest-path", manifestPath] : [])];
|
|
58
|
+
const r = run("cargo", args, { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
59
|
+
if (r.error || r.status !== 0) throw new Error(`cargo ${args.join(" ")} failed (${r.error?.message ?? `exit ${r.status}`}): ${String(r.stderr ?? "").trim().split(/\r?\n/).slice(-2).join(" | ")}`);
|
|
60
|
+
const m = JSON.parse(r.stdout); metadata ??= m;
|
|
61
|
+
units.push(...buildHelperUnits(m, readFile(path.join(m.workspace_root, "Cargo.lock")), { target, toolchain: host.toolchain }));
|
|
62
|
+
}
|
|
63
|
+
return { metadata, units, host: host.target };
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
// Every Cargo workspace (a directory holding Cargo.lock) under `root`, skipping build outputs, dependencies and vendored copies.
|
|
@@ -73,6 +84,8 @@ export function cargoWorkspaces(root, { depth = 4, fsApi = fs } = {}) {
|
|
|
73
84
|
// in the workspace's own committed list (--record-repo). Default root: /Volumes/D/claude (Mac), D:\claude (Windows), the current
|
|
74
85
|
// directory elsewhere.
|
|
75
86
|
function traceAll(argv, { env, platform, log, err }) {
|
|
87
|
+
// A unit is judged once per run: a verdict recorded in one workspace (pass, fail or unbuildable) is never rebuilt in the next.
|
|
88
|
+
const judged = new Set();
|
|
76
89
|
const roots = argv.flatMap((a, i) => (a === "--root" && argv[i + 1] ? [path.resolve(argv[i + 1])] : []));
|
|
77
90
|
if (!roots.length) roots.push(platform === "darwin" ? "/Volumes/D/claude" : platform === "win32" ? "D:\\claude" : process.cwd());
|
|
78
91
|
let keepAlive = null;
|
|
@@ -92,7 +105,7 @@ function traceAll(argv, { env, platform, log, err }) {
|
|
|
92
105
|
const jobsAt = argv.indexOf("--jobs");
|
|
93
106
|
// The host list is what Class A reads on this host; a repository's committed list is written only on request (--record-repo), so
|
|
94
107
|
// tracing never edits a checkout another owner holds.
|
|
95
|
-
const code = main(["trace", "--manifest-path", path.join(ws, "Cargo.toml"), ...(argv.includes("--record-repo") ? ["--record-repo"] : []), ...(jobsAt >= 0 ? ["--jobs", argv[jobsAt + 1]] : [])], { env, platform, log, err });
|
|
108
|
+
const code = main(["trace", "--manifest-path", path.join(ws, "Cargo.toml"), ...(argv.includes("--record-repo") ? ["--record-repo"] : []), ...(jobsAt >= 0 ? ["--jobs", argv[jobsAt + 1]] : [])], { env, platform, log, err, judged });
|
|
96
109
|
if (code !== 0) err(`right-release build-helpers trace-all: ${ws} ended with ${code}`);
|
|
97
110
|
worst = Math.max(worst, code);
|
|
98
111
|
}
|
|
@@ -101,7 +114,7 @@ function traceAll(argv, { env, platform, log, err }) {
|
|
|
101
114
|
return worst;
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
export function main(argv = process.argv.slice(2), { env = process.env, platform = process.platform, run: runRaw = null, log = console.log, err = console.error } = {}) {
|
|
117
|
+
export function main(argv = process.argv.slice(2), { env = process.env, platform = process.platform, run: runRaw = null, log = console.log, err = console.error, judged = new Set() } = {}) {
|
|
105
118
|
if (argv[0] === "trace-all") return traceAll(argv.slice(1), { env, platform, log, err });
|
|
106
119
|
// GATE-CONTRACT GC-A11: the check's cargo and rustc calls compile nothing and run directly with the real toolchain (RustRight shims off PATH),
|
|
107
120
|
// writing to a scratch target, never through the broker.
|
|
@@ -112,27 +125,22 @@ export function main(argv = process.argv.slice(2), { env = process.env, platform
|
|
|
112
125
|
// The parallel trace builds are spawned by the batch runner with the job's own env: that env carries the direct PATH too, so no build
|
|
113
126
|
// ever goes through the RustRight shim (which refuses a build that redirects its output folder).
|
|
114
127
|
const directEnv = { ...env, [pathKey]: directPath };
|
|
115
|
-
try { return mainWith(argv, { env: directEnv, platform, run, log, err }); } finally { fs.rmSync(scratchTarget, { recursive: true, force: true }); }
|
|
128
|
+
try { return mainWith(argv, { env: directEnv, platform, run, log, err, judged }); } finally { fs.rmSync(scratchTarget, { recursive: true, force: true }); }
|
|
116
129
|
}
|
|
117
130
|
|
|
118
|
-
function mainWith(argv, { env, platform, run, log, err }) {
|
|
131
|
+
function mainWith(argv, { env, platform, run, log, err, judged = new Set(), confirm = null }) {
|
|
119
132
|
const [sub, ...rest] = argv;
|
|
120
133
|
const at = rest.indexOf("--manifest-path");
|
|
121
134
|
const manifestPath = at >= 0 ? rest[at + 1] : undefined;
|
|
135
|
+
if (platform !== "darwin" && platform !== "win32") { log(`right-release build-helpers ${sub}: nothing to do on ${platform}; only broker hosts (Mac, Dell) vet build helpers`); return 0; }
|
|
122
136
|
const listFile = hostListPath(resolveManagedRoots({ env, platform }).dataRoot, platform);
|
|
123
137
|
let inv;
|
|
124
|
-
try { inv = inventory({ manifestPath, run }); } catch (e) { err(`right-release build-helpers: ${e.message}`); return 2; }
|
|
138
|
+
try { inv = inventory({ manifestPath, run, platform }); } catch (e) { err(`right-release build-helpers: ${e.message}`); return 2; }
|
|
125
139
|
// The host's own list and the repository's committed list (.rightkit/build-helper-trace.json): CI and every host read the same reviewed
|
|
126
140
|
// passes, and a host traces only what neither holds.
|
|
127
141
|
const repoFile = repoListPath(inv.metadata.workspace_root);
|
|
128
142
|
const list = mergeLists(readHostList(listFile), readHostList(repoFile));
|
|
129
143
|
const verdict = checkBuildHelpers(inv.units, list);
|
|
130
|
-
if (sub === "check" && !verdict.ok && rest.includes("--trace-missing") && platform === "linux" && verdict.untraced.length) {
|
|
131
|
-
log(`right-release build-helpers: ${verdict.untraced.length} untraced unit(s) on this Linux host; tracing them now (strace -ff)`);
|
|
132
|
-
const traced = mainWith(["trace", ...(manifestPath ? ["--manifest-path", manifestPath] : [])], { env, platform, run, log, err });
|
|
133
|
-
if (traced !== 0) return traced;
|
|
134
|
-
return mainWith(["check", ...(manifestPath ? ["--manifest-path", manifestPath] : [])], { env, platform, run, log, err });
|
|
135
|
-
}
|
|
136
144
|
if (sub === "check") {
|
|
137
145
|
if (verdict.ok) { log(`right-release build-helpers: ${inv.units.length} build-helper unit(s), every one traced on this host (${listFile})`); return 0; }
|
|
138
146
|
for (const line of buildHelperRefusal(verdict)) err(line);
|
|
@@ -152,13 +160,40 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
152
160
|
const recordRepo = rest.includes("--record-repo");
|
|
153
161
|
const cargoHome = env.CARGO_HOME ?? path.join(os.homedir(), ".cargo");
|
|
154
162
|
// Parallel: `--jobs N` builds at once (default 4). Each build is its own process tree, rooted at its own cargo, in its own scratch
|
|
155
|
-
// target, and is judged only on the writes of that tree (eslogger and ETW events name their process
|
|
156
|
-
//
|
|
163
|
+
// target, and is judged only on the writes of that tree (eslogger and ETW events name their process). The cores are shared out so the
|
|
164
|
+
// builds do not fight over them.
|
|
157
165
|
const jobsAt = rest.indexOf("--jobs");
|
|
158
|
-
|
|
166
|
+
// A confirming pass builds one unit at a time, so nothing else of this run shares the machine with it.
|
|
167
|
+
const jobs = confirm ? 1 : Math.max(1, Number.parseInt(jobsAt >= 0 ? rest[jobsAt + 1] : "4", 10) || 4);
|
|
159
168
|
const perBuildJobs = String(Math.max(1, Math.floor(os.cpus().length / jobs)));
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
// A cross target builds with the provisioned zig C toolchain; without it those units wait (never recorded as unbuildable).
|
|
170
|
+
// The toolchain is proved first: a one-line C file compiled through the target's CC wrapper. A wrapper that cannot run (on the Dell a
|
|
171
|
+
// Node junction an elevated shell refused to traverse) failed every C crate, and 0.6.29 recorded those as unbuildable.
|
|
172
|
+
const workingCrossEnv = (target) => {
|
|
173
|
+
const cc = crossCompileEnv(target, undefined, platform);
|
|
174
|
+
if (!cc) return null;
|
|
175
|
+
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-cc-"));
|
|
176
|
+
try {
|
|
177
|
+
fs.writeFileSync(path.join(probeDir, "probe.c"), "int rk_probe(void) { return 0; }\n");
|
|
178
|
+
const zigCache = { ZIG_GLOBAL_CACHE_DIR: path.join(probeDir, "zig-global"), ZIG_LOCAL_CACHE_DIR: path.join(probeDir, "zig-local") };
|
|
179
|
+
// A .cmd wrapper runs through the shell on Windows, which joins arguments unquoted: every path is quoted there.
|
|
180
|
+
const q = (s) => (platform === "win32" ? `"${s}"` : s);
|
|
181
|
+
const r = run(q(cc[`CC_${target.replaceAll("-", "_")}`]), ["-c", q(path.join(probeDir, "probe.c")), "-o", q(path.join(probeDir, "probe.o"))], { encoding: "utf8", env: { ...env, ...zigCache }, shell: platform === "win32" });
|
|
182
|
+
if (r.status === 0 && fs.existsSync(path.join(probeDir, "probe.o"))) return cc;
|
|
183
|
+
err(`right-release build-helpers trace: the ${target} C compiler wrapper does not work (${String(r.error?.message ?? r.stderr ?? r.stdout ?? `exit ${r.status}`).trim().split(/\r?\n/)[0]}); its units are not traced (rerun scripts/ci/provision-zig.mjs)`);
|
|
184
|
+
return null;
|
|
185
|
+
} finally { fs.rmSync(probeDir, { recursive: true, force: true }); }
|
|
186
|
+
};
|
|
187
|
+
const crossEnv = new Map(); const waiting = [];
|
|
188
|
+
const pending = [...verdict.untraced, ...verdict.failed.map((f) => f.unit)].filter((unit) => (confirm ? confirm.has(unit.key) : !judged.has(unit.key))).filter((unit) => {
|
|
189
|
+
if (unit.target === inv.host) return true;
|
|
190
|
+
if (!crossEnv.has(unit.target)) crossEnv.set(unit.target, workingCrossEnv(unit.target));
|
|
191
|
+
if (crossEnv.get(unit.target)) return true;
|
|
192
|
+
waiting.push(unit); return false;
|
|
193
|
+
});
|
|
194
|
+
if (waiting.length) err(`right-release build-helpers trace: ${waiting.length} cross-target unit(s) not traced: the zig C toolchain is not provisioned or does not work (scripts/ci/provision-zig.mjs)`);
|
|
195
|
+
log(`right-release build-helpers trace: ${pending.length} unit(s)${confirm ? " to confirm" : ""}, ${jobs} at a time`);
|
|
196
|
+
const firstFails = new Map();
|
|
162
197
|
if (!pending.length) return 0;
|
|
163
198
|
const prepared = pending.map((unit) => {
|
|
164
199
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-"));
|
|
@@ -173,18 +208,20 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
173
208
|
fs.writeFileSync(path.join(probe, "src", "lib.rs"), "");
|
|
174
209
|
try { fs.copyFileSync(path.join(inv.metadata.workspace_root, "Cargo.lock"), path.join(probe, "Cargo.lock")); } catch { /* the probe resolves on its own */ }
|
|
175
210
|
// Release profile, like every other build (GATE-CONTRACT GC-A15): no dev or debug build anywhere, a throwaway probe included.
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
211
|
+
// Built for the unit's own target, as the gate builds it: the host's, or a cross-check target with --target and zig.
|
|
212
|
+
const cross = unit.target !== inv.host;
|
|
213
|
+
const buildArgs = ["build", "--profile", "release", "--manifest-path", path.join(probe, "Cargo.toml"), "--target-dir", target, ...(cross ? ["--target", unit.target] : [])];
|
|
214
|
+
// zig (the cross C compiler) keeps its caches in this build's scratch folder: its default global cache is a host folder outside the
|
|
215
|
+
// allowed roots, and every crate with C code was failed for zig's own cache writes (ring, rustls, blake3, libsqlite3-sys on 0.6.29).
|
|
216
|
+
const zigCache = cross ? { ZIG_GLOBAL_CACHE_DIR: path.join(scratch, "zig-global"), ZIG_LOCAL_CACHE_DIR: path.join(scratch, "zig-local") } : {};
|
|
217
|
+
const buildEnv = { ...env, ...(cross ? crossEnv.get(unit.target) : {}), ...zigCache, CARGO_TARGET_DIR: target, CARGO_BUILD_JOBS: perBuildJobs };
|
|
218
|
+
const job = { cmd: "cargo", args: buildArgs, env: buildEnv };
|
|
219
|
+
job.label = `${unit.crate} ${unit.version}${cross ? ` (${unit.target})` : ""}`;
|
|
220
|
+
return { unit, scratch, target, job };
|
|
184
221
|
});
|
|
185
222
|
const sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-session-"));
|
|
186
223
|
const traceFile = path.join(sessionDir, "trace.txt");
|
|
187
|
-
// One tracer session for the whole run (macOS eslogger, Windows ETW)
|
|
224
|
+
// One tracer session for the whole run (macOS eslogger, Windows ETW).
|
|
188
225
|
let stopTracer = () => {};
|
|
189
226
|
let etl = null;
|
|
190
227
|
if (platform === "darwin") {
|
|
@@ -245,29 +282,61 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
245
282
|
const xml = path.join(sessionDir, "trace.xml");
|
|
246
283
|
const exported = run("tracerpt", [etl.file, "-o", xml, "-of", "XML", "-y"], { encoding: "utf8" });
|
|
247
284
|
if (exported.status !== 0) { err("right-release build-helpers trace: tracerpt could not export the ETW trace"); return 2; }
|
|
248
|
-
try { recorded = fs.statSync(xml).size > 0; if (recorded) byRoot = etwWritesByRoot(xml, roots, devices); } catch { /* no output */ }
|
|
285
|
+
try { recorded = fs.statSync(xml).size > 0; if (recorded) byRoot = etwWritesByRoot(xml, roots, devices, { toolchainDirs: [env.ProgramFiles, env["ProgramFiles(x86)"]].filter(Boolean).map((d) => path.win32.join(d, "Microsoft Visual Studio")) }); } catch { /* no output */ }
|
|
249
286
|
}
|
|
250
|
-
if (
|
|
287
|
+
if (!recorded && roots.size) { err(`right-release build-helpers trace: the ${platform === "darwin" ? "eslogger" : "ETW"} session recorded nothing; nothing recorded`); return 2; }
|
|
251
288
|
prepared.forEach((b, k) => {
|
|
252
289
|
const r = results[k] ?? {};
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
290
|
+
if (!Number.isInteger(r.status)) { err(`right-release build-helpers trace: ${b.unit.crate} ${b.unit.version} did not finish (${r.error ?? "no result"}); not recorded`); return; }
|
|
291
|
+
if (r.status !== 0 && confirm) {
|
|
292
|
+
// It built in the first pass, so a failed rebuild is a passing problem (memory, a lock), not "unbuildable": the first FAIL stands.
|
|
293
|
+
const before = [...confirm.get(b.unit.key)];
|
|
294
|
+
judged.add(b.unit.key);
|
|
295
|
+
recordTrace(listFile, b.unit, { ok: false, outside: before });
|
|
296
|
+
if (recordRepo) recordTrace(repoFile, b.unit, { ok: false, outside: before });
|
|
297
|
+
log(`FAIL ${b.unit.crate} ${b.unit.version} (${b.unit.target}): the rebuild did not build; the first trace stands: wrote ${before.slice(0, 5).join(", ")}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (r.status !== 0) {
|
|
301
|
+
// It does not build here for this target, so nothing this host's gate runs builds it either: recorded once, skipped from then on.
|
|
302
|
+
const reason = String(r.error ?? `exit ${r.status}`);
|
|
303
|
+
judged.add(b.unit.key);
|
|
304
|
+
recordTrace(listFile, b.unit, { ok: false, unbuildable: true, outside: [reason] });
|
|
305
|
+
if (recordRepo) recordTrace(repoFile, b.unit, { ok: false, unbuildable: true, outside: [reason] });
|
|
306
|
+
log(`skip ${b.unit.crate} ${b.unit.version} (${b.unit.target}): does not build on this host (${reason.slice(0, 160)})`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const writes = byRoot.get(r.pid) ?? [];
|
|
257
310
|
const allowed = platform === "win32" ? [b.scratch, cargoHome, os.tmpdir(), env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, "Temp") : null]
|
|
258
|
-
: [b.scratch, cargoHome, os.tmpdir(), "/private/var/folders", "/dev"
|
|
311
|
+
: [b.scratch, cargoHome, os.tmpdir(), "/private/var/folders", "/dev"];
|
|
259
312
|
// Allowed folders by their real path: a linked CARGO_HOME (~/.cargo -> /Volumes/D/...) or temp (/var -> /private/var) is reported by
|
|
260
313
|
// the tracer under the real path, so comparing the link's path would fail every crate.
|
|
261
314
|
const real = (d) => { try { return fs.realpathSync.native(d); } catch { return d; } };
|
|
262
315
|
const roots = allowed.filter(Boolean).flatMap((d) => [d, real(d)]);
|
|
263
|
-
|
|
264
|
-
|
|
316
|
+
let verdictOf = judgeTrace(writes, { allowed: roots, platform, mustSee: real(b.target) });
|
|
317
|
+
// A FAIL must repeat (Dell, 2026-11-03: Windows' recorder pinned other programs' writes on a build now and then). The first FAIL is
|
|
318
|
+
// held and the unit is rebuilt alone; it fails only on the outside paths both builds wrote, or when either build went unobserved.
|
|
319
|
+
// Only Windows' recorder sees the whole machine, so only there can another program's write land on a build: on the Mac (eslogger
|
|
320
|
+
// follows the build's own processes) an outside write fails at once, and the rebuild only retries a build that went unobserved.
|
|
321
|
+
const unseen = (o) => o.startsWith("(the tracer recorded no write");
|
|
322
|
+
if (!verdictOf.ok && !confirm && (platform === "win32" || verdictOf.outside.every(unseen))) { firstFails.set(b.unit.key, new Set(verdictOf.outside)); log(`recheck ${b.unit.crate} ${b.unit.version} (${b.unit.target}): rebuilding it alone`); return; }
|
|
323
|
+
if (!verdictOf.ok && confirm) {
|
|
324
|
+
const before = confirm.get(b.unit.key);
|
|
325
|
+
// An unobserved first build proves nothing either way: then every outside write of the rebuild counts.
|
|
326
|
+
const firstUnseen = [...before].some(unseen);
|
|
327
|
+
const repeated = verdictOf.outside.filter((o) => !unseen(o) && (firstUnseen || before.has(o)));
|
|
328
|
+
const unobserved = verdictOf.outside.some(unseen);
|
|
329
|
+
verdictOf = repeated.length || unobserved ? { ok: false, outside: repeated.length ? repeated : verdictOf.outside } : { ok: true, outside: [] };
|
|
330
|
+
}
|
|
331
|
+
judged.add(b.unit.key);
|
|
332
|
+
recordTrace(listFile, b.unit, verdictOf);
|
|
265
333
|
// --record-repo also writes the verdict into the repository's committed list, for review and commit by the repository's owner.
|
|
266
|
-
if (recordRepo) recordTrace(repoFile, b.unit,
|
|
267
|
-
log(`${
|
|
334
|
+
if (recordRepo) recordTrace(repoFile, b.unit, verdictOf);
|
|
335
|
+
log(`${verdictOf.ok ? "pass" : "FAIL"} ${b.unit.crate} ${b.unit.version} (${b.unit.target})${verdictOf.ok ? "" : `: wrote ${verdictOf.outside.slice(0, 5).join(", ")}`}`);
|
|
268
336
|
});
|
|
269
337
|
for (const b of prepared) fs.rmSync(b.scratch, { recursive: true, force: true });
|
|
270
338
|
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
339
|
+
if (firstFails.size) return mainWith(argv, { env, platform, run, log, err, judged, confirm: firstFails });
|
|
271
340
|
return 0;
|
|
272
341
|
}
|
|
273
342
|
err("usage: right-release build-helpers check|trace [--manifest-path <Cargo.toml>] [--record-repo] [--jobs N] | trace-all [--root <dir>]... [--jobs N]");
|
package/cli/right-release.mjs
CHANGED
|
@@ -181,6 +181,9 @@ if (first === "--version" || first === "-v") {
|
|
|
181
181
|
// Local qualification (default): both native hosts qualify the crates through the RustRight broker and a candidate is cut.
|
|
182
182
|
// It uploads nothing; the approval commit and publish-reviewed.yml publish the pinned bytes (docs/CI.md "Local qualification").
|
|
183
183
|
run("local-qualify.mjs", args.slice(1));
|
|
184
|
+
} else if (first === "publish-local") {
|
|
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));
|
|
184
187
|
} else if (first === "publish") {
|
|
185
188
|
const rest = args.slice(1);
|
|
186
189
|
// RightKit crates and @rightkit packages reach a registry from CI only: publish.yml qualification, the approval
|
|
@@ -352,6 +355,9 @@ Commands:
|
|
|
352
355
|
release Compatibility alias for build
|
|
353
356
|
publish Compatibility alias for upload
|
|
354
357
|
check-release-dependencies [--platform mac|win|linux | --all-targets] Check app-forbidden release crates
|
|
358
|
+
publish-local npm --package @rightkit/<name> --commit <sha>
|
|
359
|
+
Mac: Class A on the pushed commit, pack it, publish with the
|
|
360
|
+
Keychain token (npm-rightkit-publish), verify the registry shasum
|
|
355
361
|
publish npm --package @rightkit/<name> [--dry-run] Gate preflight for npm publish
|
|
356
362
|
publish npm --package @rightkit/<name> --publish --tarball <gated.tgz>
|
|
357
363
|
Publish the gated tarball with the Keychain token
|
package/deterministic-scans.mjs
CHANGED
|
@@ -1253,7 +1253,8 @@ export function planScans({
|
|
|
1253
1253
|
// inside its build directories; an untraced or out-of-bounds unit refuses Class A, naming it. Runs the gate's own command, directly (GC-A11).
|
|
1254
1254
|
// It runs in its workspace (cwd), so a unit's scope keeps it only for the Cargo workspaces of its own closure (app-closure CARGO_WORKSPACE_SCAN),
|
|
1255
1255
|
// like metadata, deny and audit: an npm-only unit traces nothing.
|
|
1256
|
-
|
|
1256
|
+
// Build helpers are vetted only on broker hosts (Mac, Dell); a Linux host or CI runner is a throwaway container (Adrian, 2026-11-03).
|
|
1257
|
+
if (platform === "darwin" || platform === "win32") for (const ws of touchedWs) add({ id: `build-helpers${wsIdSuffix(ws)}`, class: A, tool: "node", args: [BUILD_HELPERS_COMMAND, "check", "--manifest-path", "Cargo.toml"], ...(ws !== "." ? { cwd: ws } : {}), root: "content" });
|
|
1257
1258
|
// Locked package listings are observations in RustRight (#1216). They run through the selected executor
|
|
1258
1259
|
// from the checked Cargo inputs, just like metadata; an executor refusal remains a failed scan.
|
|
1259
1260
|
for (const name of crateNames.filter((n) => rustMap?.get(n).publish !== false)) add({ id: rustScanId.packageList(name), class: A, ...cargoReadIn(wsOf(name), "package", "-p", crateOf(name), "--list", "--allow-dirty", "--locked") });
|
package/package.json
CHANGED
package/publication-gate.mjs
CHANGED
|
@@ -370,7 +370,7 @@ export function verifyNpmRegistryBytes({ name, version, sha1 }, deps = {}) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
function readKeychainToken(run) {
|
|
373
|
+
export function readKeychainToken(run = spawnSync) {
|
|
374
374
|
if (process.platform !== "darwin") throw new Error(`npm publish token lookup is implemented for the macOS Keychain (${NPM_KEYCHAIN_SERVICE}) only`);
|
|
375
375
|
const result = run("security", ["find-generic-password", "-s", NPM_KEYCHAIN_SERVICE, "-w"], { encoding: "utf8", windowsHide: true });
|
|
376
376
|
const token = (result.stdout ?? "").trim();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `right-release publish-local npm --package @rightkit/<name> --commit <sha>`: the local npm route (Adrian, 2026-11-03, rightkit#1471:
|
|
3
|
+
// "i want you to publish it from local. the npm and crates are both local. we will do a provenance skip fuck your ci. feel free to push
|
|
4
|
+
// but no ci"). Mac only: the token is the granular @rightkit token with 2FA bypass in the Keychain item npm-rightkit-publish, read inside
|
|
5
|
+
// this process, never printed, never on a command line, never left on disk.
|
|
6
|
+
//
|
|
7
|
+
// It publishes exactly the bytes of one commit, and only after:
|
|
8
|
+
// 1. the commit is on origin/main (pushed; [skip ci] is fine);
|
|
9
|
+
// 2. the gate's Class A for the package passes on that commit (`rightkit-gate --to check --unit npm:<name> --commit <sha>`, this
|
|
10
|
+
// package's own gate, so a release publishes through the gate it ships);
|
|
11
|
+
// 3. the version is not on npm yet.
|
|
12
|
+
// The package folder is exported from the commit (git archive) and packed there; npm provenance is off (rightkit#1472). After publishing,
|
|
13
|
+
// the registry's shasum must equal the packed tarball's, or the command fails.
|
|
14
|
+
import crypto from "node:crypto";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { NPM_KEYCHAIN_SERVICE, readKeychainToken } from "./publication-gate.mjs";
|
|
21
|
+
|
|
22
|
+
const REGISTRY = "https://registry.npmjs.org/";
|
|
23
|
+
const GATE = fileURLToPath(new URL("./bin/rightkit-gate.mjs", import.meta.url));
|
|
24
|
+
|
|
25
|
+
function fail(message) { throw new Error(message); }
|
|
26
|
+
|
|
27
|
+
export function parseArgs(argv) {
|
|
28
|
+
const out = { kind: argv[0], pkg: null, commit: null, repo: process.cwd() };
|
|
29
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
30
|
+
if (argv[i] === "--package") out.pkg = argv[++i];
|
|
31
|
+
else if (argv[i] === "--commit") out.commit = argv[++i];
|
|
32
|
+
else if (argv[i] === "--repo") out.repo = path.resolve(argv[++i]);
|
|
33
|
+
else fail(`unknown option ${argv[i]}`);
|
|
34
|
+
}
|
|
35
|
+
if (out.kind !== "npm") fail("publish-local supports npm; crates publish with publish-local cargo once it lands");
|
|
36
|
+
if (!/^@rightkit\/[a-z0-9-]+$/.test(out.pkg ?? "")) fail("--package @rightkit/<name> is required");
|
|
37
|
+
if (!/^[0-9a-f]{7,40}$/.test(out.commit ?? "")) fail("--commit <sha> is required");
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function publishLocalNpm(argv, { run = spawnSync, log = console.log, platform = process.platform, readToken = () => readKeychainToken(run) } = {}) {
|
|
42
|
+
const o = parseArgs(argv);
|
|
43
|
+
if (platform !== "darwin") fail(`publish-local npm runs on the Mac (the token is the Keychain item ${NPM_KEYCHAIN_SERVICE}); this host is ${platform}`);
|
|
44
|
+
const git = (args) => {
|
|
45
|
+
const r = run("git", args, { cwd: o.repo, encoding: "utf8", maxBuffer: 1 << 26 });
|
|
46
|
+
if (r.status !== 0) fail(`git ${args.join(" ")} failed: ${String(r.stderr ?? "").trim()}`);
|
|
47
|
+
return String(r.stdout ?? "").trim();
|
|
48
|
+
};
|
|
49
|
+
const commit = git(["rev-parse", "--verify", `${o.commit}^{commit}`]);
|
|
50
|
+
git(["fetch", "-q", "origin", "main"]);
|
|
51
|
+
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])`);
|
|
52
|
+
const dir = `packages/${o.pkg.slice("@rightkit/".length)}`;
|
|
53
|
+
const manifest = JSON.parse(git(["show", `${commit}:${dir}/package.json`]));
|
|
54
|
+
if (manifest.name !== o.pkg) fail(`${dir}/package.json at ${commit} names ${manifest.name}, not ${o.pkg}`);
|
|
55
|
+
const version = manifest.version;
|
|
56
|
+
|
|
57
|
+
const already = run("npm", ["view", `${o.pkg}@${version}`, "version", "--prefer-online", "--registry", REGISTRY], { encoding: "utf8" });
|
|
58
|
+
if (String(already.stdout ?? "").trim() === version) fail(`${o.pkg}@${version} is already on npm; bump the version`);
|
|
59
|
+
|
|
60
|
+
log(`publish-local: Class A for npm:${o.pkg} on ${commit.slice(0, 12)}`);
|
|
61
|
+
const classA = run(process.execPath, [GATE, "--to", "check", "--unit", `npm:${o.pkg}`, "--commit", commit], { cwd: o.repo, stdio: ["ignore", "inherit", "inherit"] });
|
|
62
|
+
if (classA.status !== 0) fail(`Class A failed for npm:${o.pkg} on ${commit} (exit ${classA.status}); nothing published`);
|
|
63
|
+
|
|
64
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), "rk-publish-local-"));
|
|
65
|
+
try {
|
|
66
|
+
const archive = run("git", ["archive", "--format=tar", commit, dir], { cwd: o.repo, maxBuffer: 1 << 30 });
|
|
67
|
+
if (archive.status !== 0) fail(`git archive ${commit} ${dir} failed`);
|
|
68
|
+
const tarIn = run("tar", ["-x", "-C", work], { input: archive.stdout });
|
|
69
|
+
if (tarIn.status !== 0) fail("unpacking the commit's package folder failed");
|
|
70
|
+
const pkgDir = path.join(work, dir);
|
|
71
|
+
const packed = run("pnpm", ["pack", "--pack-destination", work], { cwd: pkgDir, encoding: "utf8" });
|
|
72
|
+
if (packed.status !== 0) fail(`pnpm pack failed: ${String(packed.stderr ?? packed.stdout ?? "").trim().split(/\r?\n/).slice(-3).join(" | ")}`);
|
|
73
|
+
const tgz = fs.readdirSync(work).find((f) => f.endsWith(".tgz"));
|
|
74
|
+
if (!tgz) fail("pnpm pack wrote no tarball");
|
|
75
|
+
const bytes = fs.readFileSync(path.join(work, tgz));
|
|
76
|
+
const sha1 = crypto.createHash("sha1").update(bytes).digest("hex");
|
|
77
|
+
log(`publish-local: packed ${tgz} sha1 ${sha1}`);
|
|
78
|
+
|
|
79
|
+
const token = readToken();
|
|
80
|
+
const userconfig = path.join(work, "npmrc");
|
|
81
|
+
fs.writeFileSync(userconfig, "//registry.npmjs.org/:_authToken=${RIGHTKIT_NPM_TOKEN}\n", { mode: 0o600 });
|
|
82
|
+
const redact = (text) => String(text ?? "").split(token).join("<redacted>");
|
|
83
|
+
const pub = run("npm", ["publish", path.join(work, tgz), "--access", "public", "--provenance=false", "--registry", REGISTRY, "--userconfig", userconfig],
|
|
84
|
+
{ encoding: "utf8", env: { ...process.env, RIGHTKIT_NPM_TOKEN: token } });
|
|
85
|
+
if (pub.status !== 0) fail(`npm publish failed:\n${redact(`${pub.stdout ?? ""}${pub.stderr ?? ""}`).trim()}`);
|
|
86
|
+
|
|
87
|
+
// The registry may take a moment to serve a new version.
|
|
88
|
+
let shasum = "";
|
|
89
|
+
for (let i = 0; i < 20 && !shasum; i += 1) {
|
|
90
|
+
const view = run("npm", ["view", `${o.pkg}@${version}`, "dist.shasum", "--prefer-online", "--registry", REGISTRY], { encoding: "utf8" });
|
|
91
|
+
shasum = String(view.stdout ?? "").trim();
|
|
92
|
+
if (!shasum) run("sleep", ["15"]);
|
|
93
|
+
}
|
|
94
|
+
if (shasum !== sha1) fail(`registry shasum ${shasum || "(not served yet)"} != packed ${sha1} for ${o.pkg}@${version}`);
|
|
95
|
+
log(`publish-local: ${o.pkg}@${version} published from ${commit.slice(0, 12)}; registry shasum ${shasum} matches`);
|
|
96
|
+
return { name: o.pkg, version, commit, shasum };
|
|
97
|
+
} finally {
|
|
98
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
103
|
+
try { publishLocalNpm(process.argv.slice(2)); } catch (e) { console.error(`right-release publish-local: ${e.message}`); process.exit(1); }
|
|
104
|
+
}
|
package/rightkit-versions.json
CHANGED
|
@@ -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.
|
|
52
|
+
"@rightkit/release": "0.6.30",
|
|
53
53
|
"@rightkit/settings-ui": "0.7.0",
|
|
54
54
|
"@rightkit/social-ui": "0.1.0",
|
|
55
55
|
"@rightkit/system-ui": "0.1.0",
|
|
@@ -512,7 +512,7 @@
|
|
|
512
512
|
"rightkit-app-settings": "0.3.1",
|
|
513
513
|
"rightkit-apple-ops": "0.1.1",
|
|
514
514
|
"rightkit-approvals": "0.1.0",
|
|
515
|
-
"rightkit-asr": "0.
|
|
515
|
+
"rightkit-asr": "0.4.0",
|
|
516
516
|
"rightkit-audio": "0.3.1",
|
|
517
517
|
"rightkit-autoedit": "0.1.1",
|
|
518
518
|
"rightkit-automation": "0.6.4",
|