@rightkit/release 0.2.40 → 0.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-release.mjs +7 -4
- package/build-release.test.mjs +6 -0
- package/package.json +1 -1
- package/right-suite-contract.test.mjs +1 -1
- package/rightkit-versions.json +1 -1
- package/target-bridge.mjs +119 -0
- package/target-bridge.test.mjs +155 -0
package/build-release.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
23
23
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs } from "./release-invocation.mjs";
|
|
24
24
|
import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
|
|
25
25
|
import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
|
|
26
|
+
import { createTargetBridge } from "./target-bridge.mjs";
|
|
26
27
|
|
|
27
28
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
const WORKER = path.join(TOOL_ROOT, "release.mjs");
|
|
@@ -125,8 +126,10 @@ try {
|
|
|
125
126
|
tools: toolVersions,
|
|
126
127
|
createdAt: new Date().toISOString(),
|
|
127
128
|
};
|
|
129
|
+
const targetLink = path.join(appRoot, "src-tauri", "target");
|
|
130
|
+
const targetBridge = createTargetBridge({ link: targetLink, target: env.CARGO_TARGET_DIR });
|
|
128
131
|
|
|
129
|
-
const result = await runBuildStateMachine({
|
|
132
|
+
const result = await targetBridge.run(async () => runBuildStateMachine({
|
|
130
133
|
root: repoRoot,
|
|
131
134
|
app: config.app,
|
|
132
135
|
version: config.version,
|
|
@@ -158,9 +161,9 @@ try {
|
|
|
158
161
|
prepare: async () => {
|
|
159
162
|
throwIfInterrupted();
|
|
160
163
|
const cacheTarget = env.CARGO_TARGET_DIR;
|
|
161
|
-
const targetLink = path.join(appRoot, "src-tauri", "target");
|
|
162
164
|
mkdirSync(cacheTarget, { recursive: true });
|
|
163
|
-
if (
|
|
165
|
+
if (cacheMode === "shared") targetBridge.ensure();
|
|
166
|
+
else if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
|
|
164
167
|
else if (realpathSync(targetLink) !== realpathSync(cacheTarget)) {
|
|
165
168
|
fail(`primary checkout target exists and is not the release cache link: ${targetLink}`);
|
|
166
169
|
}
|
|
@@ -186,7 +189,7 @@ try {
|
|
|
186
189
|
checkpoint(stateRoot, "sealed");
|
|
187
190
|
},
|
|
188
191
|
},
|
|
189
|
-
});
|
|
192
|
+
}));
|
|
190
193
|
if (cacheMode === "shared") {
|
|
191
194
|
const snapshot = inspectCache({ cacheRoot: sharedCacheRoot });
|
|
192
195
|
const plan = planCachePrune({ snapshot, policy: readCachePolicy(process.env), protectedEntryIds: new Set([sharedLayout.id]) });
|
package/build-release.test.mjs
CHANGED
|
@@ -42,3 +42,9 @@ test("dirty release config is rejected before the config module is imported", ()
|
|
|
42
42
|
assert.ok(guard >= 0, "config cleanliness guard must exist");
|
|
43
43
|
assert.ok(guard < imported, "config cleanliness guard must run before module import");
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
test("shared Cache V2 target bridge is owned by a finally-cleaned lifecycle", () => {
|
|
47
|
+
assert.match(source, /createTargetBridge\(\{ link: targetLink, target: env\.CARGO_TARGET_DIR \}\)/);
|
|
48
|
+
assert.match(source, /targetBridge\.run\(.*runBuildStateMachine/s);
|
|
49
|
+
assert.match(source, /if \(cacheMode === "shared"\) targetBridge\.ensure\(\)/);
|
|
50
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -392,7 +392,7 @@ test("mac release config uses a dedicated non-recursive package entry point", ()
|
|
|
392
392
|
test("RightKit exposes one current version manifest", () => {
|
|
393
393
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
394
394
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
395
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
395
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.41");
|
|
396
396
|
assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
|
|
397
397
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
398
398
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
package/rightkit-versions.json
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, realpathSync, symlinkSync, unlinkSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function createTargetBridge({
|
|
5
|
+
link,
|
|
6
|
+
target,
|
|
7
|
+
platform = process.platform,
|
|
8
|
+
lstat = lstatSync,
|
|
9
|
+
mkdir = mkdirSync,
|
|
10
|
+
realpath = realpathSync,
|
|
11
|
+
symlink = symlinkSync,
|
|
12
|
+
unlink = unlinkSync,
|
|
13
|
+
}) {
|
|
14
|
+
let acquired = false;
|
|
15
|
+
let created = false;
|
|
16
|
+
let ownership = null;
|
|
17
|
+
|
|
18
|
+
const bridge = {
|
|
19
|
+
ensure() {
|
|
20
|
+
mkdir(target, { recursive: true });
|
|
21
|
+
let entry = readEntry(link, lstat);
|
|
22
|
+
if (!entry) {
|
|
23
|
+
symlink(target, link, platform === "win32" ? "junction" : "dir");
|
|
24
|
+
entry = lstat(link);
|
|
25
|
+
if (!entry.isSymbolicLink()) throw new Error(`RightKit target bridge was not created as a symbolic link: ${link}`);
|
|
26
|
+
if (!sameRealPath(link, target, realpath, platform)) {
|
|
27
|
+
throw new Error(`RightKit target bridge is not the owned shared cache target after creation: ${link}`);
|
|
28
|
+
}
|
|
29
|
+
ownership = entryIdentity(entry);
|
|
30
|
+
created = true;
|
|
31
|
+
} else {
|
|
32
|
+
if (!entry.isSymbolicLink()) throw new Error(`primary checkout target is not a symbolic link; refusing to replace it: ${link}`);
|
|
33
|
+
if (!sameRealPath(link, target, realpath, platform)) {
|
|
34
|
+
throw new Error(`primary checkout target is not the owned shared cache target; refusing to replace it: ${link}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
acquired = true;
|
|
38
|
+
return { created, link, target };
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
release() {
|
|
42
|
+
if (!acquired) return false;
|
|
43
|
+
if (!ownership) {
|
|
44
|
+
acquired = false;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const entry = readEntry(link, lstat);
|
|
48
|
+
if (!entry) {
|
|
49
|
+
acquired = false;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (!sameEntry(entry, ownership)) {
|
|
53
|
+
acquired = false;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
let exactOwnedTarget = false;
|
|
57
|
+
try {
|
|
58
|
+
exactOwnedTarget = sameRealPath(link, target, realpath, platform);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error?.code !== "ENOENT") throw error;
|
|
61
|
+
}
|
|
62
|
+
if (!exactOwnedTarget) {
|
|
63
|
+
acquired = false;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
unlink(link);
|
|
67
|
+
acquired = false;
|
|
68
|
+
ownership = null;
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async run(operation) {
|
|
73
|
+
let operationError;
|
|
74
|
+
try {
|
|
75
|
+
return await operation(bridge);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
operationError = error;
|
|
78
|
+
throw error;
|
|
79
|
+
} finally {
|
|
80
|
+
try {
|
|
81
|
+
bridge.release();
|
|
82
|
+
} catch (cleanupError) {
|
|
83
|
+
if (!operationError) throw cleanupError;
|
|
84
|
+
operationError.cleanupError = cleanupError;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return bridge;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function entryIdentity(stats) {
|
|
94
|
+
return { dev: stats.dev, ino: stats.ino, symbolicLink: stats.isSymbolicLink() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sameEntry(stats, expected) {
|
|
98
|
+
return stats.dev === expected.dev
|
|
99
|
+
&& stats.ino === expected.ino
|
|
100
|
+
&& stats.isSymbolicLink() === expected.symbolicLink
|
|
101
|
+
&& expected.symbolicLink;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readEntry(file, lstat) {
|
|
105
|
+
try {
|
|
106
|
+
return lstat(file);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error?.code === "ENOENT") return null;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sameRealPath(left, right, realpath, platform) {
|
|
114
|
+
const canonical = (value) => {
|
|
115
|
+
const resolved = path.normalize(realpath(value));
|
|
116
|
+
return platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
117
|
+
};
|
|
118
|
+
return canonical(left) === canonical(right);
|
|
119
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { createTargetBridge } from "./target-bridge.mjs";
|
|
8
|
+
|
|
9
|
+
function fixture() {
|
|
10
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "rightkit-target-bridge-"));
|
|
11
|
+
const target = path.join(root, "shared", "target");
|
|
12
|
+
const link = path.join(root, "app", "src-tauri", "target");
|
|
13
|
+
mkdirSync(target, { recursive: true });
|
|
14
|
+
mkdirSync(path.dirname(link), { recursive: true });
|
|
15
|
+
writeFileSync(path.join(target, "keep.txt"), "shared-cache\n");
|
|
16
|
+
return { root, target, link };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("removes an invocation-created bridge after success without touching the shared target", async () => {
|
|
20
|
+
const fx = fixture();
|
|
21
|
+
const bridge = createTargetBridge(fx);
|
|
22
|
+
await bridge.run(async () => {
|
|
23
|
+
bridge.ensure();
|
|
24
|
+
assert.equal(realpathSync(fx.link), realpathSync(fx.target));
|
|
25
|
+
});
|
|
26
|
+
assert.throws(() => lstatSync(fx.link), { code: "ENOENT" });
|
|
27
|
+
assert.equal(readFileSync(path.join(fx.target, "keep.txt"), "utf8"), "shared-cache\n");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("removes an invocation-created bridge after the build throws", async () => {
|
|
31
|
+
const fx = fixture();
|
|
32
|
+
const bridge = createTargetBridge(fx);
|
|
33
|
+
await assert.rejects(
|
|
34
|
+
bridge.run(async () => {
|
|
35
|
+
bridge.ensure();
|
|
36
|
+
throw new Error("package failed");
|
|
37
|
+
}),
|
|
38
|
+
/package failed/,
|
|
39
|
+
);
|
|
40
|
+
assert.throws(() => lstatSync(fx.link), { code: "ENOENT" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("removes an invocation-created bridge after a signal-style abort", async () => {
|
|
44
|
+
const fx = fixture();
|
|
45
|
+
const bridge = createTargetBridge(fx);
|
|
46
|
+
await assert.rejects(
|
|
47
|
+
bridge.run(async () => {
|
|
48
|
+
bridge.ensure();
|
|
49
|
+
throw new Error("release interrupted by SIGTERM");
|
|
50
|
+
}),
|
|
51
|
+
/SIGTERM/,
|
|
52
|
+
);
|
|
53
|
+
assert.throws(() => lstatSync(fx.link), { code: "ENOENT" });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("accepts but preserves a pre-existing exact shared-target bridge", async () => {
|
|
57
|
+
const fx = fixture();
|
|
58
|
+
symlinkSync(fx.target, fx.link, process.platform === "win32" ? "junction" : "dir");
|
|
59
|
+
await createTargetBridge(fx).run(async (bridge) => {
|
|
60
|
+
assert.deepEqual(bridge.ensure(), { created: false, link: fx.link, target: fx.target });
|
|
61
|
+
});
|
|
62
|
+
assert.equal(realpathSync(fx.link), realpathSync(fx.target));
|
|
63
|
+
assert.equal(readFileSync(path.join(fx.target, "keep.txt"), "utf8"), "shared-cache\n");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("fails closed when the new bridge is replaced immediately after symlink creation", () => {
|
|
67
|
+
const fx = fixture();
|
|
68
|
+
const other = path.join(fx.root, "racing-target");
|
|
69
|
+
mkdirSync(other);
|
|
70
|
+
const bridge = createTargetBridge({
|
|
71
|
+
...fx,
|
|
72
|
+
symlink(target, link, type) {
|
|
73
|
+
symlinkSync(target, link, type);
|
|
74
|
+
unlinkSync(link);
|
|
75
|
+
symlinkSync(other, link, type);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
assert.throws(() => bridge.ensure(), /owned shared cache target/i);
|
|
79
|
+
assert.equal(realpathSync(fx.link), realpathSync(other));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("fails closed and preserves an unrelated symlink", async () => {
|
|
83
|
+
const fx = fixture();
|
|
84
|
+
const other = path.join(fx.root, "other-target");
|
|
85
|
+
mkdirSync(other);
|
|
86
|
+
symlinkSync(other, fx.link, process.platform === "win32" ? "junction" : "dir");
|
|
87
|
+
await assert.rejects(
|
|
88
|
+
createTargetBridge(fx).run(async (bridge) => bridge.ensure()),
|
|
89
|
+
/not the owned shared cache target/i,
|
|
90
|
+
);
|
|
91
|
+
assert.equal(realpathSync(fx.link), realpathSync(other));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("preserves a same-target symlink that replaces an acquired bridge", () => {
|
|
95
|
+
const fx = fixture();
|
|
96
|
+
let successfulStats = 0;
|
|
97
|
+
const bridge = createTargetBridge({
|
|
98
|
+
...fx,
|
|
99
|
+
lstat(file) {
|
|
100
|
+
const stats = lstatSync(file);
|
|
101
|
+
successfulStats += 1;
|
|
102
|
+
if (successfulStats === 1) return stats;
|
|
103
|
+
return { dev: stats.dev, ino: stats.ino + 1, isSymbolicLink: () => stats.isSymbolicLink() };
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
bridge.ensure();
|
|
107
|
+
unlinkSync(fx.link);
|
|
108
|
+
symlinkSync(fx.target, fx.link, process.platform === "win32" ? "junction" : "dir");
|
|
109
|
+
assert.equal(bridge.release(), false);
|
|
110
|
+
assert.equal(realpathSync(fx.link), realpathSync(fx.target));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("preserves a different-target symlink that replaces an acquired bridge", () => {
|
|
114
|
+
const fx = fixture();
|
|
115
|
+
const other = path.join(fx.root, "replacement-target");
|
|
116
|
+
mkdirSync(other);
|
|
117
|
+
const bridge = createTargetBridge(fx);
|
|
118
|
+
bridge.ensure();
|
|
119
|
+
unlinkSync(fx.link);
|
|
120
|
+
symlinkSync(other, fx.link, process.platform === "win32" ? "junction" : "dir");
|
|
121
|
+
assert.equal(bridge.release(), false);
|
|
122
|
+
assert.equal(realpathSync(fx.link), realpathSync(other));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("fails closed and preserves a real target directory with user content", async () => {
|
|
126
|
+
const fx = fixture();
|
|
127
|
+
mkdirSync(fx.link);
|
|
128
|
+
writeFileSync(path.join(fx.link, "user.txt"), "keep\n");
|
|
129
|
+
await assert.rejects(
|
|
130
|
+
createTargetBridge(fx).run(async (bridge) => bridge.ensure()),
|
|
131
|
+
/not a symbolic link/i,
|
|
132
|
+
);
|
|
133
|
+
assert.equal(readFileSync(path.join(fx.link, "user.txt"), "utf8"), "keep\n");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("cleanup is idempotent and reports process errors without masking a build failure", async () => {
|
|
137
|
+
const fx = fixture();
|
|
138
|
+
const bridge = createTargetBridge(fx);
|
|
139
|
+
bridge.ensure();
|
|
140
|
+
assert.equal(bridge.release(), true);
|
|
141
|
+
assert.equal(bridge.release(), false);
|
|
142
|
+
|
|
143
|
+
const cleanupFailure = new Error("unlink denied");
|
|
144
|
+
const failing = createTargetBridge({ ...fx, unlink: () => { throw cleanupFailure; } });
|
|
145
|
+
const buildFailure = new Error("build aborted");
|
|
146
|
+
await assert.rejects(
|
|
147
|
+
failing.run(async () => {
|
|
148
|
+
failing.ensure();
|
|
149
|
+
throw buildFailure;
|
|
150
|
+
}),
|
|
151
|
+
(error) => error === buildFailure && error.cleanupError === cleanupFailure,
|
|
152
|
+
);
|
|
153
|
+
assert.equal(lstatSync(fx.link).isSymbolicLink(), true);
|
|
154
|
+
unlinkSync(fx.link);
|
|
155
|
+
});
|