@rightkit/release 0.2.39 → 0.2.40
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-invocation-contract.test.mjs +33 -5
- package/build-release.mjs +24 -31
- package/build-release.test.mjs +20 -0
- package/package.json +1 -1
- package/release-invocation.mjs +21 -7
- package/release-state.mjs +7 -0
- package/release-state.test.mjs +34 -0
- package/right-suite-contract.test.mjs +78 -11
- package/rightkit-versions.json +1 -1
|
@@ -53,15 +53,28 @@ test("allows release invocation from the primary Git checkout", () => {
|
|
|
53
53
|
assert.doesNotThrow(() => assertPrimaryReleaseCheckout(primary));
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
test("
|
|
56
|
+
test("rejects a detached HEAD even in the primary Git checkout", () => {
|
|
57
|
+
const primary = makeRepo();
|
|
58
|
+
git(primary, "checkout", "--detach");
|
|
59
|
+
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => assertPrimaryReleaseCheckout(primary),
|
|
62
|
+
/detached HEAD is forbidden/i,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("reports a dirty tracked file declared as a build input", () => {
|
|
57
67
|
const primary = makeRepo();
|
|
58
68
|
writeFileSync(path.join(primary, "src", "app.js"), "export const app = false;\n");
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(dirtyBuildInputs(primary, buildInputs), ["src/app.js"]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("reports an untracked file declared as a build input", () => {
|
|
74
|
+
const primary = makeRepo();
|
|
59
75
|
writeFileSync(path.join(primary, "src", "new-runtime.js"), "export {};\n");
|
|
60
76
|
|
|
61
|
-
assert.deepEqual(dirtyBuildInputs(primary, buildInputs), [
|
|
62
|
-
"src/app.js",
|
|
63
|
-
"src/new-runtime.js",
|
|
64
|
-
]);
|
|
77
|
+
assert.deepEqual(dirtyBuildInputs(primary, buildInputs), ["src/new-runtime.js"]);
|
|
65
78
|
});
|
|
66
79
|
|
|
67
80
|
test("ignores dirty files outside the packaged app input set", () => {
|
|
@@ -73,6 +86,21 @@ test("ignores dirty files outside the packaged app input set", () => {
|
|
|
73
86
|
assert.deepEqual(dirtyBuildInputs(primary, buildInputs), []);
|
|
74
87
|
});
|
|
75
88
|
|
|
89
|
+
test("required release metadata cannot be hidden by build-input exclusions", () => {
|
|
90
|
+
const primary = makeRepo();
|
|
91
|
+
writeFileSync(path.join(primary, "package.json"), "{\"version\":\"2.0.0\"}\n");
|
|
92
|
+
|
|
93
|
+
assert.deepEqual(dirtyBuildInputs(primary, {
|
|
94
|
+
...buildInputs,
|
|
95
|
+
exclude: [...buildInputs.exclude, "package.json"],
|
|
96
|
+
required: ["package.json"],
|
|
97
|
+
}), ["package.json"]);
|
|
98
|
+
assert.deepEqual(dirtyBuildInputs(primary, {
|
|
99
|
+
include: ["package.json"],
|
|
100
|
+
required: ["package.json"],
|
|
101
|
+
}), ["package.json"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
76
104
|
test("checks only build-relevant fields in mixed-purpose JSON manifests", () => {
|
|
77
105
|
const primary = makeRepo();
|
|
78
106
|
writeFileSync(path.join(primary, "package.json"), `${JSON.stringify({ scripts: { bakeoff: "node eval.mjs" } })}\n`);
|
package/build-release.mjs
CHANGED
|
@@ -42,12 +42,15 @@ for (let i = 0; i < args.length; i += 1) {
|
|
|
42
42
|
}
|
|
43
43
|
if (platform !== "win" && platform !== "mac") fail("--platform must be win or mac");
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const configPath = path.resolve(configName);
|
|
46
|
+
const invocationRoot = path.dirname(configPath);
|
|
46
47
|
const repoRoot = git(invocationRoot, ["rev-parse", "--show-toplevel"]);
|
|
47
48
|
assertPrimaryReleaseCheckout(repoRoot);
|
|
48
|
-
const relativeConfig = path.relative(repoRoot,
|
|
49
|
+
const relativeConfig = path.relative(repoRoot, configPath).replaceAll("\\", "/");
|
|
49
50
|
if (relativeConfig.startsWith("../")) fail("release config must live inside the Git repository");
|
|
50
|
-
const
|
|
51
|
+
const appRoot = path.dirname(configPath);
|
|
52
|
+
const layout = resolveReleaseLayout({ repoRoot, configPath });
|
|
53
|
+
const vaultRoot = layout.vaultRoot;
|
|
51
54
|
mkdirSync(path.join(vaultRoot, "locks"), { recursive: true });
|
|
52
55
|
const lock = acquireLock(path.join(vaultRoot, "locks", `${platform}.lock.json`));
|
|
53
56
|
let child = null;
|
|
@@ -65,31 +68,20 @@ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
|
65
68
|
|
|
66
69
|
try {
|
|
67
70
|
throwIfInterrupted();
|
|
68
|
-
|
|
69
|
-
const commit = git(repoRoot, ["rev-parse", dryRun ? "HEAD" : "origin/main"]);
|
|
71
|
+
const commit = git(repoRoot, ["rev-parse", "HEAD"]);
|
|
70
72
|
const shortCommit = commit.slice(0, 8);
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
mkdirSync(path.dirname(worktree), { recursive: true });
|
|
75
|
-
runChecked("git", ["worktree", "add", "--detach", worktree, commit], repoRoot);
|
|
76
|
-
} else if (git(worktree, ["rev-parse", "HEAD"]) !== commit) {
|
|
77
|
-
fail(`release worktree exists at the wrong commit: ${worktree}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const worktreeConfigPath = path.join(worktree, relativeConfig);
|
|
81
|
-
const appRoot = path.dirname(worktreeConfigPath);
|
|
82
|
-
const layout = resolveReleaseLayout({ repoRoot, configPath: path.resolve(configName) });
|
|
83
|
-
const config = (await import(`${pathToFileURL(worktreeConfigPath).href}?commit=${commit}`)).default;
|
|
73
|
+
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
74
|
+
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
75
|
+
const config = (await import(`${pathToFileURL(configPath).href}?commit=${commit}`)).default;
|
|
84
76
|
if (!config?.app || !config?.version) fail("release config must expose app and version");
|
|
85
77
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(config.app) || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(config.version)) {
|
|
86
78
|
fail("app and version must be safe release-id components");
|
|
87
79
|
}
|
|
88
80
|
const target = config.targets?.[platform];
|
|
89
81
|
if (!target?.package) fail(`${config.app} has no ${platform} package command`);
|
|
90
|
-
const
|
|
82
|
+
const requiredInputs = inputPaths(appRoot, configPath, target);
|
|
83
|
+
const dirtyInputs = dirtyBuildInputs(repoRoot, repoBuildInputs(repoRoot, appRoot, target.buildInputs ?? config.buildInputs, requiredInputs), commit);
|
|
91
84
|
if (dirtyInputs.length > 0) fail(`dirty files can change the packaged app; resolve them before release:\n${dirtyInputs.map((file) => `- ${file}`).join("\n")}`);
|
|
92
|
-
const requiredInputs = inputPaths(appRoot, worktreeConfigPath, target);
|
|
93
85
|
const toolVersions = collectToolVersions(config.packageManager ?? "pnpm");
|
|
94
86
|
const inputHashes = hashInputs(requiredInputs);
|
|
95
87
|
const cargoLock = requiredInputs.find((file) => /Cargo\.lock$/i.test(file));
|
|
@@ -141,6 +133,8 @@ try {
|
|
|
141
133
|
commit,
|
|
142
134
|
platform,
|
|
143
135
|
requiredInputs,
|
|
136
|
+
inputHashes,
|
|
137
|
+
cacheKey,
|
|
144
138
|
ops: {
|
|
145
139
|
preflight: async () => {
|
|
146
140
|
throwIfInterrupted();
|
|
@@ -168,7 +162,7 @@ try {
|
|
|
168
162
|
mkdirSync(cacheTarget, { recursive: true });
|
|
169
163
|
if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
|
|
170
164
|
else if (realpathSync(targetLink) !== realpathSync(cacheTarget)) {
|
|
171
|
-
fail(`
|
|
165
|
+
fail(`primary checkout target exists and is not the release cache link: ${targetLink}`);
|
|
172
166
|
}
|
|
173
167
|
await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget);
|
|
174
168
|
},
|
|
@@ -176,7 +170,7 @@ try {
|
|
|
176
170
|
throwIfInterrupted();
|
|
177
171
|
await runProgress(
|
|
178
172
|
process.execPath,
|
|
179
|
-
[WORKER, "--config",
|
|
173
|
+
[WORKER, "--config", configPath, "--platform", platform, "--no-upload"],
|
|
180
174
|
appRoot,
|
|
181
175
|
env,
|
|
182
176
|
[env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
|
|
@@ -186,7 +180,7 @@ try {
|
|
|
186
180
|
checkpoint(stateRoot, "hardened");
|
|
187
181
|
},
|
|
188
182
|
seal: async ({ sealedDir }) => {
|
|
189
|
-
sealRelease({ configRoot:
|
|
183
|
+
sealRelease({ configRoot: appRoot, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey });
|
|
190
184
|
verifySealedRelease(sealedDir);
|
|
191
185
|
if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
|
|
192
186
|
checkpoint(stateRoot, "sealed");
|
|
@@ -220,20 +214,19 @@ function inputPaths(appRoot, configPath, target) {
|
|
|
220
214
|
return required;
|
|
221
215
|
}
|
|
222
216
|
|
|
223
|
-
function repoBuildInputs(repoRoot, appRoot,
|
|
217
|
+
function repoBuildInputs(repoRoot, appRoot, configured, requiredInputs) {
|
|
224
218
|
if (!Array.isArray(configured?.include) || configured.include.length === 0) {
|
|
225
219
|
fail("release config must declare non-empty buildInputs.include paths");
|
|
226
220
|
}
|
|
227
221
|
const appPrefix = path.relative(repoRoot, appRoot).replaceAll("\\", "/");
|
|
228
|
-
const qualify = (pattern) => [appPrefix, pattern.replaceAll("\\", "/").replace(/^\.\//, "")].filter(Boolean).join("/");
|
|
222
|
+
const qualify = (pattern) => path.posix.normalize([appPrefix, pattern.replaceAll("\\", "/").replace(/^\.\//, "")].filter(Boolean).join("/"));
|
|
223
|
+
const required = requiredInputs
|
|
224
|
+
.map((file) => path.relative(repoRoot, file).replaceAll("\\", "/"))
|
|
225
|
+
.filter((file) => file && file !== ".." && !file.startsWith("../") && !path.isAbsolute(file));
|
|
229
226
|
return {
|
|
230
|
-
include:
|
|
231
|
-
relativeConfig,
|
|
232
|
-
qualify("package.json"),
|
|
233
|
-
qualify("pnpm-lock.yaml"),
|
|
234
|
-
...configured.include.map(qualify),
|
|
235
|
-
],
|
|
227
|
+
include: configured.include.map(qualify),
|
|
236
228
|
exclude: (configured.exclude ?? []).map(qualify),
|
|
229
|
+
required,
|
|
237
230
|
json: Object.fromEntries(Object.entries(configured.json ?? {}).map(([file, fields]) => [qualify(file), fields])),
|
|
238
231
|
};
|
|
239
232
|
}
|
package/build-release.test.mjs
CHANGED
|
@@ -22,3 +22,23 @@ test("macOS builds default to Cache V2 while explicit legacy mode remains availa
|
|
|
22
22
|
assert.match(source, /assertPrimaryReleaseCheckout\(repoRoot\)/);
|
|
23
23
|
assert.match(source, /dirtyBuildInputs\(repoRoot/);
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
test("production builds package from the real primary app checkout", () => {
|
|
27
|
+
assert.match(source, /const configPath = path\.resolve\(configName\)/);
|
|
28
|
+
assert.match(source, /const appRoot = path\.dirname\(configPath\)/);
|
|
29
|
+
assert.match(source, /\[WORKER, "--config", configPath, "--platform", platform, "--no-upload"\]/);
|
|
30
|
+
assert.match(source, /sealRelease\(\{ configRoot: appRoot,/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("production builds never create or use an internal Git worktree", () => {
|
|
34
|
+
assert.doesNotMatch(source, /git", \["worktree", "add"/);
|
|
35
|
+
assert.doesNotMatch(source, /path\.join\(vaultRoot, "worktrees"/);
|
|
36
|
+
assert.doesNotMatch(source, /worktreeConfigPath/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("dirty release config is rejected before the config module is imported", () => {
|
|
40
|
+
const guard = source.indexOf("dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit)");
|
|
41
|
+
const imported = source.indexOf("await import(");
|
|
42
|
+
assert.ok(guard >= 0, "config cleanliness guard must exist");
|
|
43
|
+
assert.ok(guard < imported, "config cleanliness guard must run before module import");
|
|
44
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.40",
|
|
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
|
@@ -21,6 +21,8 @@ export function assertPrimaryReleaseCheckout(repoRoot) {
|
|
|
21
21
|
if (canonical(repoRoot) !== canonical(primary)) {
|
|
22
22
|
throw new Error(`right-release must be invoked from the primary Git worktree: ${primary}`);
|
|
23
23
|
}
|
|
24
|
+
const branch = spawnSync("git", ["symbolic-ref", "--quiet", "HEAD"], { cwd: repoRoot, encoding: "utf8", windowsHide: true });
|
|
25
|
+
if (branch.status !== 0) throw new Error("right-release requires a branch-attached primary Git checkout; detached HEAD is forbidden");
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function pathspec(pattern, exclude = false) {
|
|
@@ -55,9 +57,25 @@ export function dirtyBuildInputs(repoRoot, inputs, baseline = "HEAD") {
|
|
|
55
57
|
if (!Array.isArray(inputs?.include) || inputs.include.length === 0) {
|
|
56
58
|
throw new Error("release config must declare non-empty buildInputs.include paths");
|
|
57
59
|
}
|
|
60
|
+
const required = new Set(inputs.required ?? []);
|
|
61
|
+
const files = [
|
|
62
|
+
...statusFiles(repoRoot, inputs.include, inputs.exclude),
|
|
63
|
+
...statusFiles(repoRoot, [...required], []),
|
|
64
|
+
];
|
|
65
|
+
return [...new Set(files)]
|
|
66
|
+
.filter((file) => required.has(file)
|
|
67
|
+
? fileChangedFromBaseline(repoRoot, file, baseline)
|
|
68
|
+
: inputs.json?.[file]
|
|
69
|
+
? jsonProjectionChanged(repoRoot, file, inputs.json[file], baseline)
|
|
70
|
+
: fileChangedFromBaseline(repoRoot, file, baseline))
|
|
71
|
+
.sort();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function statusFiles(repoRoot, include, exclude = []) {
|
|
75
|
+
if (!include.length) return [];
|
|
58
76
|
const specs = [
|
|
59
|
-
...
|
|
60
|
-
...
|
|
77
|
+
...include.map((pattern) => pathspec(pattern)),
|
|
78
|
+
...exclude.map((pattern) => pathspec(pattern, true)),
|
|
61
79
|
];
|
|
62
80
|
const output = git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...specs]);
|
|
63
81
|
const records = output.split("\0");
|
|
@@ -69,9 +87,5 @@ export function dirtyBuildInputs(repoRoot, inputs, baseline = "HEAD") {
|
|
|
69
87
|
files.push(record.slice(3).replaceAll("\\", "/"));
|
|
70
88
|
if (/[RC]/.test(status)) index += 1;
|
|
71
89
|
}
|
|
72
|
-
return
|
|
73
|
-
.filter((file) => inputs.json?.[file]
|
|
74
|
-
? jsonProjectionChanged(repoRoot, file, inputs.json[file], baseline)
|
|
75
|
-
: fileChangedFromBaseline(repoRoot, file, baseline))
|
|
76
|
-
.sort();
|
|
90
|
+
return files;
|
|
77
91
|
}
|
package/release-state.mjs
CHANGED
|
@@ -111,6 +111,8 @@ export async function runBuildStateMachine({
|
|
|
111
111
|
commit,
|
|
112
112
|
platform,
|
|
113
113
|
requiredInputs = [],
|
|
114
|
+
inputHashes,
|
|
115
|
+
cacheKey,
|
|
114
116
|
ops,
|
|
115
117
|
}) {
|
|
116
118
|
const releaseId = `${app}-${version}-${commit.slice(0, 8)}`;
|
|
@@ -121,6 +123,11 @@ export async function runBuildStateMachine({
|
|
|
121
123
|
if (sealed.manifest.app !== app || sealed.manifest.version !== version || sealed.manifest.commit !== commit) {
|
|
122
124
|
throw new Error(`sealed release identity mismatch: ${releaseId}`);
|
|
123
125
|
}
|
|
126
|
+
if (sealed.manifest.platform !== platform) throw new Error(`sealed release platform identity mismatch: ${releaseId}`);
|
|
127
|
+
if (inputHashes && JSON.stringify(sealed.manifest.inputs) !== JSON.stringify(inputHashes)) {
|
|
128
|
+
throw new Error(`sealed release input identity mismatch: ${releaseId}`);
|
|
129
|
+
}
|
|
130
|
+
if (cacheKey && sealed.manifest.cacheKey !== cacheKey) throw new Error(`sealed release cache identity mismatch: ${releaseId}`);
|
|
124
131
|
return { status: "sealed", resumed: true, releaseId, sealedDir };
|
|
125
132
|
}
|
|
126
133
|
await ops.preflight?.({ root, app, version, commit, platform, requiredInputs, releaseId });
|
package/release-state.test.mjs
CHANGED
|
@@ -198,6 +198,40 @@ test("a valid sealed release resumes without rebuilding", async () => {
|
|
|
198
198
|
assert.equal(builds, 0);
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
+
test("sealed build resume rejects changed input or cache identity", async () => {
|
|
202
|
+
const fx = fixture();
|
|
203
|
+
fx.manifest.inputs = { "src-tauri/Cargo.lock": "old-hash" };
|
|
204
|
+
fx.manifest.cacheKey = "old-cache";
|
|
205
|
+
writeFileSync(path.join(fx.sealedDir, "release-manifest.json"), `${JSON.stringify(fx.manifest, null, 2)}\n`);
|
|
206
|
+
|
|
207
|
+
await assert.rejects(
|
|
208
|
+
runBuildStateMachine({
|
|
209
|
+
root: fx.root,
|
|
210
|
+
app: fx.manifest.app,
|
|
211
|
+
version: fx.manifest.version,
|
|
212
|
+
commit: fx.manifest.commit,
|
|
213
|
+
platform: fx.manifest.platform,
|
|
214
|
+
inputHashes: { "src-tauri/Cargo.lock": "new-hash" },
|
|
215
|
+
cacheKey: fx.manifest.cacheKey,
|
|
216
|
+
ops: {},
|
|
217
|
+
}),
|
|
218
|
+
/sealed release input identity mismatch/i,
|
|
219
|
+
);
|
|
220
|
+
await assert.rejects(
|
|
221
|
+
runBuildStateMachine({
|
|
222
|
+
root: fx.root,
|
|
223
|
+
app: fx.manifest.app,
|
|
224
|
+
version: fx.manifest.version,
|
|
225
|
+
commit: fx.manifest.commit,
|
|
226
|
+
platform: fx.manifest.platform,
|
|
227
|
+
inputHashes: fx.manifest.inputs,
|
|
228
|
+
cacheKey: "new-cache",
|
|
229
|
+
ops: {},
|
|
230
|
+
}),
|
|
231
|
+
/sealed release cache identity mismatch/i,
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
201
235
|
test("manifest or file tampering blocks upload before any mutation", async () => {
|
|
202
236
|
const fx = fixture();
|
|
203
237
|
writeFileSync(fx.installer, "changed");
|
|
@@ -32,6 +32,33 @@ const apps = [
|
|
|
32
32
|
{ key: "coderight", root: "coderight/apps/coderight-tauri", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
+
function assertReleasePackageScripts(scripts, label) {
|
|
36
|
+
for (const platform of ["mac", "win"]) {
|
|
37
|
+
assert.equal(scripts[`release:build:${platform}`], `right-release build --platform ${platform}`, `${label} must use the tier-neutral ${platform} build entry point`);
|
|
38
|
+
for (const tier of ["patch", "update"]) {
|
|
39
|
+
assert.equal(scripts[`release:upload:${tier}:${platform}`], `right-release upload --platform ${platform} --tier ${tier}`, `${label} must select ${tier} only during ${platform} upload`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const genericTauri = scripts.tauri;
|
|
44
|
+
if (typeof genericTauri === "string") {
|
|
45
|
+
assert.doesNotMatch(
|
|
46
|
+
genericTauri.trim(),
|
|
47
|
+
/^(?:(?:pnpm|npm)\s+(?:(?:exec|dlx)\s+)?|npx\s+)?(?:(?:\.\/)?node_modules\/\.bin\/)?(?:@tauri-apps\/cli\s+)?tauri(?:\s|$)/,
|
|
48
|
+
`${label} generic \"tauri\" package script bypasses the RightKit release entry points; use an explicitly named dev, debug, unsigned, or local command instead`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertMacPackageEntry(packageCommand, scripts, label) {
|
|
54
|
+
assert.equal(packageCommand.cmd, "pnpm", `${label} must use the shared pnpm package entry point`);
|
|
55
|
+
assert.deepEqual(packageCommand.args, ["run", "rightkit:package:mac"], `${label} R2 release must use the dedicated signed and notarized macOS package entry point`);
|
|
56
|
+
const implementation = scripts["rightkit:package:mac"];
|
|
57
|
+
assert.equal(typeof implementation, "string", `${label} must define rightkit:package:mac`);
|
|
58
|
+
assert.doesNotMatch(implementation, /\bright-release\b/, `${label} rightkit:package:mac must package directly instead of recursing into the release build`);
|
|
59
|
+
assert.doesNotMatch(implementation, /\bpnpm(?:\s+run)?\s+rightkit:package:mac\b/, `${label} rightkit:package:mac must not recurse into itself`);
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
function resolveCargoVersionContract(versionManifest, canonicalVersions) {
|
|
36
63
|
const consumer = new Map(Object.entries(versionManifest.cargo ?? {}));
|
|
37
64
|
const staged = new Map(Object.entries(versionManifest.stagedCargo ?? {}));
|
|
@@ -314,10 +341,58 @@ test("Cargo version contract rejects staged versions that mismatch canonical man
|
|
|
314
341
|
);
|
|
315
342
|
});
|
|
316
343
|
|
|
344
|
+
test("release package scripts use tier-neutral builds and tiered uploads", () => {
|
|
345
|
+
const scripts = {
|
|
346
|
+
"release:build:mac": "right-release build --platform mac",
|
|
347
|
+
"release:build:win": "right-release build --platform win",
|
|
348
|
+
"release:upload:patch:mac": "right-release upload --platform mac --tier patch",
|
|
349
|
+
"release:upload:patch:win": "right-release upload --platform win --tier patch",
|
|
350
|
+
"release:upload:update:mac": "right-release upload --platform mac --tier update",
|
|
351
|
+
"release:upload:update:win": "right-release upload --platform win --tier update",
|
|
352
|
+
};
|
|
353
|
+
assert.doesNotThrow(() => assertReleasePackageScripts(scripts, "fixture"));
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("release package scripts reject a generic direct Tauri alias but allow named local commands", () => {
|
|
357
|
+
const scripts = {
|
|
358
|
+
"release:build:mac": "right-release build --platform mac",
|
|
359
|
+
"release:build:win": "right-release build --platform win",
|
|
360
|
+
"release:upload:patch:mac": "right-release upload --platform mac --tier patch",
|
|
361
|
+
"release:upload:patch:win": "right-release upload --platform win --tier patch",
|
|
362
|
+
"release:upload:update:mac": "right-release upload --platform mac --tier update",
|
|
363
|
+
"release:upload:update:win": "right-release upload --platform win --tier update",
|
|
364
|
+
"tauri:dev": "tauri dev",
|
|
365
|
+
"mac:build:debug": "tauri build --debug",
|
|
366
|
+
"build:win:unsigned": "tauri build --bundles nsis --no-sign",
|
|
367
|
+
"cargo:check": "cargo check --locked",
|
|
368
|
+
};
|
|
369
|
+
assert.doesNotThrow(() => assertReleasePackageScripts(scripts, "fixture"));
|
|
370
|
+
for (const genericTauri of ["tauri", "pnpm exec tauri", "pnpm dlx tauri", "./node_modules/.bin/tauri"]) {
|
|
371
|
+
assert.throws(
|
|
372
|
+
() => assertReleasePackageScripts({ ...scripts, tauri: genericTauri }, "fixture"),
|
|
373
|
+
/generic \"tauri\" package script bypasses/i,
|
|
374
|
+
genericTauri,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("mac release config uses a dedicated non-recursive package entry point", () => {
|
|
380
|
+
const command = { cmd: "pnpm", args: ["run", "rightkit:package:mac"] };
|
|
381
|
+
assert.doesNotThrow(() => assertMacPackageEntry(command, { "rightkit:package:mac": "node scripts/mac-dmg.mjs --notarize" }, "fixture"));
|
|
382
|
+
assert.throws(
|
|
383
|
+
() => assertMacPackageEntry(command, { "rightkit:package:mac": "right-release build --platform mac" }, "fixture"),
|
|
384
|
+
/instead of recursing into the release build/i,
|
|
385
|
+
);
|
|
386
|
+
assert.throws(
|
|
387
|
+
() => assertMacPackageEntry(command, { "rightkit:package:mac": "pnpm run rightkit:package:mac" }, "fixture"),
|
|
388
|
+
/must not recurse into itself/i,
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
317
392
|
test("RightKit exposes one current version manifest", () => {
|
|
318
393
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
319
394
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
320
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
395
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.40");
|
|
321
396
|
assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
|
|
322
397
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
323
398
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
@@ -399,14 +474,7 @@ for (const app of apps) {
|
|
|
399
474
|
assert.equal(pkg.scripts["release:doctor"], "right-release doctor");
|
|
400
475
|
assert.equal(pkg.scripts["release:mac"], undefined, "tierless release entry points are forbidden");
|
|
401
476
|
assert.equal(pkg.scripts["release:win"], undefined, "tierless release entry points are forbidden");
|
|
402
|
-
|
|
403
|
-
assert.equal(pkg.scripts["release:patch:win"], "right-release --platform win --tier patch");
|
|
404
|
-
assert.equal(pkg.scripts["release:update:mac"], "right-release --platform mac --tier update");
|
|
405
|
-
assert.equal(pkg.scripts["release:update:win"], "right-release --platform win --tier update");
|
|
406
|
-
assert.equal(pkg.scripts["publish:patch:mac"], "right-release publish --platform mac --tier patch");
|
|
407
|
-
assert.equal(pkg.scripts["publish:patch:win"], "right-release publish --platform win --tier patch");
|
|
408
|
-
assert.equal(pkg.scripts["publish:update:mac"], "right-release publish --platform mac --tier update");
|
|
409
|
-
assert.equal(pkg.scripts["publish:update:win"], "right-release publish --platform win --tier update");
|
|
477
|
+
assertReleasePackageScripts(pkg.scripts, app.key);
|
|
410
478
|
assert.equal(pkg.scripts["deps:check"], "right-release deps --check");
|
|
411
479
|
assert.equal(pkg.scripts["deps:update"], "right-release deps --update");
|
|
412
480
|
assert.ok(!Object.entries(pkg.scripts).some(([name, command]) => /^(?:release|publish):/i.test(name) && /unsigned|--no-sign/i.test(command)), `${app.key} release/publish commands must not expose an unsigned macOS DMG mode`);
|
|
@@ -431,8 +499,7 @@ for (const app of apps) {
|
|
|
431
499
|
assert.match(updater.key, /\/updates\/(mac|windows)\/current\//, `${app.key} ${platform} updaters must replace the stable current R2 object`);
|
|
432
500
|
}
|
|
433
501
|
}
|
|
434
|
-
|
|
435
|
-
assert.deepEqual(config.targets.mac.package.args, ["run", "mac:dmg:notarized"], `${app.key} R2 release must use the signed and notarized macOS package entry point`);
|
|
502
|
+
assertMacPackageEntry(config.targets.mac.package, pkg.scripts, app.key);
|
|
436
503
|
for (const installer of config.targets.mac.installer.artifacts) {
|
|
437
504
|
assert.equal(path.dirname(installer.file), ".", `${app.key} macOS installer must be copied to the app package root before upload`);
|
|
438
505
|
}
|
package/rightkit-versions.json
CHANGED