@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
package/addon-command.test.mjs
CHANGED
|
@@ -13,8 +13,8 @@ function fixture() {
|
|
|
13
13
|
mkdirSync(path.join(root, "legal")); mkdirSync(path.join(root, "assets"));
|
|
14
14
|
for (const name of ["LICENSE", "EULA.txt", "PRIVACY.md", "THIRD-PARTY-NOTICES.txt"]) writeFileSync(path.join(root, "legal", name), name);
|
|
15
15
|
writeFileSync(path.join(root, "assets", "tab.png"), "icon");
|
|
16
|
-
const files = [["command", "membrane", "out/membrane", true], ["service", "
|
|
17
|
-
writeFileSync(path.join(root, "right-addon.config.mjs"), `export default ${JSON.stringify({ schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: [], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "
|
|
16
|
+
const files = [["command", "membrane", "out/membrane", true], ["service", "membrane-service", "out/membrane-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false], ["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false]].map(([role, name, source, executable]) => ({ role, name, source, executable }));
|
|
17
|
+
writeFileSync(path.join(root, "right-addon.config.mjs"), `export default ${JSON.stringify({ schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: [], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "membrane-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "false", args: [] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "false", args: [] }, signing: { contract: "azure-artifact-signing-v1" }, files } } }, null, 2)};\n`);
|
|
18
18
|
execFileSync("git", ["init", "--initial-branch", "main"], { cwd: root }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: root }); execFileSync("git", ["config", "user.name", "Test"], { cwd: root }); execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-m", "init"], { cwd: root });
|
|
19
19
|
return root;
|
|
20
20
|
}
|
|
@@ -25,7 +25,7 @@ test("add-on dry-run creates no release state and never executes build", () => {
|
|
|
25
25
|
|
|
26
26
|
test("local adoption verifies lock and stages portable plus Tauri-qualified binaries", async () => {
|
|
27
27
|
const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?test=${Date.now()}`)).default;
|
|
28
|
-
mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "
|
|
28
|
+
mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "membrane-service"), "service");
|
|
29
29
|
const manifest = createAddonManifest({ config, root, platform: "mac", commit: "d".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
|
|
30
30
|
const sealed = path.join(root, "sealed"); mkdirSync(sealed);
|
|
31
31
|
for (const file of manifest.files) writeFileSync(path.join(sealed, file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source)));
|
|
@@ -44,11 +44,11 @@ test("local adoption verifies lock and stages portable plus Tauri-qualified bina
|
|
|
44
44
|
|
|
45
45
|
test("remote downloader accepts only immutable manifest plus file routes", async () => {
|
|
46
46
|
const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?remote=${Date.now()}`)).default;
|
|
47
|
-
mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "
|
|
47
|
+
mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "membrane-service"), "service");
|
|
48
48
|
const manifest = createAddonManifest({ config, root, platform: "mac", commit: "e".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
|
|
49
49
|
const digest = addonManifestSha256(manifest); const base = `https://example.test/membrane/addons/mac/sha256/${digest}/`; const bytes = new Map([[`${base}addon-manifest.json`, Buffer.from(canonicalAddonManifest(manifest))], ...manifest.files.map((file) => [base + encodeURIComponent(file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source))])]);
|
|
50
50
|
const destination = path.join(root, "remote"); mkdirSync(destination);
|
|
51
51
|
await downloadImmutableAddOn(`${base}addon-manifest.json`, destination, { fetchImpl: async (url) => ({ ok: bytes.has(url), status: bytes.has(url) ? 200 : 404, arrayBuffer: async () => bytes.get(url) }) });
|
|
52
|
-
assert.equal(readFileSync(path.join(destination, "
|
|
52
|
+
assert.equal(readFileSync(path.join(destination, "membrane-service"), "utf8"), "service");
|
|
53
53
|
assert.equal(immutableAddonManifestUrl(manifest, `https://github.com/Orthic-Labs/Membrane/releases/download/addon-membrane-v0.1.0-mac-sha256-${digest}/addon-manifest.json`, digest), true);
|
|
54
54
|
});
|
package/addon-contract.test.mjs
CHANGED
|
@@ -8,12 +8,12 @@ import { addonManifestSha256, addonRoutes, createAddonManifest, validateAddonCon
|
|
|
8
8
|
function fixture() {
|
|
9
9
|
const root = mkdtempSync(path.join(os.tmpdir(), "right-addon-contract-"));
|
|
10
10
|
mkdirSync(path.join(root, "bin")); mkdirSync(path.join(root, "assets")); mkdirSync(path.join(root, "legal"));
|
|
11
|
-
for (const [file, bytes] of [["bin/membrane", "membrane"], ["bin/
|
|
11
|
+
for (const [file, bytes] of [["bin/membrane", "membrane"], ["bin/membrane-service", "service"], ["assets/tab.png", "icon"], ["legal/LICENSE", "license"], ["legal/EULA.txt", "eula"], ["legal/PRIVACY.md", "privacy"], ["legal/THIRD-PARTY-NOTICES.txt", "notice"]]) writeFileSync(path.join(root, file), bytes);
|
|
12
12
|
const files = [
|
|
13
|
-
["command", "membrane", "bin/membrane", true], ["service", "
|
|
13
|
+
["command", "membrane", "bin/membrane", true], ["service", "membrane-service", "bin/membrane-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false],
|
|
14
14
|
["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false],
|
|
15
15
|
].map(([role, name, source, executable]) => ({ role, name, source, executable }));
|
|
16
|
-
return { root, config: { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: ["test"], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "
|
|
16
|
+
return { root, config: { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: ["test"], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "membrane-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "azure-artifact-signing-v1" }, files } } } };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
test("add-on manifest has portable immutable identity and routes", () => {
|
package/build-release.mjs
CHANGED
|
@@ -26,12 +26,14 @@ import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInpu
|
|
|
26
26
|
import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
|
|
27
27
|
import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
|
|
28
28
|
import { createTargetBridge } from "./target-bridge.mjs";
|
|
29
|
-
import {
|
|
29
|
+
import { brokerManagedHost } from "./cargo-contract.mjs";
|
|
30
|
+
import { resolveTargetRoot } from "./cargo-target.mjs";
|
|
30
31
|
import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
|
|
31
32
|
import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
|
|
32
33
|
import { assertCleanSource } from "./source-gate.mjs";
|
|
33
34
|
import { acquireHeavyWorkSlot, heavyCommandEnvironment, terminateProcessTree } from "./heavy-command.mjs";
|
|
34
35
|
import { processGroupCpuSeconds } from "./process-liveness.mjs";
|
|
36
|
+
import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimits } from "./progress-control.mjs";
|
|
35
37
|
|
|
36
38
|
// Fingerprint of the pipeline code that can change the bytes we ship or the way
|
|
37
39
|
// they are signed — deliberately NOT the package version, which moves for docs
|
|
@@ -103,6 +105,7 @@ let suiteSlot;
|
|
|
103
105
|
let cacheLease;
|
|
104
106
|
let heavySlot;
|
|
105
107
|
let interrupted;
|
|
108
|
+
let requestActiveRunStop = null;
|
|
106
109
|
|
|
107
110
|
// A dry-run is an inspection contract, not a partial build. It must finish
|
|
108
111
|
// before locks, cache directories, receipts, dependency installation, or any
|
|
@@ -123,9 +126,10 @@ if (dryRun) {
|
|
|
123
126
|
|
|
124
127
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
125
128
|
process.once(signal, () => {
|
|
126
|
-
if (child?.pid) killTree(child.pid);
|
|
127
129
|
interrupted = new Error(`release interrupted by ${signal}`);
|
|
128
130
|
process.exitCode = signal === "SIGINT" ? 130 : 143;
|
|
131
|
+
if (requestActiveRunStop) void requestActiveRunStop(interrupted.message);
|
|
132
|
+
else if (child?.pid) killTree(child.pid);
|
|
129
133
|
});
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -174,13 +178,23 @@ try {
|
|
|
174
178
|
// shells take the Cache V2 branch, bridge src-tauri/target into the release cache,
|
|
175
179
|
// and then lose CARGO_TARGET_DIR to the broker's unconditional override — so the
|
|
176
180
|
// app bundle landed in the broker workspace and packaging failed on an empty cache.
|
|
177
|
-
const managedCargoTarget = (process.env
|
|
178
|
-
? resolveManagedCargoTarget(cargoToml)
|
|
179
|
-
: null;
|
|
181
|
+
const managedCargoTarget = brokerManagedHost(process.env) ? resolveTargetRoot(cargoToml) : null;
|
|
180
182
|
const cacheIdentity = resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
|
|
181
183
|
const cacheKey = hashFileText(JSON.stringify({ cache: cacheIdentity.fingerprint, signingIdentity })).slice(0, 16);
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
+
const cacheModeRequested = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" || platform === "win" ? "shared" : "legacy");
|
|
185
|
+
if (cacheModeRequested !== "legacy" && cacheModeRequested !== "shared") fail("RIGHT_RELEASE_CACHE_MODE must be legacy or shared");
|
|
186
|
+
// On a broker-managed host, Cache V2's lease/slot/prune/floor/mark chain is not
|
|
187
|
+
// running against the directory that will actually hold build output — the
|
|
188
|
+
// broker owns CARGO_TARGET_DIR unconditionally (see managedCargoTarget above).
|
|
189
|
+
// Leaving cacheMode at "shared" there ran that whole chain against an
|
|
190
|
+
// essentially unrelated cache root: acquireSuiteBuildSlot/acquireCacheLease
|
|
191
|
+
// guarded a lock nothing else respects, and assertWriteVolumeFloors could
|
|
192
|
+
// fail() the build outright on a volume that isn't where the real ~18 GB of
|
|
193
|
+
// output lives. Force broker so none of that machinery runs.
|
|
194
|
+
const cacheMode = managedCargoTarget ? "broker" : cacheModeRequested;
|
|
195
|
+
if (managedCargoTarget && cacheModeRequested === "shared") {
|
|
196
|
+
console.error("[right-release] broker-managed host: the build broker owns CARGO_TARGET_DIR; Cache V2 shared-cache lease/prune/floor checks skipped");
|
|
197
|
+
}
|
|
184
198
|
const sharedCacheRoot = cacheMode === "shared" ? resolveSharedCacheRoot({ platform, env: process.env }) : undefined;
|
|
185
199
|
const sharedLayout = cacheMode === "shared"
|
|
186
200
|
? resolveCacheLayout({ cacheRoot: sharedCacheRoot, platform, architecture: cacheIdentity.architecture, app: config.app, fingerprint: cacheKey, kind: "release" })
|
|
@@ -216,9 +230,9 @@ try {
|
|
|
216
230
|
delete env[name];
|
|
217
231
|
}
|
|
218
232
|
}
|
|
219
|
-
if (cacheMode
|
|
233
|
+
if (cacheMode !== "shared") delete env.RIGHT_RELEASE_CACHE_OWNER;
|
|
220
234
|
if (cacheMode === "shared" && env.RIGHT_RELEASE_CACHE_OWNER !== "rightkit-v2") fail("shared cache ownership token was not configured");
|
|
221
|
-
if (cacheMode
|
|
235
|
+
if (cacheMode !== "shared" && !commandExists("sccache")) delete env.RUSTC_WRAPPER;
|
|
222
236
|
if (process.env.RIGHTSUITE_HEAVY_WORK_OWNER !== "1") heavySlot = acquireHeavyWorkSlot();
|
|
223
237
|
if (cacheMode === "shared") {
|
|
224
238
|
suiteSlot = acquireSuiteBuildSlot({ layout: sharedLayout, pid: process.pid, argv: process.argv, waitMs: Number(process.env.BUILD_GUARD_WAIT_SECS ?? 900) * 1000 });
|
|
@@ -250,6 +264,7 @@ try {
|
|
|
250
264
|
link: targetLink,
|
|
251
265
|
target: managedCargoTarget ?? env.CARGO_TARGET_DIR,
|
|
252
266
|
ownedRoot: path.dirname(managedCargoTarget ?? env.CARGO_TARGET_DIR),
|
|
267
|
+
brokerManaged: Boolean(managedCargoTarget),
|
|
253
268
|
});
|
|
254
269
|
|
|
255
270
|
const result = await targetBridge.run(async () => runBuildStateMachine({
|
|
@@ -285,7 +300,13 @@ try {
|
|
|
285
300
|
throwIfInterrupted();
|
|
286
301
|
const cacheTarget = managedCargoTarget ?? env.CARGO_TARGET_DIR;
|
|
287
302
|
mkdirSync(cacheTarget, { recursive: true });
|
|
288
|
-
|
|
303
|
+
// Broker-managed hosts route through the same target bridge as shared
|
|
304
|
+
// mode even though cacheMode is forced to "broker" above: the naive
|
|
305
|
+
// else-branch below fail()s outright when targetLink is a real
|
|
306
|
+
// directory, which is exactly the "corruption" misreading that nearly
|
|
307
|
+
// cost ~5.9 GB of warm broker intermediates. The bridge's brokerManaged
|
|
308
|
+
// flag already knows to leave a real directory untouched instead.
|
|
309
|
+
if (cacheMode === "shared" || managedCargoTarget) targetBridge.ensure();
|
|
289
310
|
else if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
|
|
290
311
|
else if (realpathSync(targetLink) !== realpathSync(cacheTarget)) {
|
|
291
312
|
fail(`primary checkout target exists and is not the release cache link: ${targetLink}`);
|
|
@@ -307,7 +328,7 @@ try {
|
|
|
307
328
|
checkpoint(stateRoot, "hardened");
|
|
308
329
|
},
|
|
309
330
|
seal: async ({ sealedDir }) => {
|
|
310
|
-
sealRelease({ configRoot: appRoot, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity });
|
|
331
|
+
sealRelease({ configRoot: appRoot, managedCargoTarget, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity });
|
|
311
332
|
verifySealedRelease(sealedDir);
|
|
312
333
|
if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
|
|
313
334
|
checkpoint(stateRoot, "sealed");
|
|
@@ -347,17 +368,32 @@ function sqlCipherFeatures(text) {
|
|
|
347
368
|
.filter((value) => /sqlcipher|openssl/i.test(value));
|
|
348
369
|
}
|
|
349
370
|
|
|
350
|
-
function sealRelease({ configRoot, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity }) {
|
|
371
|
+
function sealRelease({ configRoot, managedCargoTarget, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity }) {
|
|
372
|
+
// Every app's right-release.config.mjs names installer/updater artifacts
|
|
373
|
+
// relative to the local `src-tauri/target`. On a broker-managed host that
|
|
374
|
+
// directory is never populated (the broker owns CARGO_TARGET_DIR and
|
|
375
|
+
// targetBridge.ensure() deliberately leaves a real local directory alone,
|
|
376
|
+
// see target-bridge.mjs) so those paths must resolve against the broker's
|
|
377
|
+
// own target root instead, or every managed build fails at seal after a
|
|
378
|
+
// full compile, sign, and notarize.
|
|
379
|
+
const targetPrefix = "src-tauri/target/";
|
|
380
|
+
const resolveArtifactPath = (file) => {
|
|
381
|
+
const normalized = file.replaceAll(path.win32.sep, "/");
|
|
382
|
+
if (managedCargoTarget && normalized.startsWith(targetPrefix)) {
|
|
383
|
+
return path.join(managedCargoTarget, normalized.slice(targetPrefix.length));
|
|
384
|
+
}
|
|
385
|
+
return path.resolve(configRoot, file);
|
|
386
|
+
};
|
|
351
387
|
const sources = new Map();
|
|
352
388
|
const addSource = (source, role) => {
|
|
353
389
|
const current = sources.get(source) ?? new Set();
|
|
354
390
|
current.add(role);
|
|
355
391
|
sources.set(source, current);
|
|
356
392
|
};
|
|
357
|
-
for (const artifact of target.installer?.artifacts ?? []) addSource(
|
|
393
|
+
for (const artifact of target.installer?.artifacts ?? []) addSource(resolveArtifactPath(artifact.file), "installer");
|
|
358
394
|
for (const artifact of target.updater?.artifacts ?? []) {
|
|
359
|
-
addSource(
|
|
360
|
-
addSource(
|
|
395
|
+
addSource(resolveArtifactPath(artifact.file), "updater");
|
|
396
|
+
addSource(resolveArtifactPath(artifact.signature), "updater-signature");
|
|
361
397
|
}
|
|
362
398
|
for (const file of sources.keys()) if (!existsSync(file)) fail(`cannot seal missing artifact: ${file}`);
|
|
363
399
|
const temp = `${sealedDir}.tmp-${process.pid}`;
|
|
@@ -374,14 +410,15 @@ function sealRelease({ configRoot, sealedDir, releaseId, config, target, platfor
|
|
|
374
410
|
}
|
|
375
411
|
const routes = { patch: [], update: [] };
|
|
376
412
|
for (const artifact of target.installer?.artifacts ?? []) {
|
|
377
|
-
const source =
|
|
413
|
+
const source = resolveArtifactPath(artifact.file);
|
|
378
414
|
routes.patch.push({ role: "installer", name: sourceNames.get(source), bucket: "public", key: artifact.key });
|
|
379
415
|
}
|
|
380
416
|
for (const artifact of target.updater?.artifacts ?? []) {
|
|
381
|
-
const source =
|
|
417
|
+
const source = resolveArtifactPath(artifact.file);
|
|
418
|
+
const signatureSource = resolveArtifactPath(artifact.signature);
|
|
382
419
|
const patchKey = artifact.patchKey ?? artifact.key.replace("/updates/", "/installers/");
|
|
383
|
-
routes.patch.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(
|
|
384
|
-
routes.update.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(
|
|
420
|
+
routes.patch.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(signatureSource), bucket: "public", key: patchKey, platform: artifact.platform });
|
|
421
|
+
routes.update.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(signatureSource), bucket: "private", key: artifact.key, platform: artifact.platform });
|
|
385
422
|
}
|
|
386
423
|
writeJson(path.join(temp, "hardening.json"), { schema: 1, passed: true, checkedAt: new Date().toISOString(), files: files.map((file) => file.sha256) });
|
|
387
424
|
const manifest = {
|
|
@@ -427,26 +464,18 @@ function collectToolVersions(packageManager) {
|
|
|
427
464
|
}
|
|
428
465
|
}
|
|
429
466
|
|
|
430
|
-
function resolveManagedCargoTarget(manifestPath) {
|
|
431
|
-
if (!manifestPath) fail("managed release requires a Cargo.toml build input");
|
|
432
|
-
const output = commandOutputAt(
|
|
433
|
-
"cargo",
|
|
434
|
-
["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath],
|
|
435
|
-
path.dirname(manifestPath),
|
|
436
|
-
);
|
|
437
|
-
const target = JSON.parse(output).target_directory;
|
|
438
|
-
if (!target || !path.isAbsolute(target)) fail("managed Cargo metadata returned no absolute target directory");
|
|
439
|
-
return target;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
467
|
async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = null) {
|
|
443
|
-
const inactivityMs =
|
|
444
|
-
const absoluteMs = Number(process.env.RIGHT_RELEASE_ABSOLUTE_MS ?? 90 * 60 * 1000);
|
|
468
|
+
const { inactivityMs, absoluteMs } = releaseProgressLimits(process.env);
|
|
445
469
|
let lastProgress = Date.now();
|
|
446
470
|
const watchDirs = Array.isArray(watchDir) ? watchDir : [watchDir];
|
|
447
471
|
let lastMtime = Math.max(...watchDirs.map(newestMtime));
|
|
448
472
|
const started = Date.now();
|
|
449
473
|
await new Promise((resolve, reject) => {
|
|
474
|
+
let settled = false;
|
|
475
|
+
let stopping = false;
|
|
476
|
+
let managedRequestId = null;
|
|
477
|
+
const settleResolve = () => { if (!settled) { settled = true; resolve(); } };
|
|
478
|
+
const settleReject = (error) => { if (!settled) { settled = true; reject(error); } };
|
|
450
479
|
child = spawn(cmd, runArgs, { cwd, env, stdio: ["inherit", "pipe", "pipe"], windowsHide: true, shell: process.platform === "win32", detached: process.platform !== "win32" });
|
|
451
480
|
try {
|
|
452
481
|
if (child.pid && heavySlot) heavySlot.trackChild(child.pid, cmd);
|
|
@@ -465,6 +494,7 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
|
|
|
465
494
|
stream.on("data", (chunk) => {
|
|
466
495
|
lastProgress = Date.now();
|
|
467
496
|
tail = `${tail}${chunk}`.slice(-64 * 1024);
|
|
497
|
+
if (!managedRequestId) managedRequestId = firstManagedRequestId(tail);
|
|
468
498
|
output.write(chunk);
|
|
469
499
|
});
|
|
470
500
|
}
|
|
@@ -475,12 +505,24 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
|
|
|
475
505
|
const cpu = processGroupCpuSeconds(child.pid);
|
|
476
506
|
if (cpu !== null && lastCpu !== null && cpu > lastCpu) lastProgress = Date.now();
|
|
477
507
|
if (cpu !== null) lastCpu = cpu;
|
|
478
|
-
if (Date.now() - started > absoluteMs) stop(`absolute limit exceeded (${Math.round(absoluteMs / 60000)}m)`, cpu);
|
|
479
|
-
else if (Date.now() - lastProgress > inactivityMs) stop(`no output, file, or CPU progress for ${Math.round(inactivityMs / 60000)}m`, cpu);
|
|
508
|
+
if (Date.now() - started > absoluteMs) void stop(`absolute limit exceeded (${Math.round(absoluteMs / 60000)}m)`, cpu);
|
|
509
|
+
else if (Date.now() - lastProgress > inactivityMs) void stop(`no output, file, or CPU progress for ${Math.round(inactivityMs / 60000)}m`, cpu);
|
|
480
510
|
}, Math.min(30_000, Math.max(250, Math.floor(inactivityMs / 4))));
|
|
481
|
-
const cleanup = () => {
|
|
482
|
-
|
|
511
|
+
const cleanup = () => {
|
|
512
|
+
clearInterval(monitor);
|
|
513
|
+
closeWatchers();
|
|
514
|
+
if (requestActiveRunStop === stop) requestActiveRunStop = null;
|
|
515
|
+
};
|
|
516
|
+
const stop = async (reason, cpu = null) => {
|
|
517
|
+
if (stopping || settled) return;
|
|
518
|
+
stopping = true;
|
|
483
519
|
cleanup();
|
|
520
|
+
let cancellation = { status: "NO_MANAGED_REQUEST" };
|
|
521
|
+
try {
|
|
522
|
+
cancellation = await cancelAndReapManagedRequest(managedRequestId, { cwd, env });
|
|
523
|
+
} catch (error) {
|
|
524
|
+
cancellation = { status: "CANCEL_REAP_FAILED", error: error.message };
|
|
525
|
+
}
|
|
484
526
|
if (diagnosticPath) {
|
|
485
527
|
try {
|
|
486
528
|
mkdirSync(path.dirname(diagnosticPath), { recursive: true });
|
|
@@ -491,6 +533,8 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
|
|
|
491
533
|
`watched: ${watchDirs.join(path.delimiter)}`,
|
|
492
534
|
`elapsedMs: ${Date.now() - started}`,
|
|
493
535
|
`cpuSeconds: ${cpu ?? "unavailable"}`,
|
|
536
|
+
`managedRequestId: ${managedRequestId ?? "none"}`,
|
|
537
|
+
`managedCancellation: ${JSON.stringify(cancellation)}`,
|
|
494
538
|
`killedAt: ${new Date().toISOString()}`,
|
|
495
539
|
"",
|
|
496
540
|
"--- last output ---",
|
|
@@ -500,11 +544,13 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
|
|
|
500
544
|
console.error(`right-release: stall diagnostic written to ${diagnosticPath}`);
|
|
501
545
|
} catch { /* diagnostics are best effort; never mask the stall itself */ }
|
|
502
546
|
}
|
|
503
|
-
killTree(child.pid);
|
|
504
|
-
|
|
547
|
+
if (child?.pid) killTree(child.pid);
|
|
548
|
+
child = null;
|
|
549
|
+
settleReject(new Error(`release step stalled: ${reason}`));
|
|
505
550
|
};
|
|
506
|
-
|
|
507
|
-
child.once("
|
|
551
|
+
requestActiveRunStop = stop;
|
|
552
|
+
child.once("error", (error) => { cleanup(); child = null; if (!stopping) settleReject(error); });
|
|
553
|
+
child.once("exit", (code) => { cleanup(); child = null; if (!stopping) code === 0 ? settleResolve() : settleReject(new Error(`${cmd} exited ${code}`)); });
|
|
508
554
|
});
|
|
509
555
|
}
|
|
510
556
|
|
package/build-release.test.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { execFileSync, spawnSync } from "node:child_process";
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseCpuSeconds, processGroupCpuSeconds } from "./process-liveness.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { brokerManagedHost } from "./cargo-contract.mjs";
|
|
10
10
|
|
|
11
11
|
const source = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs"), "utf8");
|
|
12
12
|
const build = path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs");
|
|
@@ -27,7 +27,7 @@ test("macOS and Windows builds default to Cache V2 while explicit legacy mode re
|
|
|
27
27
|
assert.match(source, /right-release build/);
|
|
28
28
|
assert.match(source, /RIGHT_RELEASE_CACHE_MODE/);
|
|
29
29
|
assert.match(source, /platform === "mac" \|\| platform === "win" \? "shared" : "legacy"/);
|
|
30
|
-
assert.match(source, /
|
|
30
|
+
assert.match(source, /cacheModeRequested !== "legacy" && cacheModeRequested !== "shared"/);
|
|
31
31
|
assert.match(source, /resolveSharedCacheRoot/);
|
|
32
32
|
assert.match(source, /acquireSuiteBuildSlot/);
|
|
33
33
|
assert.match(source, /acquireHeavyWorkSlot/);
|
|
@@ -54,6 +54,24 @@ test("production builds package from the real primary app checkout", () => {
|
|
|
54
54
|
assert.match(source, /sealRelease\(\{ configRoot: appRoot,/);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
test("sealing resolves installer and updater artifacts against the broker target on a managed host", () => {
|
|
58
|
+
assert.match(source, /sealRelease\(\{ configRoot: appRoot, managedCargoTarget,/);
|
|
59
|
+
assert.match(source, /function sealRelease\(\{ configRoot, managedCargoTarget,/);
|
|
60
|
+
assert.match(source, /const targetPrefix = "src-tauri\/target\/";/);
|
|
61
|
+
assert.match(source, /if \(managedCargoTarget && normalized\.startsWith\(targetPrefix\)\) \{/);
|
|
62
|
+
// Every configRoot-relative artifact lookup inside sealRelease must route
|
|
63
|
+
// through the resolver, or a managed build fails at seal after a full
|
|
64
|
+
// compile, sign, and notarize because src-tauri/target is never populated
|
|
65
|
+
// locally on a broker-managed host (target-bridge.mjs leaves it alone).
|
|
66
|
+
assert.doesNotMatch(source, /addSource\(path\.resolve\(configRoot, artifact\.file\)/);
|
|
67
|
+
assert.doesNotMatch(source, /addSource\(path\.resolve\(configRoot, artifact\.signature\)/);
|
|
68
|
+
assert.match(source, /addSource\(resolveArtifactPath\(artifact\.file\), "installer"\)/);
|
|
69
|
+
assert.match(source, /addSource\(resolveArtifactPath\(artifact\.file\), "updater"\)/);
|
|
70
|
+
assert.match(source, /addSource\(resolveArtifactPath\(artifact\.signature\), "updater-signature"\)/);
|
|
71
|
+
assert.match(source, /const source = resolveArtifactPath\(artifact\.file\);/);
|
|
72
|
+
assert.match(source, /const signatureSource = resolveArtifactPath\(artifact\.signature\);/);
|
|
73
|
+
});
|
|
74
|
+
|
|
57
75
|
test("production builds never create or use an internal Git worktree", () => {
|
|
58
76
|
assert.doesNotMatch(source, /git", \["worktree", "add"/);
|
|
59
77
|
assert.doesNotMatch(source, /path\.join\(vaultRoot, "worktrees"/);
|
|
@@ -74,7 +92,30 @@ test("shared Cache V2 target bridge is owned by a finally-cleaned lifecycle", ()
|
|
|
74
92
|
// next build until someone deletes the link by hand.
|
|
75
93
|
assert.match(source, /ownedRoot: path\.dirname\(managedCargoTarget \?\? env\.CARGO_TARGET_DIR\)/);
|
|
76
94
|
assert.match(source, /targetBridge\.run\(.*runBuildStateMachine/s);
|
|
77
|
-
|
|
95
|
+
// A broker-managed host also routes through the bridge even though cacheMode
|
|
96
|
+
// is forced to "broker" there — its brokerManaged flag is what keeps a real
|
|
97
|
+
// (non-symlink) target directory from being treated as corruption.
|
|
98
|
+
assert.match(source, /if \(cacheMode === "shared" \|\| managedCargoTarget\) targetBridge\.ensure\(\)/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a broker-managed host forces broker cacheMode so Cache V2's lease/prune/floor chain never runs against an unrelated volume", () => {
|
|
102
|
+
assert.match(
|
|
103
|
+
source,
|
|
104
|
+
/const cacheMode = managedCargoTarget \? "broker" : cacheModeRequested;/,
|
|
105
|
+
);
|
|
106
|
+
// Every Cache V2 lease/slot/prune/floor/mark call stays gated on cacheMode,
|
|
107
|
+
// never on cacheModeRequested directly, so forcing cacheMode to "broker" on a
|
|
108
|
+
// broker host is sufficient to skip all of them without a second gate.
|
|
109
|
+
for (const guarded of [
|
|
110
|
+
/acquireSuiteBuildSlot\(\{ layout: sharedLayout/,
|
|
111
|
+
/cacheLease = acquireCacheLease\(\{ layout: sharedLayout/,
|
|
112
|
+
/planCachePrune\(\{ snapshot, policy, protectedEntryIds: new Set\(\[sharedLayout\.id\]\) \}\)/,
|
|
113
|
+
/assertWriteVolumeFloors\(\{ volumes, policy,/,
|
|
114
|
+
/markCacheEntrySuccessful\(\{ layout: sharedLayout \}\)/,
|
|
115
|
+
]) {
|
|
116
|
+
assert.match(source, guarded);
|
|
117
|
+
}
|
|
118
|
+
assert.doesNotMatch(source, /if \(cacheModeRequested === "shared"\) \{\s*\n\s*suiteSlot/);
|
|
78
119
|
});
|
|
79
120
|
|
|
80
121
|
test("Windows seal and cache identity bind the signing contract, config, and receipts", () => {
|
|
@@ -122,16 +163,17 @@ test("CPU sampling degrades to the old behaviour rather than inventing a verdict
|
|
|
122
163
|
// bridged src-tauri/target into the release cache, and then lost CARGO_TARGET_DIR
|
|
123
164
|
// to the broker's unconditional override. The app built into the broker workspace
|
|
124
165
|
// and packaging failed on an empty cache before notarize/staple.
|
|
125
|
-
const managedHostDetected = (env, platform, exists) =>
|
|
126
|
-
Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET || managedCargoShimOnPath(env, platform, exists));
|
|
166
|
+
const managedHostDetected = (env, platform, exists) => brokerManagedHost(env, platform, exists);
|
|
127
167
|
|
|
128
168
|
test("managed-host gate fires from the managed shim on PATH when no broker socket is exported", () => {
|
|
129
|
-
// The build gate is exactly the
|
|
169
|
+
// The build gate is exactly the shared brokerManagedHost predicate cargo-contract.mjs exports,
|
|
170
|
+
// so there is exactly one definition of "is this a broker-managed host" in the codebase.
|
|
130
171
|
assert.match(
|
|
131
172
|
source,
|
|
132
|
-
/const managedCargoTarget = \(process\.env
|
|
173
|
+
/const managedCargoTarget = brokerManagedHost\(process\.env\) \? resolveTargetRoot\(cargoToml\) : null;/,
|
|
133
174
|
);
|
|
134
|
-
assert.match(source, /import \{
|
|
175
|
+
assert.match(source, /import \{ brokerManagedHost \} from "\.\/cargo-contract\.mjs";/);
|
|
176
|
+
assert.match(source, /import \{ resolveTargetRoot \} from "\.\/cargo-target\.mjs";/);
|
|
135
177
|
|
|
136
178
|
const shim = "/Volumes/D/.rightkit-managed/agent-bin/cargo";
|
|
137
179
|
const exists = (candidate) => candidate === shim;
|
|
@@ -151,6 +193,23 @@ test("managed-host gate fires from the managed shim on PATH when no broker socke
|
|
|
151
193
|
assert.equal(managedHostDetected({ PATH: `/usr/local/bin:${path.dirname(shim)}` }, "darwin", (c) => both.has(c)), false);
|
|
152
194
|
});
|
|
153
195
|
|
|
196
|
+
test("brokerManagedHost is the single shared predicate: env var alone, shim alone, both, or neither", () => {
|
|
197
|
+
const shim = "/Volumes/D/.rightkit-managed/agent-bin/cargo";
|
|
198
|
+
const exists = (candidate) => candidate === shim;
|
|
199
|
+
|
|
200
|
+
// Broker socket alone (login shell), no shim resolvable.
|
|
201
|
+
assert.equal(brokerManagedHost({ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock", PATH: "/usr/bin" }, "darwin", () => false), true);
|
|
202
|
+
// Shim on PATH alone (agent/non-login shell), no socket exported.
|
|
203
|
+
assert.equal(brokerManagedHost({ PATH: `${path.dirname(shim)}:/usr/bin` }, "darwin", exists), true);
|
|
204
|
+
// Both signals present.
|
|
205
|
+
assert.equal(
|
|
206
|
+
brokerManagedHost({ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock", PATH: `${path.dirname(shim)}:/usr/bin` }, "darwin", exists),
|
|
207
|
+
true,
|
|
208
|
+
);
|
|
209
|
+
// Neither signal: unmanaged host.
|
|
210
|
+
assert.equal(brokerManagedHost({ PATH: "/usr/bin:/usr/local/bin" }, "darwin", () => false), false);
|
|
211
|
+
});
|
|
212
|
+
|
|
154
213
|
test("the managed branch hands the Rust target directory to the broker while the unmanaged branch keeps Cache V2", () => {
|
|
155
214
|
// Managed: CARGO_TARGET_DIR (and the rest of the Cache V2 environment) is deleted
|
|
156
215
|
// from the child environment, so the broker's own override is the only one.
|
|
@@ -164,7 +223,9 @@ test("the managed branch hands the Rust target directory to the broker while the
|
|
|
164
223
|
assert.match(source, /ownedRoot: path\.dirname\(managedCargoTarget \?\? env\.CARGO_TARGET_DIR\),/);
|
|
165
224
|
// The managed target is read from crate metadata through the shim, which is what
|
|
166
225
|
// makes the bridge target equal to the directory the broker actually builds into.
|
|
167
|
-
|
|
226
|
+
// resolveTargetRoot (cargo-target.mjs) is the single shared implementation —
|
|
227
|
+
// build-release.mjs no longer defines its own copy.
|
|
228
|
+
assert.doesNotMatch(source, /function resolveManagedCargoTarget/);
|
|
168
229
|
// Unmanaged (managedCargoTarget === null): Cache V2 still supplies CARGO_TARGET_DIR.
|
|
169
230
|
assert.match(source, /\.\.\.releaseEnvironment\(\{ root: vaultRoot, cacheRoot: sharedCacheRoot,/);
|
|
170
231
|
});
|
package/cache-command.mjs
CHANGED
|
@@ -13,12 +13,28 @@ import {
|
|
|
13
13
|
resolveSharedCacheRoot,
|
|
14
14
|
} from "./cache-policy.mjs";
|
|
15
15
|
import { resolveReleaseLayout } from "./release-state.mjs";
|
|
16
|
+
import { brokerManagedHost } from "./cargo-contract.mjs";
|
|
17
|
+
|
|
18
|
+
// On a broker-managed host, CARGO_TARGET_DIR is not Cache V2's to own: the build
|
|
19
|
+
// broker unconditionally overrides it to its own per-namespace workspace, so the
|
|
20
|
+
// Cache V2 target layout under cacheRoot is legitimately empty (marker files only)
|
|
21
|
+
// while the real ~18 GB of build output lives in the broker's workspace. Inspecting,
|
|
22
|
+
// migrating, or pruning against that layout on a broker host reads real output as
|
|
23
|
+
// missing/corrupt and can recommend destroying it — never phrase this as a problem.
|
|
24
|
+
const BROKER_MANAGED_MESSAGE = "broker-managed host: the build broker owns CARGO_TARGET_DIR; Cache V2 target checks skipped";
|
|
25
|
+
const BROKER_GATED_COMMANDS = new Set(["status", "prune", "migrate"]);
|
|
16
26
|
|
|
17
27
|
export async function runCacheCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, stderr = console.error } = {}) {
|
|
18
28
|
const [command, ...rest] = args;
|
|
19
29
|
const json = rest.includes("--json");
|
|
20
30
|
const platform = platformFrom(env);
|
|
21
31
|
const cacheRoot = resolveSharedCacheRoot({ platform, env });
|
|
32
|
+
if (BROKER_GATED_COMMANDS.has(command) && brokerManagedHost(env)) {
|
|
33
|
+
const result = { schema: 1, command, cacheRoot, brokerManaged: true, skipped: true, message: BROKER_MANAGED_MESSAGE };
|
|
34
|
+
if (json) stdout(JSON.stringify(result));
|
|
35
|
+
else stdout(BROKER_MANAGED_MESSAGE);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
22
38
|
if (command === "status") {
|
|
23
39
|
rejectUnknown(rest, new Set(["--json"]));
|
|
24
40
|
const snapshot = inspectCache({ cacheRoot });
|
package/cache-command.test.mjs
CHANGED
|
@@ -9,8 +9,24 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const cli = path.join(root, "cli", "right-release.mjs");
|
|
11
11
|
|
|
12
|
+
// This suite must exercise the non-broker cache-command path deterministically,
|
|
13
|
+
// regardless of whether the machine running the tests is itself broker-managed
|
|
14
|
+
// (this workspace's own agent shells are: the managed cargo shim sits on PATH).
|
|
15
|
+
// Strip both broker signals from the inherited environment so "unmanaged host"
|
|
16
|
+
// tests stay unmanaged; individual tests opt back into broker signals explicitly.
|
|
17
|
+
function unmanagedEnv(base = process.env) {
|
|
18
|
+
const sanitized = { ...base };
|
|
19
|
+
delete sanitized.RIGHTKIT_BUILD_BROKER_SOCKET;
|
|
20
|
+
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
21
|
+
sanitized.PATH = String(base.PATH ?? "")
|
|
22
|
+
.split(delimiter)
|
|
23
|
+
.filter((entry) => !/(?:^|[/\\])(?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin$/i.test(entry))
|
|
24
|
+
.join(delimiter);
|
|
25
|
+
return sanitized;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
function run(args, cacheRoot, env = {}) {
|
|
13
|
-
return spawnSync(process.execPath, [cli, "cache", ...args], { encoding: "utf8", env: { ...
|
|
29
|
+
return spawnSync(process.execPath, [cli, "cache", ...args], { encoding: "utf8", env: { ...unmanagedEnv(), RIGHT_RELEASE_CACHE_ROOT: cacheRoot, ...env } });
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
test("cache status has human and schema-stable JSON output", () => {
|
|
@@ -73,3 +89,30 @@ test("cache migrate uses the repository vault Cargo home, not an app-local looka
|
|
|
73
89
|
assert.equal(readFileSync(path.join(cacheRoot, "cargo-home", "registry"), "utf8"), "vault");
|
|
74
90
|
assert.equal(readFileSync(path.join(appCargoHome, "registry"), "utf8"), "app-local");
|
|
75
91
|
});
|
|
92
|
+
|
|
93
|
+
test("status, prune, and migrate short-circuit cleanly on a broker-managed host without proposing a repair", () => {
|
|
94
|
+
const cacheRoot = mkdtempSync(path.join(os.tmpdir(), "rightkit-cache-command-"));
|
|
95
|
+
const brokerEnv = { RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock" };
|
|
96
|
+
|
|
97
|
+
const status = run(["status", "--json"], cacheRoot, brokerEnv);
|
|
98
|
+
assert.equal(status.status, 0, status.stderr);
|
|
99
|
+
const statusBody = JSON.parse(status.stdout);
|
|
100
|
+
assert.equal(statusBody.brokerManaged, true);
|
|
101
|
+
assert.match(statusBody.message, /broker-managed host/);
|
|
102
|
+
assert.doesNotMatch(status.stdout + status.stderr, /warn|repair|corrupt/i);
|
|
103
|
+
|
|
104
|
+
const prune = run(["prune", "--dry-run", "--json"], cacheRoot, brokerEnv);
|
|
105
|
+
assert.equal(prune.status, 0, prune.stderr);
|
|
106
|
+
assert.equal(JSON.parse(prune.stdout).brokerManaged, true);
|
|
107
|
+
|
|
108
|
+
// migrate short-circuits before even requiring --config, since a broker host has
|
|
109
|
+
// nothing under Cache V2's target layout for it to migrate into.
|
|
110
|
+
const migrate = run(["migrate", "--json"], cacheRoot, brokerEnv);
|
|
111
|
+
assert.equal(migrate.status, 0, migrate.stderr);
|
|
112
|
+
assert.equal(JSON.parse(migrate.stdout).brokerManaged, true);
|
|
113
|
+
|
|
114
|
+
// Non-broker host: unchanged behaviour.
|
|
115
|
+
const unmanaged = run(["status", "--json"], cacheRoot);
|
|
116
|
+
assert.equal(unmanaged.status, 0, unmanaged.stderr);
|
|
117
|
+
assert.equal(JSON.parse(unmanaged.stdout).brokerManaged, undefined);
|
|
118
|
+
});
|
package/cache-policy.mjs
CHANGED
|
@@ -30,21 +30,59 @@ export const CACHE_LOCK_ORDER = ["release", "suite-build-slot", "gc", "entry-lea
|
|
|
30
30
|
|
|
31
31
|
const MARKER = ".rightkit-cache-entry.json";
|
|
32
32
|
|
|
33
|
-
export function resolveSharedCacheRoot({ platform = process.platform, env = process.env, home = os.homedir()
|
|
34
|
-
const override = env.RIGHT_RELEASE_CACHE_ROOT;
|
|
35
|
-
if (override) {
|
|
36
|
-
if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
|
|
37
|
-
return resolveForPlatform(override, platform);
|
|
38
|
-
}
|
|
33
|
+
export function resolveSharedCacheRoot({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
39
34
|
if (platform === "darwin" || platform === "mac") {
|
|
35
|
+
const override = env.RIGHT_RELEASE_CACHE_ROOT;
|
|
36
|
+
if (override) {
|
|
37
|
+
if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
|
|
38
|
+
return resolveForPlatform(override, platform);
|
|
39
|
+
}
|
|
40
40
|
throw new Error("RIGHT_RELEASE_CACHE_ROOT is required on macOS; set it to external storage");
|
|
41
41
|
}
|
|
42
|
+
return resolveStableRoot({
|
|
43
|
+
platform,
|
|
44
|
+
env,
|
|
45
|
+
home,
|
|
46
|
+
explicitOverride: env.RIGHT_RELEASE_CACHE_ROOT,
|
|
47
|
+
overrideName: "RIGHT_RELEASE_CACHE_ROOT",
|
|
48
|
+
win32EnvVar: "LOCALAPPDATA",
|
|
49
|
+
win32Segments: ["RightSuite", "Cache", "release"],
|
|
50
|
+
otherSegments: ["rightsuite", "release"],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// resolveStableRoot centralizes platform-native root derivation for both cache roots
|
|
55
|
+
// (waste-tolerant, may split) and lock roots (must never split, or mutual exclusion
|
|
56
|
+
// silently breaks). Route every such root through this one primitive.
|
|
57
|
+
export function resolveStableRoot({
|
|
58
|
+
platform = process.platform,
|
|
59
|
+
env = process.env,
|
|
60
|
+
home = os.homedir(),
|
|
61
|
+
explicitOverride,
|
|
62
|
+
overrideName,
|
|
63
|
+
darwinSegments,
|
|
64
|
+
win32EnvVar = "LOCALAPPDATA",
|
|
65
|
+
win32Segments,
|
|
66
|
+
otherSegments,
|
|
67
|
+
otherBaseSegments = [".cache"],
|
|
68
|
+
} = {}) {
|
|
69
|
+
if (explicitOverride) {
|
|
70
|
+
if (!isAbsoluteForPlatform(explicitOverride, platform)) throw new Error(`${overrideName} must be an absolute path`);
|
|
71
|
+
return resolveForPlatform(explicitOverride, platform);
|
|
72
|
+
}
|
|
42
73
|
if (platform === "win32" || platform === "win") {
|
|
43
|
-
const local = env
|
|
44
|
-
if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error(
|
|
45
|
-
return path.win32.resolve(local,
|
|
74
|
+
const local = env[win32EnvVar];
|
|
75
|
+
if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error(`${win32EnvVar} must be an absolute path for RightSuite`);
|
|
76
|
+
return path.win32.resolve(local, ...win32Segments);
|
|
77
|
+
}
|
|
78
|
+
if (platform === "darwin" || platform === "mac") {
|
|
79
|
+
return path.posix.join(home, ...darwinSegments);
|
|
46
80
|
}
|
|
47
|
-
|
|
81
|
+
// Never read a shell-profile-scoped variable (XDG_CACHE_HOME, XDG_CONFIG_HOME,
|
|
82
|
+
// npm_config_cache, etc.) as an implicit default here: those differ between login
|
|
83
|
+
// and non-login shells, so the same process can resolve to two different roots
|
|
84
|
+
// depending on how it was launched. Always derive from the OS home directory instead.
|
|
85
|
+
return path.posix.resolve(path.posix.join(home, ...otherBaseSegments), ...otherSegments);
|
|
48
86
|
}
|
|
49
87
|
|
|
50
88
|
export function resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint, kind = "release" } = {}) {
|