@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.
- package/addon-command.test.mjs +5 -5
- package/addon-contract.test.mjs +3 -3
- package/build-release.mjs +87 -41
- package/build-release.test.mjs +70 -9
- package/cache-command.mjs +16 -0
- package/cache-command.test.mjs +44 -1
- package/cache-policy.mjs +48 -10
- package/cache-policy.test.mjs +43 -1
- package/cargo-contract.mjs +5 -1
- package/cargo-guard.mjs +53 -7
- package/cargo-guard.test.mjs +64 -1
- package/cargo-target.mjs +47 -0
- package/cargo-target.test.mjs +133 -0
- package/cli/right-release.mjs +1 -0
- package/github-release.test.mjs +3 -3
- package/heavy-command.mjs +13 -7
- package/heavy-command.test.mjs +8 -0
- package/package.json +2 -1
- package/preflight.mjs +13 -5
- package/progress-control.mjs +68 -0
- package/progress-control.test.mjs +39 -0
- package/registry-parity.mjs +79 -0
- package/registry-parity.test.mjs +30 -0
- package/release-state.mjs +1 -1
- package/release.test.mjs +30 -0
- package/right-suite-contract.test.mjs +127 -2
- package/rightkit-versions.json +4 -2
- package/standalone-clone-verify.test.mjs +1 -1
- package/target-bridge.mjs +10 -0
- package/target-bridge.test.mjs +35 -0
|
@@ -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.
|
|
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");
|
package/rightkit-versions.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.`,
|
package/target-bridge.test.mjs
CHANGED
|
@@ -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);
|