@rightkit/release 0.2.47 → 0.2.49

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
@@ -28,7 +28,7 @@ import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
28
28
  import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
29
29
 
30
30
  /** Assemble preflight inputs from the app's own files (mirrors release.mjs). */
31
- function buildPreflight({ config, appRoot, repoRoot, platform }) {
31
+ function buildPreflight({ config, configPath, appRoot, repoRoot, platform }) {
32
32
  let version;
33
33
  try {
34
34
  version = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8")).version;
@@ -40,6 +40,7 @@ function buildPreflight({ config, appRoot, repoRoot, platform }) {
40
40
  appRoot,
41
41
  repoRoot,
42
42
  app: config.app,
43
+ configPath,
43
44
  // The version check is the doctor's job: by the time `build` runs, seal
44
45
  // collision is already enforced downstream, and duplicating it here would
45
46
  // reject a legitimate resume of an in-flight build.
@@ -127,7 +128,7 @@ try {
127
128
  // Preflight BEFORE any compile. These preconditions used to be discovered one
128
129
  // per build cycle, minutes deep, with errors that named a symptom instead of a
129
130
  // cause. Blocking problems stop here, in seconds, naming the fix.
130
- const preflight = buildPreflight({ config, appRoot, repoRoot: layout.repoRoot, platform });
131
+ const preflight = buildPreflight({ config, configPath, appRoot, repoRoot: layout.repoRoot, platform });
131
132
  const preflightBlockers = preflightFailures(preflight);
132
133
  if (preflightBlockers.length > 0) {
133
134
  fail(`preflight found ${preflightBlockers.length} blocking problem(s):\n${formatPreflight(preflightBlockers)}`);
package/cache-policy.mjs CHANGED
@@ -21,7 +21,7 @@ import { spawnSync } from "node:child_process";
21
21
 
22
22
  export const CACHE_SCHEMA = 1;
23
23
  export const DEFAULT_TARGET_MAX_BYTES = 12 * 1024 ** 3;
24
- export const DEFAULT_SCCACHE_MAX_BYTES = 8 * 1024 ** 3;
24
+ export const DEFAULT_SCCACHE_MAX_BYTES = 32 * 1024 ** 3;
25
25
  export const DEFAULT_DESIRED_FREE_BYTES = 60 * 1024 ** 3;
26
26
  export const DEFAULT_HARD_FREE_BYTES = 25 * 1024 ** 3;
27
27
  // Any build holds release -> suite slot -> GC -> entry lease; prune holds only GC.
@@ -35,7 +35,9 @@ 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.posix.join(home, "Library", "Caches", "RightSuite", "release");
38
+ if (platform === "darwin" || platform === "mac") {
39
+ throw new Error("RIGHT_RELEASE_CACHE_ROOT is required on macOS; set it to external storage");
40
+ }
39
41
  if (platform === "win32" || platform === "win") {
40
42
  const local = env.LOCALAPPDATA;
41
43
  if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error("LOCALAPPDATA must be an absolute path for the RightSuite cache");
@@ -92,10 +94,41 @@ export function resolveSharedCacheIdentity({ cargoLockPath, cargoTomlPath, rustc
92
94
  const target = targetTriple || host;
93
95
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(target)) throw new Error("invalid Cargo target triple");
94
96
  const architecture = target.split("-")[0];
95
- const cargoLockSha256 = cargoLockPath && existsSync(cargoLockPath) ? createHash("sha256").update(readFileSync(cargoLockPath)).digest("hex") : "none";
97
+ const cargoLock = cargoLockPath && existsSync(cargoLockPath) ? readFileSync(cargoLockPath, "utf8") : null;
98
+ const cargoLockSha256 = cargoLock === null ? "none" : sha256(cargoLock);
99
+ const cargoBuildGraphSha256 = cargoLock === null ? "none" : sha256(normalizeCargoLockForCache(cargoLock));
96
100
  const nativeFeatures = features ?? cargoNativeFeatures(cargoTomlPath);
97
- const payload = JSON.stringify({ cargoLockSha256, rustc, target, architecture, profile, features: [...nativeFeatures].sort() });
98
- return { cargoLockSha256, rustc, target, architecture, profile, features: [...nativeFeatures].sort(), fingerprint: createHash("sha256").update(payload).digest("hex").slice(0, 16) };
101
+ const payload = JSON.stringify({ cargoBuildGraphSha256, rustc, target, architecture, profile, features: [...nativeFeatures].sort() });
102
+ return { cargoLockSha256, cargoBuildGraphSha256, rustc, target, architecture, profile, features: [...nativeFeatures].sort(), fingerprint: sha256(payload).slice(0, 16) };
103
+ }
104
+
105
+ function sha256(value) {
106
+ return createHash("sha256").update(value).digest("hex");
107
+ }
108
+
109
+ /**
110
+ * Cargo invalidates local crates from their manifests and sources inside a
111
+ * reused target directory. Their package version is release metadata, not a
112
+ * dependency artifact identity. Registry and Git packages retain every byte,
113
+ * including source, version, and checksum.
114
+ */
115
+ function normalizeCargoLockForCache(source) {
116
+ const lines = String(source).replace(/\r\n?/g, "\n").split("\n");
117
+ const normalized = [];
118
+ for (let start = 0; start < lines.length;) {
119
+ if (lines[start].trim() !== "[[package]]") {
120
+ normalized.push(lines[start]);
121
+ start += 1;
122
+ continue;
123
+ }
124
+ let end = start + 1;
125
+ while (end < lines.length && lines[end].trim() !== "[[package]]") end += 1;
126
+ const block = lines.slice(start, end);
127
+ const external = block.some((line) => /^\s*(?:source|checksum)\s*=/.test(line));
128
+ normalized.push(...(external ? block : block.filter((line) => !/^\s*version\s*=/.test(line))));
129
+ start = end;
130
+ }
131
+ return normalized.join("\n");
99
132
  }
100
133
 
101
134
  export function ensureCacheEntry({ layout, metadata = {}, now = new Date() } = {}) {
@@ -57,7 +57,7 @@ function migrationFixture() {
57
57
  }
58
58
 
59
59
  test("cache roots are platform-native and overrides must be absolute", () => {
60
- assert.equal(resolveSharedCacheRoot({ platform: "mac", home: "/Users/test", env: {} }), "/Users/test/Library/Caches/RightSuite/release");
60
+ assert.throws(() => resolveSharedCacheRoot({ platform: "mac", home: "/Users/test", env: {} }), /RIGHT_RELEASE_CACHE_ROOT is required/);
61
61
  assert.equal(resolveSharedCacheRoot({ platform: "win", env: { LOCALAPPDATA: "C:/Users/test/AppData/Local" } }), "C:\\Users\\test\\AppData\\Local\\RightSuite\\Cache\\release");
62
62
  assert.equal(resolveSharedCacheRoot({ platform: "linux", home: "/home/test", xdgCacheHome: "/tmp/xdg", env: {} }), "/tmp/xdg/rightsuite/release");
63
63
  assert.equal(resolveSharedCacheRoot({ platform: "mac", env: { RIGHT_RELEASE_CACHE_ROOT: "/tmp/cache" } }), "/tmp/cache");
@@ -119,6 +119,21 @@ test("build and migration derive identical native-feature cache fingerprints", (
119
119
  assert.notEqual(build.fingerprint, resolveSharedCacheIdentity({ ...inputs, features: [] }).fingerprint);
120
120
  });
121
121
 
122
+ test("cache identity ignores local workspace version bumps but keeps registry dependency identity", () => {
123
+ const base = root();
124
+ const cargoLock = path.join(base, "Cargo.lock");
125
+ mkdirSync(base, { recursive: true });
126
+ const lock = (appVersion, depVersion, checksum) => `version = 4\n\n[[package]]\nname = "fixture"\nversion = "${appVersion}"\ndependencies = [\n "serde",\n]\n\n[[package]]\nname = "serde"\nversion = "${depVersion}"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "${checksum}"\n`;
127
+ const inputs = { cargoLockPath: cargoLock, rustcVerbose: "rustc 1.91.0\nhost: aarch64-apple-darwin", targetTriple: "aarch64-apple-darwin" };
128
+
129
+ writeFileSync(cargoLock, lock("1.0.0", "1.0.0", "aaa"));
130
+ const original = resolveSharedCacheIdentity(inputs);
131
+ writeFileSync(cargoLock, lock("1.0.1", "1.0.0", "aaa"));
132
+ assert.equal(resolveSharedCacheIdentity(inputs).fingerprint, original.fingerprint);
133
+ writeFileSync(cargoLock, lock("1.0.1", "1.0.1", "bbb"));
134
+ assert.notEqual(resolveSharedCacheIdentity(inputs).fingerprint, original.fingerprint);
135
+ });
136
+
122
137
  test("migration moves an actual src-tauri target symlink and keeps dry-run physically pure", () => {
123
138
  const fixture = migrationFixture();
124
139
  const preview = migrateLegacyCache({ ...fixture, dryRun: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.47",
3
+ "version": "0.2.49",
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/preflight.mjs CHANGED
@@ -18,7 +18,7 @@
18
18
  // certs on purpose and timestamps the signature, so "expires in 2 days" is
19
19
  // the healthy state, not a problem.)
20
20
 
21
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
21
+ import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
22
22
  import path from "node:path";
23
23
  import { spawnSync } from "node:child_process";
24
24
 
@@ -86,7 +86,7 @@ export function resolveWindowsPerl({
86
86
  } = {}) {
87
87
  const explicit = [env.OPENSSL_SRC_PERL, env.PERL].filter(Boolean);
88
88
  for (const candidate of [...explicit, ...candidates]) {
89
- if (exists(candidate)) return { perlPath: candidate, dir: path.dirname(candidate) };
89
+ if (exists(candidate)) return { perlPath: candidate, dir: path.win32.dirname(candidate) };
90
90
  }
91
91
  return null;
92
92
  }
@@ -167,6 +167,7 @@ export function collectPreflight({
167
167
  repoRoot,
168
168
  app,
169
169
  version,
170
+ configPath,
170
171
  cargoLockPaths = [],
171
172
  minFreeGb = 25,
172
173
  env = process.env,
@@ -174,6 +175,25 @@ export function collectPreflight({
174
175
  const checks = [];
175
176
  const windows = platform === "win" || platform === "win32";
176
177
 
178
+ if (appRoot) {
179
+ const target = path.join(appRoot, "src-tauri", "target");
180
+ let entry = null;
181
+ try {
182
+ entry = lstatSync(target);
183
+ } catch (error) {
184
+ if (error?.code !== "ENOENT") throw error;
185
+ }
186
+ checks.push(
187
+ !entry || entry.isSymbolicLink()
188
+ ? ok("target-bridge", entry ? "src-tauri/target is a symbolic link" : "src-tauri/target is ready for the shared cache bridge")
189
+ : fail(
190
+ "target-bridge",
191
+ `${target} is a real directory; bridge setup will refuse it`,
192
+ `run right-release cache migrate --config ${JSON.stringify(configPath ?? path.join(appRoot, "right-release.config.mjs"))} --dry-run, then repeat with --apply before building`,
193
+ ),
194
+ );
195
+ }
196
+
177
197
  if (version && app && repoRoot) {
178
198
  const sealedDir = path.join(repoRoot, ".right-release", "sealed");
179
199
  const sealed = nextFreeVersion(sealedDir, app, version);
@@ -25,6 +25,11 @@ test("CLI routes upload and legacy publish only to the upload state machine", ()
25
25
  assert.doesNotMatch(source, /first === "publish"[\s\S]{0,500}run\("release\.mjs"/);
26
26
  });
27
27
 
28
+ test("upload dry-run cannot write a verified marker", () => {
29
+ const source = readFileSync(upload, "utf8");
30
+ assert.match(source, /if \(!dryRun\)\s+writeJson\(verifiedMarker,/);
31
+ });
32
+
28
33
  test("build is tier-neutral and rejects a tier before touching a repository", () => {
29
34
  const result = spawnSync(process.execPath, [build, "--platform", "win", "--tier", "patch"], {
30
35
  cwd: mkdtempSync(path.join(os.tmpdir(), "right-build-cli-")),
package/release-state.mjs CHANGED
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
4
4
  import path from "node:path";
5
- import { DEFAULT_SCCACHE_MAX_BYTES, resolveCacheLayout } from "./cache-policy.mjs";
5
+ import { readCachePolicy, resolveCacheLayout } from "./cache-policy.mjs";
6
6
 
7
7
  export function cacheFingerprint({ cargoLockSha256, rustc, target, architecture, features = [] }) {
8
8
  const payload = JSON.stringify({
@@ -61,17 +61,18 @@ export function watchProgress(paths, onProgress) {
61
61
  };
62
62
  }
63
63
 
64
- export function releaseEnvironment({ root, cacheRoot, platform, architecture, app, cacheKey, kind = "release", appRoot, mode = "legacy" }) {
64
+ export function releaseEnvironment({ root, cacheRoot, platform, architecture, app, cacheKey, kind = "release", appRoot, mode = "legacy", env = process.env }) {
65
65
  if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
66
66
  if (mode !== "legacy" && mode !== "shared") throw new Error(`invalid cache mode: ${mode}`);
67
67
  if (mode === "shared") {
68
68
  if (!cacheRoot || !platform || !architecture || !app || !cacheKey || !appRoot) throw new Error("shared release environment requires cacheRoot, platform, architecture, app, cacheKey, and appRoot");
69
69
  const layout = resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint: cacheKey, kind });
70
+ const policy = readCachePolicy(env);
70
71
  return {
71
72
  CARGO_TARGET_DIR: layout.targetDir,
72
73
  CARGO_HOME: layout.cargoHome,
73
74
  SCCACHE_DIR: layout.sccacheDir,
74
- SCCACHE_CACHE_SIZE: `${DEFAULT_SCCACHE_MAX_BYTES / 1024 ** 3}G`,
75
+ SCCACHE_CACHE_SIZE: `${policy.sccacheMaxBytes / 1024 ** 3}G`,
75
76
  SCCACHE_BASEDIRS: [path.resolve(root), path.resolve(appRoot)].join(path.delimiter),
76
77
  RUSTC_WRAPPER: "sccache",
77
78
  RIGHT_RELEASE_CACHE_OWNER: "rightkit-v2",
@@ -63,6 +63,22 @@ test("shared release environment is isolated from the app vault", () => {
63
63
  assert.match(env.CARGO_TARGET_DIR, /RightSuite[\\/]release[\\/]targets[\\/]mac[\\/]fixture[\\/]abcdef0123456789$/);
64
64
  assert.match(env.CARGO_HOME, /RightSuite[\\/]release[\\/]cargo-home$/);
65
65
  assert.equal(env.RIGHT_RELEASE_CACHE_OWNER, "rightkit-v2");
66
+ assert.equal(env.SCCACHE_CACHE_SIZE, "32G");
67
+ });
68
+
69
+ test("shared release environment honors the cache policy override", () => {
70
+ const env = releaseEnvironment({
71
+ root: "/suite/.right-release",
72
+ cacheRoot: "/cache",
73
+ platform: "mac",
74
+ architecture: "aarch64",
75
+ app: "fixture",
76
+ appRoot: "/suite/fixture",
77
+ cacheKey: "abcdef0123456789",
78
+ mode: "shared",
79
+ env: { RIGHT_RELEASE_SCCACHE_MAX_BYTES: String(48 * 1024 ** 3) },
80
+ });
81
+ assert.equal(env.SCCACHE_CACHE_SIZE, "48G");
66
82
  });
67
83
 
68
84
  function sha256(value) {
package/release.mjs CHANGED
@@ -103,7 +103,7 @@ if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.pl
103
103
  * Assemble preflight inputs from the app's own files. Kept here (not in
104
104
  * preflight.mjs) so the check module stays free of release.mjs's config shape.
105
105
  */
106
- function releasePreflight({ config, root, repoRoot, platform }) {
106
+ function releasePreflight({ config, configPath, root, repoRoot, platform }) {
107
107
  let version;
108
108
  try {
109
109
  version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
@@ -123,6 +123,7 @@ function releasePreflight({ config, root, repoRoot, platform }) {
123
123
  repoRoot,
124
124
  app: config.app,
125
125
  version,
126
+ configPath,
126
127
  cargoLockPaths,
127
128
  });
128
129
  }
@@ -158,7 +159,7 @@ if (opts.doctor) {
158
159
 
159
160
  // Config above, MACHINE below. Printing config never told anyone whether this
160
161
  // box could finish a build; these checks do.
161
- const checks = releasePreflight({ config, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform });
162
+ const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform });
162
163
  console.log("preflight:");
163
164
  console.log(formatPreflight(checks));
164
165
  const failures = preflightFailures(checks);
package/release.test.mjs CHANGED
@@ -10,12 +10,13 @@ const release = fileURLToPath(new URL("./release.mjs", import.meta.url));
10
10
  const cli = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
11
11
  const versions = JSON.parse(readFileSync(new URL("./rightkit-versions.json", import.meta.url), "utf8"));
12
12
  const defaultBuildInputs = { include: ["package.json", "pnpm-lock.yaml", "src-tauri/**"] };
13
+ const hostPlatform = process.platform === "darwin" ? "mac" : process.platform === "win32" ? "win" : process.platform;
13
14
 
14
15
  function git(cwd, ...args) {
15
16
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
16
17
  }
17
18
 
18
- function fixture({ signed = true, publish = false, packageJson, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs } = {}) {
19
+ function fixture({ signed = true, publish = false, packageJson, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win" } = {}) {
19
20
  const fixtureRoot = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
20
21
  const dir = repoCargoConfig ? path.join(fixtureRoot, "apps", "fixture") : fixtureRoot;
21
22
  mkdirSync(dir, { recursive: true });
@@ -25,7 +26,7 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
25
26
  JSON.stringify(
26
27
  packageJson ?? {
27
28
  name: "fixture",
28
- scripts: { "release:patch:win": "right-release --platform win --tier patch" },
29
+ scripts: { [`release:patch:${platform}`]: `right-release --platform ${platform} --tier patch` },
29
30
  packageManager: versions.packageManager,
30
31
  dependencies: {
31
32
  "@rightkit/license": versions.npm["@rightkit/license"],
@@ -45,13 +46,13 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
45
46
  packageManager: "pnpm",
46
47
  checks: [],
47
48
  targets: {
48
- win: {
49
+ [platform]: {
49
50
  ...(buildInputs ? { buildInputs } : {}),
50
51
  signed,
51
52
  package: { cmd: "node", args: ["-e", "process.exit(0)"] },
52
53
  ...(publish ? { publish: { cmd: "node", args: ["publish-update.mjs"] } } : {}),
53
54
  artifacts: [],
54
- sign: { files: ["fixture.exe"] },
55
+ ...(platform === "win" ? { sign: { files: ["fixture.exe"] } } : {}),
55
56
  updater: { artifacts: [{ file: "fixture.exe", signature: "fixture.exe.sig", platform: "windows-x86_64", key: "fixture/fixture.exe" }] },
56
57
  hardening: [],
57
58
  },
@@ -146,7 +147,7 @@ function runRaw(config, ...args) {
146
147
  }
147
148
 
148
149
  function runDoctor(config, ...args) {
149
- return spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", "win", ...args], {
150
+ return spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", hostPlatform, ...args], {
150
151
  encoding: "utf8",
151
152
  });
152
153
  }
@@ -219,10 +220,11 @@ test("rejects hosted workflow files before release work starts", () => {
219
220
 
220
221
  test("doctor accepts an exact crates.io RightKit pin with benign Cargo config", () => {
221
222
  const config = fixture({
223
+ platform: hostPlatform,
222
224
  cargoManifest: `[package]\nname = "fixture"\nversion = "0.0.0"\nedition = "2021"\n[dependencies]\nrightkit-license = "=0.1.2"`,
223
225
  cargoConfig: `[net]\nretry = 2\n[registries.private]\nindex = "https://example.test/index"`,
224
226
  });
225
- const result = run(config, "--doctor");
227
+ const result = runDoctor(config);
226
228
  assert.equal(result.status, 0, result.stderr);
227
229
  assert.match(result.stdout, /right-release/);
228
230
  });
@@ -236,7 +238,7 @@ test("doctor rejects a selected target with missing or empty buildInputs.include
236
238
  });
237
239
 
238
240
  test("doctor passes a clean selected target", () => {
239
- const result = runDoctor(fixture());
241
+ const result = runDoctor(fixture({ platform: hostPlatform }));
240
242
  assert.equal(result.status, 0, result.stderr);
241
243
  // Derived from the version manifest, not hardcoded: a literal here has to be
242
244
  // hand-edited on every bump and fails the suite when someone forgets.
@@ -244,12 +246,20 @@ test("doctor passes a clean selected target", () => {
244
246
  assert.match(result.stdout, new RegExp(`right-release ${expected.replace(/\./g, "\\.")}`));
245
247
  });
246
248
 
249
+ test("doctor rejects a real src-tauri target before release work", () => {
250
+ const config = fixture({ platform: hostPlatform });
251
+ mkdirSync(path.join(path.dirname(config), "src-tauri", "target"));
252
+ const result = runDoctor(config);
253
+ assert.notEqual(result.status, 0);
254
+ assert.match(`${result.stdout}\n${result.stderr}`, /target-bridge[\s\S]*real directory[\s\S]*cache migrate/i);
255
+ });
256
+
247
257
  test("doctor permits unrelated dirt", () => {
248
- const config = fixture();
258
+ const config = fixture({ platform: hostPlatform });
249
259
  const root = path.dirname(config);
250
260
  mkdirSync(path.join(root, "notes"));
251
261
  writeFileSync(path.join(root, "notes", "scratch.md"), "unrelated\n");
252
- const result = run(config, "--doctor");
262
+ const result = runDoctor(config);
253
263
  assert.equal(result.status, 0, result.stderr);
254
264
  });
255
265
 
@@ -302,11 +312,12 @@ test("doctor rejects linked and detached checkouts like build runtime", () => {
302
312
 
303
313
  test("doctor scopes Cargo inspection to the configured app inside a monorepo", () => {
304
314
  const config = fixture({
315
+ platform: hostPlatform,
305
316
  cargoManifest: `[package]\nname = "fixture"\nversion = "0.0.0"\nedition = "2021"\n[dependencies]\nrightkit-license = "=0.1.2"`,
306
317
  repoCargoConfig: `[net]\nretry = 2`,
307
318
  siblingCargoManifest: `[package]\nname = "archived-copy"\nversion = "0.0.0"\n[dependencies]\nrightkit-license = { path = "../../rightkit-license" }`,
308
319
  });
309
- const result = run(config, "--doctor");
320
+ const result = runDoctor(config);
310
321
  assert.equal(result.status, 0, result.stderr);
311
322
  });
312
323
 
@@ -442,6 +442,7 @@ test("RightKit exposes one current version manifest", () => {
442
442
  "@rightkit/legal": "0.3.0",
443
443
  "@rightkit/legal-ui": "0.1.0",
444
444
  "@rightkit/license": "0.1.6",
445
+ "@rightkit/release": "0.2.49",
445
446
  });
446
447
  assert.deepEqual(versions.legacyNpm, {
447
448
  "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46"],
@@ -499,7 +500,14 @@ for (const app of apps) {
499
500
  );
500
501
  if (allowed.size) assert.ok(allowed.has(specifier), `${app.key} ${name} must match a published rollout version`);
501
502
  }
502
- assert.equal(pkg.packageManager, versions.packageManager);
503
+ const releaseVersion = pkg.devDependencies?.["@rightkit/release"];
504
+ const acceptedPackageManagers = releaseVersion === versions.stagedNpm?.["@rightkit/release"]
505
+ ? [versions.packageManager]
506
+ : [versions.packageManager, "pnpm@11.12.0"];
507
+ assert.ok(
508
+ acceptedPackageManagers.includes(pkg.packageManager),
509
+ `${app.key} packageManager must match its @rightkit/release rollout lane`,
510
+ );
503
511
  assert.ok(
504
512
  new Set([
505
513
  versions.npm["@rightkit/release"],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": 1,
3
- "packageManager": "pnpm@11.12.0",
3
+ "packageManager": "pnpm@11.17.0",
4
4
  "npm": {
5
5
  "@rightkit/legal": "0.2.0",
6
6
  "@rightkit/license": "0.1.5",
@@ -14,7 +14,8 @@
14
14
  "stagedNpm": {
15
15
  "@rightkit/legal": "0.3.0",
16
16
  "@rightkit/legal-ui": "0.1.0",
17
- "@rightkit/license": "0.1.6"
17
+ "@rightkit/license": "0.1.6",
18
+ "@rightkit/release": "0.2.49"
18
19
  },
19
20
  "legacyNpm": {
20
21
  "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46"]
@@ -22,7 +22,7 @@ test("standalone verifier clones, installs, doctors from a nested app root, and
22
22
  writeFileSync(path.join(source, "apps", "desktop", "package.json"), JSON.stringify({
23
23
  name: "standalone-fixture",
24
24
  private: true,
25
- packageManager: "pnpm@11.12.0",
25
+ packageManager: "pnpm@11.17.0",
26
26
  scripts: { "release:doctor": "node doctor.mjs" },
27
27
  }), "utf8");
28
28
  writeFileSync(path.join(source, "apps", "desktop", "pnpm-lock.yaml"), "lockfileVersion: '9.0'\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\nimporters:\n .: {}\n", "utf8");
package/target-bridge.mjs CHANGED
@@ -27,8 +27,29 @@ export function createTargetBridge({
27
27
 
28
28
  const bridge = {
29
29
  ensure() {
30
- mkdir(target, { recursive: true });
31
30
  let entry = readEntry(link, lstat);
31
+ if (entry && !entry.isSymbolicLink()) {
32
+ throw new Error(
33
+ `primary checkout target is not a symbolic link; refusing to replace it: ${link}\n` +
34
+ ` It is a real directory holding build output. Run right-release cache migrate before building.`,
35
+ );
36
+ }
37
+ let pointsAtTarget = false;
38
+ if (entry) {
39
+ try {
40
+ pointsAtTarget = sameRealPath(link, target, realpath, platform);
41
+ } catch (error) {
42
+ if (error?.code !== "ENOENT") throw error;
43
+ }
44
+ if (!pointsAtTarget && !isReclaimableLink(link, ownedRoot, realpath, platform)) {
45
+ throw new Error(
46
+ `primary checkout target is not the owned shared cache target; refusing to replace it: ${link}\n` +
47
+ ` It is a symlink pointing outside this app's release cache, so it was not created by RightKit.\n` +
48
+ ` Repoint or remove it yourself, then re-run.`,
49
+ );
50
+ }
51
+ }
52
+ mkdir(target, { recursive: true });
32
53
  if (!entry) {
33
54
  symlink(target, link, platform === "win32" ? "junction" : "dir");
34
55
  entry = lstat(link);
@@ -39,40 +60,17 @@ export function createTargetBridge({
39
60
  ownership = entryIdentity(entry);
40
61
  created = true;
41
62
  } else {
42
- if (!entry.isSymbolicLink()) {
43
- throw new Error(
44
- `primary checkout target is not a symbolic link; refusing to replace it: ${link}\n` +
45
- ` It is a real directory holding build output. Delete it yourself if that output is disposable, then re-run.`,
46
- );
47
- }
48
- // A dangling link (its cache entry was pruned) makes realpath throw, so
49
- // this comparison must tolerate ENOENT rather than crash before the
50
- // reclaim path below gets a chance to handle exactly that case.
51
- let pointsAtTarget = false;
52
- try {
53
- pointsAtTarget = sameRealPath(link, target, realpath, platform);
54
- } catch (error) {
55
- if (error?.code !== "ENOENT") throw error;
56
- }
57
63
  if (!pointsAtTarget) {
58
64
  // Stale link from an earlier fingerprint, or one whose cache entry a
59
65
  // prune already removed. Both are ours to repoint.
60
- if (isReclaimableLink(link, ownedRoot, realpath, platform)) {
61
- unlink(link);
62
- symlink(target, link, platform === "win32" ? "junction" : "dir");
63
- entry = lstat(link);
64
- if (!sameRealPath(link, target, realpath, platform)) {
65
- throw new Error(`RightKit target bridge did not repoint to the owned shared cache target: ${link}`);
66
- }
67
- ownership = entryIdentity(entry);
68
- created = true;
69
- } else {
70
- throw new Error(
71
- `primary checkout target is not the owned shared cache target; refusing to replace it: ${link}\n` +
72
- ` It is a symlink pointing outside this app's release cache, so it was not created by RightKit.\n` +
73
- ` Repoint or remove it yourself, then re-run.`,
74
- );
66
+ unlink(link);
67
+ symlink(target, link, platform === "win32" ? "junction" : "dir");
68
+ entry = lstat(link);
69
+ if (!sameRealPath(link, target, realpath, platform)) {
70
+ throw new Error(`RightKit target bridge did not repoint to the owned shared cache target: ${link}`);
75
71
  }
72
+ ownership = entryIdentity(entry);
73
+ created = true;
76
74
  }
77
75
  }
78
76
  acquired = true;
@@ -133,6 +133,16 @@ test("fails closed and preserves a real target directory with user content", asy
133
133
  assert.equal(readFileSync(path.join(fx.link, "user.txt"), "utf8"), "keep\n");
134
134
  });
135
135
 
136
+ test("rejecting a real target does not create the shared cache destination", () => {
137
+ const root = mkdtempSync(path.join(os.tmpdir(), "rightkit-target-bridge-pure-"));
138
+ const target = path.join(root, "shared", "target");
139
+ const link = path.join(root, "app", "src-tauri", "target");
140
+ mkdirSync(link, { recursive: true });
141
+
142
+ assert.throws(() => createTargetBridge({ link, target }).ensure(), /not a symbolic link/i);
143
+ assert.equal(existsSync(target), false);
144
+ });
145
+
136
146
  test("cleanup is idempotent and reports process errors without masking a build failure", async () => {
137
147
  const fx = fixture();
138
148
  const bridge = createTargetBridge(fx);
@@ -185,6 +195,7 @@ test("a link pointing outside the owned root is still refused", () => {
185
195
 
186
196
  const bridge = createTargetBridge({ link, target: newTarget, ownedRoot });
187
197
  assert.throws(() => bridge.ensure(), /refusing to replace it/);
198
+ assert.equal(existsSync(newTarget), false);
188
199
  assert.equal(realpathSync(link), realpathSync(foreign));
189
200
  rmSync(dir, { recursive: true, force: true });
190
201
  });
@@ -67,8 +67,8 @@ await runUploadStateMachine({
67
67
  discardBackup: async () => rmSync(backupRoot, { recursive: true, force: true }),
68
68
  },
69
69
  });
70
- writeJson(verifiedMarker, { schema: 1, releaseId, platform, tier, verifiedAt: new Date().toISOString() });
71
- console.log(`right-release upload: verified ${releaseId} tier=${tier}`);
70
+ if (!dryRun) writeJson(verifiedMarker, { schema: 1, releaseId, platform, tier, verifiedAt: new Date().toISOString() });
71
+ console.log(`right-release upload: ${dryRun ? "dry-run " : ""}verified ${releaseId} tier=${tier}`);
72
72
 
73
73
  function verifyPlatformTrust(release) {
74
74
  if (dryRun) return;