@rightkit/release 0.6.15 → 0.6.16

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.
@@ -2500,7 +2500,15 @@ export async function runScans(plan, options = {}) {
2500
2500
  const spawnArgs = shellcheckIdentity ? ["-shellcheck", shellcheckIdentity.path, ...finalArgs] : finalArgs;
2501
2501
  const trustedDirs = trusts.map((tool) => identities.find((i) => i.tool === tool)).filter(Boolean).map((i) => path.dirname(i.path));
2502
2502
  const pathKey = Object.keys({ ...deps.env, ...s.env }).find((k) => k.toLowerCase() === "path") ?? "PATH";
2503
- const trustEnv = trustedDirs.length ? { [pathKey]: [...trustedDirs, ({ ...deps.env, ...s.env })[pathKey]].filter(Boolean).join(deps.platform === "win32" ? ";" : ":") } : {};
2503
+ // The gate's cargo wrapper stays first on PATH, ahead of the trusted directories: an enrolled analyzer's directory may also hold
2504
+ // a real cargo (cargo-deny in ~/.cargo/bin beside rustup's cargo), which would otherwise answer the scan's `cargo` unbound and,
2505
+ // with several workspaces, write into the sentinel (rightkit#1327).
2506
+ const scanPath = ({ ...deps.env, ...s.env })[pathKey];
2507
+ const pathSep = deps.platform === "win32" ? ";" : ":";
2508
+ const scanCargo = ({ ...deps.env, ...s.env }).CARGO;
2509
+ const wrapperBin = (cargoLedger || options.cargoSentinel) && typeof scanCargo === "string" && typeof scanPath === "string" ? (deps.platform === "win32" ? path.win32 : path.posix).dirname(scanCargo) : null;
2510
+ const wrapperFirst = wrapperBin !== null && scanPath.split(pathSep)[0] === wrapperBin;
2511
+ const trustEnv = trustedDirs.length ? { [pathKey]: (wrapperFirst ? [wrapperBin, ...trustedDirs, scanPath.slice(wrapperBin.length + 1)] : [...trustedDirs, scanPath]).filter(Boolean).join(pathSep) } : {};
2504
2512
  try { r = await deps.spawn(p.command, spawnArgs, { cwd: path.join(rootDir(s.root), s.cwd ?? ""), env: { ...deps.env, ...sentinelEnv, ...readTargetEnv, ...s.env, ...(extraEnv ?? {}), ...guardEnv, ...ledgerEnv, ...(carrier ? carrier.made.env : {}), ...(carrier ? scanOperationEnv({ ...deps.env, ...s.env }, limitMs) : {}), ...trustEnv }, signal: scanSignal, timeout: limitMs }); } finally { clearTimeout(clock); advisory?.cleanup?.(); }
2505
2513
  }
2506
2514
  if (signal?.aborted && (!r || r.error || r.status !== 0)) return { ...row, status: "not-run", code: "CANCELLED", detail: "cancelled while running" };
@@ -46,6 +46,46 @@ export function collectNpmPackages(groups, exclude = ["@rightkit/"]) {
46
46
  .sort((a, b) => `${a.name}@${a.versions.join(",")}`.localeCompare(`${b.name}@${b.versions.join(",")}`));
47
47
  }
48
48
 
