@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 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 = inputPaths(appRoot, configPath, target);
84
- const dirtyInputs = dirtyBuildInputs(repoRoot, repoBuildInputs(repoRoot, appRoot, target.buildInputs ?? config.buildInputs, requiredInputs), commit);
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.41",
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": {
@@ -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 configPath = path.resolve(opts.config);
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 fixture({ signed = true, publish = false, packageJson, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest } = {}) {
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(new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
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.41");
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");
@@ -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.41",
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",