@rightkit/release 0.6.29 → 0.6.31

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.
@@ -294,7 +294,7 @@ export function esloggerWritesByRoot(file, roots) {
294
294
  }
295
295
 
296
296
  /** tracerpt XML from `file`, streamed by `</Event>`: Map(root pid -> written paths) for every pid in `roots`, kernel paths mapped by `devices`. */
297
- export function etwWritesByRoot(file, roots, devices = {}) {
297
+ export function etwWritesByRoot(file, roots, devices = {}, { toolchainDirs = [] } = {}) {
298
298
  const field = (ev, name) => new RegExp(`<Data Name="${name}">([^<]*)</Data>`).exec(ev)?.[1];
299
299
  const id = (ev) => Number(/<EventID>(\d+)<\/EventID>/.exec(ev)?.[1]);
300
300
  const pidOf = (ev) => Number(/ProcessID="(\d+)"/.exec(ev)?.[1]);
@@ -322,7 +322,14 @@ export function etwWritesByRoot(file, roots, devices = {}) {
322
322
  const pid = Number(field(ev, "ProcessID"));
323
323
  if (n === 1) {
324
324
  const ppid = Number(field(ev, "ParentProcessID"));
325
- if (roots.has(pid) && ppid === runner) live.set(pid, pid);
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);
326
333
  else if (live.has(ppid)) live.set(pid, live.get(ppid));
327
334
  else live.delete(pid);
328
335
  } else if (n === 2) live.delete(pid);
@@ -84,6 +84,8 @@ export function cargoWorkspaces(root, { depth = 4, fsApi = fs } = {}) {
84
84
  // in the workspace's own committed list (--record-repo). Default root: /Volumes/D/claude (Mac), D:\claude (Windows), the current
85
85
  // directory elsewhere.
86
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();
87
89
  const roots = argv.flatMap((a, i) => (a === "--root" && argv[i + 1] ? [path.resolve(argv[i + 1])] : []));
88
90
  if (!roots.length) roots.push(platform === "darwin" ? "/Volumes/D/claude" : platform === "win32" ? "D:\\claude" : process.cwd());
89
91
  let keepAlive = null;
@@ -103,7 +105,7 @@ function traceAll(argv, { env, platform, log, err }) {
103
105
  const jobsAt = argv.indexOf("--jobs");
104
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
105
107
  // tracing never edits a checkout another owner holds.
106
- 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 });
107
109
  if (code !== 0) err(`right-release build-helpers trace-all: ${ws} ended with ${code}`);
108
110
  worst = Math.max(worst, code);
109
111
  }
@@ -112,7 +114,7 @@ function traceAll(argv, { env, platform, log, err }) {
112
114
  return worst;
113
115
  }
114
116
 
115
- 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() } = {}) {
116
118
  if (argv[0] === "trace-all") return traceAll(argv.slice(1), { env, platform, log, err });
117
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),
118
120
  // writing to a scratch target, never through the broker.
@@ -123,10 +125,10 @@ export function main(argv = process.argv.slice(2), { env = process.env, platform
123
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
124
126
  // ever goes through the RustRight shim (which refuses a build that redirects its output folder).
125
127
  const directEnv = { ...env, [pathKey]: directPath };
126
- 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 }); }
127
129
  }
128
130
 
