@rightkit/release 0.2.41 → 0.2.43
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 +4 -39
- package/build-release.test.mjs +4 -3
- package/cache-command.test.mjs +2 -1
- package/cache-policy.mjs +5 -4
- package/cargo-contract.mjs +1 -1
- package/create-mac-updater.test.mjs +1 -1
- package/package.json +1 -1
- package/release-invocation.mjs +48 -0
- package/release-state.test.mjs +2 -2
- package/release.mjs +33 -2
- package/release.test.mjs +96 -3
- package/right-suite-contract.test.mjs +48 -3
- package/rightkit-versions.json +2 -2
- package/standalone-clone-verify.mjs +22 -1
package/build-release.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { spawn, spawnSync } from "node:child_process";
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
23
|
-
import { assertPrimaryReleaseCheckout, dirtyBuildInputs } from "./release-invocation.mjs";
|
|
23
|
+
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
24
24
|
import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
|
|
25
25
|
import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
|
|
26
26
|
import { createTargetBridge } from "./target-bridge.mjs";
|
|
@@ -80,8 +80,8 @@ try {
|
|
|
80
80
|
}
|
|
81
81
|
const target = config.targets?.[platform];
|
|
82
82
|
if (!target?.package) fail(`${config.app} has no ${platform} package command`);
|
|
83
|
-
const requiredInputs =
|
|
84
|
-
const dirtyInputs = dirtyBuildInputs(repoRoot,
|
|
83
|
+
const { requiredInputs, buildInputs } = resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target });
|
|
84
|
+
const dirtyInputs = dirtyBuildInputs(repoRoot, buildInputs, commit);
|
|
85
85
|
if (dirtyInputs.length > 0) fail(`dirty files can change the packaged app; resolve them before release:\n${dirtyInputs.map((file) => `- ${file}`).join("\n")}`);
|
|
86
86
|
const toolVersions = collectToolVersions(config.packageManager ?? "pnpm");
|
|
87
87
|
const inputHashes = hashInputs(requiredInputs);
|
|
@@ -89,7 +89,7 @@ try {
|
|
|
89
89
|
const cargoToml = requiredInputs.find((file) => /Cargo\.toml$/i.test(file));
|
|
90
90
|
const cacheIdentity = resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
|
|
91
91
|
const cacheKey = cacheIdentity.fingerprint;
|
|
92
|
-
const cacheMode = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" ? "shared" : "legacy");
|
|
92
|
+
const cacheMode = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" || platform === "win" ? "shared" : "legacy");
|
|
93
93
|
if (cacheMode !== "legacy" && cacheMode !== "shared") fail("RIGHT_RELEASE_CACHE_MODE must be legacy or shared");
|
|
94
94
|
const sharedCacheRoot = cacheMode === "shared" ? resolveSharedCacheRoot({ platform, env: process.env }) : undefined;
|
|
95
95
|
const sharedLayout = cacheMode === "shared"
|
|
@@ -203,37 +203,6 @@ try {
|
|
|
203
203
|
lock.release();
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
function inputPaths(appRoot, configPath, target) {
|
|
207
|
-
const candidates = [
|
|
208
|
-
configPath,
|
|
209
|
-
path.join(appRoot, "package.json"),
|
|
210
|
-
path.join(appRoot, "pnpm-lock.yaml"),
|
|
211
|
-
path.join(appRoot, "src-tauri", "Cargo.toml"),
|
|
212
|
-
path.join(appRoot, "src-tauri", "Cargo.lock"),
|
|
213
|
-
...(target.preflight?.files ?? []).map((file) => path.resolve(appRoot, expandEnv(file))),
|
|
214
|
-
];
|
|
215
|
-
const required = [...new Set(candidates)];
|
|
216
|
-
for (const file of required) if (!existsSync(file)) fail(`missing required release input: ${file}`);
|
|
217
|
-
return required;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function repoBuildInputs(repoRoot, appRoot, configured, requiredInputs) {
|
|
221
|
-
if (!Array.isArray(configured?.include) || configured.include.length === 0) {
|
|
222
|
-
fail("release config must declare non-empty buildInputs.include paths");
|
|
223
|
-
}
|
|
224
|
-
const appPrefix = path.relative(repoRoot, appRoot).replaceAll("\\", "/");
|
|
225
|
-
const qualify = (pattern) => path.posix.normalize([appPrefix, pattern.replaceAll("\\", "/").replace(/^\.\//, "")].filter(Boolean).join("/"));
|
|
226
|
-
const required = requiredInputs
|
|
227
|
-
.map((file) => path.relative(repoRoot, file).replaceAll("\\", "/"))
|
|
228
|
-
.filter((file) => file && file !== ".." && !file.startsWith("../") && !path.isAbsolute(file));
|
|
229
|
-
return {
|
|
230
|
-
include: configured.include.map(qualify),
|
|
231
|
-
exclude: (configured.exclude ?? []).map(qualify),
|
|
232
|
-
required,
|
|
233
|
-
json: Object.fromEntries(Object.entries(configured.json ?? {}).map(([file, fields]) => [qualify(file), fields])),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
206
|
function hashInputs(files) {
|
|
238
207
|
return Object.fromEntries(files.map((file) => [path.relative(path.dirname(path.dirname(file)), file), hashFile(file)]));
|
|
239
208
|
}
|
|
@@ -436,10 +405,6 @@ function killTree(pid) {
|
|
|
436
405
|
else { try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch { /* gone */ } } }
|
|
437
406
|
}
|
|
438
407
|
|
|
439
|
-
function expandEnv(value) {
|
|
440
|
-
return String(value).replace(/%([^%]+)%/g, (_, name) => process.env[name] ?? "").replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? "");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
408
|
function writeJson(file, value) {
|
|
444
409
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
445
410
|
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
package/build-release.test.mjs
CHANGED
|
@@ -2,12 +2,13 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
6
|
-
const source = readFileSync(path.join(path.dirname(
|
|
7
|
+
const source = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs"), "utf8");
|
|
7
8
|
|
|
8
|
-
test("macOS builds default to Cache V2 while explicit legacy mode remains available", () => {
|
|
9
|
+
test("macOS and Windows builds default to Cache V2 while explicit legacy mode remains available", () => {
|
|
9
10
|
assert.match(source, /RIGHT_RELEASE_CACHE_MODE/);
|
|
10
|
-
assert.match(source, /platform === "mac" \? "shared" : "legacy"/);
|
|
11
|
+
assert.match(source, /platform === "mac" \|\| platform === "win" \? "shared" : "legacy"/);
|
|
11
12
|
assert.match(source, /cacheMode !== "legacy" && cacheMode !== "shared"/);
|
|
12
13
|
assert.match(source, /resolveSharedCacheRoot/);
|
|
13
14
|
assert.match(source, /acquireSuiteBuildSlot/);
|
package/cache-command.test.mjs
CHANGED
|
@@ -4,8 +4,9 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
|
-
const root = path.dirname(
|
|
9
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const cli = path.join(root, "cli", "right-release.mjs");
|
|
10
11
|
|
|
11
12
|
function run(args, cacheRoot, env = {}) {
|
package/cache-policy.mjs
CHANGED
|
@@ -35,13 +35,13 @@ export function resolveSharedCacheRoot({ platform = process.platform, env = proc
|
|
|
35
35
|
if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
|
|
36
36
|
return resolveForPlatform(override, platform);
|
|
37
37
|
}
|
|
38
|
-
if (platform === "darwin" || platform === "mac") return path.join(home, "Library", "Caches", "RightSuite", "release");
|
|
38
|
+
if (platform === "darwin" || platform === "mac") return path.posix.join(home, "Library", "Caches", "RightSuite", "release");
|
|
39
39
|
if (platform === "win32" || platform === "win") {
|
|
40
40
|
const local = env.LOCALAPPDATA;
|
|
41
41
|
if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error("LOCALAPPDATA must be an absolute path for the RightSuite cache");
|
|
42
42
|
return path.win32.resolve(local, "RightSuite", "Cache", "release");
|
|
43
43
|
}
|
|
44
|
-
return path.resolve(xdgCacheHome || env.XDG_CACHE_HOME || path.join(home, ".cache"), "rightsuite", "release");
|
|
44
|
+
return path.posix.resolve(xdgCacheHome || env.XDG_CACHE_HOME || path.posix.join(home, ".cache"), "rightsuite", "release");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint, kind = "release" } = {}) {
|
|
@@ -417,7 +417,8 @@ function mkdirSafeDescendant(root, dir) {
|
|
|
417
417
|
else mkdirSync(current);
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
|
-
function
|
|
421
|
-
function
|
|
420
|
+
function isWindowsAbsolute(value) { return /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value); }
|
|
421
|
+
function isAbsoluteForPlatform(value) { return isWindowsAbsolute(value) || path.posix.isAbsolute(value); }
|
|
422
|
+
function resolveForPlatform(value) { return isWindowsAbsolute(value) ? path.win32.resolve(value) : path.posix.resolve(value); }
|
|
422
423
|
function readRustcVerbose() { const result = spawnSync("rustc", ["-vV"], { encoding: "utf8", windowsHide: true }); if (result.status !== 0) throw new Error(`rustc -vV failed: ${(result.stderr || result.error?.message || `exit ${result.status}`).trim()}`); return result.stdout.trim(); }
|
|
423
424
|
function cargoNativeFeatures(cargoTomlPath) { if (!cargoTomlPath || !existsSync(cargoTomlPath)) return []; return [...readFileSync(cargoTomlPath, "utf8").matchAll(/features\s*=\s*\[([^\]]+)\]/g)].flatMap((match) => match[1].match(/"([^"]+)"/g) ?? []).map((value) => value.slice(1, -1)).filter((value) => /sqlcipher|openssl/i.test(value)); }
|
package/cargo-contract.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
-
const SKIP_DIRECTORIES = new Set([".audit", ".cache", ".git", ".claude", ".worktrees", "node_modules", "target", "vendor"]);
|
|
6
|
+
const SKIP_DIRECTORIES = new Set([".audit", ".cache", ".git", ".claude", ".right-release", ".worktrees", "node_modules", "target", "vendor"]);
|
|
7
7
|
const CRATES_IO_SOURCES = new Set([
|
|
8
8
|
"registry+https://github.com/rust-lang/crates.io-index",
|
|
9
9
|
"registry+https://index.crates.io/",
|
|
@@ -8,7 +8,7 @@ const helper = fileURLToPath(new URL("./create-mac-updater.mjs", import.meta.url
|
|
|
8
8
|
test("documents the final signed-app to signed-updater sequence in dry-run mode", () => {
|
|
9
9
|
const result = spawnSync(process.execPath, [helper, "--app", "bundle/Test.app", "--output", "bundle/Test.app.tar.gz", "--dry-run"], { encoding: "utf8" });
|
|
10
10
|
assert.equal(result.status, 0, result.stderr);
|
|
11
|
-
assert.match(result.stdout, /COPYFILE_DISABLE=1 tar .*Test\.app\.tar\.gz.*Test\.app
|
|
11
|
+
assert.match(result.stdout, /COPYFILE_DISABLE=1 tar .*Test\.app\.tar\.gz.*Test\.app[\\/]Contents/);
|
|
12
12
|
assert.doesNotMatch(result.stdout, / Test\.app$/m);
|
|
13
13
|
assert.match(result.stdout, /tauri signer sign .*Test\.app\.tar\.gz/);
|
|
14
14
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
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-invocation.mjs
CHANGED
|
@@ -25,6 +25,48 @@ export function assertPrimaryReleaseCheckout(repoRoot) {
|
|
|
25
25
|
if (branch.status !== 0) throw new Error("right-release requires a branch-attached primary Git checkout; detached HEAD is forbidden");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export function resolveConfiguredBuildInputs(config, target, label = "release config") {
|
|
29
|
+
const configured = target?.buildInputs ?? config?.buildInputs;
|
|
30
|
+
if (!Array.isArray(configured?.include) || configured.include.length === 0) {
|
|
31
|
+
throw new Error(`${label} must declare non-empty buildInputs.include paths`);
|
|
32
|
+
}
|
|
33
|
+
return configured;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target, env = process.env }) {
|
|
37
|
+
const requiredInputs = [
|
|
38
|
+
configPath,
|
|
39
|
+
path.join(appRoot, "package.json"),
|
|
40
|
+
path.join(appRoot, "pnpm-lock.yaml"),
|
|
41
|
+
path.join(appRoot, "src-tauri", "Cargo.toml"),
|
|
42
|
+
path.join(appRoot, "src-tauri", "Cargo.lock"),
|
|
43
|
+
...(target?.preflight?.files ?? []).map((file) => path.resolve(appRoot, expandEnv(file, env))),
|
|
44
|
+
].filter((file, index, files) => files.indexOf(file) === index);
|
|
45
|
+
for (const file of requiredInputs) {
|
|
46
|
+
if (!existsSync(file)) throw new Error(`missing required release input: ${file}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const configured = resolveConfiguredBuildInputs(config, target);
|
|
50
|
+
const appPrefix = path.relative(repoRoot, appRoot).replaceAll("\\", "/");
|
|
51
|
+
const qualify = (pattern) => path.posix.normalize([
|
|
52
|
+
appPrefix,
|
|
53
|
+
pattern.replaceAll("\\", "/").replace(/^\.\//, ""),
|
|
54
|
+
].filter(Boolean).join("/"));
|
|
55
|
+
const required = requiredInputs
|
|
56
|
+
.map((file) => path.relative(repoRoot, file).replaceAll("\\", "/"))
|
|
57
|
+
.filter((file) => file && file !== ".." && !file.startsWith("../") && !path.isAbsolute(file));
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
requiredInputs,
|
|
61
|
+
buildInputs: {
|
|
62
|
+
include: configured.include.map(qualify),
|
|
63
|
+
exclude: (configured.exclude ?? []).map(qualify),
|
|
64
|
+
required,
|
|
65
|
+
json: Object.fromEntries(Object.entries(configured.json ?? {}).map(([file, fields]) => [qualify(file), fields])),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
28
70
|
function pathspec(pattern, exclude = false) {
|
|
29
71
|
const normalized = pattern.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
30
72
|
return `:(top,${exclude ? "exclude," : ""}glob)${normalized}`;
|
|
@@ -89,3 +131,9 @@ function statusFiles(repoRoot, include, exclude = []) {
|
|
|
89
131
|
}
|
|
90
132
|
return files;
|
|
91
133
|
}
|
|
134
|
+
|
|
135
|
+
function expandEnv(value, env) {
|
|
136
|
+
return String(value)
|
|
137
|
+
.replace(/%([^%]+)%/g, (_, name) => env[name] ?? "")
|
|
138
|
+
.replace(/\$\{([^}]+)\}/g, (_, name) => env[name] ?? "");
|
|
139
|
+
}
|
package/release-state.test.mjs
CHANGED
|
@@ -60,8 +60,8 @@ test("shared release environment is isolated from the app vault", () => {
|
|
|
60
60
|
cacheKey: "abcdef0123456789",
|
|
61
61
|
mode: "shared",
|
|
62
62
|
});
|
|
63
|
-
assert.match(env.CARGO_TARGET_DIR, /RightSuite
|
|
64
|
-
assert.match(env.CARGO_HOME, /RightSuite
|
|
63
|
+
assert.match(env.CARGO_TARGET_DIR, /RightSuite[\\/]release[\\/]targets[\\/]mac[\\/]fixture[\\/]abcdef0123456789$/);
|
|
64
|
+
assert.match(env.CARGO_HOME, /RightSuite[\\/]release[\\/]cargo-home$/);
|
|
65
65
|
assert.equal(env.RIGHT_RELEASE_CACHE_OWNER, "rightkit-v2");
|
|
66
66
|
});
|
|
67
67
|
|
package/release.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { validateRightKitCargoContract } from "./cargo-contract.mjs";
|
|
8
8
|
import { assertQaBackdoorContract } from "./qa-contract.mjs";
|
|
9
9
|
import { assertLegalReleaseContract } from "./legal-contract.mjs";
|
|
10
|
+
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
10
11
|
|
|
11
12
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -63,7 +64,20 @@ if (opts.upload) fail("combined build+upload was removed; run right-release buil
|
|
|
63
64
|
if (opts.tier && !TIERS.has(opts.tier)) usage(2, `invalid --tier: ${opts.tier} (expected patch|update)`);
|
|
64
65
|
const sccacheVersion = assertSharedSccachePrerequisite();
|
|
65
66
|
|
|
66
|
-
const
|
|
67
|
+
const requestedConfigPath = path.resolve(opts.config);
|
|
68
|
+
const configPath = opts.doctor ? realpathSync(requestedConfigPath) : requestedConfigPath;
|
|
69
|
+
let doctorInvocation;
|
|
70
|
+
if (opts.doctor) {
|
|
71
|
+
const invocationRoot = path.dirname(configPath);
|
|
72
|
+
const repoRoot = git(invocationRoot, ["rev-parse", "--show-toplevel"]);
|
|
73
|
+
assertPrimaryReleaseCheckout(repoRoot);
|
|
74
|
+
const relativeConfig = path.relative(repoRoot, configPath).replaceAll("\\", "/");
|
|
75
|
+
if (relativeConfig.startsWith("../")) fail("release config must live inside the Git repository");
|
|
76
|
+
const commit = git(repoRoot, ["rev-parse", "HEAD"]);
|
|
77
|
+
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
78
|
+
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
79
|
+
doctorInvocation = { repoRoot, commit };
|
|
80
|
+
}
|
|
67
81
|
const config = (await import(pathToFileURL(configPath))).default;
|
|
68
82
|
if (!config) usage(2, `config did not export default: ${configPath}`);
|
|
69
83
|
if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<missing>"} (expected 1)`);
|
|
@@ -85,6 +99,17 @@ if (opts.platform === "win" && !target.sign?.files?.length) {
|
|
|
85
99
|
}
|
|
86
100
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
87
101
|
if (opts.doctor) {
|
|
102
|
+
const { buildInputs } = resolveReleaseBuildInputs({
|
|
103
|
+
repoRoot: doctorInvocation.repoRoot,
|
|
104
|
+
appRoot: root,
|
|
105
|
+
configPath,
|
|
106
|
+
config,
|
|
107
|
+
target,
|
|
108
|
+
});
|
|
109
|
+
const dirtyInputs = dirtyBuildInputs(doctorInvocation.repoRoot, buildInputs, doctorInvocation.commit);
|
|
110
|
+
if (dirtyInputs.length > 0) {
|
|
111
|
+
fail(`dirty files can change the packaged app; resolve them before release:\n${dirtyInputs.map((file) => `- ${file}`).join("\n")}`);
|
|
112
|
+
}
|
|
88
113
|
console.log(`right-release ${VERSION}`);
|
|
89
114
|
console.log(`config: ${configPath}`);
|
|
90
115
|
console.log(`app: ${config.app}`);
|
|
@@ -178,6 +203,12 @@ function fail(message) {
|
|
|
178
203
|
process.exit(1);
|
|
179
204
|
}
|
|
180
205
|
|
|
206
|
+
function git(cwd, runArgs) {
|
|
207
|
+
const result = spawnSync("git", runArgs, { cwd, encoding: "utf8", windowsHide: true });
|
|
208
|
+
if (result.status !== 0) fail(`git ${runArgs.join(" ")} failed: ${(result.stderr ?? result.stdout ?? "").trim()}`);
|
|
209
|
+
return result.stdout.trim();
|
|
210
|
+
}
|
|
211
|
+
|
|
181
212
|
function assertSharedSccachePrerequisite() {
|
|
182
213
|
if (process.env.RIGHT_RELEASE_CACHE_MODE !== "shared") return null;
|
|
183
214
|
const result = spawnSync("sccache", ["--version"], { encoding: "utf8", windowsHide: true });
|
package/release.test.mjs
CHANGED
|
@@ -2,14 +2,20 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
|
|
9
9
|
const release = fileURLToPath(new URL("./release.mjs", import.meta.url));
|
|
10
|
+
const cli = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
|
|
10
11
|
const versions = JSON.parse(readFileSync(new URL("./rightkit-versions.json", import.meta.url), "utf8"));
|
|
12
|
+
const defaultBuildInputs = { include: ["package.json", "pnpm-lock.yaml", "src-tauri/**"] };
|
|
11
13
|
|
|
12
|
-
function
|
|
14
|
+
function git(cwd, ...args) {
|
|
15
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fixture({ signed = true, publish = false, packageJson, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs } = {}) {
|
|
13
19
|
const fixtureRoot = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
|
|
14
20
|
const dir = repoCargoConfig ? path.join(fixtureRoot, "apps", "fixture") : fixtureRoot;
|
|
15
21
|
mkdirSync(dir, { recursive: true });
|
|
@@ -40,6 +46,7 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
|
|
|
40
46
|
checks: [],
|
|
41
47
|
targets: {
|
|
42
48
|
win: {
|
|
49
|
+
...(buildInputs ? { buildInputs } : {}),
|
|
43
50
|
signed,
|
|
44
51
|
package: { cmd: "node", args: ["-e", "process.exit(0)"] },
|
|
45
52
|
...(publish ? { publish: { cmd: "node", args: ["publish-update.mjs"] } } : {}),
|
|
@@ -51,6 +58,12 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
|
|
|
51
58
|
},
|
|
52
59
|
})};\n`,
|
|
53
60
|
);
|
|
61
|
+
writeFileSync(path.join(dir, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n", "utf8");
|
|
62
|
+
const tauriDir = path.join(dir, "src-tauri");
|
|
63
|
+
mkdirSync(tauriDir, { recursive: true });
|
|
64
|
+
writeFileSync(path.join(tauriDir, "Cargo.toml"), `[package]\nname = "fixture-tauri"\nversion = "0.0.0"\nedition = "2021"\n[lib]\npath = "lib.rs"\n`, "utf8");
|
|
65
|
+
writeFileSync(path.join(tauriDir, "Cargo.lock"), "# fixture lock\n", "utf8");
|
|
66
|
+
writeFileSync(path.join(tauriDir, "lib.rs"), "pub fn fixture() {}\n", "utf8");
|
|
54
67
|
if (cargoManifest) {
|
|
55
68
|
writeFileSync(path.join(dir, "Cargo.toml"), `${cargoManifest}\n[lib]\npath = "fixture.rs"\n`, "utf8");
|
|
56
69
|
writeFileSync(path.join(dir, "fixture.rs"), "", "utf8");
|
|
@@ -61,7 +74,6 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
|
|
|
61
74
|
writeFileSync(path.join(cargoDir, "config.toml"), cargoConfig, "utf8");
|
|
62
75
|
}
|
|
63
76
|
if (repoCargoConfig) {
|
|
64
|
-
mkdirSync(path.join(fixtureRoot, ".git"), { recursive: true });
|
|
65
77
|
const cargoDir = path.join(fixtureRoot, ".cargo");
|
|
66
78
|
mkdirSync(cargoDir, { recursive: true });
|
|
67
79
|
writeFileSync(path.join(cargoDir, "config.toml"), repoCargoConfig, "utf8");
|
|
@@ -71,6 +83,11 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
|
|
|
71
83
|
mkdirSync(sibling, { recursive: true });
|
|
72
84
|
writeFileSync(path.join(sibling, "Cargo.toml"), siblingCargoManifest, "utf8");
|
|
73
85
|
}
|
|
86
|
+
git(fixtureRoot, "init", "--initial-branch", "main");
|
|
87
|
+
git(fixtureRoot, "config", "user.email", "release-test@example.com");
|
|
88
|
+
git(fixtureRoot, "config", "user.name", "Release Test");
|
|
89
|
+
git(fixtureRoot, "add", ".");
|
|
90
|
+
git(fixtureRoot, "commit", "-m", "initial");
|
|
74
91
|
return config;
|
|
75
92
|
}
|
|
76
93
|
|
|
@@ -128,6 +145,12 @@ function runRaw(config, ...args) {
|
|
|
128
145
|
});
|
|
129
146
|
}
|
|
130
147
|
|
|
148
|
+
function runDoctor(config, ...args) {
|
|
149
|
+
return spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", "win", ...args], {
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
131
154
|
test("accepts a tier-neutral internal build", () => {
|
|
132
155
|
const result = run(fixture());
|
|
133
156
|
assert.equal(result.status, 0, result.stderr);
|
|
@@ -204,6 +227,76 @@ test("doctor accepts an exact crates.io RightKit pin with benign Cargo config",
|
|
|
204
227
|
assert.match(result.stdout, /right-release/);
|
|
205
228
|
});
|
|
206
229
|
|
|
230
|
+
test("doctor rejects a selected target with missing or empty buildInputs.include", () => {
|
|
231
|
+
for (const buildInputs of [null, { include: [] }]) {
|
|
232
|
+
const result = run(fixture({ buildInputs }), "--doctor");
|
|
233
|
+
assert.notEqual(result.status, 0);
|
|
234
|
+
assert.match(result.stderr, /non-empty buildInputs\.include paths/i);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("doctor passes a clean selected target", () => {
|
|
239
|
+
const result = runDoctor(fixture());
|
|
240
|
+
assert.equal(result.status, 0, result.stderr);
|
|
241
|
+
assert.match(result.stdout, /right-release 0\.2\.43/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("doctor permits unrelated dirt", () => {
|
|
245
|
+
const config = fixture();
|
|
246
|
+
const root = path.dirname(config);
|
|
247
|
+
mkdirSync(path.join(root, "notes"));
|
|
248
|
+
writeFileSync(path.join(root, "notes", "scratch.md"), "unrelated\n");
|
|
249
|
+
const result = run(config, "--doctor");
|
|
250
|
+
assert.equal(result.status, 0, result.stderr);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("doctor rejects tracked dirt inside declared buildInputs with the exact path", () => {
|
|
254
|
+
const config = fixture();
|
|
255
|
+
writeFileSync(path.join(path.dirname(config), "src-tauri", "lib.rs"), "pub fn changed() {}\n");
|
|
256
|
+
const result = run(config, "--doctor");
|
|
257
|
+
assert.notEqual(result.status, 0);
|
|
258
|
+
assert.match(result.stderr, /dirty files can change the packaged app[\s\S]*- src-tauri\/lib\.rs/i);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("doctor rejects untracked dirt inside declared buildInputs with the exact path", () => {
|
|
262
|
+
const config = fixture();
|
|
263
|
+
writeFileSync(path.join(path.dirname(config), "src-tauri", "new-runtime.rs"), "pub fn new_runtime() {}\n");
|
|
264
|
+
const result = run(config, "--doctor");
|
|
265
|
+
assert.notEqual(result.status, 0);
|
|
266
|
+
assert.match(result.stderr, /dirty files can change the packaged app[\s\S]*- src-tauri\/new-runtime\.rs/i);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("doctor rejects dirty required release metadata even when it is not declared", () => {
|
|
270
|
+
const config = fixture({ buildInputs: { include: ["src-tauri/lib.rs"] } });
|
|
271
|
+
writeFileSync(path.join(path.dirname(config), "pnpm-lock.yaml"), "lockfileVersion: '9.1'\n");
|
|
272
|
+
const result = run(config, "--doctor");
|
|
273
|
+
assert.notEqual(result.status, 0);
|
|
274
|
+
assert.match(result.stderr, /dirty files can change the packaged app[\s\S]*- pnpm-lock\.yaml/i);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("doctor rejects a dirty release config before importing it", () => {
|
|
278
|
+
const config = fixture();
|
|
279
|
+
writeFileSync(config, `${readFileSync(config, "utf8")}\n// dirty config\n`);
|
|
280
|
+
const result = run(config, "--doctor");
|
|
281
|
+
assert.notEqual(result.status, 0);
|
|
282
|
+
assert.match(result.stderr, /dirty release config cannot be executed[\s\S]*- right-release\.config\.mjs/i);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("doctor rejects linked and detached checkouts like build runtime", () => {
|
|
286
|
+
const primaryConfig = fixture();
|
|
287
|
+
const primary = path.dirname(primaryConfig);
|
|
288
|
+
const linked = `${primary}-linked`;
|
|
289
|
+
git(primary, "worktree", "add", "--detach", linked);
|
|
290
|
+
const linkedResult = run(path.join(linked, "right-release.config.mjs"), "--doctor");
|
|
291
|
+
assert.notEqual(linkedResult.status, 0);
|
|
292
|
+
assert.match(linkedResult.stderr, /must be invoked from the primary Git worktree/i);
|
|
293
|
+
|
|
294
|
+
git(primary, "checkout", "--detach");
|
|
295
|
+
const detachedResult = run(primaryConfig, "--doctor");
|
|
296
|
+
assert.notEqual(detachedResult.status, 0);
|
|
297
|
+
assert.match(detachedResult.stderr, /detached HEAD is forbidden/i);
|
|
298
|
+
});
|
|
299
|
+
|
|
207
300
|
test("doctor scopes Cargo inspection to the configured app inside a monorepo", () => {
|
|
208
301
|
const config = fixture({
|
|
209
302
|
cargoManifest: `[package]\nname = "fixture"\nversion = "0.0.0"\nedition = "2021"\n[dependencies]\nrightkit-license = "=0.1.2"`,
|
|
@@ -11,8 +11,12 @@ import {
|
|
|
11
11
|
validateRightKitCargoContract,
|
|
12
12
|
} from "./cargo-contract.mjs";
|
|
13
13
|
import { assertAsrAdapterPair } from "./asr-artifact-adoption.mjs";
|
|
14
|
+
import { resolveConfiguredBuildInputs } from "./release-invocation.mjs";
|
|
14
15
|
|
|
15
|
-
const workspace = path.resolve(
|
|
16
|
+
const workspace = path.resolve(
|
|
17
|
+
process.env.RIGHT_SUITE_CONTRACT_WORKSPACE
|
|
18
|
+
?? new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"),
|
|
19
|
+
);
|
|
16
20
|
// The Right Suite web layer's repo + filesystem path is `rightsites` post the 2026-07-15 naming lock;
|
|
17
21
|
// a not-yet-renamed checkout may still have `rightapps`. Resolve whichever exists so the contract
|
|
18
22
|
// passes on both. (The runtime service namespace stays `rightapps` — that is deliberately untouched.)
|
|
@@ -59,6 +63,10 @@ function assertMacPackageEntry(packageCommand, scripts, label) {
|
|
|
59
63
|
assert.doesNotMatch(implementation, /\bpnpm(?:\s+run)?\s+rightkit:package:mac\b/, `${label} rightkit:package:mac must not recurse into itself`);
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
function assertBuildInputs(config, target, label) {
|
|
67
|
+
resolveConfiguredBuildInputs(config, target, label);
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
function resolveCargoVersionContract(versionManifest, canonicalVersions) {
|
|
63
71
|
const consumer = new Map(Object.entries(versionManifest.cargo ?? {}));
|
|
64
72
|
const staged = new Map(Object.entries(versionManifest.stagedCargo ?? {}));
|
|
@@ -182,6 +190,28 @@ test("Cargo override contract accepts exact registry pins without repo-local ove
|
|
|
182
190
|
}), 0);
|
|
183
191
|
});
|
|
184
192
|
|
|
193
|
+
test("Cargo contract ignores release caches and build outputs", () => {
|
|
194
|
+
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "rightkit-cargo-scan-"));
|
|
195
|
+
const appRoot = path.join(fixtureRoot, "app");
|
|
196
|
+
mkdirSync(path.join(appRoot, ".right-release", "cache", "cargo-home", "registry", "cached"), { recursive: true });
|
|
197
|
+
writeFileSync(
|
|
198
|
+
path.join(appRoot, "Cargo.toml"),
|
|
199
|
+
'[package]\nname = "fixture"\nversion = "0.0.0"\nedition = "2021"\n[lib]\npath = "fixture.rs"\n',
|
|
200
|
+
"utf8",
|
|
201
|
+
);
|
|
202
|
+
writeFileSync(path.join(appRoot, "fixture.rs"), "", "utf8");
|
|
203
|
+
writeFileSync(
|
|
204
|
+
path.join(appRoot, ".right-release", "cache", "cargo-home", "registry", "cached", "Cargo.toml"),
|
|
205
|
+
"this is deliberately invalid cached TOML",
|
|
206
|
+
"utf8",
|
|
207
|
+
);
|
|
208
|
+
try {
|
|
209
|
+
assert.equal(validateRightKitCargoContract(appRoot, new Map(), "fixture"), 1);
|
|
210
|
+
} finally {
|
|
211
|
+
rmSync(fixtureRoot, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
185
215
|
test("Cargo override contract rejects RightKit patches and replacements in repo-local Cargo config", () => {
|
|
186
216
|
for (const config of [
|
|
187
217
|
{ localConfig: `[patch.crates-io]\nrightkit-license = { path = "../rightkit-license" }` },
|
|
@@ -389,10 +419,20 @@ test("mac release config uses a dedicated non-recursive package entry point", ()
|
|
|
389
419
|
);
|
|
390
420
|
});
|
|
391
421
|
|
|
422
|
+
test("every app platform requires effective non-empty build inputs", () => {
|
|
423
|
+
assert.doesNotThrow(() => assertBuildInputs(
|
|
424
|
+
{ buildInputs: { include: ["src/**"] } },
|
|
425
|
+
{},
|
|
426
|
+
"fixture mac",
|
|
427
|
+
));
|
|
428
|
+
assert.throws(() => assertBuildInputs({}, {}, "fixture win"), /fixture win must declare non-empty buildInputs\.include paths/);
|
|
429
|
+
assert.throws(() => assertBuildInputs({}, { buildInputs: { include: [] } }, "fixture mac"), /fixture mac must declare non-empty buildInputs\.include paths/);
|
|
430
|
+
});
|
|
431
|
+
|
|
392
432
|
test("RightKit exposes one current version manifest", () => {
|
|
393
433
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
394
434
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
395
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
435
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.43");
|
|
396
436
|
assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
|
|
397
437
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
398
438
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
@@ -404,8 +444,12 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
404
444
|
"@rightkit/license": "0.1.6",
|
|
405
445
|
});
|
|
406
446
|
assert.deepEqual(versions.legacyNpm, {
|
|
407
|
-
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31"],
|
|
447
|
+
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42"],
|
|
408
448
|
});
|
|
449
|
+
assert.ok(
|
|
450
|
+
new Set([versions.npm["@rightkit/release"], ...versions.legacyNpm["@rightkit/release"]]).has("0.2.42"),
|
|
451
|
+
"the previously published @rightkit/release 0.2.42 must remain accepted during the 0.2.43 rollout",
|
|
452
|
+
);
|
|
409
453
|
const licensePackage = JSON.parse(readFileSync(
|
|
410
454
|
path.join(workspace, "tools/rightkit/packages/license/package.json"),
|
|
411
455
|
"utf8",
|
|
@@ -486,6 +530,7 @@ for (const app of apps) {
|
|
|
486
530
|
assert.ok(config.version);
|
|
487
531
|
for (const platform of ["mac", "win"]) {
|
|
488
532
|
const target = config.targets[platform];
|
|
533
|
+
assertBuildInputs(config, target, `${app.key} ${platform}`);
|
|
489
534
|
assert.equal(target.signed, true);
|
|
490
535
|
assert.equal(target.upload, undefined, "generic uploads bypass tier manifest routing");
|
|
491
536
|
assert.equal(target.publish.cmd, "right-release");
|
package/rightkit-versions.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"@rightkit/logs": "0.1.3",
|
|
8
8
|
"@rightkit/platform-ui": "0.1.0",
|
|
9
9
|
"@rightkit/qa": "0.1.0",
|
|
10
|
-
"@rightkit/release": "0.2.
|
|
10
|
+
"@rightkit/release": "0.2.43",
|
|
11
11
|
"@rightkit/tauri": "0.1.0",
|
|
12
12
|
"@rightkit/updates": "0.2.3"
|
|
13
13
|
},
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@rightkit/license": "0.1.6"
|
|
18
18
|
},
|
|
19
19
|
"legacyNpm": {
|
|
20
|
-
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31"]
|
|
20
|
+
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42"]
|
|
21
21
|
},
|
|
22
22
|
"cargo": {
|
|
23
23
|
"rightkit-license": "0.1.2",
|
|
@@ -32,8 +32,29 @@ function run(command, args, cwd) {
|
|
|
32
32
|
function runPnpm(args, cwd, command) {
|
|
33
33
|
if (process.platform !== "win32" || command !== "pnpm") return run(command, args, cwd);
|
|
34
34
|
for (const entry of String(process.env.PATH ?? "").split(path.delimiter)) {
|
|
35
|
-
const
|
|
35
|
+
const bin = entry.replace(/^"|"$/g, "");
|
|
36
|
+
const cli = path.join(bin, "node_modules", "pnpm", "bin", "pnpm.mjs");
|
|
36
37
|
if (existsSync(cli)) return run(process.execPath, [cli, ...args], cwd);
|
|
38
|
+
const shim = path.join(bin, "pnpm.cmd");
|
|
39
|
+
if (existsSync(shim)) {
|
|
40
|
+
if (!args.every((value) => /^[A-Za-z0-9._:@+=-]+$/.test(value))) throw new Error("unsafe pnpm argument");
|
|
41
|
+
const commandLine = `"${shim}" ${args.join(" ")}`;
|
|
42
|
+
const result = spawnSync(commandLine, {
|
|
43
|
+
cwd,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
47
|
+
env: { ...process.env, CI: "1", GIT_TERMINAL_PROMPT: "0" },
|
|
48
|
+
shell: true,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
command: commandLine,
|
|
52
|
+
status: result.status,
|
|
53
|
+
stdout: String(result.stdout ?? "").trim().slice(-8000),
|
|
54
|
+
stderr: String(result.stderr ?? "").trim().slice(-8000),
|
|
55
|
+
error: result.error?.message,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
37
58
|
}
|
|
38
59
|
throw new Error("pnpm installation could not be resolved from PATH");
|
|
39
60
|
}
|