49
+ /**
50
+ * The production closure of `pnpm list --json --depth Infinity` output: every package reachable from a project's `dependencies` or
51
+ * `optionalDependencies` through the dependencies, optional dependencies and resolved peer dependencies pnpm lists, as
52
+ * `name@version` -> { name, version, path }. pnpm 12's `licenses list --prod` no longer follows a peer edge (an optional peer of a
53
+ * production package that the app also installs as a dev dependency, so `@rightkit/legal` -> `@rightkit/release` -> smol-toml went
54
+ * missing), but the package is still installed beside the production package and loadable by it; the notices must list it.
55
+ */
56
+ export function productionClosure(tree) {
57
+ if (!Array.isArray(tree)) fail("pnpm list --json did not return a project array");
58
+ const closure = new Map(), seen = new Set();
59
+ const visit = (name, node) => {
60
+ if (!node || typeof node !== "object" || typeof node.version !== "string" || node.version.startsWith("link:")) return;
61
+ const id = `${name}@${node.version}`, at = `${id}|${node.path ?? ""}`;
62
+ if (seen.has(at)) return;
63
+ seen.add(at);
64
+ if (!closure.has(id)) closure.set(id, { name, version: node.version, path: node.path });
65
+ for (const section of ["dependencies", "optionalDependencies"]) for (const [child, value] of Object.entries(node[section] ?? {})) visit(child, value);
66
+ };
67
+ for (const project of tree) for (const section of ["dependencies", "optionalDependencies"]) for (const [name, node] of Object.entries(project?.[section] ?? {})) visit(name, node);
68
+ return closure;
69
+ }
70
+
71
+ /**
72
+ * `groups` (the `--prod` listing) plus every closure package it lacks, with its license read from `allGroups` (the full listing,
73
+ * fetched lazily by `loadAll`) and its installed path from the tree. A missing package that the full listing does not know is an
74
+ * error: a notice is never silently shorter than the production closure. Excluded prefixes are skipped before the lookup.
75
+ */
76
+ export function completeProductionGroups(groups, closure, loadAll, exclude = ["@rightkit/"]) {
77
+ const listed = new Set(Object.values(groups).flatMap((entries) => entries.flatMap((entry) => entry.versions.map((v) => `${entry.name}@${v}`))));
78
+ const missing = [...closure].filter(([id, item]) => !listed.has(id) && !exclude.some((prefix) => item.name.startsWith(prefix)));
79
+ if (!missing.length) return groups;
80
+ const all = loadAll(), completed = Object.fromEntries(Object.entries(groups).map(([license, entries]) => [license, [...entries]]));
81
+ for (const [id, item] of missing) {
82
+ const hit = Object.entries(all).find(([, entries]) => entries.some((entry) => entry.name === item.name && entry.versions.includes(item.version)));
83
+ if (!hit || typeof item.path !== "string") fail(`${id} is in the production closure (pnpm list) but not in pnpm licenses list: its notice cannot be produced`);
84
+ (completed[hit[0]] ??= []).push({ name: item.name, versions: [item.version], paths: [item.path] });
85
+ }
86
+ return completed;
87
+ }
88
+
49
89
  /**
50
90
  * Render the appendix. `readNames(dir)` lists a package directory and `readText(file)` reads one file (both injectable).
51
91
  * Returns `{ html, packageCount, fallbackCount }`.
@@ -106,7 +146,15 @@ export function generateNpmNotices({ appRoot, config, flags = {}, check = false,
106
146
  });
107
147
  if (result.error) throw result.error;
108
148
  if (result.status !== 0) fail(result.stderr || "pnpm licenses list failed");
109
- const packages = collectNpmPackages(JSON.parse(result.stdout), exclude);
149
+ const run = (args) => {
150
+ const out = spawnSyncFn("pnpm", args, { cwd: appRoot, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, shell: process.platform === "win32" });
151
+ if (out.error) throw out.error;
152
+ if (out.status !== 0) fail(out.stderr || `pnpm ${args.join(" ")} failed`);
153
+ return JSON.parse(out.stdout);
154
+ };
155
+ const closure = productionClosure(run(["list", "--json", "--depth", "Infinity"]));
156
+ const groups = completeProductionGroups(JSON.parse(result.stdout), closure, () => run(["licenses", "list", "--json"]), exclude);
157
+ const packages = collectNpmPackages(groups, exclude);
110
158
  const lockHash = createHash("sha256").update(readFileSync(path.join(appRoot, "pnpm-lock.yaml"))).digest("hex");
111
159
  const { html, packageCount, fallbackCount } = renderNpmNotices({ productName, version, lockHash, packages });
112
160
  const target = path.join(appRoot, output);
@@ -244,8 +244,20 @@ function packageLockGraph(text, manifests, lockDir) {
244
244
  }
245
245
  return g;
246
246
  }
247
+ /**
248
+ * pnpm 12 writes the lockfile as several YAML documents: leading ones carry the tooling pnpm installs for itself
249
+ * (configDependencies, packageManagerDependencies), and the last one is the project lock. Only the project's document is the
250
+ * dependency graph; pnpm 6/9/11 write a single document, which is returned unchanged.
251
+ */
252
+ export function pnpmProjectDocument(text) {
253
+ const docs = text.split(/^---[ \t]*\r?$/m).filter((doc) => doc.trim());
254
+ if (docs.length <= 1) return text;
255
+ const last = docs.at(-1);
256
+ if (!/^lockfileVersion:/m.test(last)) fail("pnpm multi-document lockfile does not end with the project document");
257
+ return last;
258
+ }
247
259
  function pnpmGraph(text, manifests, lockDir, workspaceBytes) {
248
- const lock = parsePnpmYaml(text), version = String(lock.lockfileVersion), g = graph();
260
+ const lock = parsePnpmYaml(pnpmProjectDocument(text)), version = String(lock.lockfileVersion), g = graph();
249
261
  if (!/^(?:9|6)(?:\.\d+)?$/.test(version) || !object(lock.importers) || !object(lock.packages ?? {})) throw new LockError("lockfile-unsupported-format", "pnpm requires v6/v9 importers");
250
262
  const v9 = version.startsWith("9"), packages = lock.packages ?? {}, snapshots = v9 ? lock.snapshots ?? {} : packages;
251
263
  if (!object(snapshots)) fail("invalid pnpm snapshots");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
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",
@@ -32,7 +32,7 @@
32
32
  "@rightkit/qa": "0.6.2",
33
33
  "@rightkit/qa-chrome": "0.1.12",
34
34
  "@rightkit/react-shell": "0.7.2",
35
- "@rightkit/release": "0.6.15",
35
+ "@rightkit/release": "0.6.16",
36
36
  "@rightkit/settings-ui": "0.7.0",
37
37
  "@rightkit/tauri": "0.3.0",
38
38
  "@rightkit/tauri-shell": "0.1.3",