@rightkit/release 0.2.7 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { access, readFile } from "node:fs/promises";
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
5
6
  import { spawn, spawnSync } from "node:child_process";
@@ -9,7 +10,7 @@ const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
9
10
  const UPLOAD_LARGE = path.resolve(TOOL_ROOT, "upload-large.mjs");
10
11
  const SIGN_WINDOWS = path.resolve(TOOL_ROOT, "sign-windows.mjs");
11
12
  const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
12
- const VERSION = "0.2.7";
13
+ const VERSION = "0.2.9";
13
14
  const TIERS = new Set(["patch", "update"]);
14
15
  const RIGHTKIT_EXPECTED = new Map([
15
16
  ["@rightkit/license", "^0.1.5"],
@@ -87,6 +88,7 @@ if (opts.doctor) {
87
88
  process.exit(0);
88
89
  }
89
90
 
91
+ const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
90
92
  const mode = opts.upload ? "publish" : "package";
91
93
  const command = target.package;
92
94
  if (!command) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
@@ -141,6 +143,7 @@ if (opts.upload && !target.publish) {
141
143
  }
142
144
 
143
145
  console.log(`right-release: done (${Date.now() - started}ms)`);
146
+ releaseLock.release();
144
147
 
145
148
  function usage(code, message) {
146
149
  if (message) console.error(`right-release: ${message}\n`);
@@ -249,19 +252,105 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
249
252
  });
250
253
  }
251
254
 
255
+ function acquireReleaseLock(root, app, platform, tier) {
256
+ const lockDir = path.join(root, ".cache", "right-release");
257
+ const lockPath = path.join(lockDir, `${platform}.lock.json`);
258
+ mkdirSync(lockDir, { recursive: true });
259
+ const existing = readLock(lockPath);
260
+ if (existing?.pid && isRightReleaseProcess(existing.pid)) {
261
+ if (opts.dryRun) {
262
+ console.log(`dry-run: would kill previous ${app} ${platform} release pid=${existing.pid}`);
263
+ } else {
264
+ console.error(`right-release: killing previous ${app} ${platform} release pid=${existing.pid}`);
265
+ killProcessTree(existing.pid);
266
+ }
267
+ } else if (existing?.pid) {
268
+ console.error(`right-release: removing stale ${app} ${platform} release lock pid=${existing.pid}`);
269
+ }
270
+ const lock = {
271
+ pid: process.pid,
272
+ app,
273
+ platform,
274
+ tier,
275
+ startedAt: new Date().toISOString(),
276
+ cwd: root,
277
+ argv: process.argv,
278
+ };
279
+ writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`);
280
+ let released = false;
281
+ const release = () => {
282
+ if (released) return;
283
+ released = true;
284
+ const current = readLock(lockPath);
285
+ if (current?.pid === process.pid) {
286
+ try {
287
+ unlinkSync(lockPath);
288
+ } catch {
289
+ // already gone
290
+ }
291
+ }
292
+ };
293
+ process.once("exit", release);
294
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
295
+ process.once(signal, () => {
296
+ release();
297
+ process.exit(signal === "SIGINT" ? 130 : 143);
298
+ });
299
+ }
300
+ return { release };
301
+ }
302
+
303
+ function readLock(lockPath) {
304
+ if (!existsSync(lockPath)) return null;
305
+ try {
306
+ return JSON.parse(readFileSync(lockPath, "utf8"));
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+
312
+ function isRightReleaseProcess(pid) {
313
+ if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
314
+ if (!isProcessAlive(Number(pid))) return false;
315
+ const commandLine = processCommandLine(Number(pid));
316
+ return /(?:right-release|release\.mjs)/i.test(commandLine);
317
+ }
318
+
319
+ function isProcessAlive(pid) {
320
+ try {
321
+ process.kill(pid, 0);
322
+ return true;
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
327
+
328
+ function processCommandLine(pid) {
329
+ if (process.platform === "win32") {
330
+ const result = spawnSync("powershell", [
331
+ "-NoProfile",
332
+ "-Command",
333
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${Number(pid)}"; if ($p) { $p.CommandLine }`,
334
+ ], { encoding: "utf8" });
335
+ return result.stdout ?? "";
336
+ }
337
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
338
+ return result.stdout ?? "";
339
+ }
340
+
252
341
  function killProcessTree(pid) {
253
342
  if (!pid) return;
254
343
  if (process.platform === "win32") {
255
344
  spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
256
345
  return;
257
346
  }
347
+ const children = spawnSync("pgrep", ["-P", String(pid)], { encoding: "utf8" });
348
+ for (const child of (children.stdout ?? "").split(/\s+/).filter(Boolean)) {
349
+ killProcessTree(Number(child));
350
+ }
258
351
  try {
259
- process.kill(-pid, "SIGKILL");
352
+ process.kill(pid, "SIGKILL");
260
353
  } catch {
261
- try {
262
- process.kill(pid, "SIGKILL");
263
- } catch {
264
- // already gone
265
- }
354
+ // already gone
266
355
  }
267
356
  }
