@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 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 (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
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]) });
@@ -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.40",
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.40");
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");
@@ -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.40",
10
+ "@rightkit/release": "0.2.41",
11
11
  "@rightkit/tauri": "0.1.0",
12
12
  "@rightkit/updates": "0.2.3"
13
13
  },
@@ -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
+ });