@rightkit/release 0.2.33 → 0.2.35

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-release.mjs CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  import path from "node:path";
21
21
  import { spawn, spawnSync } from "node:child_process";
22
22
  import { fileURLToPath, pathToFileURL } from "node:url";
23
- import { cacheFingerprint, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine } from "./release-state.mjs";
23
+ import { cacheFingerprint, commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, watchProgress } from "./release-state.mjs";
24
24
 
25
25
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
26
26
  const WORKER = path.join(TOOL_ROOT, "release.mjs");
@@ -145,7 +145,13 @@ try {
145
145
  await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget);
146
146
  },
147
147
  build: async () => {
148
- await runProgress(process.execPath, [WORKER, "--config", worktreeConfigPath, "--platform", platform, "--no-upload"], appRoot, env, env.CARGO_TARGET_DIR);
148
+ await runProgress(
149
+ process.execPath,
150
+ [WORKER, "--config", worktreeConfigPath, "--platform", platform, "--no-upload"],
151
+ appRoot,
152
+ env,
153
+ [env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
154
+ );
149
155
  checkpoint(stateRoot, "build_complete");
150
156
  checkpoint(stateRoot, "signed");
151
157
  checkpoint(stateRoot, "hardened");
@@ -269,22 +275,25 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir) {
269
275
  const inactivityMs = Number(process.env.RIGHT_RELEASE_STALL_MS ?? 10 * 60 * 1000);
270
276
  const absoluteMs = Number(process.env.RIGHT_RELEASE_ABSOLUTE_MS ?? 90 * 60 * 1000);
271
277
  let lastProgress = Date.now();
272
- let lastMtime = newestMtime(watchDir);
278
+ const watchDirs = Array.isArray(watchDir) ? watchDir : [watchDir];
279
+ let lastMtime = Math.max(...watchDirs.map(newestMtime));
273
280
  const started = Date.now();
274
281
  await new Promise((resolve, reject) => {
275
282
  child = spawn(cmd, runArgs, { cwd, env, stdio: ["inherit", "pipe", "pipe"], windowsHide: true, shell: process.platform === "win32" });
283
+ const closeWatchers = watchProgress(watchDirs, () => { lastProgress = Date.now(); });
276
284
  for (const [stream, output] of [[child.stdout, process.stdout], [child.stderr, process.stderr]]) {
277
285
  stream.on("data", (chunk) => { lastProgress = Date.now(); output.write(chunk); });
278
286
  }
279
287
  const monitor = setInterval(() => {
280
- const mtime = newestMtime(watchDir);
288
+ const mtime = Math.max(...watchDirs.map(newestMtime));
281
289
  if (mtime > lastMtime) { lastMtime = mtime; lastProgress = Date.now(); }
282
290
  if (Date.now() - started > absoluteMs) stop(`absolute limit exceeded (${Math.round(absoluteMs / 60000)}m)`);
283
291
  else if (Date.now() - lastProgress > inactivityMs) stop(`no output or file progress for ${Math.round(inactivityMs / 60000)}m`);
284
292
  }, Math.min(30_000, Math.max(250, Math.floor(inactivityMs / 4))));
285
- const stop = (reason) => { clearInterval(monitor); killTree(child.pid); reject(new Error(`release step stalled: ${reason}`)); };
286
- child.once("error", (error) => { clearInterval(monitor); reject(error); });
287
- child.once("exit", (code) => { clearInterval(monitor); child = null; code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`)); });
293
+ const cleanup = () => { clearInterval(monitor); closeWatchers(); };
294
+ const stop = (reason) => { cleanup(); killTree(child.pid); reject(new Error(`release step stalled: ${reason}`)); };
295
+ child.once("error", (error) => { cleanup(); reject(error); });
296
+ child.once("exit", (code) => { cleanup(); child = null; code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`)); });
288
297
  });
289
298
  }
290
299
 
@@ -342,9 +351,11 @@ function commandExists(name) {
342
351
  }
343
352
 
344
353
  function commandOutput(cmd, runArgs) {
345
- const result = spawnSync(cmd, runArgs, { encoding: "utf8", windowsHide: true });
346
- if (result.status !== 0) fail(`required command failed: ${cmd} ${runArgs.join(" ")}`);
347
- return result.stdout.trim();
354
+ try {
355
+ return commandOutputPortable(cmd, runArgs);
356
+ } catch (error) {
357
+ fail(`required command failed: ${error.message}`);
358
+ }
348
359
  }
349
360
 
