@rightkit/release 0.2.67 → 0.2.69
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 +5 -3
- package/build-release.test.mjs +11 -0
- package/cargo-target.mjs +3 -2
- package/cargo-target.test.mjs +42 -93
- package/cli/right-release.mjs +0 -0
- package/package.json +10 -8
- package/pipeline-normalization-contract.test.mjs +140 -0
- package/progress-control.mjs +11 -4
- package/progress-control.test.mjs +17 -0
- package/registry-parity.test.mjs +1 -1
- package/release.mjs +49 -15
- package/release.test.mjs +65 -2
- package/right-suite-contract.test.mjs +115 -20
- package/rightkit-versions.json +25 -9
- package/standalone-clone-evidence.json +1 -1
- package/standalone-clone-verify.test.mjs +1 -1
package/build-release.mjs
CHANGED
|
@@ -322,6 +322,7 @@ try {
|
|
|
322
322
|
env,
|
|
323
323
|
[managedCargoTarget ?? env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
|
|
324
324
|
path.join(stateRoot, "stall-build.log"),
|
|
325
|
+
{ livenessOwnerTimeoutMs: target.package.livenessOwnerTimeoutMs ?? null },
|
|
325
326
|
);
|
|
326
327
|
checkpoint(stateRoot, "build_complete");
|
|
327
328
|
checkpoint(stateRoot, "signed");
|
|
@@ -464,8 +465,8 @@ function collectToolVersions(packageManager) {
|
|
|
464
465
|
}
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = null) {
|
|
468
|
-
const { inactivityMs, absoluteMs } = releaseProgressLimits(
|
|
468
|
+
async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = null, { livenessOwnerTimeoutMs = null } = {}) {
|
|
469
|
+
const { inactivityMs, absoluteMs } = releaseProgressLimits(env, { livenessOwnerTimeoutMs });
|
|
469
470
|
let lastProgress = Date.now();
|
|
470
471
|
const watchDirs = Array.isArray(watchDir) ? watchDir : [watchDir];
|
|
471
472
|
let lastMtime = Math.max(...watchDirs.map(newestMtime));
|
|
@@ -476,7 +477,8 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
|
|
|
476
477
|
let managedRequestId = null;
|
|
477
478
|
const settleResolve = () => { if (!settled) { settled = true; resolve(); } };
|
|
478
479
|
const settleReject = (error) => { if (!settled) { settled = true; reject(error); } };
|
|
479
|
-
|
|
480
|
+
const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
|
|
481
|
+
child = spawn(cmd, runArgs, { cwd, env, stdio: ["inherit", "pipe", "pipe"], windowsHide: true, shell: useShell, detached: process.platform !== "win32" });
|
|
480
482
|
try {
|
|
481
483
|
if (child.pid && heavySlot) heavySlot.trackChild(child.pid, cmd);
|
|
482
484
|
} catch (error) {
|
package/build-release.test.mjs
CHANGED
|
@@ -54,6 +54,12 @@ test("production builds package from the real primary app checkout", () => {
|
|
|
54
54
|
assert.match(source, /sealRelease\(\{ configRoot: appRoot,/);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
test("Windows native executables preserve arguments containing spaces", () => {
|
|
58
|
+
assert.match(source, /const useShell = process\.platform === "win32" && !\/\\\.\(\?:exe\|com\)\$\/i\.test\(cmd\);/);
|
|
59
|
+
assert.match(source, /shell: useShell/);
|
|
60
|
+
assert.doesNotMatch(source, /shell: process\.platform === "win32"/);
|
|
61
|
+
});
|
|
62
|
+
|
|
57
63
|
test("sealing resolves installer and updater artifacts against the broker target on a managed host", () => {
|
|
58
64
|
assert.match(source, /sealRelease\(\{ configRoot: appRoot, managedCargoTarget,/);
|
|
59
65
|
assert.match(source, /function sealRelease\(\{ configRoot, managedCargoTarget,/);
|
|
@@ -157,6 +163,11 @@ test("CPU sampling degrades to the old behaviour rather than inventing a verdict
|
|
|
157
163
|
assert.equal(processGroupCpuSeconds(123, { platform: "darwin", run: ok("") }), null);
|
|
158
164
|
});
|
|
159
165
|
|
|
166
|
+
test("package liveness owner timeout floors outer watchdog through shared release limits", () => {
|
|
167
|
+
assert.match(source, /livenessOwnerTimeoutMs: target\.package\.livenessOwnerTimeoutMs \?\? null/);
|
|
168
|
+
assert.match(source, /releaseProgressLimits\(env, \{ livenessOwnerTimeoutMs \}\)/);
|
|
169
|
+
});
|
|
170
|
+
|
|
160
171
|
// Regression: the managed-host gate must not depend on RIGHTKIT_BUILD_BROKER_SOCKET
|
|
161
172
|
// alone. That variable is exported by login shells only, while the managed cargo
|
|
162
173
|
// shim is on PATH in every shell — so an agent shell took the Cache V2 branch,
|
package/cargo-target.mjs
CHANGED
|
@@ -13,11 +13,12 @@ import { execFileSync } from "node:child_process";
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
16
|
-
export function resolveTargetRoot(manifestPath) {
|
|
16
|
+
export function resolveTargetRoot(manifestPath, options = {}) {
|
|
17
17
|
if (!manifestPath) throw new Error("resolveTargetRoot requires a Cargo.toml manifest path");
|
|
18
|
+
const execute = options.execFileSync ?? execFileSync;
|
|
18
19
|
let output;
|
|
19
20
|
try {
|
|
20
|
-
output =
|
|
21
|
+
output = execute(
|
|
21
22
|
"cargo",
|
|
22
23
|
["metadata", "--offline", "--format-version", "1", "--no-deps", "--manifest-path", manifestPath],
|
|
23
24
|
{ cwd: path.dirname(manifestPath), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
|
package/cargo-target.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, mkdirSync, 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";
|
|
@@ -11,91 +11,42 @@ import { resolveTargetRoot } from "./cargo-target.mjs";
|
|
|
11
11
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const cli = path.join(here, "cargo-target.mjs");
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
// shapes are deterministic and don't depend on the host's actual crate graph.
|
|
16
|
-
// It branches on a marker embedded in the manifest path so each test controls
|
|
17
|
-
// its own outcome independently.
|
|
18
|
-
function fakeCargoBin() {
|
|
14
|
+
function fakeCargo() {
|
|
19
15
|
const bin = mkdtempSync(path.join(os.tmpdir(), "rightkit-fake-cargo-"));
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
].join("\r\n")
|
|
30
|
-
: [
|
|
31
|
-
"#!/bin/sh",
|
|
32
|
-
'case "$*" in',
|
|
33
|
-
' *fail-manifest*) echo boom 1>&2; exit 1 ;;',
|
|
34
|
-
' *bad-target-manifest*) echo \'{"target_directory":"relative/target"}\'; exit 0 ;;',
|
|
35
|
-
` *) echo '{"target_directory":"${path.join(bin, "target")}"}'; exit 0 ;;`,
|
|
36
|
-
"esac",
|
|
37
|
-
].join("\n");
|
|
38
|
-
writeFileSync(script, body);
|
|
39
|
-
if (process.platform !== "win32") chmodSync(script, 0o755);
|
|
40
|
-
return bin;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function withFakeCargo(fn) {
|
|
44
|
-
const bin = fakeCargoBin();
|
|
45
|
-
const env = { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}` };
|
|
46
|
-
return fn({ bin, env });
|
|
16
|
+
return {
|
|
17
|
+
bin,
|
|
18
|
+
execFileSync(_command, args) {
|
|
19
|
+
const manifest = args.at(-1);
|
|
20
|
+
if (manifest.includes("fail-manifest")) throw new Error("boom");
|
|
21
|
+
if (manifest.includes("bad-target-manifest")) return '{"target_directory":"relative/target"}';
|
|
22
|
+
return JSON.stringify({ target_directory: path.join(bin, "target") });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
47
25
|
}
|
|
48
26
|
|
|
49
27
|
test("resolveTargetRoot returns the absolute target_directory Cargo metadata reports", () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const previousPath = process.env.PATH;
|
|
55
|
-
process.env.PATH = env.PATH;
|
|
56
|
-
try {
|
|
57
|
-
const result = resolveTargetRoot(manifest);
|
|
58
|
-
assert.equal(result, path.resolve(path.join(bin, "target")));
|
|
59
|
-
} finally {
|
|
60
|
-
process.env.PATH = previousPath;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
28
|
+
const { bin, execFileSync } = fakeCargo();
|
|
29
|
+
const manifest = path.join(mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-")), "Cargo.toml");
|
|
30
|
+
const result = resolveTargetRoot(manifest, { execFileSync });
|
|
31
|
+
assert.equal(result, path.resolve(path.join(bin, "target")));
|
|
63
32
|
});
|
|
64
33
|
|
|
65
34
|
test("resolveTargetRoot fails closed, naming the manifest, when target_directory is not absolute", () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
assert.throws(
|
|
74
|
-
() => resolveTargetRoot(manifest),
|
|
75
|
-
new RegExp(`metadata for .*bad-target-manifest-Cargo\\.toml did not report an absolute target_directory`),
|
|
76
|
-
);
|
|
77
|
-
} finally {
|
|
78
|
-
process.env.PATH = previousPath;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
35
|
+
const { execFileSync } = fakeCargo();
|
|
36
|
+
const manifest = path.join(mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-")), "bad-target-manifest-Cargo.toml");
|
|
37
|
+
assert.throws(
|
|
38
|
+
() => resolveTargetRoot(manifest, { execFileSync }),
|
|
39
|
+
new RegExp(`metadata for .*bad-target-manifest-Cargo\\.toml did not report an absolute target_directory`),
|
|
40
|
+
);
|
|
81
41
|
});
|
|
82
42
|
|
|
83
43
|
test("resolveTargetRoot fails closed, naming the manifest, when the metadata command exits non-zero", () => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
assert.throws(
|
|
92
|
-
() => resolveTargetRoot(manifest),
|
|
93
|
-
new RegExp(`metadata failed for .*fail-manifest-Cargo\\.toml`),
|
|
94
|
-
);
|
|
95
|
-
} finally {
|
|
96
|
-
process.env.PATH = previousPath;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
44
|
+
const { execFileSync } = fakeCargo();
|
|
45
|
+
const manifest = path.join(mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-")), "fail-manifest-Cargo.toml");
|
|
46
|
+
assert.throws(
|
|
47
|
+
() => resolveTargetRoot(manifest, { execFileSync }),
|
|
48
|
+
new RegExp(`metadata failed for .*fail-manifest-Cargo\\.toml`),
|
|
49
|
+
);
|
|
99
50
|
});
|
|
100
51
|
|
|
101
52
|
test("resolveTargetRoot throws when no manifest path is given", () => {
|
|
@@ -104,26 +55,24 @@ test("resolveTargetRoot throws when no manifest path is given", () => {
|
|
|
104
55
|
});
|
|
105
56
|
|
|
106
57
|
test("CLI mode writes only the resolved path to stdout on success", () => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
58
|
+
const manifestDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-"));
|
|
59
|
+
const manifest = path.join(manifestDir, "Cargo.toml");
|
|
60
|
+
mkdirSync(path.join(manifestDir, "src"));
|
|
61
|
+
writeFileSync(path.join(manifestDir, "src", "lib.rs"), "");
|
|
62
|
+
writeFileSync(manifest, "[package]\nname = \"fixture\"\nversion = \"0.0.0\"\n");
|
|
63
|
+
const result = spawnSync(process.execPath, [cli, manifest], { encoding: "utf8" });
|
|
64
|
+
assert.equal(result.status, 0, result.stderr);
|
|
65
|
+
assert.equal(path.isAbsolute(result.stdout.trim()), true);
|
|
115
66
|
});
|
|
116
67
|
|
|
117
68
|
test("CLI mode writes a message to stderr and exits 1 on failure", () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
assert.match(result.stderr, /metadata failed for .*fail-manifest-Cargo\.toml/);
|
|
126
|
-
});
|
|
69
|
+
const manifestDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-"));
|
|
70
|
+
const manifest = path.join(manifestDir, "fail-manifest-Cargo.toml");
|
|
71
|
+
writeFileSync(manifest, "not valid toml");
|
|
72
|
+
const result = spawnSync(process.execPath, [cli, manifest], { encoding: "utf8" });
|
|
73
|
+
assert.equal(result.status, 1);
|
|
74
|
+
assert.equal(result.stdout, "");
|
|
75
|
+
assert.match(result.stderr, /metadata failed for .*fail-manifest-Cargo\.toml/);
|
|
127
76
|
});
|
|
128
77
|
|
|
129
78
|
test("CLI mode requires a manifest argument", () => {
|
package/cli/right-release.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.69",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: native-host signed installers, updater artifacts, hardening, immutable GitHub Release upload, and add-on adoption.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
"*.py"
|
|
16
16
|
],
|
|
17
17
|
"sideEffects": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
20
|
+
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
21
|
+
"prepublishOnly": "pnpm test && pnpm test:registry-parity",
|
|
22
|
+
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
23
|
+
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
24
|
+
},
|
|
18
25
|
"publishConfig": {
|
|
19
26
|
"registry": "https://registry.npmjs.org/",
|
|
20
27
|
"access": "public"
|
|
@@ -24,10 +31,5 @@
|
|
|
24
31
|
"url": "git+https://github.com/bogusyogi/claude.git",
|
|
25
32
|
"directory": "tools/rightkit/packages/release"
|
|
26
33
|
},
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
30
|
-
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
31
|
-
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
+
"packageManager": "pnpm@11.24.0"
|
|
35
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
const workspace = path.resolve(
|
|
8
|
+
process.env.RIGHT_SUITE_CONTRACT_WORKSPACE
|
|
9
|
+
?? new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const packageRoots = [
|
|
13
|
+
"coderight/apps/coderight-tauri",
|
|
14
|
+
"cutright/apps/studio",
|
|
15
|
+
"genright",
|
|
16
|
+
"heardright/tauri-app-next",
|
|
17
|
+
"mailright",
|
|
18
|
+
"membrane/apps/membrane-hub",
|
|
19
|
+
"scraperight",
|
|
20
|
+
"viewright",
|
|
21
|
+
"voiceright",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const releaseConfigRoots = [
|
|
25
|
+
...packageRoots,
|
|
26
|
+
"tools/screenright",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const rightGitRepos = ["cutright", "legion", "membrane"];
|
|
30
|
+
const privateRepos = ["rightsites", "sellright", "workright"];
|
|
31
|
+
|
|
32
|
+
function executableSource(source) {
|
|
33
|
+
return source
|
|
34
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
35
|
+
.split("\n")
|
|
36
|
+
.filter((line) => !/^\s*(?:\/\/|#)/.test(line))
|
|
37
|
+
.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pipelineScripts(root) {
|
|
41
|
+
const repo = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8", windowsHide: true }).trim();
|
|
42
|
+
const prefix = path.relative(repo, root).replaceAll(path.sep, "/");
|
|
43
|
+
const tracked = execFileSync("git", ["-C", repo, "ls-files", "-z"], { encoding: "utf8", windowsHide: true })
|
|
44
|
+
.split("\0")
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
return tracked
|
|
47
|
+
.filter((entry) => {
|
|
48
|
+
const local = prefix ? entry.slice(prefix.length + 1) : entry;
|
|
49
|
+
if (prefix && !entry.startsWith(`${prefix}/`)) return false;
|
|
50
|
+
return (
|
|
51
|
+
local.startsWith("scripts/")
|
|
52
|
+
|| local === "package.ps1"
|
|
53
|
+
|| local === "package.sh"
|
|
54
|
+
) && /\.(?:mjs|cjs|js|ts|ps1|sh|py)$/i.test(local)
|
|
55
|
+
&& !/(?:^|[.-])test(?:[.-]|$)/i.test(path.basename(local));
|
|
56
|
+
})
|
|
57
|
+
.map((entry) => path.join(repo, entry))
|
|
58
|
+
.filter((entry) => existsSync(entry));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("product package pipelines use installed RightKit commands", () => {
|
|
62
|
+
const violations = [];
|
|
63
|
+
for (const root of packageRoots) {
|
|
64
|
+
const manifestPath = path.join(workspace, root, "package.json");
|
|
65
|
+
if (!existsSync(manifestPath)) continue;
|
|
66
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
67
|
+
for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
|
|
68
|
+
if (!/^(?:build|package|publish|release|deps|verify:rust|test:rust|lint:rust)/.test(name)) continue;
|
|
69
|
+
if (/(?:\.\.\/[\w./-]*)?tools\/right-release|tools\\right-release/i.test(command)) violations.push(`${root} ${name}: retired workspace-local right-release source`);
|
|
70
|
+
if (/(?:^|&&|\|\||;)\s*cargo\s+/i.test(command)) violations.push(`${root} ${name}: bare cargo`);
|
|
71
|
+
if (/(?:^|&&|\|\||;)\s*npx\s+/i.test(command)) violations.push(`${root} ${name}: npx`);
|
|
72
|
+
if (/(?:^|&&|\|\||;)\s*npm\s+(?:ci|install)\b/i.test(command)) violations.push(`${root} ${name}: npm install`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
assert.deepEqual(violations, []);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("release configs resolve Cargo output through RightKit", () => {
|
|
79
|
+
const violations = [];
|
|
80
|
+
for (const root of releaseConfigRoots) {
|
|
81
|
+
const configPath = path.join(workspace, root, "right-release.config.mjs");
|
|
82
|
+
if (!existsSync(configPath)) continue;
|
|
83
|
+
const source = executableSource(readFileSync(configPath, "utf8"));
|
|
84
|
+
const hardcodedTargetRoot = /(?:const|let|var)\s+\w+\s*=\s*["'`]src-tauri[\\/]target/i.test(source);
|
|
85
|
+
const hardcodedTargetArtifact = /(?:file|signature)\s*:\s*["'`]src-tauri[\\/]target/i.test(source);
|
|
86
|
+
if (!hardcodedTargetRoot && !hardcodedTargetArtifact) continue;
|
|
87
|
+
if (!/@rightkit\/release\/cargo-target\.mjs|\.\/scripts\/lib\/target-root\.mjs/.test(source)) violations.push(`${root}: no RightKit target resolver`);
|
|
88
|
+
if (!/resolve(?:TargetRoot|WindowsReleaseDir)\s*\(/.test(source)) violations.push(`${root}: resolver not invoked`);
|
|
89
|
+
if (hardcodedTargetRoot) violations.push(`${root}: hardcoded target root`);
|
|
90
|
+
if (hardcodedTargetArtifact) violations.push(`${root}: hardcoded target artifact`);
|
|
91
|
+
}
|
|
92
|
+
assert.deepEqual(violations, []);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("product pipeline children use managed Rust commands", () => {
|
|
96
|
+
const violations = [];
|
|
97
|
+
for (const root of packageRoots) {
|
|
98
|
+
for (const file of pipelineScripts(path.join(workspace, root))) {
|
|
99
|
+
const source = executableSource(readFileSync(file, "utf8"));
|
|
100
|
+
const label = path.relative(workspace, file);
|
|
101
|
+
if (/(?:spawnSync|spawn|execFileSync|execFile)\s*\(\s*["'`](?:cargo|rustc|rustdoc)["'`]/.test(source)) violations.push(`${label}: unmanaged JS child`);
|
|
102
|
+
if (/subprocess\.(?:run|Popen)\s*\(\s*\[\s*["'](?:cargo|rustc|rustdoc)["']/.test(source)) violations.push(`${label}: unmanaged Python child`);
|
|
103
|
+
if (/(?:^|\n)\s*(?:&\s*)?(?:cargo|rustc|rustdoc)\s+(?:build|check|clippy|metadata|test|-vV)\b/m.test(source)) violations.push(`${label}: unmanaged shell command`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
assert.deepEqual(violations, []);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("hosted workflows are generated by right-git", () => {
|
|
110
|
+
const violations = [];
|
|
111
|
+
for (const root of rightGitRepos) {
|
|
112
|
+
const workflowRoot = path.join(workspace, root, ".github", "workflows");
|
|
113
|
+
if (!existsSync(workflowRoot)) continue;
|
|
114
|
+
for (const entry of readdirSync(workflowRoot, { withFileTypes: true })) {
|
|
115
|
+
if (!entry.isFile() || !/\.ya?ml$/i.test(entry.name)) continue;
|
|
116
|
+
const source = readFileSync(path.join(workflowRoot, entry.name), "utf8");
|
|
117
|
+
if (!/^# Managed by right-git\b/.test(source)) violations.push(`${root}/.github/workflows/${entry.name}: not right-git managed`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const root of privateRepos) {
|
|
121
|
+
const workflowRoot = path.join(workspace, root, ".github", "workflows");
|
|
122
|
+
const workflows = existsSync(workflowRoot)
|
|
123
|
+
? readdirSync(workflowRoot).filter((entry) => /\.ya?ml$/i.test(entry))
|
|
124
|
+
: [];
|
|
125
|
+
if (workflows.length) violations.push(`${root}: private repo has ${workflows.join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
assert.deepEqual(violations, []);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("workspace memory pipelines route Rust through RightKit", () => {
|
|
131
|
+
for (const relative of [
|
|
132
|
+
"tools/pipelines/memory/dispatch/gate.py",
|
|
133
|
+
"tools/pipelines/memory/install-cortex.py",
|
|
134
|
+
"tools/pipelines/memory/verify-source-owner.py",
|
|
135
|
+
]) {
|
|
136
|
+
const source = executableSource(readFileSync(path.join(workspace, relative), "utf8"));
|
|
137
|
+
assert.doesNotMatch(source, /(?:^|&&|\|\||;)\s*cargo\s+(?:metadata|build|check|clippy|test)\b/m, `${relative} invokes bare Cargo shell command`);
|
|
138
|
+
assert.doesNotMatch(source, /subprocess\.run\(\s*\[\s*["']cargo["']/m, `${relative} invokes bare Cargo subprocess`);
|
|
139
|
+
}
|
|
140
|
+
});
|
package/progress-control.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
const MANAGED_REQUEST_PATTERN = /\brightkit(?: managed-agent)?: request ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/gi;
|
|
4
|
+
export const RELEASE_LIVENESS_OWNER_MARGIN_MS = 5 * 60 * 1000;
|
|
4
5
|
|
|
5
6
|
export function latestManagedRequestId(value, fallback = null) {
|
|
6
7
|
let latest = fallback;
|
|
@@ -13,11 +14,17 @@ export function firstManagedRequestId(value) {
|
|
|
13
14
|
return MANAGED_REQUEST_PATTERN.exec(String(value ?? ""))?.[1]?.toLowerCase() ?? null;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function releaseProgressLimits(env = process.env) {
|
|
17
|
-
const
|
|
17
|
+
export function releaseProgressLimits(env = process.env, { livenessOwnerTimeoutMs = null } = {}) {
|
|
18
|
+
const configuredInactivityMs = Number(env.RIGHT_RELEASE_STALL_MS ?? 10 * 60 * 1000);
|
|
18
19
|
const absoluteMs = Number(env.RIGHT_RELEASE_ABSOLUTE_MS ?? 4 * 60 * 60 * 1000);
|
|
19
|
-
|
|
20
|
-
if (absoluteMs
|
|
20
|
+
const ownerTimeoutMs = livenessOwnerTimeoutMs === null ? null : Number(livenessOwnerTimeoutMs);
|
|
21
|
+
if (![configuredInactivityMs, absoluteMs, ...(ownerTimeoutMs === null ? [] : [ownerTimeoutMs])].every((value) => Number.isFinite(value) && value > 0)) {
|
|
22
|
+
throw new Error("release progress limits must be positive finite milliseconds");
|
|
23
|
+
}
|
|
24
|
+
const inactivityMs = ownerTimeoutMs === null
|
|
25
|
+
? configuredInactivityMs
|
|
26
|
+
: Math.max(configuredInactivityMs, ownerTimeoutMs + RELEASE_LIVENESS_OWNER_MARGIN_MS);
|
|
27
|
+
if (absoluteMs <= inactivityMs) throw new Error("RIGHT_RELEASE_ABSOLUTE_MS must exceed effective release inactivity limit");
|
|
21
28
|
return Object.freeze({ inactivityMs, absoluteMs });
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -10,6 +10,23 @@ test("release progress stays bounded without killing a healthy long native build
|
|
|
10
10
|
assert.throws(() => releaseProgressLimits({ RIGHT_RELEASE_STALL_MS: "nope" }), /positive finite/);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
test("declared inner liveness owner always exits before the outer watchdog", () => {
|
|
14
|
+
const ownerTimeoutMs = 45 * 60 * 1000;
|
|
15
|
+
assert.deepEqual(
|
|
16
|
+
releaseProgressLimits({}, { livenessOwnerTimeoutMs: ownerTimeoutMs }),
|
|
17
|
+
{ inactivityMs: 50 * 60 * 1000, absoluteMs: 4 * 60 * 60 * 1000 },
|
|
18
|
+
);
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
releaseProgressLimits({ RIGHT_RELEASE_STALL_MS: String(60 * 60 * 1000) }, { livenessOwnerTimeoutMs: ownerTimeoutMs }),
|
|
21
|
+
{ inactivityMs: 60 * 60 * 1000, absoluteMs: 4 * 60 * 60 * 1000 },
|
|
22
|
+
);
|
|
23
|
+
assert.throws(
|
|
24
|
+
() => releaseProgressLimits({ RIGHT_RELEASE_ABSOLUTE_MS: String(49 * 60 * 1000) }, { livenessOwnerTimeoutMs: ownerTimeoutMs }),
|
|
25
|
+
/must exceed effective release inactivity limit/,
|
|
26
|
+
);
|
|
27
|
+
assert.throws(() => releaseProgressLimits({}, { livenessOwnerTimeoutMs: 0 }), /positive finite/);
|
|
28
|
+
});
|
|
29
|
+
|
|
13
30
|
test("latest managed request id follows streamed root Cargo requests", () => {
|
|
14
31
|
const first = "11111111-1111-4111-8111-111111111111";
|
|
15
32
|
const second = "22222222-2222-4222-8222-222222222222";
|
package/registry-parity.test.mjs
CHANGED
|
@@ -21,7 +21,7 @@ test('registry parity distinguishes an unpublished version from registry failure
|
|
|
21
21
|
assert.deepEqual(await verifyRegistryParity({ allowUnpublished: true, fetchImpl: notFound }), {
|
|
22
22
|
status: 'UNPUBLISHED',
|
|
23
23
|
name: '@rightkit/release',
|
|
24
|
-
version: '0.2.
|
|
24
|
+
version: '0.2.69',
|
|
25
25
|
});
|
|
26
26
|
await assert.rejects(
|
|
27
27
|
verifyRegistryParity({ fetchImpl: notFound }),
|
package/release.mjs
CHANGED
|
@@ -29,7 +29,8 @@ const RIGHTKIT_ALLOWED = buildAllowedVersions(
|
|
|
29
29
|
);
|
|
30
30
|
const RIGHTKIT_CARGO_ALLOWED = buildAllowedVersions(RIGHTKIT_VERSIONS.cargo, RIGHTKIT_VERSIONS.stagedCargo);
|
|
31
31
|
const FORBIDDEN_RIGHTKIT_SPEC = /^(?:git|file|link|workspace):|github|github\.com/i;
|
|
32
|
-
const
|
|
32
|
+
const WINDOWS_NSIS_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-nsis-v1";
|
|
33
|
+
const WINDOWS_PORTABLE_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-portable-v1";
|
|
33
34
|
const DEFAULT_NOTARIZATION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
34
35
|
|
|
35
36
|
const args = process.argv.slice(2);
|
|
@@ -91,7 +92,7 @@ if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<m
|
|
|
91
92
|
|
|
92
93
|
const root = path.dirname(configPath);
|
|
93
94
|
const workdir = path.resolve(root, config.workdir ?? ".");
|
|
94
|
-
await validateRightKitPackageContract(root, config.app);
|
|
95
|
+
await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
|
|
95
96
|
validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
|
|
96
97
|
const target = config.targets?.[opts.platform];
|
|
97
98
|
if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
|
|
@@ -101,14 +102,16 @@ const legalContract = config.legal
|
|
|
101
102
|
if (target.signed !== true) {
|
|
102
103
|
fail(`${config.app ?? "app"} ${opts.platform} must declare signed: true; unsigned release targets are forbidden`);
|
|
103
104
|
}
|
|
104
|
-
if (opts.platform === "win" && !target.sign?.files?.length) {
|
|
105
|
-
fail(`${config.app ?? "app"} win must declare sign.files for the signed release pipeline`);
|
|
106
|
-
}
|
|
107
105
|
if (opts.platform === "win") {
|
|
108
|
-
|
|
106
|
+
const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
|
|
107
|
+
const isPortable = target.signingContract === WINDOWS_PORTABLE_SIGNING_CONTRACT;
|
|
108
|
+
if (!isNsis && !isPortable) fail(`${config.app ?? "app"} win must declare signingContract: ${WINDOWS_NSIS_SIGNING_CONTRACT} or ${WINDOWS_PORTABLE_SIGNING_CONTRACT}`);
|
|
109
109
|
if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} win must declare prePackage for the raw EXE build`);
|
|
110
|
-
if (target.sign?.prePackageFiles?.length
|
|
111
|
-
if (target.sign.prePackageFiles.
|
|
110
|
+
if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} win must declare at least one sign.prePackageFiles raw EXE`);
|
|
111
|
+
if (isNsis && target.sign.prePackageFiles.length !== 1) fail(`${config.app ?? "app"} win NSIS must declare exactly one sign.prePackageFiles raw EXE`);
|
|
112
|
+
if (isNsis && !target.sign?.files?.length) fail(`${config.app ?? "app"} win NSIS must declare sign.files for installer signing`);
|
|
113
|
+
if (isNsis && target.sign.prePackageFiles.some((file) => target.sign.files.includes(file))) fail(`${config.app ?? "app"} win raw EXE and installer signing files must be distinct`);
|
|
114
|
+
if (isPortable && target.sign?.files?.length) fail(`${config.app ?? "app"} win portable release must sign every executable through sign.prePackageFiles and omit sign.files`);
|
|
112
115
|
}
|
|
113
116
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
114
117
|
/**
|
|
@@ -206,7 +209,8 @@ if (opts.platform === "win") {
|
|
|
206
209
|
// bundling, and doing that to signed bytes invalidates Authenticode — the exact
|
|
207
210
|
// defect that shipped invalid binaries. Idempotent, so an app that still patches
|
|
208
211
|
// in its own prePackage script simply reports alreadyPatched here.
|
|
209
|
-
|
|
212
|
+
const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
|
|
213
|
+
bundleMarkerReceipts = !isNsis || opts.dryRun ? [] : rawFiles.map((file) => patchTauriBundleType(file, { bundle: target.bundleMarker ?? "nsis" }));
|
|
210
214
|
for (const receipt of bundleMarkerReceipts) {
|
|
211
215
|
console.log(`right-release: bundle marker ${receipt.bundle} ${receipt.alreadyPatched ? "already applied" : "applied"} at offset ${receipt.offset} in ${path.basename(receipt.file)}`);
|
|
212
216
|
}
|
|
@@ -219,7 +223,7 @@ for (const rel of target.artifacts ?? []) {
|
|
|
219
223
|
await mustExist(path.resolve(root, rel), `missing release artifact: ${rel}`);
|
|
220
224
|
}
|
|
221
225
|
|
|
222
|
-
if (opts.platform === "win" && target.
|
|
226
|
+
if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT) {
|
|
223
227
|
const files = target.sign.files.map((p) => path.resolve(root, p));
|
|
224
228
|
for (const file of files) await mustExist(file, `missing signing artifact: ${file}`);
|
|
225
229
|
// Before the installer earns its own signature, prove it carries the bytes we
|
|
@@ -360,16 +364,16 @@ function commandTimeoutMs(command) {
|
|
|
360
364
|
: undefined;
|
|
361
365
|
}
|
|
362
366
|
|
|
363
|
-
async function validateRightKitPackageContract(root, appName) {
|
|
367
|
+
async function validateRightKitPackageContract(root, appName, hostedWorkflows) {
|
|
364
368
|
const packageJsonPath = path.join(root, "package.json");
|
|
365
369
|
const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
366
370
|
const scripts = pkg.scripts ?? {};
|
|
367
371
|
assertQaBackdoorContract(root, scripts);
|
|
368
372
|
const workflowDir = path.join(root, ".github", "workflows");
|
|
369
373
|
if (existsSync(workflowDir)) {
|
|
370
|
-
const workflows = await readdir(workflowDir);
|
|
374
|
+
const workflows = (await readdir(workflowDir)).sort();
|
|
371
375
|
if (workflows.length) {
|
|
372
|
-
|
|
376
|
+
await validateHostedWorkflows(root, appName ?? pkg.name ?? "app", workflows, hostedWorkflows);
|
|
373
377
|
}
|
|
374
378
|
}
|
|
375
379
|
if (pkg.packageManager !== RIGHTKIT_VERSIONS.packageManager) {
|
|
@@ -394,6 +398,33 @@ async function validateRightKitPackageContract(root, appName) {
|
|
|
394
398
|
}
|
|
395
399
|
}
|
|
396
400
|
|
|
401
|
+
async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
402
|
+
if (policy !== "right-git-ci-only") {
|
|
403
|
+
fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
|
|
404
|
+
}
|
|
405
|
+
if (workflows.length !== 1 || workflows[0] !== "ci.yml") {
|
|
406
|
+
fail(`${appName} right-git-ci-only policy permits only ci.yml, got: ${workflows.join(", ")}`);
|
|
407
|
+
}
|
|
408
|
+
const manifestPath = path.join(root, ".rightgit.json");
|
|
409
|
+
let manifest;
|
|
410
|
+
try {
|
|
411
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
412
|
+
} catch {
|
|
413
|
+
fail(`${appName} right-git-ci-only policy requires a valid .rightgit.json`);
|
|
414
|
+
}
|
|
415
|
+
if (manifest?.schemaVersion !== 1 || JSON.stringify(manifest?.lanes) !== JSON.stringify(["ci"])) {
|
|
416
|
+
fail(`${appName} right-git-ci-only policy requires .rightgit.json lanes=["ci"]`);
|
|
417
|
+
}
|
|
418
|
+
const source = await readFile(path.join(root, ".github", "workflows", "ci.yml"), "utf8");
|
|
419
|
+
if (!source.startsWith("# Managed by right-git")) {
|
|
420
|
+
fail(`${appName} ci.yml is not marked as right-git managed`);
|
|
421
|
+
}
|
|
422
|
+
if (/^\s*(?:actions|attestations|contents|id-token|packages|security-events):\s*write\s*$/mi.test(source)
|
|
423
|
+
|| /\bsecrets\s*\./i.test(source)) {
|
|
424
|
+
fail(`${appName} right-git managed ci.yml requests release-capable permissions or secrets`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
397
428
|
function buildAllowedVersions(published = {}, staged = {}, legacy = {}) {
|
|
398
429
|
const allowed = new Map();
|
|
399
430
|
for (const [name, value] of [
|
|
@@ -423,7 +454,7 @@ function expandEnv(value) {
|
|
|
423
454
|
|
|
424
455
|
async function signWindows(files, phase, root) {
|
|
425
456
|
const args = [SIGN_WINDOWS];
|
|
426
|
-
const receipt =
|
|
457
|
+
const receipt = windowsReceiptPath(root, phase);
|
|
427
458
|
if (opts.dryRun) args.push("--dry-run");
|
|
428
459
|
else {
|
|
429
460
|
if (existsSync(receipt)) unlinkSync(receipt);
|
|
@@ -435,6 +466,8 @@ async function signWindows(files, phase, root) {
|
|
|
435
466
|
}
|
|
436
467
|
|
|
437
468
|
function windowsReceiptPath(root, phase) {
|
|
469
|
+
const configured = phase === "raw-exe" ? target.sign?.receipt : target.sign?.installerReceipt;
|
|
470
|
+
if (configured) return path.resolve(root, configured);
|
|
438
471
|
return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `windows-${phase}.json`);
|
|
439
472
|
}
|
|
440
473
|
|
|
@@ -521,11 +554,12 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
521
554
|
}
|
|
522
555
|
const started = Date.now();
|
|
523
556
|
return new Promise((resolve) => {
|
|
557
|
+
const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
|
|
524
558
|
const child = spawn(cmd, runArgs, {
|
|
525
559
|
cwd,
|
|
526
560
|
env: { ...process.env, ...releaseEnv, ...env },
|
|
527
561
|
stdio: "inherit",
|
|
528
|
-
shell:
|
|
562
|
+
shell: useShell,
|
|
529
563
|
windowsHide: true,
|
|
530
564
|
});
|
|
531
565
|
const timer = options.timeoutMs
|
package/release.test.mjs
CHANGED
|
@@ -16,7 +16,7 @@ function git(cwd, ...args) {
|
|
|
16
16
|
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function fixture({ signed = true, publish = false, signingContract = "windows-raw-exe-authenticode-before-nsis-v1", prePackageFiles = ["raw.exe"], packageJson, packageCommand, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win" } = {}) {
|
|
19
|
+
function fixture({ signed = true, publish = false, signingContract = "windows-raw-exe-authenticode-before-nsis-v1", prePackageFiles = ["raw.exe"], signFiles = ["fixture.exe"], packageJson, packageCommand, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win", hostedWorkflows } = {}) {
|
|
20
20
|
const fixtureRoot = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
|
|
21
21
|
const dir = repoCargoConfig ? path.join(fixtureRoot, "apps", "fixture") : fixtureRoot;
|
|
22
22
|
mkdirSync(dir, { recursive: true });
|
|
@@ -45,6 +45,7 @@ function fixture({ signed = true, publish = false, signingContract = "windows-ra
|
|
|
45
45
|
app: "fixture",
|
|
46
46
|
packageManager: "pnpm",
|
|
47
47
|
checks: [],
|
|
48
|
+
...(hostedWorkflows ? { hostedWorkflows } : {}),
|
|
48
49
|
targets: {
|
|
49
50
|
[platform]: {
|
|
50
51
|
...(buildInputs ? { buildInputs } : {}),
|
|
@@ -52,7 +53,7 @@ function fixture({ signed = true, publish = false, signingContract = "windows-ra
|
|
|
52
53
|
package: packageCommand ?? { cmd: "node", args: ["-e", "process.exit(0)"] },
|
|
53
54
|
...(publish ? { publish: { cmd: "node", args: ["publish-update.mjs"] } } : {}),
|
|
54
55
|
artifacts: [],
|
|
55
|
-
...(platform === "win" ? { signingContract, prePackage: { cmd: "node", args: ["-e", "process.exit(0)"] }, sign: { prePackageFiles, files:
|
|
56
|
+
...(platform === "win" ? { signingContract, prePackage: { cmd: "node", args: ["-e", "process.exit(0)"] }, sign: { prePackageFiles, files: signFiles } } : {}),
|
|
56
57
|
updater: { artifacts: [{ file: "fixture.exe", signature: "fixture.exe.sig", platform: "windows-x86_64", key: "fixture/fixture.exe" }] },
|
|
57
58
|
hardening: [],
|
|
58
59
|
},
|
|
@@ -199,6 +200,13 @@ test("release worker reuses owned process-tree termination for Windows and remot
|
|
|
199
200
|
assert.match(source, /function killProcessTree\(pid\) \{\s*terminateProcessTree\(pid\);\s*\}/s);
|
|
200
201
|
});
|
|
201
202
|
|
|
203
|
+
test("Windows native release tools preserve artifact arguments containing spaces", () => {
|
|
204
|
+
const source = readFileSync(release, "utf8");
|
|
205
|
+
assert.match(source, /const useShell = process\.platform === "win32" && !\/\\\.\(\?:exe\|com\)\$\/i\.test\(cmd\);/);
|
|
206
|
+
assert.match(source, /shell: useShell/);
|
|
207
|
+
assert.doesNotMatch(source, /shell: process\.platform === "win32"/);
|
|
208
|
+
});
|
|
209
|
+
|
|
202
210
|
test("accepts patch and exposes it to the signed package command", () => {
|
|
203
211
|
const result = run(fixture(), "--tier=patch");
|
|
204
212
|
assert.equal(result.status, 0, result.stderr);
|
|
@@ -233,6 +241,32 @@ test("rejects Windows targets with missing or ambiguous signing configuration",
|
|
|
233
241
|
}
|
|
234
242
|
});
|
|
235
243
|
|
|
244
|
+
test("accepts portable Windows releases with multiple pre-package executables and no installer", () => {
|
|
245
|
+
const result = run(fixture({
|
|
246
|
+
signingContract: "windows-raw-exe-authenticode-before-portable-v1",
|
|
247
|
+
prePackageFiles: ["legion.exe", "legion-hook.exe", "legion-mcp.exe"],
|
|
248
|
+
signFiles: [],
|
|
249
|
+
}), "--tier=patch");
|
|
250
|
+
assert.equal(result.status, 0, result.stderr);
|
|
251
|
+
assert.match(result.stdout, /raw-exe/);
|
|
252
|
+
assert.doesNotMatch(result.stdout, /bundle marker/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("uses a configured signing receipt path for portable architecture isolation", () => {
|
|
256
|
+
const source = readFileSync(release, "utf8");
|
|
257
|
+
assert.match(source, /phase === "raw-exe" \? target\.sign\?\.receipt : target\.sign\?\.installerReceipt/);
|
|
258
|
+
assert.match(source, /if \(configured\) return path\.resolve\(root, configured\)/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("rejects portable Windows releases that declare a second installer signing phase", () => {
|
|
262
|
+
const result = run(fixture({
|
|
263
|
+
signingContract: "windows-raw-exe-authenticode-before-portable-v1",
|
|
264
|
+
prePackageFiles: ["legion.exe"],
|
|
265
|
+
}), "--tier=patch");
|
|
266
|
+
assert.notEqual(result.status, 0);
|
|
267
|
+
assert.match(result.stderr, /portable.*omit sign\.files/i);
|
|
268
|
+
});
|
|
269
|
+
|
|
236
270
|
test("rejects Git, path, or link RightKit app dependencies before release work starts", () => {
|
|
237
271
|
const result = run(
|
|
238
272
|
fixture({
|
|
@@ -266,6 +300,35 @@ test("rejects hosted workflow files before release work starts", () => {
|
|
|
266
300
|
assert.match(result.stderr, /hosted workflow.*forbidden/i);
|
|
267
301
|
});
|
|
268
302
|
|
|
303
|
+
test("accepts one right-git managed CI workflow without granting hosted release authority", () => {
|
|
304
|
+
const config = fixture({ hostedWorkflows: "right-git-ci-only" });
|
|
305
|
+
const root = path.dirname(config);
|
|
306
|
+
const workflowDir = path.join(root, ".github", "workflows");
|
|
307
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
308
|
+
writeFileSync(path.join(root, ".rightgit.json"), JSON.stringify({ schemaVersion: 1, lanes: ["ci"] }));
|
|
309
|
+
writeFileSync(path.join(workflowDir, "ci.yml"), "# Managed by right-git — do not hand-edit.\npermissions:\n contents: read\njobs: {}\n");
|
|
310
|
+
git(root, "add", ".");
|
|
311
|
+
git(root, "commit", "-m", "add managed ci");
|
|
312
|
+
|
|
313
|
+
const result = run(config, "--tier=patch");
|
|
314
|
+
assert.equal(result.status, 0, result.stderr);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("rejects a self-labeled right-git workflow with release-capable authority", () => {
|
|
318
|
+
const config = fixture({ hostedWorkflows: "right-git-ci-only" });
|
|
319
|
+
const root = path.dirname(config);
|
|
320
|
+
const workflowDir = path.join(root, ".github", "workflows");
|
|
321
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
322
|
+
writeFileSync(path.join(root, ".rightgit.json"), JSON.stringify({ schemaVersion: 1, lanes: ["ci"] }));
|
|
323
|
+
writeFileSync(path.join(workflowDir, "ci.yml"), "# Managed by right-git\npermissions:\n id-token: write\njobs: {}\n");
|
|
324
|
+
git(root, "add", ".");
|
|
325
|
+
git(root, "commit", "-m", "add unsafe ci");
|
|
326
|
+
|
|
327
|
+
const result = run(config, "--tier=patch");
|
|
328
|
+
assert.notEqual(result.status, 0);
|
|
329
|
+
assert.match(result.stderr, /release-capable permissions or secrets/i);
|
|
330
|
+
});
|
|
331
|
+
|
|
269
332
|
test("doctor accepts an exact crates.io RightKit pin with benign Cargo config", () => {
|
|
270
333
|
const config = fixture({
|
|
271
334
|
platform: hostPlatform,
|
|
@@ -481,18 +481,25 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
481
481
|
assert.equal(versions.npm["@rightkit/updates"], "0.2.3");
|
|
482
482
|
assert.deepEqual(versions.stagedNpm, {
|
|
483
483
|
"@rightkit/ax": "0.2.0",
|
|
484
|
-
"@rightkit/git": "0.2.
|
|
485
|
-
"@rightkit/hooks": "0.1.
|
|
486
|
-
"@rightkit/legal": "0.3.
|
|
484
|
+
"@rightkit/git": "0.2.1",
|
|
485
|
+
"@rightkit/hooks": "0.1.1",
|
|
486
|
+
"@rightkit/legal": "0.3.1",
|
|
487
487
|
"@rightkit/legal-ui": "0.1.1",
|
|
488
|
-
"@rightkit/license": "0.1.
|
|
489
|
-
"@rightkit/
|
|
490
|
-
"@rightkit/
|
|
488
|
+
"@rightkit/license": "0.1.7",
|
|
489
|
+
"@rightkit/logs": "0.1.4",
|
|
490
|
+
"@rightkit/platform-ui": "0.1.1",
|
|
491
|
+
"@rightkit/qa": "0.2.1",
|
|
492
|
+
"@rightkit/release": "0.2.69",
|
|
493
|
+
"@rightkit/tauri": "0.1.1",
|
|
494
|
+
"@rightkit/updates": "0.2.4",
|
|
491
495
|
});
|
|
492
496
|
assert.deepEqual(versions.legacyNpm, {
|
|
493
497
|
"@rightkit/legal-ui": ["0.1.0"],
|
|
494
|
-
"@rightkit/
|
|
495
|
-
"@rightkit/
|
|
498
|
+
"@rightkit/hooks": ["0.1.0"],
|
|
499
|
+
"@rightkit/legal": ["0.3.0"],
|
|
500
|
+
"@rightkit/license": ["0.1.6"],
|
|
501
|
+
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61", "0.2.62", "0.2.63", "0.2.64", "0.2.65", "0.2.66", "0.2.67", "0.2.68"],
|
|
502
|
+
"@rightkit/qa": ["0.1.0", "0.2.0"],
|
|
496
503
|
});
|
|
497
504
|
assert.ok(
|
|
498
505
|
new Set([versions.npm["@rightkit/release"], ...versions.legacyNpm["@rightkit/release"]]).has("0.2.42"),
|
|
@@ -710,8 +717,12 @@ for (const app of apps) {
|
|
|
710
717
|
}
|
|
711
718
|
|
|
712
719
|
for (const app of macOnlyApps) {
|
|
713
|
-
test(`${app.key} follows the macOS-only AppKit Right Release contract`, async () => {
|
|
720
|
+
test(`${app.key} follows the macOS-only AppKit Right Release contract`, async (context) => {
|
|
714
721
|
const root = path.join(workspace, app.root);
|
|
722
|
+
if (!existsSync(path.join(root, "package.json"))) {
|
|
723
|
+
context.skip(`${app.key} is not present in this checkout`);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
715
726
|
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
716
727
|
assert.equal(pkg.scripts["release:doctor"], "right-release doctor");
|
|
717
728
|
assert.equal(pkg.scripts["release:build:mac"], "right-release build --platform mac");
|
|
@@ -853,32 +864,26 @@ test("consuming suite apps resolve Cargo build output from cargo metadata, never
|
|
|
853
864
|
const cargoTargetRootHelpers = [
|
|
854
865
|
"coderight/apps/coderight-tauri/scripts/lib/target-root.mjs",
|
|
855
866
|
"heardright/tauri-app-next/scripts/lib/target-root.mjs",
|
|
867
|
+
"genright/scripts/lib/target-root.mjs",
|
|
856
868
|
"mailright/scripts/lib/target-root.mjs",
|
|
857
869
|
"orthic/scripts/lib/target-root.mjs",
|
|
858
870
|
"viewright/scripts/lib/target-root.mjs",
|
|
859
871
|
"membrane/apps/membrane-hub/scripts/lib/target-root.mjs",
|
|
860
872
|
];
|
|
861
873
|
|
|
862
|
-
test("
|
|
874
|
+
test("consumer target-root helpers delegate to RightKit's canonical resolver", () => {
|
|
863
875
|
for (const helperPath of cargoTargetRootHelpers) {
|
|
864
876
|
const appRoot = path.join(workspace, helperPath.split("/scripts/lib/target-root.mjs")[0]);
|
|
865
877
|
if (!existsSync(path.join(appRoot, "package.json"))) continue;
|
|
866
878
|
const fullPath = path.join(workspace, helperPath);
|
|
867
879
|
assert.ok(existsSync(fullPath), `${helperPath} is missing; the app must keep its shared cargo-metadata target resolver`);
|
|
868
880
|
const source = readFileSync(fullPath, "utf8");
|
|
869
|
-
assert.match(source,
|
|
870
|
-
assert.match(source, /
|
|
871
|
-
assert.match(source, /target_directory/, `${helperPath} must read target_directory from cargo metadata's output`);
|
|
881
|
+
assert.match(source, /@rightkit\/release\/cargo-target\.mjs/, `${helperPath} must import RightKit's resolver`);
|
|
882
|
+
assert.match(source, /resolveTargetRoot/, `${helperPath} must delegate target resolution to RightKit`);
|
|
872
883
|
}
|
|
873
884
|
});
|
|
874
885
|
|
|
875
|
-
|
|
876
|
-
// become a thin re-export of the shared tools/rightkit resolveTargetRoot
|
|
877
|
-
// (cargo-target.mjs) rather than its own copy of the cargo-metadata call. Until
|
|
878
|
-
// that migration lands, cargoTargetRootHelpers above still point at full local
|
|
879
|
-
// implementations, so this stays skipped to avoid failing the still-unmigrated
|
|
880
|
-
// app repos. Enable it once every app's target-root.mjs is a re-export shim.
|
|
881
|
-
test("consuming app target-root helpers are thin re-export shims, not local resolver re-implementations", { skip: true }, () => {
|
|
886
|
+
test("consuming app target-root helpers do not reimplement Cargo metadata", () => {
|
|
882
887
|
const LOCAL_RESOLVER_PATTERN = /function\s+(?:cargoTargetRoot|resolveManagedCargoTarget)\s*\([^)]*\)\s*\{[^}]*cargo[^}]*metadata/s;
|
|
883
888
|
for (const helperPath of cargoTargetRootHelpers) {
|
|
884
889
|
const appRoot = path.join(workspace, helperPath.split("/scripts/lib/target-root.mjs")[0]);
|
|
@@ -894,6 +899,96 @@ test("consuming app target-root helpers are thin re-export shims, not local reso
|
|
|
894
899
|
}
|
|
895
900
|
});
|
|
896
901
|
|
|
902
|
+
const cargoAuthorityRoots = [
|
|
903
|
+
...cargoTargetDirGuardApps,
|
|
904
|
+
"citadel",
|
|
905
|
+
"heardright",
|
|
906
|
+
"legion",
|
|
907
|
+
"rightsites",
|
|
908
|
+
"rightsuite",
|
|
909
|
+
"sellright",
|
|
910
|
+
"tools/screenright",
|
|
911
|
+
"voiceright",
|
|
912
|
+
"workright",
|
|
913
|
+
];
|
|
914
|
+
const cargoAuthorityNames = [
|
|
915
|
+
"CARGO_HOME",
|
|
916
|
+
"CARGO_TARGET_DIR",
|
|
917
|
+
"CARGO_BUILD_TARGET_DIR",
|
|
918
|
+
"CARGO_BUILD_BUILD_DIR",
|
|
919
|
+
"CARGO_BUILD_JOBS",
|
|
920
|
+
"CARGO_ENCODED_RUSTFLAGS",
|
|
921
|
+
"RUSTFLAGS",
|
|
922
|
+
"RUSTC_WRAPPER",
|
|
923
|
+
"RUSTC_WORKSPACE_WRAPPER",
|
|
924
|
+
"SCCACHE_DIR",
|
|
925
|
+
"SCCACHE_BASEDIRS",
|
|
926
|
+
"SCCACHE_CACHE_SIZE",
|
|
927
|
+
];
|
|
928
|
+
const cargoAuthorityExtensions = new Set([".mjs", ".js", ".cjs", ".ts", ".sh", ".ps1", ".py", ".json"]);
|
|
929
|
+
|
|
930
|
+
function findFirstPartyBuildScripts(root) {
|
|
931
|
+
const found = [];
|
|
932
|
+
const visit = (dir) => {
|
|
933
|
+
if (!existsSync(dir)) return;
|
|
934
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
935
|
+
if (entry.isDirectory() && [
|
|
936
|
+
".agent", ".audit", ".cache", ".git", ".right-release", "bakeoff", "dist", "docs",
|
|
937
|
+
"node_modules", "reference", "target", "tests", "vendor",
|
|
938
|
+
].includes(entry.name)) continue;
|
|
939
|
+
const full = path.join(dir, entry.name);
|
|
940
|
+
if (entry.isDirectory()) {
|
|
941
|
+
visit(full);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if (!entry.isFile() || !cargoAuthorityExtensions.has(path.extname(entry.name))) continue;
|
|
945
|
+
const relative = path.relative(root, full).replaceAll(path.sep, "/");
|
|
946
|
+
if (/(?:^|\/)(?:test-|[^/]+\.test\.)/.test(relative)) continue;
|
|
947
|
+
if (
|
|
948
|
+
relative.includes("/scripts/")
|
|
949
|
+
|| relative.startsWith("scripts/")
|
|
950
|
+
|| /(?:^|\/)(?:package\.json|package\.(?:sh|ps1)|right-release\.config\.mjs)$/.test(relative)
|
|
951
|
+
|| (!relative.includes("/") && /\.(?:sh|ps1)$/.test(relative))
|
|
952
|
+
) found.push(full);
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
visit(root);
|
|
956
|
+
return found;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function executableScriptSource(source) {
|
|
960
|
+
return source
|
|
961
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
962
|
+
.split("\n")
|
|
963
|
+
.filter((line) => !/^\s*(?:\/\/|#)/.test(line))
|
|
964
|
+
.join("\n");
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
test("product scripts never override or destroy RightKit-owned Cargo cache state", () => {
|
|
968
|
+
const authorityName = cargoAuthorityNames.join("|");
|
|
969
|
+
const jsMutation = new RegExp(`(?:delete\\s+process\\.env\\.(?:${authorityName})|process\\.env\\.(?:${authorityName})\\s*=|\\b(?:${authorityName})\\s*:)`);
|
|
970
|
+
const shellMutation = new RegExp(`(?:^|\\n)\\s*(?:export\\s+|unset\\s+|env\\s+(?:-[^\\n ]+\\s+)*-u\\s+)?(?:${authorityName})(?:\\s*=|\\b)`);
|
|
971
|
+
const powershellMutation = new RegExp(`\\$env:(?:${authorityName})\\s*=`, "i");
|
|
972
|
+
const cargoClean = /\bcargo\s+clean\b|["']cargo["'][\s\S]{0,400}?["']clean["']/;
|
|
973
|
+
|
|
974
|
+
for (const appRoot of cargoAuthorityRoots) {
|
|
975
|
+
const root = path.join(workspace, appRoot);
|
|
976
|
+
if (!existsSync(root)) continue;
|
|
977
|
+
for (const filePath of findFirstPartyBuildScripts(root)) {
|
|
978
|
+
const source = executableScriptSource(readFileSync(filePath, "utf8"));
|
|
979
|
+
const relative = path.relative(workspace, filePath);
|
|
980
|
+
assert.doesNotMatch(source, jsMutation, `${relative} mutates RightKit-owned Cargo/cache environment`);
|
|
981
|
+
assert.doesNotMatch(source, shellMutation, `${relative} mutates RightKit-owned Cargo/cache environment`);
|
|
982
|
+
assert.doesNotMatch(source, powershellMutation, `${relative} mutates RightKit-owned Cargo/cache environment`);
|
|
983
|
+
assert.doesNotMatch(source, cargoClean, `${relative} destroys reusable Cargo output with cargo clean`);
|
|
984
|
+
assert.doesNotMatch(source, /--target-dir\b/, `${relative} bypasses RightKit target ownership`);
|
|
985
|
+
assert.doesNotMatch(source, /(?:^|[\\/])\.cargo[\\/]bin(?:[\\/]|\b)/i, `${relative} bypasses managed Cargo by injecting a toolchain directory`);
|
|
986
|
+
assert.doesNotMatch(source, /build-guard\.mjs/, `${relative} depends on obsolete product-local build/cache control`);
|
|
987
|
+
}
|
|
988
|
+
assert.equal(existsSync(path.join(root, "scripts", "build-guard.mjs")), false, `${appRoot} must not carry a product-local build guard`);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
897
992
|
test("Right Suite has no hosted workflow files", () => {
|
|
898
993
|
for (const root of ["viewright", "scraperight", "heardright", "mailright", "coderight", "genright", "voiceright", "tools/rightkit"]) {
|
|
899
994
|
const workflowDir = path.join(workspace, root, ".github", "workflows");
|
package/rightkit-versions.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 2,
|
|
3
|
-
"packageManager": "pnpm@11.
|
|
3
|
+
"packageManager": "pnpm@11.24.0",
|
|
4
4
|
"npm": {
|
|
5
5
|
"@rightkit/git": "0.2.0",
|
|
6
6
|
"@rightkit/legal": "0.2.0",
|
|
@@ -14,18 +14,31 @@
|
|
|
14
14
|
},
|
|
15
15
|
"stagedNpm": {
|
|
16
16
|
"@rightkit/ax": "0.2.0",
|
|
17
|
-
"@rightkit/git": "0.2.
|
|
18
|
-
"@rightkit/hooks": "0.1.
|
|
19
|
-
"@rightkit/legal": "0.3.
|
|
17
|
+
"@rightkit/git": "0.2.1",
|
|
18
|
+
"@rightkit/hooks": "0.1.1",
|
|
19
|
+
"@rightkit/legal": "0.3.1",
|
|
20
20
|
"@rightkit/legal-ui": "0.1.1",
|
|
21
|
-
"@rightkit/license": "0.1.
|
|
22
|
-
"@rightkit/
|
|
23
|
-
"@rightkit/
|
|
21
|
+
"@rightkit/license": "0.1.7",
|
|
22
|
+
"@rightkit/logs": "0.1.4",
|
|
23
|
+
"@rightkit/platform-ui": "0.1.1",
|
|
24
|
+
"@rightkit/qa": "0.2.1",
|
|
25
|
+
"@rightkit/release": "0.2.69",
|
|
26
|
+
"@rightkit/tauri": "0.1.1",
|
|
27
|
+
"@rightkit/updates": "0.2.4"
|
|
24
28
|
},
|
|
25
29
|
"legacyNpm": {
|
|
26
30
|
"@rightkit/legal-ui": [
|
|
27
31
|
"0.1.0"
|
|
28
32
|
],
|
|
33
|
+
"@rightkit/hooks": [
|
|
34
|
+
"0.1.0"
|
|
35
|
+
],
|
|
36
|
+
"@rightkit/legal": [
|
|
37
|
+
"0.3.0"
|
|
38
|
+
],
|
|
39
|
+
"@rightkit/license": [
|
|
40
|
+
"0.1.6"
|
|
41
|
+
],
|
|
29
42
|
"@rightkit/release": [
|
|
30
43
|
"0.2.22",
|
|
31
44
|
"0.2.29",
|
|
@@ -49,10 +62,13 @@
|
|
|
49
62
|
"0.2.63",
|
|
50
63
|
"0.2.64",
|
|
51
64
|
"0.2.65",
|
|
52
|
-
"0.2.66"
|
|
65
|
+
"0.2.66",
|
|
66
|
+
"0.2.67",
|
|
67
|
+
"0.2.68"
|
|
53
68
|
],
|
|
54
69
|
"@rightkit/qa": [
|
|
55
|
-
"0.1.0"
|
|
70
|
+
"0.1.0",
|
|
71
|
+
"0.2.0"
|
|
56
72
|
]
|
|
57
73
|
},
|
|
58
74
|
"cargo": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"remote": "https://github.com/bogusyogi/viewright.git",
|
|
9
9
|
"appDir": ".",
|
|
10
10
|
"revision": "21a4171fa8add2d8114bc5f07498b60d7e8eafe5",
|
|
11
|
-
|
|
11
|
+
"packageManager": "pnpm@11.24.0",
|
|
12
12
|
"clone": {
|
|
13
13
|
"command": "git clone --depth 1 --single-branch https://github.com/bogusyogi/viewright.git C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright",
|
|
14
14
|
"status": 0,
|
|
@@ -22,7 +22,7 @@ test("standalone verifier clones, installs, doctors from a nested app root, and
|
|
|
22
22
|
writeFileSync(path.join(source, "apps", "desktop", "package.json"), JSON.stringify({
|
|
23
23
|
name: "standalone-fixture",
|
|
24
24
|
private: true,
|
|
25
|
-
packageManager: "pnpm@11.
|
|
25
|
+
packageManager: "pnpm@11.24.0",
|
|
26
26
|
scripts: { "release:doctor": "node doctor.mjs" },
|
|
27
27
|
}), "utf8");
|
|
28
28
|
writeFileSync(path.join(source, "apps", "desktop", "pnpm-lock.yaml"), "lockfileVersion: '9.0'\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\nimporters:\n .: {}\n", "utf8");
|