@rightkit/release 0.2.52 → 0.2.54
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/cargo-guard.mjs +170 -3
- package/cargo-guard.test.mjs +96 -5
- package/cli/right-release.mjs +3 -0
- package/github-release.mjs +111 -0
- package/github-release.test.mjs +46 -0
- package/heavy-command.mjs +2 -2
- package/package.json +1 -1
- package/release-state.test.mjs +3 -1
- package/right-suite-contract.test.mjs +26 -6
- package/rightkit-versions.json +3 -2
package/cargo-guard.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { fileURLToPath } from "node:url";
|
|
5
8
|
|
|
9
|
+
import { resolveSharedCacheRoot } from "./cache-policy.mjs";
|
|
6
10
|
import { runHeavyCommand } from "./heavy-command.mjs";
|
|
7
11
|
|
|
8
12
|
const LIGHT_COMMANDS = new Set(["fmt", "metadata", "tree", "fetch", "search", "locate-project", "read-manifest", "help", "version", "--version", "-V"]);
|
|
@@ -24,6 +28,81 @@ export function shouldGuardCargo(args) {
|
|
|
24
28
|
return command !== null && !LIGHT_COMMANDS.has(command);
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
export function computePolicyPath({ env = process.env, platform = process.platform, home = os.userInfo().homedir } = {}) {
|
|
32
|
+
if (env.RIGHTSUITE_COMPUTE_POLICY) return path.resolve(env.RIGHTSUITE_COMPUTE_POLICY);
|
|
33
|
+
if (platform === "win32" || platform === "win") {
|
|
34
|
+
return path.win32.join(env.LOCALAPPDATA || path.win32.join(home, "AppData", "Local"), "RightSuite", "compute-policy.json");
|
|
35
|
+
}
|
|
36
|
+
if (platform === "darwin" || platform === "mac") return path.join(home, "Library", "Application Support", "RightSuite", "compute-policy.json");
|
|
37
|
+
return path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "rightsuite", "compute-policy.json");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadComputePolicy(options = {}) {
|
|
41
|
+
const policyPath = computePolicyPath(options);
|
|
42
|
+
if (!(options.exists ?? existsSync)(policyPath)) return { schemaVersion: 1, cargoTest: "allow" };
|
|
43
|
+
const policy = JSON.parse((options.read ?? readFileSync)(policyPath, "utf8"));
|
|
44
|
+
if (!policy || Object.keys(policy).sort().join(",") !== "cargoTest,schemaVersion"
|
|
45
|
+
|| policy.schemaVersion !== 1 || !["allow", "deny"].includes(policy.cargoTest)) {
|
|
46
|
+
throw new Error(`invalid compute policy: ${policyPath}`);
|
|
47
|
+
}
|
|
48
|
+
return policy;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shellSegments(command) {
|
|
52
|
+
const segments = [[]];
|
|
53
|
+
let word = "", quote = null, escaped = false;
|
|
54
|
+
const push = () => { if (word) segments.at(-1).push(word); word = ""; };
|
|
55
|
+
for (const char of String(command)) {
|
|
56
|
+
if (escaped) { word += char; escaped = false; continue; }
|
|
57
|
+
if (char === "\\" && quote !== "'") { escaped = true; continue; }
|
|
58
|
+
if (quote) { if (char === quote) quote = null; else word += char; continue; }
|
|
59
|
+
if (char === "'" || char === '"') { quote = char; continue; }
|
|
60
|
+
if (/\s/.test(char)) { push(); if (char === "\n" && segments.at(-1).length) segments.push([]); continue; }
|
|
61
|
+
if (";&|".includes(char)) { push(); if (segments.at(-1).length) segments.push([]); continue; }
|
|
62
|
+
word += char;
|
|
63
|
+
}
|
|
64
|
+
push();
|
|
65
|
+
return segments.filter((segment) => segment.length);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function segmentRunsCargoTest(segment) {
|
|
69
|
+
let index = 0;
|
|
70
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(segment[index] ?? "")) index += 1;
|
|
71
|
+
if (path.basename(segment[index] ?? "").toLowerCase() === "env") {
|
|
72
|
+
index += 1;
|
|
73
|
+
while ((segment[index] ?? "").startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(segment[index] ?? "")) index += 1;
|
|
74
|
+
}
|
|
75
|
+
const executable = path.basename(segment[index] ?? "").toLowerCase();
|
|
76
|
+
if (["cargo", "cargo.exe"].includes(executable)) return cargoSubcommand(segment.slice(index + 1)) === "test";
|
|
77
|
+
if (["rustup", "rustup.exe"].includes(executable) && segment[index + 1] === "run") {
|
|
78
|
+
const cargoIndex = segment.findIndex((value, offset) => offset > index + 1 && ["cargo", "cargo.exe"].includes(path.basename(value).toLowerCase()));
|
|
79
|
+
return cargoIndex >= 0 && cargoSubcommand(segment.slice(cargoIndex + 1)) === "test";
|
|
80
|
+
}
|
|
81
|
+
if (["sh", "bash", "zsh"].includes(executable)) {
|
|
82
|
+
const commandIndex = segment.findIndex((value, offset) => offset > index && value === "-c");
|
|
83
|
+
return commandIndex >= 0 && shellRunsCargoTest(segment[commandIndex + 1] ?? "");
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function shellRunsCargoTest(command) {
|
|
89
|
+
return shellSegments(command).some(segmentRunsCargoTest);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function assertComputePolicyAllowsCargo(args, options = {}) {
|
|
93
|
+
if (cargoSubcommand(args) === "test" && loadComputePolicy(options).cargoTest === "deny") {
|
|
94
|
+
throw new Error("cargo test denied by global RightSuite compute policy");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function checkHookInput(input, options = {}) {
|
|
99
|
+
const event = JSON.parse(String(input));
|
|
100
|
+
const command = event?.tool_input?.command ?? event?.toolInput?.command ?? "";
|
|
101
|
+
if (loadComputePolicy(options).cargoTest === "deny" && shellRunsCargoTest(command)) {
|
|
102
|
+
throw new Error("cargo test denied by global RightSuite compute policy");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
27
106
|
export function resolveRealCargo({ env = process.env, run = spawnSync } = {}) {
|
|
28
107
|
if (env.RIGHTSUITE_REAL_CARGO) return path.resolve(env.RIGHTSUITE_REAL_CARGO);
|
|
29
108
|
const rustup = process.platform === "win32" ? "rustup.exe" : "rustup";
|
|
@@ -33,6 +112,79 @@ export function resolveRealCargo({ env = process.env, run = spawnSync } = {}) {
|
|
|
33
112
|
return path.resolve(cargo);
|
|
34
113
|
}
|
|
35
114
|
|
|
115
|
+
export function resolveRealRustc({ env = process.env, run = spawnSync } = {}) {
|
|
116
|
+
if (env.RIGHTSUITE_REAL_RUSTC) return path.resolve(env.RIGHTSUITE_REAL_RUSTC);
|
|
117
|
+
const rustup = process.platform === "win32" ? "rustup.exe" : "rustup";
|
|
118
|
+
const result = run(rustup, ["which", "rustc"], { encoding: "utf8", windowsHide: true });
|
|
119
|
+
const rustc = String(result.stdout ?? "").trim();
|
|
120
|
+
if (result.status !== 0 || !rustc) throw new Error(`cargo-guard could not resolve real rustc: ${String(result.stderr ?? "").trim()}`);
|
|
121
|
+
return path.resolve(rustc);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function pathApi(platform) {
|
|
125
|
+
return platform === "win32" || platform === "win" ? path.win32 : path.posix;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function cargoProjectRoot(args, { cwd = process.cwd(), platform = process.platform, exists = existsSync } = {}) {
|
|
129
|
+
const paths = pathApi(platform);
|
|
130
|
+
const manifestIndex = args.findIndex((value) => value === "--manifest-path");
|
|
131
|
+
const manifestValue = args.find((value) => value.startsWith("--manifest-path="))?.slice("--manifest-path=".length)
|
|
132
|
+
?? (manifestIndex >= 0 ? args[manifestIndex + 1] : null);
|
|
133
|
+
let current = paths.resolve(manifestValue ? paths.dirname(paths.resolve(cwd, manifestValue)) : cwd);
|
|
134
|
+
let selected = null;
|
|
135
|
+
for (;;) {
|
|
136
|
+
if (exists(paths.join(current, "Cargo.toml"))) selected = current;
|
|
137
|
+
const parent = paths.dirname(current);
|
|
138
|
+
if (parent === current) break;
|
|
139
|
+
current = parent;
|
|
140
|
+
}
|
|
141
|
+
return selected ?? paths.resolve(cwd);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertInsideCache(candidate, cacheRoot, label, { cwd, platform }) {
|
|
145
|
+
const paths = pathApi(platform);
|
|
146
|
+
const resolved = paths.resolve(cwd, candidate);
|
|
147
|
+
const relative = paths.relative(paths.resolve(cacheRoot), resolved);
|
|
148
|
+
if (relative === ".." || relative.startsWith(`..${paths.sep}`) || paths.isAbsolute(relative)) {
|
|
149
|
+
throw new Error(`${label} must stay inside shared cache root ${cacheRoot}; got ${candidate}`);
|
|
150
|
+
}
|
|
151
|
+
return resolved;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function cargoCacheEnvironment(args, {
|
|
155
|
+
env = process.env, cwd = process.cwd(), platform = process.platform, exists = existsSync,
|
|
156
|
+
} = {}) {
|
|
157
|
+
const paths = pathApi(platform);
|
|
158
|
+
const cacheRoot = resolveSharedCacheRoot({ platform, env });
|
|
159
|
+
const projectRoot = cargoProjectRoot(args, { cwd, platform, exists });
|
|
160
|
+
const projectName = paths.basename(projectRoot).replace(/[^A-Za-z0-9._-]+/g, "-") || "cargo";
|
|
161
|
+
const projectHash = createHash("sha256").update(platform === "win32" || platform === "win" ? projectRoot.toLowerCase() : projectRoot).digest("hex").slice(0, 12);
|
|
162
|
+
const defaultTarget = paths.join(cacheRoot, "dev-targets", `${projectName}-${projectHash}`);
|
|
163
|
+
const defaultSccache = paths.join(cacheRoot, "sccache");
|
|
164
|
+
const targetOption = args.find((value) => value.startsWith("--target-dir="));
|
|
165
|
+
const targetIndex = args.findIndex((value) => value === "--target-dir");
|
|
166
|
+
const explicitTarget = targetOption?.slice("--target-dir=".length) ?? (targetIndex >= 0 ? args[targetIndex + 1] : null);
|
|
167
|
+
if (explicitTarget) assertInsideCache(explicitTarget, cacheRoot, "--target-dir", { cwd, platform });
|
|
168
|
+
const targetDir = env.CARGO_TARGET_DIR
|
|
169
|
+
? assertInsideCache(env.CARGO_TARGET_DIR, cacheRoot, "CARGO_TARGET_DIR", { cwd, platform })
|
|
170
|
+
: defaultTarget;
|
|
171
|
+
const sccacheDir = env.SCCACHE_DIR
|
|
172
|
+
? assertInsideCache(env.SCCACHE_DIR, cacheRoot, "SCCACHE_DIR", { cwd, platform })
|
|
173
|
+
: defaultSccache;
|
|
174
|
+
const wrapper = String(env.RUSTC_WRAPPER ?? "sccache");
|
|
175
|
+
if (!["sccache", "sccache.exe"].includes(paths.basename(wrapper).toLowerCase())) {
|
|
176
|
+
throw new Error(`RUSTC_WRAPPER must be sccache; got ${wrapper || "<empty>"}`);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
...env,
|
|
180
|
+
CARGO_TARGET_DIR: targetDir,
|
|
181
|
+
RUSTC_WRAPPER: env.RUSTC_WRAPPER || "sccache",
|
|
182
|
+
SCCACHE_DIR: sccacheDir,
|
|
183
|
+
SCCACHE_CACHE_SIZE: env.SCCACHE_CACHE_SIZE || "32G",
|
|
184
|
+
RIGHTSUITE_CARGO_CACHE_GUARD: "1",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
36
188
|
function spawnCargo(command, args, env) {
|
|
37
189
|
return new Promise((resolve, reject) => {
|
|
38
190
|
const child = spawn(command, args, { env, stdio: "inherit", windowsHide: true });
|
|
@@ -41,12 +193,27 @@ function spawnCargo(command, args, env) {
|
|
|
41
193
|
});
|
|
42
194
|
}
|
|
43
195
|
|
|
44
|
-
export async function runCargoGuard(args, { env = process.env, resolveCargo = resolveRealCargo, runHeavy = runHeavyCommand, runLight = spawnCargo } = {}) {
|
|
196
|
+
export async function runCargoGuard(args, { env = process.env, policyOptions = {}, resolveCargo = resolveRealCargo, resolveRustc = resolveRealRustc, runHeavy = runHeavyCommand, runLight = spawnCargo } = {}) {
|
|
197
|
+
assertComputePolicyAllowsCargo(args, { ...policyOptions, env });
|
|
45
198
|
const cargo = resolveCargo({ env });
|
|
46
|
-
if (shouldGuardCargo(args))
|
|
199
|
+
if (shouldGuardCargo(args)) {
|
|
200
|
+
const guarded = cargoCacheEnvironment(args, { env });
|
|
201
|
+
// Pin RUSTC on every platform, not just Windows: cargo otherwise resolves
|
|
202
|
+
// bare `rustc` from its child PATH, and on this Mac that reached a stale
|
|
203
|
+
// 1.78.0 shim while cargo 1.97 passed --check-cfg — failing any crate
|
|
204
|
+
// build with "the `-Z unstable-options` flag must also be passed".
|
|
205
|
+
if (!guarded.RUSTC) guarded.RUSTC = resolveRustc({ env });
|
|
206
|
+
return runHeavy(["--", cargo, ...args], { env: guarded });
|
|
207
|
+
}
|
|
47
208
|
return runLight(cargo, args, env);
|
|
48
209
|
}
|
|
49
210
|
|
|
50
211
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
51
|
-
|
|
212
|
+
const args = process.argv.slice(2);
|
|
213
|
+
if (args[0] === "--check-hook-base64") {
|
|
214
|
+
try { checkHookInput(Buffer.from(args[1] ?? "", "base64url").toString("utf8")); }
|
|
215
|
+
catch (error) { console.error(`cargo-guard: ${error.message}`); process.exitCode = 2; }
|
|
216
|
+
} else {
|
|
217
|
+
runCargoGuard(args).then((code) => { process.exitCode = code; }).catch((error) => { console.error(`cargo-guard: ${error.message}`); process.exitCode = 1; });
|
|
218
|
+
}
|
|
52
219
|
}
|
package/cargo-guard.test.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import test from "node:test";
|
|
3
4
|
|
|
4
|
-
import { cargoSubcommand, resolveRealCargo, runCargoGuard, shouldGuardCargo } from "./cargo-guard.mjs";
|
|
5
|
+
import { assertComputePolicyAllowsCargo, cargoCacheEnvironment, cargoProjectRoot, cargoSubcommand, checkHookInput, resolveRealCargo, resolveRealRustc, runCargoGuard, shellRunsCargoTest, shouldGuardCargo } from "./cargo-guard.mjs";
|
|
5
6
|
|
|
6
7
|
test("Cargo guard serializes compiling commands but bypasses inspection and formatting", () => {
|
|
7
8
|
for (const args of [["build"], ["test"], ["check"], ["clippy"], ["nextest", "run"], ["clean"], ["+stable", "bench"]]) {
|
|
@@ -16,24 +17,114 @@ test("Cargo guard serializes compiling commands but bypasses inspection and form
|
|
|
16
17
|
assert.equal(shouldGuardCargo(["-Z", "unstable-options", "fmt"]), false);
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
test("global policy denies Cargo test before Cargo resolution", async () => {
|
|
21
|
+
const policy = { exists: () => true, read: () => '{"schemaVersion":1,"cargoTest":"deny"}', env: {} };
|
|
22
|
+
assert.throws(() => assertComputePolicyAllowsCargo(["test", "-p", "app"], policy), /denied by global/);
|
|
23
|
+
assert.doesNotThrow(() => assertComputePolicyAllowsCargo(["check"], policy));
|
|
24
|
+
let resolved = false;
|
|
25
|
+
await assert.rejects(runCargoGuard(["test"], {
|
|
26
|
+
env: { RIGHTSUITE_COMPUTE_POLICY: "/policy.json" },
|
|
27
|
+
policyOptions: { exists: () => true, read: () => '{"schemaVersion":1,"cargoTest":"deny"}' },
|
|
28
|
+
resolveCargo: () => { resolved = true; return "/real/cargo"; },
|
|
29
|
+
}), /denied by global/);
|
|
30
|
+
assert.equal(resolved, false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("Bash policy detects direct, absolute, rustup and nested Cargo tests only", () => {
|
|
34
|
+
for (const command of ["cargo test -p app", "cd app && /toolchain/bin/cargo +stable test", "rustup run stable cargo test", "bash -c 'cargo test'"]) {
|
|
35
|
+
assert.equal(shellRunsCargoTest(command), true, command);
|
|
36
|
+
}
|
|
37
|
+
for (const command of ["cargo check", "rg 'cargo test' docs", "echo cargo test", "cargo fmt --check"]) {
|
|
38
|
+
assert.equal(shellRunsCargoTest(command), false, command);
|
|
39
|
+
}
|
|
40
|
+
assert.throws(() => checkHookInput(JSON.stringify({ tool_input: { command: "cargo test" } }), {
|
|
41
|
+
exists: () => true, read: () => '{"schemaVersion":1,"cargoTest":"deny"}', env: {},
|
|
42
|
+
}), /denied by global/);
|
|
43
|
+
});
|
|
44
|
+
|
|
19
45
|
test("Cargo guard resolves real Cargo through rustup or explicit override", () => {
|
|
20
|
-
assert.equal(resolveRealCargo({ env: { RIGHTSUITE_REAL_CARGO: "/opt/toolchain/cargo" } }), "/opt/toolchain/cargo");
|
|
46
|
+
assert.equal(resolveRealCargo({ env: { RIGHTSUITE_REAL_CARGO: "/opt/toolchain/cargo" } }), path.resolve("/opt/toolchain/cargo"));
|
|
21
47
|
const calls = [];
|
|
22
48
|
const cargo = resolveRealCargo({ env: {}, run: (command, args) => { calls.push([command, args]); return { status: 0, stdout: "/real/cargo\n" }; } });
|
|
23
|
-
assert.equal(cargo, "/real/cargo");
|
|
49
|
+
assert.equal(cargo, path.resolve("/real/cargo"));
|
|
24
50
|
assert.deepEqual(calls[0][1], ["which", "cargo"]);
|
|
51
|
+
assert.equal(resolveRealRustc({ env: { RIGHTSUITE_REAL_RUSTC: "/opt/toolchain/rustc" } }), "/opt/toolchain/rustc");
|
|
52
|
+
assert.equal(resolveRealRustc({ env: {}, run: () => ({ status: 0, stdout: "/real/rustc\n" }) }), "/real/rustc");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function cacheEnv(overrides = {}) {
|
|
56
|
+
return { ...(process.platform === "win32" ? { LOCALAPPDATA: "C:\\Users\\test\\AppData\\Local" } : { RIGHT_RELEASE_CACHE_ROOT: "/tmp/rightkit-release-cache" }), ...overrides };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("Cargo guard injects project-keyed target and sccache directories", async () => {
|
|
60
|
+
let guarded;
|
|
61
|
+
await runCargoGuard(["build"], {
|
|
62
|
+
env: cacheEnv(), resolveCargo: () => "/real/cargo", resolveRustc: () => "/real/rustc",
|
|
63
|
+
runHeavy: async (_args, config) => { guarded = config.env; return 0; },
|
|
64
|
+
});
|
|
65
|
+
assert.match(guarded.CARGO_TARGET_DIR, /dev-targets[/\\][^/\\]+-[a-f0-9]{12}$/);
|
|
66
|
+
assert.match(guarded.SCCACHE_DIR, /[/\\]sccache$/);
|
|
67
|
+
assert.equal(guarded.RUSTC_WRAPPER, "sccache");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("Cargo guard rejects cache escapes and alternate compiler wrappers", async () => {
|
|
71
|
+
const run = (args, overrides) => runCargoGuard(args, {
|
|
72
|
+
env: cacheEnv(overrides), resolveCargo: () => "/real/cargo", resolveRustc: () => "/real/rustc",
|
|
73
|
+
runHeavy: async () => 0,
|
|
74
|
+
});
|
|
75
|
+
await assert.rejects(run(["build"], { CARGO_TARGET_DIR: path.resolve("outside-target") }), /CARGO_TARGET_DIR/);
|
|
76
|
+
await assert.rejects(run(["build"], { SCCACHE_DIR: path.resolve("outside-sccache") }), /SCCACHE_DIR/);
|
|
77
|
+
await assert.rejects(run(["build", `--target-dir=${path.resolve("outside-flag")}`]), /--target-dir/);
|
|
78
|
+
await assert.rejects(run(["build"], { RUSTC_WRAPPER: "rustc-wrapper" }), /RUSTC_WRAPPER/);
|
|
25
79
|
});
|
|
26
80
|
|
|
27
81
|
test("Cargo guard routes heavy and light commands without recursion", async () => {
|
|
28
82
|
const calls = [];
|
|
29
83
|
const options = {
|
|
30
|
-
env: { TEST: "1" },
|
|
84
|
+
env: { TEST: "1", RIGHT_RELEASE_CACHE_ROOT: "/cache" },
|
|
85
|
+
policyOptions: { exists: () => false },
|
|
31
86
|
resolveCargo: () => "/real/cargo",
|
|
32
87
|
runHeavy: async (args, config) => { calls.push(["heavy", args, config.env]); return 0; },
|
|
33
88
|
runLight: async (command, args, env) => { calls.push(["light", command, args, env]); return 0; },
|
|
34
89
|
};
|
|
35
90
|
assert.equal(await runCargoGuard(["test"], options), 0);
|
|
36
91
|
assert.equal(await runCargoGuard(["fmt", "--check"], options), 0);
|
|
37
|
-
assert.deepEqual(calls[0], ["heavy", ["--", "/real/cargo", "test"]
|
|
92
|
+
assert.deepEqual(calls[0].slice(0, 2), ["heavy", ["--", "/real/cargo", "test"]]);
|
|
93
|
+
assert.equal(calls[0][2].CARGO_TARGET_DIR.startsWith("/cache/dev-targets/"), true);
|
|
94
|
+
assert.equal(calls[0][2].RUSTC_WRAPPER, "sccache");
|
|
38
95
|
assert.deepEqual(calls[1], ["light", "/real/cargo", ["fmt", "--check"], options.env]);
|
|
39
96
|
});
|
|
97
|
+
|
|
98
|
+
test("Cargo guard forces target and compiler caches for each project", () => {
|
|
99
|
+
const existing = new Set(["/repo/Cargo.toml", "/repo/crate/Cargo.toml"]);
|
|
100
|
+
assert.equal(cargoProjectRoot(["test"], { cwd: "/repo/crate", platform: "mac", exists: (file) => existing.has(file) }), "/repo");
|
|
101
|
+
const env = cargoCacheEnvironment(["test"], {
|
|
102
|
+
cwd: "/repo/crate", platform: "mac", exists: (file) => existing.has(file),
|
|
103
|
+
env: { RIGHT_RELEASE_CACHE_ROOT: "/cache" },
|
|
104
|
+
});
|
|
105
|
+
assert.match(env.CARGO_TARGET_DIR, /^\/cache\/dev-targets\/repo-[a-f0-9]{12}$/);
|
|
106
|
+
assert.equal(env.RUSTC_WRAPPER, "sccache");
|
|
107
|
+
assert.equal(env.SCCACHE_DIR, "/cache/sccache");
|
|
108
|
+
assert.equal(env.RIGHTSUITE_CARGO_CACHE_GUARD, "1");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Cargo guard rejects cache bypasses and permits RightKit-owned cache paths", () => {
|
|
112
|
+
const base = { cwd: "/repo", platform: "mac", exists: () => false };
|
|
113
|
+
assert.throws(() => cargoCacheEnvironment(["test"], { ...base, env: { RIGHT_RELEASE_CACHE_ROOT: "/cache", CARGO_TARGET_DIR: "/tmp/target" } }), /CARGO_TARGET_DIR must stay inside/);
|
|
114
|
+
assert.throws(() => cargoCacheEnvironment(["test", "--target-dir", "/tmp/target"], { ...base, env: { RIGHT_RELEASE_CACHE_ROOT: "/cache" } }), /--target-dir must stay inside/);
|
|
115
|
+
assert.throws(() => cargoCacheEnvironment(["test"], { ...base, env: { RIGHT_RELEASE_CACHE_ROOT: "/cache", RUSTC_WRAPPER: "rustc-wrapper" } }), /must be sccache/);
|
|
116
|
+
const env = cargoCacheEnvironment(["test", "--target-dir=/cache/test-targets/app"], {
|
|
117
|
+
...base,
|
|
118
|
+
env: { RIGHT_RELEASE_CACHE_ROOT: "/cache", CARGO_TARGET_DIR: "/cache/test-targets/app", SCCACHE_DIR: "/cache/sccache", RUSTC_WRAPPER: "/usr/bin/sccache" },
|
|
119
|
+
});
|
|
120
|
+
assert.equal(env.CARGO_TARGET_DIR, "/cache/test-targets/app");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("Cargo guard derives native Windows cache paths", () => {
|
|
124
|
+
const env = cargoCacheEnvironment(["build"], {
|
|
125
|
+
cwd: "D:\\Claude\\citadel", platform: "win", exists: (file) => file === "D:\\Claude\\citadel\\Cargo.toml",
|
|
126
|
+
env: { LOCALAPPDATA: "C:\\Users\\test\\AppData\\Local" },
|
|
127
|
+
});
|
|
128
|
+
assert.match(env.CARGO_TARGET_DIR, /^C:\\Users\\test\\AppData\\Local\\RightSuite\\Cache\\release\\dev-targets\\citadel-[a-f0-9]{12}$/);
|
|
129
|
+
assert.equal(env.SCCACHE_DIR, "C:\\Users\\test\\AppData\\Local\\RightSuite\\Cache\\release\\sccache");
|
|
130
|
+
});
|
package/cli/right-release.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const commands = new Map([
|
|
|
19
19
|
["sign-updater", "sign-updater.mjs"],
|
|
20
20
|
["create-mac-updater", "create-mac-updater.mjs"],
|
|
21
21
|
["mirror-root-artifact", "mirror-root-artifact.mjs"],
|
|
22
|
+
["github", "github-release.mjs"],
|
|
22
23
|
]);
|
|
23
24
|
|
|
24
25
|
const args = process.argv.slice(2);
|
|
@@ -145,6 +146,8 @@ Commands:
|
|
|
145
146
|
generate-dmg-background <options> Generate the branded multi-resolution DMG background
|
|
146
147
|
mirror-root-artifact --file <path> --package-root <dir>
|
|
147
148
|
Persist a worktree artifact in the primary repo root
|
|
149
|
+
github --release <id> --platform mac|win --repo owner/repo [--dry-run]
|
|
150
|
+
Attach one sealed installer to a verified GitHub Release
|
|
148
151
|
|
|
149
152
|
Direct flags are treated as: right-release build <flags>.
|
|
150
153
|
Build is tier-neutral. Upload requires an explicit tier. Unsigned/local smoke builds stay app-local.`);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { verifySealedRelease } from "./release-state.mjs";
|
|
8
|
+
|
|
9
|
+
const REPO_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
10
|
+
const RELEASE_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
|
|
11
|
+
|
|
12
|
+
export function prepareGitHubRelease({ repoRoot, releaseId, platform, repo, stateRoot }) {
|
|
13
|
+
if (!RELEASE_RE.test(releaseId)) throw new Error(`invalid release id: ${releaseId}`);
|
|
14
|
+
if (platform !== "mac" && platform !== "win") throw new Error("platform must be mac or win");
|
|
15
|
+
if (!REPO_RE.test(repo)) throw new Error(`invalid GitHub repository: ${repo}`);
|
|
16
|
+
const platformDir = platform === "win" ? "windows" : "mac";
|
|
17
|
+
const sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", releaseId, platformDir));
|
|
18
|
+
if (sealed.manifest.platform !== platform) throw new Error("sealed release platform mismatch");
|
|
19
|
+
const installers = sealed.manifest.files.filter((file) => file.role === "installer");
|
|
20
|
+
if (installers.length !== 1) throw new Error(`sealed release must contain exactly one installer; found ${installers.length}`);
|
|
21
|
+
const releaseState = stateRoot ?? path.join(repoRoot, ".right-release", "state", releaseId, platformDir, "github");
|
|
22
|
+
mkdirSync(releaseState, { recursive: true });
|
|
23
|
+
const manifestAsset = path.join(releaseState, `${sealed.manifest.app}-${sealed.manifest.version}-${platform}-release-manifest.json`);
|
|
24
|
+
const checksumsAsset = path.join(releaseState, `${sealed.manifest.app}-${sealed.manifest.version}-${platform}-SHA256SUMS.txt`);
|
|
25
|
+
copyFileSync(sealed.manifestPath, manifestAsset);
|
|
26
|
+
writeFileSync(checksumsAsset, `${installers[0].sha256} ${installers[0].name}\n`);
|
|
27
|
+
const installer = path.join(sealed.sealedDir, installers[0].name);
|
|
28
|
+
return {
|
|
29
|
+
sealed,
|
|
30
|
+
installer,
|
|
31
|
+
assets: [installer, manifestAsset, checksumsAsset],
|
|
32
|
+
tag: `v${sealed.manifest.version}`,
|
|
33
|
+
title: `${sealed.manifest.app === "cutright" ? "CutRight Studio" : sealed.manifest.app} ${sealed.manifest.version}`,
|
|
34
|
+
notes: [
|
|
35
|
+
`Signed ${platform === "mac" ? "& notarized universal macOS" : "Windows"} installer.`,
|
|
36
|
+
"",
|
|
37
|
+
`Build commit: \`${sealed.manifest.commit}\``,
|
|
38
|
+
`SHA-256: \`${installers[0].sha256}\``,
|
|
39
|
+
].join("\n"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function publishGitHubRelease(plan, { repo, dryRun = false, run = runCommand } = {}) {
|
|
44
|
+
const visibility = JSON.parse(run("gh", ["repo", "view", repo, "--json", "visibility"]));
|
|
45
|
+
if (visibility.visibility !== "PUBLIC") throw new Error(`GitHub releases require a public repository: ${repo}`);
|
|
46
|
+
verifyPlatformTrust(plan);
|
|
47
|
+
const existing = run("gh", ["release", "view", plan.tag, "--repo", repo, "--json", "tagName"], { allowFailure: true });
|
|
48
|
+
if (dryRun) return { status: existing.ok ? "would-update" : "would-create", tag: plan.tag, assets: plan.assets };
|
|
49
|
+
if (!existing.ok) {
|
|
50
|
+
const notesFile = `${plan.assets[1]}.notes.md`;
|
|
51
|
+
writeFileSync(notesFile, `${plan.notes}\n`);
|
|
52
|
+
run("gh", ["release", "create", plan.tag, "--repo", repo, "--title", plan.title, "--notes-file", notesFile]);
|
|
53
|
+
rmSync(notesFile, { force: true });
|
|
54
|
+
}
|
|
55
|
+
run("gh", ["release", "upload", plan.tag, ...plan.assets, "--repo", repo, "--clobber"]);
|
|
56
|
+
for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
|
|
57
|
+
return { status: "verified", tag: plan.tag, assets: plan.assets };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function verifyPlatformTrust(plan) {
|
|
61
|
+
if (plan.sealed.manifest.platform !== "mac") return;
|
|
62
|
+
runCommand("spctl", ["--assess", "--type", "open", "--context", "context:primary-signature", "--verbose=2", plan.installer]);
|
|
63
|
+
runCommand("xcrun", ["stapler", "validate", plan.installer]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function verifyRemoteAsset(tag, repo, asset, run) {
|
|
67
|
+
const verifyDir = `${path.dirname(asset)}/verify-${path.basename(asset)}`;
|
|
68
|
+
rmSync(verifyDir, { recursive: true, force: true });
|
|
69
|
+
mkdirSync(verifyDir, { recursive: true });
|
|
70
|
+
run("gh", ["release", "download", tag, "--repo", repo, "--pattern", path.basename(asset), "--dir", verifyDir]);
|
|
71
|
+
const downloaded = path.join(verifyDir, path.basename(asset));
|
|
72
|
+
if (!existsSync(downloaded) || sha256(downloaded) !== sha256(asset)) throw new Error(`GitHub release asset hash mismatch: ${path.basename(asset)}`);
|
|
73
|
+
rmSync(verifyDir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sha256(file) {
|
|
77
|
+
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runCommand(command, args, { allowFailure = false } = {}) {
|
|
81
|
+
const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
|
|
82
|
+
const output = result.stdout?.trim() ?? "";
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
if (allowFailure) return { ok: false, output, error: result.stderr?.trim() ?? "" };
|
|
85
|
+
throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
|
|
86
|
+
}
|
|
87
|
+
return allowFailure ? { ok: true, output } : output;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function usage(code) {
|
|
91
|
+
console.log("usage: right-release github --release <sealed-id> --platform mac|win --repo owner/repo [--dry-run]");
|
|
92
|
+
process.exit(code);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
96
|
+
const args = process.argv.slice(2);
|
|
97
|
+
const options = { platform: process.platform === "win32" ? "win" : "mac", releaseId: "", repo: "", dryRun: false };
|
|
98
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
99
|
+
if (args[index] === "--platform") options.platform = args[++index];
|
|
100
|
+
else if (args[index] === "--release") options.releaseId = args[++index];
|
|
101
|
+
else if (args[index] === "--repo") options.repo = args[++index];
|
|
102
|
+
else if (args[index] === "--dry-run") options.dryRun = true;
|
|
103
|
+
else if (args[index] === "-h" || args[index] === "--help") usage(0);
|
|
104
|
+
else throw new Error(`unknown argument: ${args[index]}`);
|
|
105
|
+
}
|
|
106
|
+
if (!options.releaseId || !options.repo) usage(2);
|
|
107
|
+
const repoRoot = runCommand("git", ["rev-parse", "--show-toplevel"]);
|
|
108
|
+
const plan = prepareGitHubRelease({ repoRoot, ...options });
|
|
109
|
+
const result = publishGitHubRelease(plan, options);
|
|
110
|
+
console.log(`right-release github: ${result.status} ${result.tag}`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
|
|
8
|
+
|
|
9
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
10
|
+
|
|
11
|
+
function fixture() {
|
|
12
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "right-release-github-"));
|
|
13
|
+
const releaseId = "cutright-1.2.3-abcdef12";
|
|
14
|
+
const sealedDir = path.join(root, ".right-release", "sealed", releaseId, "windows");
|
|
15
|
+
mkdirSync(sealedDir, { recursive: true });
|
|
16
|
+
writeFileSync(path.join(sealedDir, "CutRight.exe"), "signed-installer");
|
|
17
|
+
writeFileSync(path.join(sealedDir, "release-manifest.json"), `${JSON.stringify({
|
|
18
|
+
schema: 1, releaseId, app: "cutright", version: "1.2.3", commit: "abcdef1234567890", platform: "win",
|
|
19
|
+
files: [{ role: "installer", name: "CutRight.exe", sha256: sha256("signed-installer"), sizeBytes: 16 }],
|
|
20
|
+
checkpoints: ["sealed"],
|
|
21
|
+
})}\n`);
|
|
22
|
+
return { root, releaseId };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("GitHub plan derives tag, notes, installer, manifest & checksums only from sealed bytes", () => {
|
|
26
|
+
const fx = fixture();
|
|
27
|
+
const plan = prepareGitHubRelease({ repoRoot: fx.root, releaseId: fx.releaseId, platform: "win", repo: "Orthic-Labs/CutRight" });
|
|
28
|
+
assert.equal(plan.tag, "v1.2.3");
|
|
29
|
+
assert.equal(plan.assets.length, 3);
|
|
30
|
+
assert.match(plan.notes, /abcdef1234567890/);
|
|
31
|
+
assert.equal(readFileSync(plan.assets[2], "utf8"), `${sha256("signed-installer")} CutRight.exe\n`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("dry-run checks public visibility without creating or uploading a release", () => {
|
|
35
|
+
const fx = fixture();
|
|
36
|
+
const plan = prepareGitHubRelease({ repoRoot: fx.root, releaseId: fx.releaseId, platform: "win", repo: "Orthic-Labs/CutRight" });
|
|
37
|
+
const calls = [];
|
|
38
|
+
const run = (_command, args, options = {}) => {
|
|
39
|
+
calls.push(args);
|
|
40
|
+
if (args[0] === "repo") return JSON.stringify({ visibility: "PUBLIC" });
|
|
41
|
+
if (args[0] === "release" && args[1] === "view") return options.allowFailure ? { ok: false } : "";
|
|
42
|
+
throw new Error(`unexpected mutation: ${args.join(" ")}`);
|
|
43
|
+
};
|
|
44
|
+
assert.equal(publishGitHubRelease(plan, { repo: "Orthic-Labs/CutRight", dryRun: true, run }).status, "would-create");
|
|
45
|
+
assert.equal(calls.length, 2);
|
|
46
|
+
});
|
package/heavy-command.mjs
CHANGED
|
@@ -15,8 +15,8 @@ export function heavyWorkRoot({ platform = process.platform, env = process.env,
|
|
|
15
15
|
if (!env.LOCALAPPDATA) throw new Error("LOCALAPPDATA is required for the RightSuite heavy-work guard");
|
|
16
16
|
return path.win32.resolve(env.LOCALAPPDATA, "RightSuite", "heavy-work");
|
|
17
17
|
}
|
|
18
|
-
if (platform === "darwin" || platform === "mac") return path.join(home, "Library", "Caches", "RightSuite", "heavy-work");
|
|
19
|
-
return path.resolve(env.XDG_CACHE_HOME || path.join(home, ".cache"), "rightsuite", "heavy-work");
|
|
18
|
+
if (platform === "darwin" || platform === "mac") return path.posix.join(home, "Library", "Caches", "RightSuite", "heavy-work");
|
|
19
|
+
return path.posix.resolve(env.XDG_CACHE_HOME || path.posix.join(home, ".cache"), "rightsuite", "heavy-work");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function heavyCommandEnvironment(env = process.env) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.54",
|
|
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.test.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
verifySealedRelease,
|
|
18
18
|
} from "./release-state.mjs";
|
|
19
19
|
|
|
20
|
+
const expectedPnpmVersion = JSON.parse(readFileSync(new URL("./rightkit-versions.json", import.meta.url), "utf8")).packageManager.replace(/^pnpm@/, "");
|
|
21
|
+
|
|
20
22
|
test("progress watcher observes writes in nested Cargo target directories", async () => {
|
|
21
23
|
const root = mkdtempSync(path.join(os.tmpdir(), "right-release-watch-"));
|
|
22
24
|
const nested = path.join(root, "release", "build", "openssl");
|
|
@@ -36,7 +38,7 @@ test("progress watcher observes writes in nested Cargo target directories", asyn
|
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
test("portable command capture resolves Windows command shims", { skip: process.platform !== "win32" }, () => {
|
|
39
|
-
assert.
|
|
41
|
+
assert.equal(commandOutputPortable("pnpm", ["--version"]), expectedPnpmVersion);
|
|
40
42
|
});
|
|
41
43
|
|
|
42
44
|
test("nested app configs keep the vault at repo root and build from the app root", () => {
|
|
@@ -64,7 +64,13 @@ function assertReleasePackageScripts(scripts, label) {
|
|
|
64
64
|
|
|
65
65
|
function assertMacPackageEntry(packageCommand, scripts, label) {
|
|
66
66
|
assert.equal(packageCommand.cmd, "pnpm", `${label} must use the shared pnpm package entry point`);
|
|
67
|
-
assert.
|
|
67
|
+
assert.ok(
|
|
68
|
+
[
|
|
69
|
+
["run", "rightkit:package:mac"],
|
|
70
|
+
["run", "rightkit:package:mac", "--", "--notarize"],
|
|
71
|
+
].some((allowed) => JSON.stringify(packageCommand.args) === JSON.stringify(allowed)),
|
|
72
|
+
`${label} R2 release must use the dedicated signed and notarized macOS package entry point`,
|
|
73
|
+
);
|
|
68
74
|
const implementation = scripts["rightkit:package:mac"];
|
|
69
75
|
assert.equal(typeof implementation, "string", `${label} must define rightkit:package:mac`);
|
|
70
76
|
assert.doesNotMatch(implementation, /\bright-release\b/, `${label} rightkit:package:mac must package directly instead of recursing into the release build`);
|
|
@@ -417,6 +423,11 @@ test("release package scripts reject a generic direct Tauri alias but allow name
|
|
|
417
423
|
test("mac release config uses a dedicated non-recursive package entry point", () => {
|
|
418
424
|
const command = { cmd: "pnpm", args: ["run", "rightkit:package:mac"] };
|
|
419
425
|
assert.doesNotThrow(() => assertMacPackageEntry(command, { "rightkit:package:mac": "node scripts/mac-dmg.mjs --notarize" }, "fixture"));
|
|
426
|
+
assert.doesNotThrow(() => assertMacPackageEntry(
|
|
427
|
+
{ cmd: "pnpm", args: ["run", "rightkit:package:mac", "--", "--notarize"] },
|
|
428
|
+
{ "rightkit:package:mac": "node scripts/mac-dmg.mjs" },
|
|
429
|
+
"fixture",
|
|
430
|
+
));
|
|
420
431
|
assert.throws(
|
|
421
432
|
() => assertMacPackageEntry(command, { "rightkit:package:mac": "right-release build --platform mac" }, "fixture"),
|
|
422
433
|
/instead of recursing into the release build/i,
|
|
@@ -450,10 +461,10 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
450
461
|
"@rightkit/legal": "0.3.0",
|
|
451
462
|
"@rightkit/legal-ui": "0.1.0",
|
|
452
463
|
"@rightkit/license": "0.1.6",
|
|
453
|
-
"@rightkit/release": "0.2.
|
|
464
|
+
"@rightkit/release": "0.2.54",
|
|
454
465
|
});
|
|
455
466
|
assert.deepEqual(versions.legacyNpm, {
|
|
456
|
-
"@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"],
|
|
467
|
+
"@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"],
|
|
457
468
|
});
|
|
458
469
|
assert.ok(
|
|
459
470
|
new Set([versions.npm["@rightkit/release"], ...versions.legacyNpm["@rightkit/release"]]).has("0.2.42"),
|
|
@@ -641,12 +652,21 @@ for (const app of macOnlyApps) {
|
|
|
641
652
|
cmd: "right-release",
|
|
642
653
|
args: ["publish-update", "--config", "right-release.config.mjs", "--platform", "mac"],
|
|
643
654
|
});
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
655
|
+
const updaters = config.targets.mac.updater.artifacts;
|
|
656
|
+
assert.deepEqual(
|
|
657
|
+
updaters.map(({ platform }) => platform).sort(),
|
|
658
|
+
["darwin-aarch64", "darwin-x86_64"],
|
|
659
|
+
`${app.key} universal DMG must serve both Mac architectures`,
|
|
660
|
+
);
|
|
661
|
+
const updater = updaters[0];
|
|
647
662
|
assert.match(updater.file, /\.dmg$/);
|
|
648
663
|
assert.equal(updater.signature, `${updater.file}.sig`);
|
|
649
664
|
assert.equal(updater.key, `${app.key}/updates/mac/current/ScreenRight.dmg`);
|
|
665
|
+
for (const candidate of updaters) {
|
|
666
|
+
assert.equal(candidate.file, updater.file, `${app.key} updater rows must share one universal DMG`);
|
|
667
|
+
assert.equal(candidate.signature, updater.signature, `${app.key} updater rows must share one signature`);
|
|
668
|
+
assert.equal(candidate.key, updater.key, `${app.key} updater rows must share one current R2 object`);
|
|
669
|
+
}
|
|
650
670
|
assertBuildInputs(config, config.targets.mac, `${app.key} mac`);
|
|
651
671
|
assertMacPackageEntry(config.targets.mac.package, pkg.scripts, app.key);
|
|
652
672
|
assert.ok(config.targets.mac.installer.artifacts.length >= 1);
|
package/rightkit-versions.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"packageManager": "pnpm@11.18.0",
|
|
4
4
|
"npm": {
|
|
5
|
+
"@rightkit/git": "0.2.0",
|
|
5
6
|
"@rightkit/legal": "0.2.0",
|
|
6
7
|
"@rightkit/license": "0.1.5",
|
|
7
8
|
"@rightkit/logs": "0.1.3",
|
|
@@ -15,10 +16,10 @@
|
|
|
15
16
|
"@rightkit/legal": "0.3.0",
|
|
16
17
|
"@rightkit/legal-ui": "0.1.0",
|
|
17
18
|
"@rightkit/license": "0.1.6",
|
|
18
|
-
"@rightkit/release": "0.2.
|
|
19
|
+
"@rightkit/release": "0.2.54"
|
|
19
20
|
},
|
|
20
21
|
"legacyNpm": {
|
|
21
|
-
"@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"]
|
|
22
|
+
"@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"]
|
|
22
23
|
},
|
|
23
24
|
"cargo": {
|
|
24
25
|
"rightkit-license": "0.1.2",
|