@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 +1 -1
- package/release.mjs +96 -7
- package/release.test.mjs +55 -2
- package/right-suite-contract.test.mjs +1 -1
- package/sign-updater.mjs +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
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(
|
|
352
|
+
process.kill(pid, "SIGKILL");
|
|
260
353
|
} catch {
|
|
261
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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",
|