@rightkit/release 0.2.46 → 0.2.47
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 +8 -1
- package/build-release.test.mjs +5 -1
- package/package.json +1 -1
- package/right-suite-contract.test.mjs +2 -2
- package/rightkit-versions.json +2 -2
- package/target-bridge.mjs +67 -3
- package/target-bridge.test.mjs +52 -1
package/build-release.mjs
CHANGED
|
@@ -173,7 +173,14 @@ try {
|
|
|
173
173
|
createdAt: new Date().toISOString(),
|
|
174
174
|
};
|
|
175
175
|
const targetLink = path.join(appRoot, "src-tauri", "target");
|
|
176
|
-
|
|
176
|
+
// ownedRoot is the per-app cache parent holding every fingerprint dir, so a
|
|
177
|
+
// link left by an earlier fingerprint is recognised as ours and repointed
|
|
178
|
+
// instead of hard-stopping the build.
|
|
179
|
+
const targetBridge = createTargetBridge({
|
|
180
|
+
link: targetLink,
|
|
181
|
+
target: env.CARGO_TARGET_DIR,
|
|
182
|
+
ownedRoot: path.dirname(env.CARGO_TARGET_DIR),
|
|
183
|
+
});
|
|
177
184
|
|
|
178
185
|
const result = await targetBridge.run(async () => runBuildStateMachine({
|
|
179
186
|
root: repoRoot,
|
package/build-release.test.mjs
CHANGED
|
@@ -45,7 +45,11 @@ test("dirty release config is rejected before the config module is imported", ()
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test("shared Cache V2 target bridge is owned by a finally-cleaned lifecycle", () => {
|
|
48
|
-
assert.match(source, /createTargetBridge\(\{
|
|
48
|
+
assert.match(source, /createTargetBridge\(\{\s*link: targetLink,\s*target: env\.CARGO_TARGET_DIR,/);
|
|
49
|
+
// ownedRoot must stay wired: without it the bridge refuses every link left by
|
|
50
|
+
// an earlier fingerprint, so any Cargo.lock or version bump hard-stops the
|
|
51
|
+
// next build until someone deletes the link by hand.
|
|
52
|
+
assert.match(source, /ownedRoot: path\.dirname\(env\.CARGO_TARGET_DIR\)/);
|
|
49
53
|
assert.match(source, /targetBridge\.run\(.*runBuildStateMachine/s);
|
|
50
54
|
assert.match(source, /if \(cacheMode === "shared"\) targetBridge\.ensure\(\)/);
|
|
51
55
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.47",
|
|
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": {
|
|
@@ -432,7 +432,7 @@ test("every app platform requires effective non-empty build inputs", () => {
|
|
|
432
432
|
test("RightKit exposes one current version manifest", () => {
|
|
433
433
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
434
434
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
435
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
435
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.47");
|
|
436
436
|
assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
|
|
437
437
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
438
438
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
@@ -444,7 +444,7 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
444
444
|
"@rightkit/license": "0.1.6",
|
|
445
445
|
});
|
|
446
446
|
assert.deepEqual(versions.legacyNpm, {
|
|
447
|
-
"@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"],
|
|
447
|
+
"@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"],
|
|
448
448
|
});
|
|
449
449
|
assert.ok(
|
|
450
450
|
new Set([versions.npm["@rightkit/release"], ...versions.legacyNpm["@rightkit/release"]]).has("0.2.42"),
|
package/rightkit-versions.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"@rightkit/logs": "0.1.3",
|
|
8
8
|
"@rightkit/platform-ui": "0.1.0",
|
|
9
9
|
"@rightkit/qa": "0.1.0",
|
|
10
|
-
"@rightkit/release": "0.2.
|
|
10
|
+
"@rightkit/release": "0.2.47",
|
|
11
11
|
"@rightkit/tauri": "0.1.0",
|
|
12
12
|
"@rightkit/updates": "0.2.3"
|
|
13
13
|
},
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@rightkit/license": "0.1.6"
|
|
18
18
|
},
|
|
19
19
|
"legacyNpm": {
|
|
20
|
-
"@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"]
|
|
20
|
+
"@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"]
|
|
21
21
|
},
|
|
22
22
|
"cargo": {
|
|
23
23
|
"rightkit-license": "0.1.2",
|
package/target-bridge.mjs
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { lstatSync, mkdirSync, realpathSync, symlinkSync, unlinkSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @param ownedRoot Directory this bridge owns (the app's per-fingerprint cache
|
|
6
|
+
* parent). An existing link resolving INSIDE it was created by this system for
|
|
7
|
+
* an earlier fingerprint, so it is safe to relink: a symlink is a pointer, not
|
|
8
|
+
* data, and the directory it pointed at is untouched. Without this, every
|
|
9
|
+
* fingerprint change — i.e. every Cargo.lock or version bump — left a stale
|
|
10
|
+
* link that hard-stopped the next build until someone deleted it by hand.
|
|
11
|
+
* Omit it to keep the strict refuse-everything behavior.
|
|
12
|
+
*/
|
|
4
13
|
export function createTargetBridge({
|
|
5
14
|
link,
|
|
6
15
|
target,
|
|
16
|
+
ownedRoot,
|
|
7
17
|
platform = process.platform,
|
|
8
18
|
lstat = lstatSync,
|
|
9
19
|
mkdir = mkdirSync,
|
|
@@ -29,9 +39,40 @@ export function createTargetBridge({
|
|
|
29
39
|
ownership = entryIdentity(entry);
|
|
30
40
|
created = true;
|
|
31
41
|
} else {
|
|
32
|
-
if (!entry.isSymbolicLink())
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
if (!entry.isSymbolicLink()) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`primary checkout target is not a symbolic link; refusing to replace it: ${link}\n` +
|
|
45
|
+
` It is a real directory holding build output. Delete it yourself if that output is disposable, then re-run.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
// A dangling link (its cache entry was pruned) makes realpath throw, so
|
|
49
|
+
// this comparison must tolerate ENOENT rather than crash before the
|
|
50
|
+
// reclaim path below gets a chance to handle exactly that case.
|
|
51
|
+
let pointsAtTarget = false;
|
|
52
|
+
try {
|
|
53
|
+
pointsAtTarget = sameRealPath(link, target, realpath, platform);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error?.code !== "ENOENT") throw error;
|
|
56
|
+
}
|
|
57
|
+
if (!pointsAtTarget) {
|
|
58
|
+
// Stale link from an earlier fingerprint, or one whose cache entry a
|
|
59
|
+
// prune already removed. Both are ours to repoint.
|
|
60
|
+
if (isReclaimableLink(link, ownedRoot, realpath, platform)) {
|
|
61
|
+
unlink(link);
|
|
62
|
+
symlink(target, link, platform === "win32" ? "junction" : "dir");
|
|
63
|
+
entry = lstat(link);
|
|
64
|
+
if (!sameRealPath(link, target, realpath, platform)) {
|
|
65
|
+
throw new Error(`RightKit target bridge did not repoint to the owned shared cache target: ${link}`);
|
|
66
|
+
}
|
|
67
|
+
ownership = entryIdentity(entry);
|
|
68
|
+
created = true;
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`primary checkout target is not the owned shared cache target; refusing to replace it: ${link}\n` +
|
|
72
|
+
` It is a symlink pointing outside this app's release cache, so it was not created by RightKit.\n` +
|
|
73
|
+
` Repoint or remove it yourself, then re-run.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
35
76
|
}
|
|
36
77
|
}
|
|
37
78
|
acquired = true;
|
|
@@ -110,6 +151,29 @@ function readEntry(file, lstat) {
|
|
|
110
151
|
}
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
/**
|
|
155
|
+
* True when `link` is a symlink RightKit may repoint: it resolves inside
|
|
156
|
+
* `ownedRoot`, or it dangles entirely (its cache entry was pruned). A link
|
|
157
|
+
* resolving anywhere else is someone else's and is never touched.
|
|
158
|
+
*/
|
|
159
|
+
function isReclaimableLink(link, ownedRoot, realpath, platform) {
|
|
160
|
+
if (!ownedRoot) return false;
|
|
161
|
+
const canonical = (value) => {
|
|
162
|
+
const resolved = path.normalize(value);
|
|
163
|
+
return platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
164
|
+
};
|
|
165
|
+
let resolved;
|
|
166
|
+
try {
|
|
167
|
+
resolved = canonical(realpath(link));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// Dangling link: the entry it pointed at is gone, so nothing can be lost.
|
|
170
|
+
if (error?.code === "ENOENT") return true;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
const root = canonical(ownedRoot);
|
|
174
|
+
return resolved === root || resolved.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
113
177
|
function sameRealPath(left, right, realpath, platform) {
|
|
114
178
|
const canonical = (value) => {
|
|
115
179
|
const resolved = path.normalize(realpath(value));
|
package/target-bridge.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
@@ -153,3 +153,54 @@ test("cleanup is idempotent and reports process errors without masking a build f
|
|
|
153
153
|
assert.equal(lstatSync(fx.link).isSymbolicLink(), true);
|
|
154
154
|
unlinkSync(fx.link);
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
test("a stale link from an earlier fingerprint is repointed, not a hard stop", () => {
|
|
158
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "bridge-stale-"));
|
|
159
|
+
const ownedRoot = path.join(dir, "cache", "heardright");
|
|
160
|
+
const oldTarget = path.join(ownedRoot, "aaaaaaaa");
|
|
161
|
+
const newTarget = path.join(ownedRoot, "bbbbbbbb");
|
|
162
|
+
mkdirSync(oldTarget, { recursive: true });
|
|
163
|
+
const link = path.join(dir, "target");
|
|
164
|
+
symlinkSync(oldTarget, link, "junction");
|
|
165
|
+
|
|
166
|
+
const bridge = createTargetBridge({ link, target: newTarget, ownedRoot });
|
|
167
|
+
const result = bridge.ensure();
|
|
168
|
+
|
|
169
|
+
assert.equal(result.created, true);
|
|
170
|
+
assert.equal(realpathSync(link), realpathSync(newTarget));
|
|
171
|
+
// The directory the stale link pointed at must survive: it is cache, and the
|
|
172
|
+
// link was only ever a pointer to it.
|
|
173
|
+
assert.equal(existsSync(oldTarget), true);
|
|
174
|
+
rmSync(dir, { recursive: true, force: true });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a link pointing outside the owned root is still refused", () => {
|
|
178
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "bridge-foreign-"));
|
|
179
|
+
const ownedRoot = path.join(dir, "cache", "heardright");
|
|
180
|
+
const foreign = path.join(dir, "somewhere-else");
|
|
181
|
+
const newTarget = path.join(ownedRoot, "bbbbbbbb");
|
|
182
|
+
mkdirSync(foreign, { recursive: true });
|
|
183
|
+
const link = path.join(dir, "target");
|
|
184
|
+
symlinkSync(foreign, link, "junction");
|
|
185
|
+
|
|
186
|
+
const bridge = createTargetBridge({ link, target: newTarget, ownedRoot });
|
|
187
|
+
assert.throws(() => bridge.ensure(), /refusing to replace it/);
|
|
188
|
+
assert.equal(realpathSync(link), realpathSync(foreign));
|
|
189
|
+
rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a dangling link whose cache entry was pruned is reclaimed", () => {
|
|
193
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "bridge-dangling-"));
|
|
194
|
+
const ownedRoot = path.join(dir, "cache", "heardright");
|
|
195
|
+
const pruned = path.join(ownedRoot, "aaaaaaaa");
|
|
196
|
+
const newTarget = path.join(ownedRoot, "bbbbbbbb");
|
|
197
|
+
mkdirSync(pruned, { recursive: true });
|
|
198
|
+
const link = path.join(dir, "target");
|
|
199
|
+
symlinkSync(pruned, link, "junction");
|
|
200
|
+
rmSync(pruned, { recursive: true, force: true });
|
|
201
|
+
|
|
202
|
+
const bridge = createTargetBridge({ link, target: newTarget, ownedRoot });
|
|
203
|
+
assert.equal(bridge.ensure().created, true);
|
|
204
|
+
assert.equal(realpathSync(link), realpathSync(newTarget));
|
|
205
|
+
rmSync(dir, { recursive: true, force: true });
|
|
206
|
+
});
|