350
361
  function git(cwd, runArgs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
4
4
  "description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
5
5
  "type": "module",
6
6
  "bin": {
package/release-state.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync, watch } from "node:fs";
3
4
  import path from "node:path";
4
5
 
5
6
  export function cacheFingerprint({ cargoLockSha256, rustc, target, features = [] }) {
@@ -23,6 +24,41 @@ export function resolveReleaseLayout({ repoRoot, configPath }) {
23
24
  };
24
25
  }
25
26
 
27
+ export function commandOutputPortable(cmd, args, { cwd, env = process.env } = {}) {
28
+ const windows = process.platform === "win32";
29
+ if (windows && ![cmd, ...args].every((value) => /^[A-Za-z0-9._:/@+=-]+$/.test(value))) {
30
+ throw new Error("unsafe Windows command token");
31
+ }
32
+ const executable = windows ? (env.ComSpec || env.COMSPEC || "cmd.exe") : cmd;
33
+ const executableArgs = windows ? ["/d", "/s", "/c", [cmd, ...args].join(" ")] : args;
34
+ const result = spawnSync(executable, executableArgs, {
35
+ cwd,
36
+ env,
37
+ encoding: "utf8",
38
+ windowsHide: true,
39
+ });
40
+ if (result.status !== 0) {
41
+ const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
42
+ throw new Error(`${cmd} ${args.join(" ")} failed: ${detail}`);
43
+ }
44
+ return result.stdout.trim();
45
+ }
46
+
47
+ export function watchProgress(paths, onProgress) {
48
+ const watchers = [];
49
+ for (const candidate of paths) {
50
+ if (!candidate || !existsSync(candidate)) continue;
51
+ try {
52
+ watchers.push(watch(candidate, { recursive: true }, onProgress));
53
+ } catch {
54
+ try { watchers.push(watch(candidate, onProgress)); } catch { /* output still counts as progress */ }
55
+ }
56
+ }
57
+ return () => {
58
+ for (const watcher of watchers) watcher.close();
59
+ };
60
+ }
61
+
26
62
  export function releaseEnvironment({ root, platform, cacheKey, kind = "release" }) {
27
63
  if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
28
64
  const targetKind = kind === "release" ? "cargo-target" : "test-target";
@@ -1,12 +1,15 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { createHash } from "node:crypto";
3
3
  import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { writeFile } from "node:fs/promises";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  import test from "node:test";
7
8
 
8
9
  import {
9
10
  cacheFingerprint,
11
+ commandOutputPortable,
12
+ watchProgress,
10
13
  resolveReleaseLayout,
11
14
  releaseEnvironment,
12
15
  runBuildStateMachine,
@@ -14,6 +17,28 @@ import {
14
17
  verifySealedRelease,
15
18
  } from "./release-state.mjs";
16
19
 
20
+ test("progress watcher observes writes in nested Cargo target directories", async () => {
21
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-release-watch-"));
22
+ const nested = path.join(root, "release", "build", "openssl");
23
+ mkdirSync(nested, { recursive: true });
24
+ let resolveProgress;
25
+ const progress = new Promise((resolve) => { resolveProgress = resolve; });
26
+ const close = watchProgress([root], resolveProgress);
27
+ try {
28
+ await writeFile(path.join(nested, "object.lib"), "progress");
29
+ await Promise.race([
30
+ progress,
31
+ new Promise((_, reject) => setTimeout(() => reject(new Error("nested progress event missing")), 2_000)),
32
+ ]);
33
+ } finally {
34
+ close();
35
+ }
36
+ });
37
+
38
+ test("portable command capture resolves Windows command shims", { skip: process.platform !== "win32" }, () => {
39
+ assert.match(commandOutputPortable("pnpm", ["--version"]), /^11\.12\.0$/);
40
+ });
41
+
17
42
  test("nested app configs keep the vault at repo root and build from the app root", () => {
18
43
  const layout = resolveReleaseLayout({
19
44
  repoRoot: "D:/suite/heardright",
@@ -317,7 +317,7 @@ test("Cargo version contract rejects staged versions that mismatch canonical man
317
317
  test("RightKit exposes one current version manifest", () => {
318
318
  assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
319
319
  assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
320
- assert.equal(versions.npm["@rightkit/release"], "0.2.33");
320
+ assert.equal(versions.npm["@rightkit/release"], "0.2.35");
321
321
  assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
322
322
  assert.equal(versions.npm["@rightkit/license"], "0.1.5");
323
323
  assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
@@ -7,7 +7,7 @@
7
7
  "@rightkit/logs": "0.1.3",
8
8
  "@rightkit/platform-ui": "0.1.0",
9
9
  "@rightkit/qa": "0.1.0",
10
- "@rightkit/release": "0.2.33",
10
+ "@rightkit/release": "0.2.35",
11
11
  "@rightkit/tauri": "0.1.0",
12
12
  "@rightkit/updates": "0.2.3"
13
13
  },