129
- function mainWith(argv, { env, platform, run, log, err }) {
131
+ function mainWith(argv, { env, platform, run, log, err, judged = new Set(), confirm = null }) {
130
132
  const [sub, ...rest] = argv;
131
133
  const at = rest.indexOf("--manifest-path");
132
134
  const manifestPath = at >= 0 ? rest[at + 1] : undefined;
@@ -161,18 +163,37 @@ function mainWith(argv, { env, platform, run, log, err }) {
161
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
162
164
  // builds do not fight over them.
163
165
  const jobsAt = rest.indexOf("--jobs");
164
- const jobs = Math.max(1, Number.parseInt(jobsAt >= 0 ? rest[jobsAt + 1] : "4", 10) || 4);
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);
165
168
  const perBuildJobs = String(Math.max(1, Math.floor(os.cpus().length / jobs)));
166
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
+ };
167
187
  const crossEnv = new Map(); const waiting = [];
168
- const pending = [...verdict.untraced, ...verdict.failed.map((f) => f.unit)].filter((unit) => {
188
+ const pending = [...verdict.untraced, ...verdict.failed.map((f) => f.unit)].filter((unit) => (confirm ? confirm.has(unit.key) : !judged.has(unit.key))).filter((unit) => {
169
189
  if (unit.target === inv.host) return true;
170
- if (!crossEnv.has(unit.target)) crossEnv.set(unit.target, crossCompileEnv(unit.target, undefined, platform) ?? null);
190
+ if (!crossEnv.has(unit.target)) crossEnv.set(unit.target, workingCrossEnv(unit.target));
171
191
  if (crossEnv.get(unit.target)) return true;
172
192
  waiting.push(unit); return false;
173
193
  });
174
- if (waiting.length) err(`right-release build-helpers trace: ${waiting.length} cross-target unit(s) not traced: the zig C toolchain is not provisioned (scripts/ci/provision-zig.mjs)`);
175
- log(`right-release build-helpers trace: ${pending.length} unit(s), ${jobs} at a time`);
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();
176
197
  if (!pending.length) return 0;
177
198
  const prepared = pending.map((unit) => {
178
199
  const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-"));
@@ -190,7 +211,10 @@ function mainWith(argv, { env, platform, run, log, err }) {
190
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.
191
212
  const cross = unit.target !== inv.host;
192
213
  const buildArgs = ["build", "--profile", "release", "--manifest-path", path.join(probe, "Cargo.toml"), "--target-dir", target, ...(cross ? ["--target", unit.target] : [])];
193
- const buildEnv = { ...env, ...(cross ? crossEnv.get(unit.target) : {}), CARGO_TARGET_DIR: target, CARGO_BUILD_JOBS: perBuildJobs };
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 };
194
218
  const job = { cmd: "cargo", args: buildArgs, env: buildEnv };
195
219
  job.label = `${unit.crate} ${unit.version}${cross ? ` (${unit.target})` : ""}`;
196
220
  return { unit, scratch, target, job };
@@ -258,15 +282,25 @@ function mainWith(argv, { env, platform, run, log, err }) {
258
282
  const xml = path.join(sessionDir, "trace.xml");
259
283
  const exported = run("tracerpt", [etl.file, "-o", xml, "-of", "XML", "-y"], { encoding: "utf8" });
260
284
  if (exported.status !== 0) { err("right-release build-helpers trace: tracerpt could not export the ETW trace"); return 2; }
261
- 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 */ }
262
286
  }
263
287
  if (!recorded && roots.size) { err(`right-release build-helpers trace: the ${platform === "darwin" ? "eslogger" : "ETW"} session recorded nothing; nothing recorded`); return 2; }
264
288
  prepared.forEach((b, k) => {
265
289
  const r = results[k] ?? {};
266
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
+ }
267
300
  if (r.status !== 0) {
268
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.
269
302
  const reason = String(r.error ?? `exit ${r.status}`);
303
+ judged.add(b.unit.key);
270
304
  recordTrace(listFile, b.unit, { ok: false, unbuildable: true, outside: [reason] });
271
305
  if (recordRepo) recordTrace(repoFile, b.unit, { ok: false, unbuildable: true, outside: [reason] });
272
306
  log(`skip ${b.unit.crate} ${b.unit.version} (${b.unit.target}): does not build on this host (${reason.slice(0, 160)})`);
@@ -279,14 +313,30 @@ function mainWith(argv, { env, platform, run, log, err }) {
279
313
  // the tracer under the real path, so comparing the link's path would fail every crate.
280
314
  const real = (d) => { try { return fs.realpathSync.native(d); } catch { return d; } };
281
315
  const roots = allowed.filter(Boolean).flatMap((d) => [d, real(d)]);
282
- const judged = judgeTrace(writes, { allowed: roots, platform, mustSee: real(b.target) });
283
- recordTrace(listFile, b.unit, judged);
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);
284
333
  // --record-repo also writes the verdict into the repository's committed list, for review and commit by the repository's owner.
285
- if (recordRepo) recordTrace(repoFile, b.unit, judged);
286
- log(`${judged.ok ? "pass" : "FAIL"} ${b.unit.crate} ${b.unit.version}${judged.ok ? "" : `: wrote ${judged.outside.slice(0, 5).join(", ")}`}`);
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(", ")}`}`);
287
336
  });
288
337
  for (const b of prepared) fs.rmSync(b.scratch, { recursive: true, force: true });
289
338
  fs.rmSync(sessionDir, { recursive: true, force: true });
339
+ if (firstFails.size) return mainWith(argv, { env, platform, run, log, err, judged, confirm: firstFails });
290
340
  return 0;
291
341
  }
292
342
  err("usage: right-release build-helpers check|trace [--manifest-path <Cargo.toml>] [--record-repo] [--jobs N] | trace-all [--root <dir>]... [--jobs N]");
@@ -2432,7 +2432,10 @@ export function cargoReadTargetDir({ tmpRoot, roots }) {
2432
2432
  export async function runScans(plan, options = {}) {
2433
2433
  const deps = {
2434
2434
  // Concurrent runs need a spawn that does not block the event loop.
2435
- spawn: Number(options.jobs ?? 1) > 1 || options.signal ? defaultSpawnAsync : defaultSpawn,
2435
+ // Always asynchronous (A9): a synchronous spawn holds the event loop, so no scan's time limit can fire while it runs. The 2026-11-02 runs
2436
+ // show it: every scan that finished at the same moment reported the same inflated time (shellcheck and actionlint 1,655 s, against a
2437
+ // 5 min limit), held by a synchronous broker call. One lane used the synchronous spawn, so a single-lane run had no working limit.
2438
+ spawn: defaultSpawnAsync,
2436
2439
  // Locating a tool (rustfmt's `which`/`where` fallback) is synchronous whatever `spawn` is.
2437
2440
  lookupSpawn: defaultSpawn, exists: fs.existsSync, readFile: (f) => fs.readFileSync(f, "utf8"), readdir: fs.readdirSync,
2438
2441
  env: process.env, platform: process.platform, execPath: process.execPath,
@@ -2558,7 +2561,8 @@ export async function runScans(plan, options = {}) {
2558
2561
  let row = await scanRow(s, extraEnv);
2559
2562
  // rightkit#1459: a class A scan that timed out is run once more before it fails Class A (a hung tool, e.g. actionlint on 2026-11-02, ran
2560
2563
  // 27 min and failed a run whose rerun passed in 1 s). Class B and carried scans are not retried: a compile that times out is a real failure.
2561
- if (row.status === "fail" && row.code === "TIMEOUT" && s.class === SCAN_CLASS.A && !signal?.aborted) {
2564
+ // Test suites are not retried either: a slow test is a finding, not a hung tool.
2565
+ if (row.status === "fail" && row.code === "TIMEOUT" && s.class === SCAN_CLASS.A && s.kind !== "test" && !signal?.aborted) {
2562
2566
  const first = row;
2563
2567
  row = await scanRow(s, extraEnv);
2564
2568
  row = { ...row, retried: { code: first.code, detail: first.detail } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.29",
3
+ "version": "0.6.31",
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",
@@ -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.29",
52
+ "@rightkit/release": "0.6.31",
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.3.4",
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",
package/snapshot-git.mjs CHANGED
@@ -133,9 +133,18 @@ function discoveryHome() {
133
133
  }
134
134
 
135
135
  /** git run in `cwd` against the checkout's own repository (found from `cwd`), under the sealed environment (no GIT_DIR, no index) plus GIT_NO_REPLACE_OBJECTS. Returns the spawn result. */
136
+ // The sealed environment hides the user's global git config, so the user's `safe.directory` never reaches git: a checkout owned by
137
+ // another account (on the Dell, D:\Claude\viewright and postright belong to BUILTIN\Administrators) was refused as "dubious ownership"
138
+ // from 0.6.9 on. The checkout itself is named on the command line (command-line config is protected configuration, which git accepts for
139
+ // safe.directory), forward slashes as git compares them, and fsmonitor stays off so the checkout's own config can start no program.
140
+ export function discoverySafeArgs(cwd, platform = process.platform) {
141
+ const dir = (platform === "win32" ? path.win32 : path).resolve(cwd);
142
+ return ["-c", `safe.directory=${platform === "win32" ? dir.replaceAll("\\", "/") : dir}`, "-c", "core.fsmonitor=false"];
143
+ }
144
+
136
145
  export function discoveryRun(cwd, args, { platform = process.platform, input } = {}) {
137
146
  const env = sealedEnv(discoveryHome(), { platform, gitDir: null, index: false });
138
- return spawnSync("git", args, { cwd, env, input, encoding: "utf8", maxBuffer: MAX_BUFFER });
147
+ return spawnSync("git", [...discoverySafeArgs(cwd, platform), ...args], { cwd, env, input, encoding: "utf8", maxBuffer: MAX_BUFFER });
139
148
  }
140
149
  /** As discoveryRun, returning trimmed-by-caller stdout or throwing an Error naming the command. */
141
150
  export function discoveryGit(cwd, args, options) {