@rightkit/release 0.2.41 → 0.2.42
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 +3 -38
- package/package.json +1 -1
- package/release-invocation.mjs +48 -0
- package/release.mjs +33 -2
- package/release.test.mjs +96 -3
- package/right-suite-contract.test.mjs +26 -3
- package/rightkit-versions.json +2 -2
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);
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.42",
|
|
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.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\.42/);
|
|
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 ?? {}));
|
|
@@ -389,10 +397,20 @@ test("mac release config uses a dedicated non-recursive package entry point", ()
|
|
|
389
397
|
);
|
|
390
398
|
});
|
|
391
399
|
|
|
400
|
+
test("every app platform requires effective non-empty build inputs", () => {
|
|
401
|
+
assert.doesNotThrow(() => assertBuildInputs(
|
|
402
|
+
{ buildInputs: { include: ["src/**"] } },
|
|
403
|
+
{},
|
|
404
|
+
"fixture mac",
|
|
405
|
+
));
|
|
406
|
+
assert.throws(() => assertBuildInputs({}, {}, "fixture win"), /fixture win must declare non-empty buildInputs\.include paths/);
|
|
407
|
+
assert.throws(() => assertBuildInputs({}, { buildInputs: { include: [] } }, "fixture mac"), /fixture mac must declare non-empty buildInputs\.include paths/);
|
|
408
|
+
});
|
|
409
|
+
|
|
392
410
|
test("RightKit exposes one current version manifest", () => {
|
|
393
411
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
394
412
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
395
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
413
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.42");
|
|
396
414
|
assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
|
|
397
415
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
398
416
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
@@ -404,8 +422,12 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
404
422
|
"@rightkit/license": "0.1.6",
|
|
405
423
|
});
|
|
406
424
|
assert.deepEqual(versions.legacyNpm, {
|
|
407
|
-
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31"],
|
|
425
|
+
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41"],
|
|
408
426
|
});
|
|
427
|
+
assert.ok(
|
|
428
|
+
new Set([versions.npm["@rightkit/release"], ...versions.legacyNpm["@rightkit/release"]]).has("0.2.41"),
|
|
429
|
+
"the previously published @rightkit/release 0.2.41 must remain accepted during the 0.2.42 rollout",
|
|
430
|
+
);
|
|
409
431
|
const licensePackage = JSON.parse(readFileSync(
|
|
410
432
|
path.join(workspace, "tools/rightkit/packages/license/package.json"),
|
|
411
433
|
"utf8",
|
|
@@ -486,6 +508,7 @@ for (const app of apps) {
|
|
|
486
508
|
assert.ok(config.version);
|
|
487
509
|
for (const platform of ["mac", "win"]) {
|
|
488
510
|
const target = config.targets[platform];
|
|
511
|
+
assertBuildInputs(config, target, `${app.key} ${platform}`);
|
|
489
512
|
assert.equal(target.signed, true);
|
|
490
513
|
assert.equal(target.upload, undefined, "generic uploads bypass tier manifest routing");
|
|
491
514
|
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.42",
|
|
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"]
|
|
21
21
|
},
|
|
22
22
|
"cargo": {
|
|
23
23
|
"rightkit-license": "0.1.2",
|