@rightkit/release 0.2.34 → 0.2.36
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 +16 -7
- package/package.json +1 -1
- package/release-cli-contract.test.mjs +11 -0
- package/release-state.mjs +16 -1
- package/release-state.test.mjs +20 -0
- package/right-suite-contract.test.mjs +1 -1
- package/rightkit-versions.json +1 -1
- package/sign-windows.mjs +10 -5
- package/upload-release.mjs +2 -3
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, commandOutputPortable, 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(
|
|
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
|
-
|
|
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
|
|
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
|
|
286
|
-
|
|
287
|
-
child.once("
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
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": {
|
|
@@ -10,6 +10,7 @@ const root = path.dirname(fileURLToPath(import.meta.url));
|
|
|
10
10
|
const cli = path.join(root, "cli", "right-release.mjs");
|
|
11
11
|
const build = path.join(root, "build-release.mjs");
|
|
12
12
|
const upload = path.join(root, "upload-release.mjs");
|
|
13
|
+
const signWindows = path.join(root, "sign-windows.mjs");
|
|
13
14
|
|
|
14
15
|
test("CLI routes build and release only to the build state machine", () => {
|
|
15
16
|
const source = readFileSync(cli, "utf8");
|
|
@@ -41,3 +42,13 @@ test("upload requires an explicit patch or update tier before reading a release"
|
|
|
41
42
|
assert.notEqual(result.status, 0);
|
|
42
43
|
assert.match(result.stderr, /tier is required.*patch\|update/i);
|
|
43
44
|
});
|
|
45
|
+
|
|
46
|
+
test("Windows upload trust verification uses signtool instead of PowerShell modules", () => {
|
|
47
|
+
const uploadSource = readFileSync(upload, "utf8");
|
|
48
|
+
const signingSource = readFileSync(signWindows, "utf8");
|
|
49
|
+
assert.match(uploadSource, /SIGN_WINDOWS/);
|
|
50
|
+
assert.match(uploadSource, /--verify-only/);
|
|
51
|
+
assert.doesNotMatch(uploadSource, /Get-AuthenticodeSignature/);
|
|
52
|
+
assert.match(signingSource, /verifyOnly/);
|
|
53
|
+
assert.match(signingSource, /\["verify", "\/pa", "\/v", file\]/);
|
|
54
|
+
});
|
package/release-state.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, watch } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
export function cacheFingerprint({ cargoLockSha256, rustc, target, features = [] }) {
|
|
@@ -44,6 +44,21 @@ export function commandOutputPortable(cmd, args, { cwd, env = process.env } = {}
|
|
|
44
44
|
return result.stdout.trim();
|
|
45
45
|
}
|
|
46
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
|
+
|
|
47
62
|
export function releaseEnvironment({ root, platform, cacheKey, kind = "release" }) {
|
|
48
63
|
if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
|
|
49
64
|
const targetKind = kind === "release" ? "cargo-target" : "test-target";
|
package/release-state.test.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
@@ -8,6 +9,7 @@ import test from "node:test";
|
|
|
8
9
|
import {
|
|
9
10
|
cacheFingerprint,
|
|
10
11
|
commandOutputPortable,
|
|
12
|
+
watchProgress,
|
|
11
13
|
resolveReleaseLayout,
|
|
12
14
|
releaseEnvironment,
|
|
13
15
|
runBuildStateMachine,
|
|
@@ -15,6 +17,24 @@ import {
|
|
|
15
17
|
verifySealedRelease,
|
|
16
18
|
} from "./release-state.mjs";
|
|
17
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
|
+
|
|
18
38
|
test("portable command capture resolves Windows command shims", { skip: process.platform !== "win32" }, () => {
|
|
19
39
|
assert.match(commandOutputPortable("pnpm", ["--version"]), /^11\.12\.0$/);
|
|
20
40
|
});
|
|
@@ -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.
|
|
320
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.36");
|
|
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");
|
package/rightkit-versions.json
CHANGED
package/sign-windows.mjs
CHANGED
|
@@ -18,17 +18,24 @@ const userEnvCache = new Map();
|
|
|
18
18
|
|
|
19
19
|
const args = process.argv.slice(2);
|
|
20
20
|
const dryRun = args.includes("--dry-run");
|
|
21
|
-
const
|
|
21
|
+
const verifyOnly = args.includes("--verify-only");
|
|
22
|
+
const files = args.filter((arg) => arg !== "--dry-run" && arg !== "--verify-only").map((arg) => path.resolve(arg));
|
|
22
23
|
|
|
23
|
-
if (!files.length) fail("usage: node sign-windows.mjs [--dry-run] <file>...");
|
|
24
|
+
if (!files.length) fail("usage: node sign-windows.mjs [--dry-run|--verify-only] <file>...");
|
|
24
25
|
if (dryRun) {
|
|
25
|
-
console.log(`dry-run: Azure Artifact Signing ${files.join(", ")}`);
|
|
26
|
+
console.log(`dry-run: ${verifyOnly ? "Authenticode verification" : "Azure Artifact Signing"} ${files.join(", ")}`);
|
|
26
27
|
process.exit(0);
|
|
27
28
|
}
|
|
28
29
|
if (process.platform !== "win32") fail("Windows signing must run on Windows");
|
|
29
30
|
for (const file of files) if (!existsSync(file)) fail(`missing file: ${file}`);
|
|
30
31
|
|
|
32
|
+
const childEnv = signingEnv();
|
|
31
33
|
const signtool = findFirst([env("AZURE_SIGNTOOL_PATH"), env("SIGNTOOL_PATH"), ...signtoolCandidates()]);
|
|
34
|
+
if (!signtool) fail("signtool.exe not found; set AZURE_SIGNTOOL_PATH or install Windows SDK Build Tools");
|
|
35
|
+
if (verifyOnly) {
|
|
36
|
+
for (const file of files) run(signtool, ["verify", "/pa", "/v", file]);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
32
39
|
const dlib = findFirst([
|
|
33
40
|
env("AZURE_CODESIGN_DLIB_PATH"),
|
|
34
41
|
env("AZURE_ARTIFACT_SIGNING_DLIB_PATH"),
|
|
@@ -36,12 +43,10 @@ const dlib = findFirst([
|
|
|
36
43
|
"C:\\Program Files\\Microsoft Azure Artifact Signing Client Tools\\x64\\Azure.CodeSigning.Dlib.dll",
|
|
37
44
|
"C:\\Program Files (x86)\\Microsoft\\ArtifactSigningClientTools\\bin\\x64\\Azure.CodeSigning.Dlib.dll",
|
|
38
45
|
]);
|
|
39
|
-
if (!signtool) fail("signtool.exe not found; set AZURE_SIGNTOOL_PATH or install Windows SDK Build Tools");
|
|
40
46
|
if (!dlib) fail("Azure.CodeSigning.Dlib.dll not found; set AZURE_CODESIGN_DLIB_PATH or install Azure Artifact Signing Client Tools");
|
|
41
47
|
|
|
42
48
|
const tempDir = mkdtempSync(path.join(os.tmpdir(), "right-sign-"));
|
|
43
49
|
const metadata = metadataPath(tempDir);
|
|
44
|
-
const childEnv = signingEnv();
|
|
45
50
|
try {
|
|
46
51
|
for (const file of files) {
|
|
47
52
|
run(signtool, ["sign", "/v", "/fd", "SHA256", "/tr", "http://timestamp.acs.microsoft.com", "/td", "SHA256", "/dlib", dlib, "/dmdf", metadata, file]);
|
package/upload-release.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { runUploadStateMachine, verifySealedRelease } from "./release-state.mjs"
|
|
|
9
9
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const PUBLISH_UPDATE = path.join(TOOL_ROOT, "publish-update.mjs");
|
|
11
11
|
const HARDENING = path.join(TOOL_ROOT, "hardeningscan.mjs");
|
|
12
|
+
const SIGN_WINDOWS = path.join(TOOL_ROOT, "sign-windows.mjs");
|
|
12
13
|
const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
|
|
13
14
|
const PUBLIC_BASE = (process.env.RIGHTAPPS_PUBLIC_DOWNLOAD_BASE || "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev").replace(/\/$/, "");
|
|
14
15
|
const API_BASE = (process.env.RIGHTAPPS_API_URL || "https://api.spoares.com").replace(/\/$/, "");
|
|
@@ -73,9 +74,7 @@ function verifyPlatformTrust(release) {
|
|
|
73
74
|
if (platform === "win") {
|
|
74
75
|
for (const artifact of artifacts) {
|
|
75
76
|
const file = path.join(release.sealedDir, artifact.name);
|
|
76
|
-
|
|
77
|
-
const status = commandOutput("powershell", ["-NoProfile", "-Command", `(Get-AuthenticodeSignature -LiteralPath '${escaped}').Status`]);
|
|
78
|
-
if (status.trim() !== "Valid") throw new Error(`Authenticode verification failed for ${artifact.name}: ${status}`);
|
|
77
|
+
runChecked(process.execPath, [SIGN_WINDOWS, "--verify-only", file], repoRoot);
|
|
79
78
|
}
|
|
80
79
|
} else {
|
|
81
80
|
for (const artifact of artifacts.filter((file) => /\.dmg$/i.test(file.name))) {
|