package/release.test.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { spawnSync } from "node:child_process";
@@ -18,7 +18,7 @@ function fixture({ signed = true, publish = false, packageJson } = {}) {
18
18
  name: "fixture",
19
19
  scripts: { "release:patch:win": "right-release --platform win --tier patch" },
20
20
  dependencies: { "@rightkit/license": "^0.1.5", "@rightkit/logs": "^0.1.3" },
21
- devDependencies: { "@rightkit/release": "^0.2.7" },
21
+ devDependencies: { "@rightkit/release": "^0.2.9" },
22
22
  },
23
23
  null,
24
24
  2,
@@ -47,12 +47,56 @@ function fixture({ signed = true, publish = false, packageJson } = {}) {
47
47
  return config;
48
48
  }
49
49
 
50
+ function lockFixture() {
51
+ const dir = mkdtempSync(path.join(os.tmpdir(), "right-release-lock-test-"));
52
+ const config = path.join(dir, "right-release.config.mjs");
53
+ writeFileSync(
54
+ path.join(dir, "package.json"),
55
+ JSON.stringify(
56
+ {
57
+ name: "fixture-lock",
58
+ scripts: { "release:patch:mac": "right-release --platform mac --tier patch" },
59
+ dependencies: { "@rightkit/license": "^0.1.5", "@rightkit/logs": "^0.1.3" },
60
+ devDependencies: { "@rightkit/release": "^0.2.9" },
61
+ },
62
+ null,
63
+ 2,
64
+ ),
65
+ );
66
+ writeFileSync(
67
+ config,
68
+ `export default ${JSON.stringify({
69
+ schema: 1,
70
+ app: "fixture-lock",
71
+ packageManager: "pnpm",
72
+ checks: [],
73
+ targets: {
74
+ mac: {
75
+ signed: true,
76
+ package: { cmd: "node", args: ["-e", "process.exit(0)"] },
77
+ artifacts: [],
78
+ hardening: [],
79
+ installer: { artifacts: [] },
80
+ updater: { artifacts: [] },
81
+ },
82
+ },
83
+ })};\n`,
84
+ );
85
+ return { dir, config };
86
+ }
87
+
50
88
  function run(config, ...args) {
51
89
  return spawnSync(process.execPath, [release, "--config", config, "--platform", "win", "--dry-run", ...args], {
52
90
  encoding: "utf8",
53
91
  });
54
92
  }
55
93
 
94
+ function runRaw(config, ...args) {
95
+ return spawnSync(process.execPath, [release, "--config", config, ...args], {
96
+ encoding: "utf8",
97
+ });
98
+ }
99
+
56
100
  test("rejects a release without an explicit entitlement tier", () => {
57
101
  const result = run(fixture());
58
102
  assert.notEqual(result.status, 0);
@@ -136,3 +180,12 @@ test("Windows update re-signs the updater artifact after Azure code signing", ()
136
180
  assert.ok(updaterAt > azureAt, result.stdout);
137
181
  assert.ok(publishAt > updaterAt, result.stdout);
138
182
  });
183
+
184
+ test("release lock is single-flight and is cleaned after success", () => {
185
+ const { dir, config } = lockFixture();
186
+ const lockPath = path.join(dir, ".cache", "right-release", "mac.lock.json");
187
+ const result = runRaw(config, "--platform", "mac", "--tier=patch");
188
+ assert.equal(result.status, 0, result.stderr);
189
+ assert.match(result.stdout, /right-release: done/);
190
+ assert.equal(existsSync(lockPath), false);
191
+ });
@@ -8,7 +8,7 @@ const workspace = path.resolve(new URL("../..", import.meta.url).pathname.replac
8
8
  const pubkey = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI5Mzk1RjlGRjQ2NjI2MUQKUldRZEptYjBuMTg1S1VSUXlBdFM4WmtzaHArYko0U2hRMDVlSDJmSExVZG82Q0hoQ2srUlhqanAK";
9
9
  const licensePackage = "^0.1.5";
10
10
  const logsPackage = "^0.1.3";
11
- const releasePackage = "^0.2.7";
11
+ const releasePackage = "^0.2.9";
12
12
  const apps = [
13
13
  { key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json", host: "viewright.cc" },
14
14
  { key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json", host: "scraperight.cc" },
package/sign-updater.mjs CHANGED
@@ -41,7 +41,17 @@ for (const file of files) {
41
41
  rmSync(`${file}.sig`, { force: true });
42
42
  console.log(`${dryRun ? "[dry-run] " : ""}pnpm exec tauri signer sign ${file}`);
43
43
  if (dryRun) continue;
44
- const result = spawnSync("pnpm", ["exec", "tauri", "signer", "sign", file], {
44
+ const result = process.platform === "win32"
45
+ ? spawnSync(
46
+ process.env.ComSpec || "cmd.exe",
47
+ ["/d", "/s", "/c", `pnpm exec tauri signer sign ${file}`],
48
+ {
49
+ cwd: process.cwd(),
50
+ env,
51
+ stdio: "inherit",
52
+ },
53
+ )
54
+ : spawnSync("pnpm", ["exec", "tauri", "signer", "sign", file], {
45
55
  cwd: process.cwd(),
46
56
  env,
47
57
  stdio: "inherit",