@rightkit/release 0.2.65 → 0.2.67

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.
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { cancelAndReapManagedRequest, firstManagedRequestId, latestManagedRequestId, releaseProgressLimits, terminalResultFromOutput } from "./progress-control.mjs";
5
+
6
+ test("release progress stays bounded without killing a healthy long native build at 90 minutes", () => {
7
+ assert.deepEqual(releaseProgressLimits({}), { inactivityMs: 600_000, absoluteMs: 14_400_000 });
8
+ assert.deepEqual(releaseProgressLimits({ RIGHT_RELEASE_STALL_MS: "1000", RIGHT_RELEASE_ABSOLUTE_MS: "5000" }), { inactivityMs: 1_000, absoluteMs: 5_000 });
9
+ assert.throws(() => releaseProgressLimits({ RIGHT_RELEASE_STALL_MS: "1000", RIGHT_RELEASE_ABSOLUTE_MS: "1000" }), /must exceed/);
10
+ assert.throws(() => releaseProgressLimits({ RIGHT_RELEASE_STALL_MS: "nope" }), /positive finite/);
11
+ });
12
+
13
+ test("latest managed request id follows streamed root Cargo requests", () => {
14
+ const first = "11111111-1111-4111-8111-111111111111";
15
+ const second = "22222222-2222-4222-8222-222222222222";
16
+ assert.equal(latestManagedRequestId(`rightkit: request ${first}\nrightkit managed-agent: request ${second}`), second);
17
+ assert.equal(firstManagedRequestId(`rightkit: request ${first}\nrightkit managed-agent: request ${second}`), first);
18
+ assert.equal(latestManagedRequestId("ordinary output", first), first);
19
+ });
20
+
21
+ test("managed cancellation issues CANCEL, waits for terminal RESULT, & returns reap receipt", async () => {
22
+ const id = "33333333-3333-4333-8333-333333333333";
23
+ const calls = [];
24
+ const run = async (args) => {
25
+ calls.push(args);
26
+ if (args[0] === "cancel") return { code: 0, output: JSON.stringify({ type: "CANCEL_REQUESTED", id }) };
27
+ return { code: 130, output: `compiler tail\n${JSON.stringify({ type: "RESULT", id, status: "CANCELLED", exitCode: 130, receipt: { reap: { reaped: true } } })}\n` };
28
+ };
29
+ assert.deepEqual(await cancelAndReapManagedRequest(id, { run }), {
30
+ status: "CANCELLED", id, exitCode: 130, receipt: { reap: { reaped: true } }, cancelExitCode: 0, attachExitCode: 130,
31
+ });
32
+ assert.deepEqual(calls, [["cancel", id], ["attach", id]]);
33
+ });
34
+
35
+ test("missing terminal broker result fails closed", async () => {
36
+ const run = async () => ({ code: 125, output: "broker closed", timedOut: false });
37
+ await assert.rejects(cancelAndReapManagedRequest("44444444-4444-4444-8444-444444444444", { run }), /lacked terminal RESULT/);
38
+ assert.equal(terminalResultFromOutput("noise\n"), null);
39
+ });
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { basename, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const packageRoot = fileURLToPath(new URL('.', import.meta.url));
10
+
11
+ async function files(root, relative = '') {
12
+ const entries = await readdir(join(root, relative), { withFileTypes: true });
13
+ const result = [];
14
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
15
+ const path = relative ? `${relative}/${entry.name}` : entry.name;
16
+ if (entry.isDirectory()) result.push(...await files(root, path));
17
+ else if (entry.isFile()) result.push(path);
18
+ }
19
+ return result;
20
+ }
21
+
22
+ export async function packedTree(root) {
23
+ const paths = await files(root);
24
+ const values = [];
25
+ for (const path of paths) {
26
+ const bytes = await readFile(join(root, path));
27
+ values.push([path, createHash('sha256').update(bytes).digest('hex')]);
28
+ }
29
+ return values;
30
+ }
31
+
32
+ export function comparePackedTrees(local, published) {
33
+ const left = JSON.stringify(local); const right = JSON.stringify(published);
34
+ if (left === right) return Object.freeze({ equal: true, differences: [] });
35
+ const localMap = new Map(local); const publishedMap = new Map(published);
36
+ const differences = [...new Set([...localMap.keys(), ...publishedMap.keys()])]
37
+ .sort()
38
+ .filter((path) => localMap.get(path) !== publishedMap.get(path));
39
+ return Object.freeze({ equal: false, differences: Object.freeze(differences) });
40
+ }
41
+
42
+ async function extract(archive, destination) {
43
+ await mkdir(destination, { recursive: true });
44
+ execFileSync('tar', ['-xzf', archive, '-C', destination], { stdio: 'pipe' });
45
+ return join(destination, 'package');
46
+ }
47
+
48
+ export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl = fetch } = {}) {
49
+ const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
50
+ const encodedName = encodeURIComponent(manifest.name);
51
+ const metadata = await fetchImpl(`https://registry.npmjs.org/${encodedName}/${manifest.version}`);
52
+ if (metadata.status === 404 && allowUnpublished) return Object.freeze({ status: 'UNPUBLISHED', name: manifest.name, version: manifest.version });
53
+ if (!metadata.ok) throw new Error(`registry metadata failed: HTTP ${metadata.status}`);
54
+ const record = await metadata.json();
55
+ if (typeof record?.dist?.tarball !== 'string') throw new Error('registry metadata omitted dist.tarball');
56
+ const root = await mkdtemp(join(tmpdir(), 'rightkit-release-parity-'));
57
+ try {
58
+ const localDir = join(root, 'local'); const publishedArchive = join(root, 'published.tgz');
59
+ await mkdir(localDir, { recursive: true });
60
+ const packed = JSON.parse(execFileSync('pnpm', ['pack', '--pack-destination', localDir, '--json'], { cwd: packageRoot, encoding: 'utf8' }));
61
+ const localArchive = join(localDir, basename(packed[0]?.filename ?? ''));
62
+ const response = await fetchImpl(record.dist.tarball);
63
+ if (!response.ok) throw new Error(`registry tarball failed: HTTP ${response.status}`);
64
+ await writeFile(publishedArchive, Buffer.from(await response.arrayBuffer()));
65
+ const localTree = await packedTree(await extract(localArchive, join(root, 'local-tree')));
66
+ const publishedTree = await packedTree(await extract(publishedArchive, join(root, 'published-tree')));
67
+ const comparison = comparePackedTrees(localTree, publishedTree);
68
+ if (!comparison.equal) throw new Error(`published ${manifest.name}@${manifest.version} differs from source pack: ${comparison.differences.join(', ')}`);
69
+ return Object.freeze({ status: 'MATCH', name: manifest.name, version: manifest.version, files: localTree.length });
70
+ } finally {
71
+ await rm(root, { recursive: true, force: true });
72
+ }
73
+ }
74
+
75
+ if (import.meta.url === `file://${process.argv[1]}`) {
76
+ verifyRegistryParity({ allowUnpublished: process.argv.includes('--allow-unpublished') })
77
+ .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
78
+ .catch((error) => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; });
79
+ }
@@ -0,0 +1,30 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { comparePackedTrees, verifyRegistryParity } from './registry-parity.mjs';
4
+
5
+ test('registry parity rejects same-version packed-byte drift', () => {
6
+ assert.deepEqual(comparePackedTrees([['build-release.mjs', 'new']], [['build-release.mjs', 'old']]), {
7
+ equal: false,
8
+ differences: ['build-release.mjs'],
9
+ });
10
+ });
11
+
12
+ test('registry parity accepts identical packed trees', () => {
13
+ assert.deepEqual(comparePackedTrees([['a.mjs', 'one'], ['package.json', 'two']], [['a.mjs', 'one'], ['package.json', 'two']]), {
14
+ equal: true,
15
+ differences: [],
16
+ });
17
+ });
18
+
19
+ test('registry parity distinguishes an unpublished version from registry failure', async () => {
20
+ const notFound = async () => ({ status: 404, ok: false });
21
+ assert.deepEqual(await verifyRegistryParity({ allowUnpublished: true, fetchImpl: notFound }), {
22
+ status: 'UNPUBLISHED',
23
+ name: '@rightkit/release',
24
+ version: '0.2.67',
25
+ });
26
+ await assert.rejects(
27
+ verifyRegistryParity({ fetchImpl: notFound }),
28
+ /registry metadata failed: HTTP 404/,
29
+ );
30
+ });
package/release-state.mjs CHANGED
@@ -82,7 +82,7 @@ function watchDirectoryTree(root, onProgress, watchers, fallback, { watchFactory
82
82
 
83
83
  export function releaseEnvironment({ root, cacheRoot, platform, architecture, app, cacheKey, kind = "release", appRoot, mode = "legacy", env = process.env }) {
84
84
  if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
85
- if (mode !== "legacy" && mode !== "shared") throw new Error(`invalid cache mode: ${mode}`);
85
+ if (mode !== "legacy" && mode !== "shared" && mode !== "broker") throw new Error(`invalid cache mode: ${mode}`);
86
86
  if (mode === "shared") {
87
87
  if (!cacheRoot || !platform || !architecture || !app || !cacheKey || !appRoot) throw new Error("shared release environment requires cacheRoot, platform, architecture, app, cacheKey, and appRoot");
88
88
  const layout = resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint: cacheKey, kind });
package/release.test.mjs CHANGED
@@ -134,21 +134,39 @@ function lockFixture() {
134
134
  return { dir, config };
135
135
  }
136
136
 
137
+ // This suite must exercise the non-broker path deterministically regardless of
138
+ // whether the machine running the tests is itself broker-managed (this
139
+ // workspace's own agent shells are: the managed cargo shim sits on PATH).
140
+ // Strip both broker signals from the inherited environment before spawning.
141
+ function unmanagedEnv() {
142
+ const sanitized = { ...process.env };
143
+ delete sanitized.RIGHTKIT_BUILD_BROKER_SOCKET;
144
+ const delimiter = process.platform === "win32" ? ";" : ":";
145
+ sanitized.PATH = String(process.env.PATH ?? "")
146
+ .split(delimiter)
147
+ .filter((entry) => !/(?:^|[/\\])(?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin$/i.test(entry))
148
+ .join(delimiter);
149
+ return sanitized;
150
+ }
151
+
137
152
  function run(config, ...args) {
138
153
  return spawnSync(process.execPath, [release, "--config", config, "--platform", "win", "--dry-run", ...args], {
139
154
  encoding: "utf8",
155
+ env: unmanagedEnv(),
140
156
  });
141
157
  }
142
158
 
143
159
  function runRaw(config, ...args) {
144
160
  return spawnSync(process.execPath, [release, "--config", config, ...args], {
145
161
  encoding: "utf8",
162
+ env: unmanagedEnv(),
146
163
  });
147
164
  }
148
165
 
149
166
  function runDoctor(config, ...args) {
150
167
  return spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", hostPlatform, ...args], {
151
168
  encoding: "utf8",
169
+ env: unmanagedEnv(),
152
170
  });
153
171
  }
154
172
 
@@ -284,6 +302,18 @@ test("doctor rejects a real src-tauri target before release work", () => {
284
302
  assert.match(`${result.stdout}\n${result.stderr}`, /target-bridge[\s\S]*real directory[\s\S]*cache migrate/i);
285
303
  });
286
304
 
305
+ test("doctor accepts a real src-tauri target on a broker-managed host instead of proposing cache migrate", () => {
306
+ const config = fixture({ platform: hostPlatform });
307
+ mkdirSync(path.join(path.dirname(config), "src-tauri", "target"));
308
+ const result = spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", hostPlatform], {
309
+ encoding: "utf8",
310
+ env: { ...unmanagedEnv(), RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock" },
311
+ });
312
+ assert.equal(result.status, 0, result.stderr);
313
+ assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /cache migrate/i);
314
+ assert.match(result.stdout, /target-bridge[\s\S]*broker-managed host, left untouched/i);
315
+ });
316
+
287
317
  test("doctor permits unrelated dirt", () => {
288
318
  const config = fixture({ platform: hostPlatform });
289
319
  const root = path.dirname(config);
@@ -47,6 +47,24 @@ const macOnlyApps = [
47
47
  { key: "screenright", root: "tools/screenright", releaseFiles: ["scripts/release-package.mjs"] },
48
48
  ];
49
49
 
50
+ // Consuming suite apps whose packaging/build scripts must resolve Cargo build
51
+ // output from `cargo metadata`'s `target_directory`, never by reading
52
+ // CARGO_TARGET_DIR (or a shell/PowerShell equivalent) directly to locate it.
53
+ // This defect recurred three times across the suite (broker-managed hosts own
54
+ // CARGO_TARGET_DIR, so a stale/wrong value silently reads the wrong tree).
55
+ // rightsites and tools/screenright are intentionally out of scope here.
56
+ const cargoTargetDirGuardApps = [
57
+ "coderight/apps/coderight-tauri",
58
+ "cutright/apps/studio",
59
+ "genright",
60
+ "heardright/tauri-app-next",
61
+ "mailright",
62
+ "membrane/apps/membrane-hub",
63
+ "orthic",
64
+ "scraperight",
65
+ "viewright",
66
+ ];
67
+
50
68
  function assertReleasePackageScripts(scripts, label) {
51
69
  for (const platform of ["mac", "win"]) {
52
70
  assert.equal(scripts[`release:build:${platform}`], `right-release build --platform ${platform}`, `${label} must use the tier-neutral ${platform} build entry point`);
@@ -468,12 +486,12 @@ test("RightKit exposes one current version manifest", () => {
468
486
  "@rightkit/legal": "0.3.0",
469
487
  "@rightkit/legal-ui": "0.1.1",
470
488
  "@rightkit/license": "0.1.6",
471
- "@rightkit/release": "0.2.65",
489
+ "@rightkit/release": "0.2.67",
472
490
  "@rightkit/qa": "0.2.0",
473
491
  });
474
492
  assert.deepEqual(versions.legacyNpm, {
475
493
  "@rightkit/legal-ui": ["0.1.0"],
476
- "@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", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61", "0.2.62", "0.2.63", "0.2.64"],
494
+ "@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", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61", "0.2.62", "0.2.63", "0.2.64", "0.2.65", "0.2.66"],
477
495
  "@rightkit/qa": ["0.1.0"],
478
496
  });
479
497
  assert.ok(
@@ -769,6 +787,113 @@ function readCanonicalCrateVersions() {
769
787
  );
770
788
  }
771
789
 
790
+ const CARGO_TARGET_DIR_GUARD_EXTENSIONS = new Set([".mjs", ".js", ".ts", ".sh", ".ps1"]);
791
+ const CARGO_TARGET_DIR_GUARD_ALLOW_MARKER = /rightkit-allow-cargo-target-dir:\s*\S/;
792
+ // Test/contract files assert *about* the CARGO_TARGET_DIR pattern (e.g. inside
793
+ // a regex literal passed to assert.match/assert.doesNotMatch) rather than
794
+ // locating build output with it; they are not packaging/build scripts.
795
+ const CARGO_TARGET_DIR_GUARD_SKIP_BASENAME = /(?:^test-.*|\.test)\.(?:mjs|js|ts)$/;
796
+
797
+ function cargoTargetDirGuardPatternFor(extension) {
798
+ if (extension === ".ps1") return /\$env:CARGO_TARGET_DIR\b/;
799
+ if (extension === ".sh") return /\$\{?CARGO_TARGET_DIR\b/;
800
+ return /process\.env(?:\.CARGO_TARGET_DIR\b|\[["']CARGO_TARGET_DIR["']\])/;
801
+ }
802
+
803
+ function findCargoTargetDirGuardFiles(root) {
804
+ const found = [];
805
+ const visit = (dir) => {
806
+ if (!existsSync(dir)) return;
807
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
808
+ if (entry.isDirectory() && ["node_modules", "target", "vendor", ".git", ".cache", ".right-release"].includes(entry.name)) continue;
809
+ const full = path.join(dir, entry.name);
810
+ if (entry.isDirectory()) visit(full);
811
+ else if (entry.isFile() && CARGO_TARGET_DIR_GUARD_EXTENSIONS.has(path.extname(entry.name))) found.push(full);
812
+ }
813
+ };
814
+ const scriptsDir = path.join(root, "scripts");
815
+ if (existsSync(scriptsDir)) visit(scriptsDir);
816
+ for (const topLevel of ["package.sh", "package.ps1"]) {
817
+ const candidate = path.join(root, topLevel);
818
+ if (existsSync(candidate)) found.push(candidate);
819
+ }
820
+ return found;
821
+ }
822
+
823
+ test("consuming suite apps resolve Cargo build output from cargo metadata, never CARGO_TARGET_DIR directly", () => {
824
+ for (const appRoot of cargoTargetDirGuardApps) {
825
+ const root = path.join(workspace, appRoot);
826
+ if (!existsSync(root)) continue;
827
+ for (const filePath of findCargoTargetDirGuardFiles(root)) {
828
+ const basename = path.basename(filePath);
829
+ if (CARGO_TARGET_DIR_GUARD_SKIP_BASENAME.test(basename)) continue;
830
+ const extension = path.extname(filePath);
831
+ const pattern = cargoTargetDirGuardPatternFor(extension);
832
+ const lines = readFileSync(filePath, "utf8").split("\n");
833
+ const relativePath = path.relative(workspace, filePath);
834
+ lines.forEach((line, index) => {
835
+ if (!pattern.test(line)) return;
836
+ if (/^\s*(?:\/\/|#)/.test(line)) return; // prose describing the pattern, not code reading it
837
+ const previousLine = index > 0 ? lines[index - 1] : "";
838
+ const allowed = CARGO_TARGET_DIR_GUARD_ALLOW_MARKER.test(line) || CARGO_TARGET_DIR_GUARD_ALLOW_MARKER.test(previousLine);
839
+ assert.ok(
840
+ allowed,
841
+ `${relativePath}:${index + 1} reads CARGO_TARGET_DIR directly to locate Cargo build output: \`${line.trim()}\`\n`
842
+ + "resolve build output from `cargo metadata`'s `target_directory`; the environment is never authoritative for locating output.\n"
843
+ + "add a `rightkit-allow-cargo-target-dir: <reason>` marker on this line (or the line above) only if this use is not locating output.",
844
+ );
845
+ });
846
+ }
847
+ }
848
+ });
849
+
850
+ // The shared resolver helpers themselves must keep asking cargo metadata for
851
+ // target_directory. Guarding only the wrong pattern's reintroduction is not
852
+ // enough — gutting/deleting the correct implementation must also fail here.
853
+ const cargoTargetRootHelpers = [
854
+ "coderight/apps/coderight-tauri/scripts/lib/target-root.mjs",
855
+ "heardright/tauri-app-next/scripts/lib/target-root.mjs",
856
+ "mailright/scripts/lib/target-root.mjs",
857
+ "orthic/scripts/lib/target-root.mjs",
858
+ "viewright/scripts/lib/target-root.mjs",
859
+ "membrane/apps/membrane-hub/scripts/lib/target-root.mjs",
860
+ ];
861
+
862
+ test("shared target-root helpers still resolve build output via cargo metadata's target_directory", () => {
863
+ for (const helperPath of cargoTargetRootHelpers) {
864
+ const appRoot = path.join(workspace, helperPath.split("/scripts/lib/target-root.mjs")[0]);
865
+ if (!existsSync(path.join(appRoot, "package.json"))) continue;
866
+ const fullPath = path.join(workspace, helperPath);
867
+ assert.ok(existsSync(fullPath), `${helperPath} is missing; the app must keep its shared cargo-metadata target resolver`);
868
+ const source = readFileSync(fullPath, "utf8");
869
+ assert.match(source, /cargo/, `${helperPath} must invoke cargo`);
870
+ assert.match(source, /metadata/, `${helperPath} must call cargo metadata`);
871
+ assert.match(source, /target_directory/, `${helperPath} must read target_directory from cargo metadata's output`);
872
+ }
873
+ });
874
+
875
+ // After the app migration lands, every consuming app's target-root helper must
876
+ // become a thin re-export of the shared tools/rightkit resolveTargetRoot
877
+ // (cargo-target.mjs) rather than its own copy of the cargo-metadata call. Until
878
+ // that migration lands, cargoTargetRootHelpers above still point at full local
879
+ // implementations, so this stays skipped to avoid failing the still-unmigrated
880
+ // app repos. Enable it once every app's target-root.mjs is a re-export shim.
881
+ test("consuming app target-root helpers are thin re-export shims, not local resolver re-implementations", { skip: true }, () => {
882
+ const LOCAL_RESOLVER_PATTERN = /function\s+(?:cargoTargetRoot|resolveManagedCargoTarget)\s*\([^)]*\)\s*\{[^}]*cargo[^}]*metadata/s;
883
+ for (const helperPath of cargoTargetRootHelpers) {
884
+ const appRoot = path.join(workspace, helperPath.split("/scripts/lib/target-root.mjs")[0]);
885
+ if (!existsSync(appRoot)) continue;
886
+ const fullPath = path.join(workspace, helperPath);
887
+ if (!existsSync(fullPath)) continue;
888
+ const source = readFileSync(fullPath, "utf8");
889
+ assert.doesNotMatch(
890
+ source,
891
+ LOCAL_RESOLVER_PATTERN,
892
+ `${helperPath} must re-export @rightkit/release's resolveTargetRoot instead of reimplementing the cargo metadata call locally`,
893
+ );
894
+ }
895
+ });
896
+
772
897
  test("Right Suite has no hosted workflow files", () => {
773
898
  for (const root of ["viewright", "scraperight", "heardright", "mailright", "coderight", "genright", "voiceright", "tools/rightkit"]) {
774
899
  const workflowDir = path.join(workspace, root, ".github", "workflows");
@@ -19,7 +19,7 @@
19
19
  "@rightkit/legal": "0.3.0",
20
20
  "@rightkit/legal-ui": "0.1.1",
21
21
  "@rightkit/license": "0.1.6",
22
- "@rightkit/release": "0.2.65",
22
+ "@rightkit/release": "0.2.67",
23
23
  "@rightkit/qa": "0.2.0"
24
24
  },
25
25
  "legacyNpm": {
@@ -47,7 +47,9 @@
47
47
  "0.2.61",
48
48
  "0.2.62",
49
49
  "0.2.63",
50
- "0.2.64"
50
+ "0.2.64",
51
+ "0.2.65",
52
+ "0.2.66"
51
53
  ],
52
54
  "@rightkit/qa": [
53
55
  "0.1.0"
@@ -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.17.0",
25
+ packageManager: "pnpm@11.18.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
@@ -9,12 +9,21 @@ import path from "node:path";
9
9
  * fingerprint change — i.e. every Cargo.lock or version bump — left a stale
10
10
  * link that hard-stopped the next build until someone deleted it by hand.
11
11
  * Omit it to keep the strict refuse-everything behavior.
12
+ * @param brokerManaged True when a build broker owns CARGO_TARGET_DIR on this
13
+ * host. A real (non-symlink) directory at `link` is then not corruption to
14
+ * repair — it is the broker's own build output, or a leftover from before the
15
+ * broker took ownership, and either way is not this bridge's to touch. The
16
+ * non-broker path used to name that same directory "corrupt" and point at
17
+ * `cache migrate`, which renames it away; on a broker host that came within
18
+ * one step of moving ~5.9 GB of warm intermediates the broker still expects
19
+ * to find in place. Leave it untouched instead.
12
20
  */
13
21
  export function createTargetBridge({
14
22
  link,
15
23
  target,
16
24
  ownedRoot,
17
25
  platform = process.platform,
26
+ brokerManaged = false,
18
27
  lstat = lstatSync,
19
28
  mkdir = mkdirSync,
20
29
  realpath = realpathSync,
@@ -29,6 +38,7 @@ export function createTargetBridge({
29
38
  ensure() {
30
39
  let entry = readEntry(link, lstat);
31
40
  if (entry && !entry.isSymbolicLink()) {
41
+ if (brokerManaged) return { created: false, link, target, brokerManaged: true };
32
42
  throw new Error(
33
43
  `primary checkout target is not a symbolic link; refusing to replace it: ${link}\n` +
34
44
  ` It is a real directory holding build output. Run right-release cache migrate before building.`,
@@ -160,6 +160,41 @@ test("rejecting a real target does not create the shared cache destination", ()
160
160
  assert.equal(existsSync(target), false);
161
161
  });
162
162
 
163
+ test("broker-managed: a real target directory is left untouched instead of treated as corruption", async () => {
164
+ const fx = fixture();
165
+ mkdirSync(fx.link);
166
+ writeFileSync(path.join(fx.link, "user.txt"), "keep\n");
167
+ const result = await createTargetBridge({ ...fx, brokerManaged: true }).run(async (bridge) => bridge.ensure());
168
+ assert.equal(result.brokerManaged, true);
169
+ assert.equal(result.created, false);
170
+ // Nothing was removed, replaced, or relinked: the real directory and its
171
+ // content are exactly as they were, and no shared-cache destination was
172
+ // fabricated in its place.
173
+ assert.equal(readFileSync(path.join(fx.link, "user.txt"), "utf8"), "keep\n");
174
+ assert.equal(lstatSync(fx.link).isSymbolicLink(), false);
175
+ });
176
+
177
+ test("broker-managed: release() is a safe no-op after leaving a real target untouched", () => {
178
+ const fx = fixture();
179
+ mkdirSync(fx.link);
180
+ writeFileSync(path.join(fx.link, "user.txt"), "keep\n");
181
+ const bridge = createTargetBridge({ ...fx, brokerManaged: true });
182
+ bridge.ensure();
183
+ assert.equal(bridge.release(), false);
184
+ assert.equal(readFileSync(path.join(fx.link, "user.txt"), "utf8"), "keep\n");
185
+ });
186
+
187
+ test("broker-managed has no effect on the non-broker path: a symlink bridge still behaves exactly as before", async () => {
188
+ const fx = fixture();
189
+ const result = await createTargetBridge({ ...fx, brokerManaged: true }).run(async (bridge) => {
190
+ const ensured = bridge.ensure();
191
+ assert.equal(lstatSync(fx.link).isSymbolicLink(), true);
192
+ return ensured;
193
+ });
194
+ assert.equal(result.created, true);
195
+ assert.equal(result.brokerManaged, undefined);
196
+ });
197
+
163
198
  test("cleanup is idempotent and reports process errors without masking a build failure", async () => {
164
199
  const fx = fixture();
165
200
  const bridge = createTargetBridge(fx);