@rightkit/release 0.2.47 → 0.2.48
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 +3 -2
- package/cache-policy.mjs +38 -5
- package/cache-policy.test.mjs +16 -1
- package/package.json +1 -1
- package/preflight.mjs +22 -2
- package/release-state.mjs +4 -3
- package/release-state.test.mjs +16 -0
- package/release.mjs +3 -2
- package/release.test.mjs +21 -10
- package/right-suite-contract.test.mjs +9 -1
- package/rightkit-versions.json +3 -2
- package/standalone-clone-verify.test.mjs +1 -1
- package/target-bridge.mjs +29 -31
- package/target-bridge.test.mjs +11 -0
package/build-release.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
|
|
|
28
28
|
import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
|
|
29
29
|
|
|
30
30
|
/** Assemble preflight inputs from the app's own files (mirrors release.mjs). */
|
|
31
|
-
function buildPreflight({ config, appRoot, repoRoot, platform }) {
|
|
31
|
+
function buildPreflight({ config, configPath, appRoot, repoRoot, platform }) {
|
|
32
32
|
let version;
|
|
33
33
|
try {
|
|
34
34
|
version = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8")).version;
|
|
@@ -40,6 +40,7 @@ function buildPreflight({ config, appRoot, repoRoot, platform }) {
|
|
|
40
40
|
appRoot,
|
|
41
41
|
repoRoot,
|
|
42
42
|
app: config.app,
|
|
43
|
+
configPath,
|
|
43
44
|
// The version check is the doctor's job: by the time `build` runs, seal
|
|
44
45
|
// collision is already enforced downstream, and duplicating it here would
|
|
45
46
|
// reject a legitimate resume of an in-flight build.
|
|
@@ -127,7 +128,7 @@ try {
|
|
|
127
128
|
// Preflight BEFORE any compile. These preconditions used to be discovered one
|
|
128
129
|
// per build cycle, minutes deep, with errors that named a symptom instead of a
|
|
129
130
|
// cause. Blocking problems stop here, in seconds, naming the fix.
|
|
130
|
-
const preflight = buildPreflight({ config, appRoot, repoRoot: layout.repoRoot, platform });
|
|
131
|
+
const preflight = buildPreflight({ config, configPath, appRoot, repoRoot: layout.repoRoot, platform });
|
|
131
132
|
const preflightBlockers = preflightFailures(preflight);
|
|
132
133
|
if (preflightBlockers.length > 0) {
|
|
133
134
|
fail(`preflight found ${preflightBlockers.length} blocking problem(s):\n${formatPreflight(preflightBlockers)}`);
|
package/cache-policy.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { spawnSync } from "node:child_process";
|
|
|
21
21
|
|
|
22
22
|
export const CACHE_SCHEMA = 1;
|
|
23
23
|
export const DEFAULT_TARGET_MAX_BYTES = 12 * 1024 ** 3;
|
|
24
|
-
export const DEFAULT_SCCACHE_MAX_BYTES =
|
|
24
|
+
export const DEFAULT_SCCACHE_MAX_BYTES = 32 * 1024 ** 3;
|
|
25
25
|
export const DEFAULT_DESIRED_FREE_BYTES = 60 * 1024 ** 3;
|
|
26
26
|
export const DEFAULT_HARD_FREE_BYTES = 25 * 1024 ** 3;
|
|
27
27
|
// Any build holds release -> suite slot -> GC -> entry lease; prune holds only GC.
|
|
@@ -35,7 +35,9 @@ export function resolveSharedCacheRoot({ platform = process.platform, env = proc
|
|
|
35
35
|
if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
|
|
36
36
|
return resolveForPlatform(override, platform);
|
|
37
37
|
}
|
|
38
|
-
if (platform === "darwin" || platform === "mac")
|
|
38
|
+
if (platform === "darwin" || platform === "mac") {
|
|
39
|
+
throw new Error("RIGHT_RELEASE_CACHE_ROOT is required on macOS; set it to external storage");
|
|
40
|
+
}
|
|
39
41
|
if (platform === "win32" || platform === "win") {
|
|
40
42
|
const local = env.LOCALAPPDATA;
|
|
41
43
|
if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error("LOCALAPPDATA must be an absolute path for the RightSuite cache");
|
|
@@ -92,10 +94,41 @@ export function resolveSharedCacheIdentity({ cargoLockPath, cargoTomlPath, rustc
|
|
|
92
94
|
const target = targetTriple || host;
|
|
93
95
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(target)) throw new Error("invalid Cargo target triple");
|
|
94
96
|
const architecture = target.split("-")[0];
|
|
95
|
-
const
|
|
97
|
+
const cargoLock = cargoLockPath && existsSync(cargoLockPath) ? readFileSync(cargoLockPath, "utf8") : null;
|
|
98
|
+
const cargoLockSha256 = cargoLock === null ? "none" : sha256(cargoLock);
|
|
99
|
+
const cargoBuildGraphSha256 = cargoLock === null ? "none" : sha256(normalizeCargoLockForCache(cargoLock));
|
|
96
100
|
const nativeFeatures = features ?? cargoNativeFeatures(cargoTomlPath);
|
|
97
|
-
const payload = JSON.stringify({
|
|
98
|
-
return { cargoLockSha256, rustc, target, architecture, profile, features: [...nativeFeatures].sort(), fingerprint:
|
|
101
|
+
const payload = JSON.stringify({ cargoBuildGraphSha256, rustc, target, architecture, profile, features: [...nativeFeatures].sort() });
|
|
102
|
+
return { cargoLockSha256, cargoBuildGraphSha256, rustc, target, architecture, profile, features: [...nativeFeatures].sort(), fingerprint: sha256(payload).slice(0, 16) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sha256(value) {
|
|
106
|
+
return createHash("sha256").update(value).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Cargo invalidates local crates from their manifests and sources inside a
|
|
111
|
+
* reused target directory. Their package version is release metadata, not a
|
|
112
|
+
* dependency artifact identity. Registry and Git packages retain every byte,
|
|
113
|
+
* including source, version, and checksum.
|
|
114
|
+
*/
|
|
115
|
+
function normalizeCargoLockForCache(source) {
|
|
116
|
+
const lines = String(source).replace(/\r\n?/g, "\n").split("\n");
|
|
117
|
+
const normalized = [];
|
|
118
|
+
for (let start = 0; start < lines.length;) {
|
|
119
|
+
if (lines[start].trim() !== "[[package]]") {
|
|
120
|
+
normalized.push(lines[start]);
|
|
121
|
+
start += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
let end = start + 1;
|
|
125
|
+
while (end < lines.length && lines[end].trim() !== "[[package]]") end += 1;
|
|
126
|
+
const block = lines.slice(start, end);
|
|
127
|
+
const external = block.some((line) => /^\s*(?:source|checksum)\s*=/.test(line));
|
|
128
|
+
normalized.push(...(external ? block : block.filter((line) => !/^\s*version\s*=/.test(line))));
|
|
129
|
+
start = end;
|
|
130
|
+
}
|
|
131
|
+
return normalized.join("\n");
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
export function ensureCacheEntry({ layout, metadata = {}, now = new Date() } = {}) {
|
package/cache-policy.test.mjs
CHANGED
|
@@ -57,7 +57,7 @@ function migrationFixture() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
test("cache roots are platform-native and overrides must be absolute", () => {
|
|
60
|
-
assert.
|
|
60
|
+
assert.throws(() => resolveSharedCacheRoot({ platform: "mac", home: "/Users/test", env: {} }), /RIGHT_RELEASE_CACHE_ROOT is required/);
|
|
61
61
|
assert.equal(resolveSharedCacheRoot({ platform: "win", env: { LOCALAPPDATA: "C:/Users/test/AppData/Local" } }), "C:\\Users\\test\\AppData\\Local\\RightSuite\\Cache\\release");
|
|
62
62
|
assert.equal(resolveSharedCacheRoot({ platform: "linux", home: "/home/test", xdgCacheHome: "/tmp/xdg", env: {} }), "/tmp/xdg/rightsuite/release");
|
|
63
63
|
assert.equal(resolveSharedCacheRoot({ platform: "mac", env: { RIGHT_RELEASE_CACHE_ROOT: "/tmp/cache" } }), "/tmp/cache");
|
|
@@ -119,6 +119,21 @@ test("build and migration derive identical native-feature cache fingerprints", (
|
|
|
119
119
|
assert.notEqual(build.fingerprint, resolveSharedCacheIdentity({ ...inputs, features: [] }).fingerprint);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
test("cache identity ignores local workspace version bumps but keeps registry dependency identity", () => {
|
|
123
|
+
const base = root();
|
|
124
|
+
const cargoLock = path.join(base, "Cargo.lock");
|
|
125
|
+
mkdirSync(base, { recursive: true });
|
|
126
|
+
const lock = (appVersion, depVersion, checksum) => `version = 4\n\n[[package]]\nname = "fixture"\nversion = "${appVersion}"\ndependencies = [\n "serde",\n]\n\n[[package]]\nname = "serde"\nversion = "${depVersion}"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\nchecksum = "${checksum}"\n`;
|
|
127
|
+
const inputs = { cargoLockPath: cargoLock, rustcVerbose: "rustc 1.91.0\nhost: aarch64-apple-darwin", targetTriple: "aarch64-apple-darwin" };
|
|
128
|
+
|
|
129
|
+
writeFileSync(cargoLock, lock("1.0.0", "1.0.0", "aaa"));
|
|
130
|
+
const original = resolveSharedCacheIdentity(inputs);
|
|
131
|
+
writeFileSync(cargoLock, lock("1.0.1", "1.0.0", "aaa"));
|
|
132
|
+
assert.equal(resolveSharedCacheIdentity(inputs).fingerprint, original.fingerprint);
|
|
133
|
+
writeFileSync(cargoLock, lock("1.0.1", "1.0.1", "bbb"));
|
|
134
|
+
assert.notEqual(resolveSharedCacheIdentity(inputs).fingerprint, original.fingerprint);
|
|
135
|
+
});
|
|
136
|
+
|
|
122
137
|
test("migration moves an actual src-tauri target symlink and keeps dry-run physically pure", () => {
|
|
123
138
|
const fixture = migrationFixture();
|
|
124
139
|
const preview = migrateLegacyCache({ ...fixture, dryRun: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.48",
|
|
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": {
|
package/preflight.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// certs on purpose and timestamps the signature, so "expires in 2 days" is
|
|
19
19
|
// the healthy state, not a problem.)
|
|
20
20
|
|
|
21
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import { spawnSync } from "node:child_process";
|
|
24
24
|
|
|
@@ -86,7 +86,7 @@ export function resolveWindowsPerl({
|
|
|
86
86
|
} = {}) {
|
|
87
87
|
const explicit = [env.OPENSSL_SRC_PERL, env.PERL].filter(Boolean);
|
|
88
88
|
for (const candidate of [...explicit, ...candidates]) {
|
|
89
|
-
if (exists(candidate)) return { perlPath: candidate, dir: path.dirname(candidate) };
|
|
89
|
+
if (exists(candidate)) return { perlPath: candidate, dir: path.win32.dirname(candidate) };
|
|
90
90
|
}
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
@@ -167,6 +167,7 @@ export function collectPreflight({
|
|
|
167
167
|
repoRoot,
|
|
168
168
|
app,
|
|
169
169
|
version,
|
|
170
|
+
configPath,
|
|
170
171
|
cargoLockPaths = [],
|
|
171
172
|
minFreeGb = 25,
|
|
172
173
|
env = process.env,
|
|
@@ -174,6 +175,25 @@ export function collectPreflight({
|
|
|
174
175
|
const checks = [];
|
|
175
176
|
const windows = platform === "win" || platform === "win32";
|
|
176
177
|
|
|
178
|
+
if (appRoot) {
|
|
179
|
+
const target = path.join(appRoot, "src-tauri", "target");
|
|
180
|
+
let entry = null;
|
|
181
|
+
try {
|
|
182
|
+
entry = lstatSync(target);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error?.code !== "ENOENT") throw error;
|
|
185
|
+
}
|
|
186
|
+
checks.push(
|
|
187
|
+
!entry || entry.isSymbolicLink()
|
|
188
|
+
? ok("target-bridge", entry ? "src-tauri/target is a symbolic link" : "src-tauri/target is ready for the shared cache bridge")
|
|
189
|
+
: fail(
|
|
190
|
+
"target-bridge",
|
|
191
|
+
`${target} is a real directory; bridge setup will refuse it`,
|
|
192
|
+
`run right-release cache migrate --config ${JSON.stringify(configPath ?? path.join(appRoot, "right-release.config.mjs"))} --dry-run, then repeat with --apply before building`,
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
if (version && app && repoRoot) {
|
|
178
198
|
const sealedDir = path.join(repoRoot, ".right-release", "sealed");
|
|
179
199
|
const sealed = nextFreeVersion(sealedDir, app, version);
|
package/release-state.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { readCachePolicy, resolveCacheLayout } from "./cache-policy.mjs";
|
|
6
6
|
|
|
7
7
|
export function cacheFingerprint({ cargoLockSha256, rustc, target, architecture, features = [] }) {
|
|
8
8
|
const payload = JSON.stringify({
|
|
@@ -61,17 +61,18 @@ export function watchProgress(paths, onProgress) {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function releaseEnvironment({ root, cacheRoot, platform, architecture, app, cacheKey, kind = "release", appRoot, mode = "legacy" }) {
|
|
64
|
+
export function releaseEnvironment({ root, cacheRoot, platform, architecture, app, cacheKey, kind = "release", appRoot, mode = "legacy", env = process.env }) {
|
|
65
65
|
if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
|
|
66
66
|
if (mode !== "legacy" && mode !== "shared") throw new Error(`invalid cache mode: ${mode}`);
|
|
67
67
|
if (mode === "shared") {
|
|
68
68
|
if (!cacheRoot || !platform || !architecture || !app || !cacheKey || !appRoot) throw new Error("shared release environment requires cacheRoot, platform, architecture, app, cacheKey, and appRoot");
|
|
69
69
|
const layout = resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint: cacheKey, kind });
|
|
70
|
+
const policy = readCachePolicy(env);
|
|
70
71
|
return {
|
|
71
72
|
CARGO_TARGET_DIR: layout.targetDir,
|
|
72
73
|
CARGO_HOME: layout.cargoHome,
|
|
73
74
|
SCCACHE_DIR: layout.sccacheDir,
|
|
74
|
-
SCCACHE_CACHE_SIZE: `${
|
|
75
|
+
SCCACHE_CACHE_SIZE: `${policy.sccacheMaxBytes / 1024 ** 3}G`,
|
|
75
76
|
SCCACHE_BASEDIRS: [path.resolve(root), path.resolve(appRoot)].join(path.delimiter),
|
|
76
77
|
RUSTC_WRAPPER: "sccache",
|
|
77
78
|
RIGHT_RELEASE_CACHE_OWNER: "rightkit-v2",
|
package/release-state.test.mjs
CHANGED
|
@@ -63,6 +63,22 @@ test("shared release environment is isolated from the app vault", () => {
|
|
|
63
63
|
assert.match(env.CARGO_TARGET_DIR, /RightSuite[\\/]release[\\/]targets[\\/]mac[\\/]fixture[\\/]abcdef0123456789$/);
|
|
64
64
|
assert.match(env.CARGO_HOME, /RightSuite[\\/]release[\\/]cargo-home$/);
|
|
65
65
|
assert.equal(env.RIGHT_RELEASE_CACHE_OWNER, "rightkit-v2");
|
|
66
|
+
assert.equal(env.SCCACHE_CACHE_SIZE, "32G");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("shared release environment honors the cache policy override", () => {
|
|
70
|
+
const env = releaseEnvironment({
|
|
71
|
+
root: "/suite/.right-release",
|
|
72
|
+
cacheRoot: "/cache",
|
|
73
|
+
platform: "mac",
|
|
74
|
+
architecture: "aarch64",
|
|
75
|
+
app: "fixture",
|
|
76
|
+
appRoot: "/suite/fixture",
|
|
77
|
+
cacheKey: "abcdef0123456789",
|
|
78
|
+
mode: "shared",
|
|
79
|
+
env: { RIGHT_RELEASE_SCCACHE_MAX_BYTES: String(48 * 1024 ** 3) },
|
|
80
|
+
});
|
|
81
|
+
assert.equal(env.SCCACHE_CACHE_SIZE, "48G");
|
|
66
82
|
});
|
|
67
83
|
|
|
68
84
|
function sha256(value) {
|
package/release.mjs
CHANGED
|
@@ -103,7 +103,7 @@ if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.pl
|
|
|
103
103
|
* Assemble preflight inputs from the app's own files. Kept here (not in
|
|
104
104
|
* preflight.mjs) so the check module stays free of release.mjs's config shape.
|
|
105
105
|
*/
|
|
106
|
-
function releasePreflight({ config, root, repoRoot, platform }) {
|
|
106
|
+
function releasePreflight({ config, configPath, root, repoRoot, platform }) {
|
|
107
107
|
let version;
|
|
108
108
|
try {
|
|
109
109
|
version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
|
|
@@ -123,6 +123,7 @@ function releasePreflight({ config, root, repoRoot, platform }) {
|
|
|
123
123
|
repoRoot,
|
|
124
124
|
app: config.app,
|
|
125
125
|
version,
|
|
126
|
+
configPath,
|
|
126
127
|
cargoLockPaths,
|
|
127
128
|
});
|
|
128
129
|
}
|
|
@@ -158,7 +159,7 @@ if (opts.doctor) {
|
|
|
158
159
|
|
|
159
160
|
// Config above, MACHINE below. Printing config never told anyone whether this
|
|
160
161
|
// box could finish a build; these checks do.
|
|
161
|
-
const checks = releasePreflight({ config, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform });
|
|
162
|
+
const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform });
|
|
162
163
|
console.log("preflight:");
|
|
163
164
|
console.log(formatPreflight(checks));
|
|
164
165
|
const failures = preflightFailures(checks);
|
package/release.test.mjs
CHANGED
|
@@ -10,12 +10,13 @@ const release = fileURLToPath(new URL("./release.mjs", import.meta.url));
|
|
|
10
10
|
const cli = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
|
|
11
11
|
const versions = JSON.parse(readFileSync(new URL("./rightkit-versions.json", import.meta.url), "utf8"));
|
|
12
12
|
const defaultBuildInputs = { include: ["package.json", "pnpm-lock.yaml", "src-tauri/**"] };
|
|
13
|
+
const hostPlatform = process.platform === "darwin" ? "mac" : process.platform === "win32" ? "win" : process.platform;
|
|
13
14
|
|
|
14
15
|
function git(cwd, ...args) {
|
|
15
16
|
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function fixture({ signed = true, publish = false, packageJson, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs } = {}) {
|
|
19
|
+
function fixture({ signed = true, publish = false, packageJson, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win" } = {}) {
|
|
19
20
|
const fixtureRoot = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
|
|
20
21
|
const dir = repoCargoConfig ? path.join(fixtureRoot, "apps", "fixture") : fixtureRoot;
|
|
21
22
|
mkdirSync(dir, { recursive: true });
|
|
@@ -25,7 +26,7 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
|
|
|
25
26
|
JSON.stringify(
|
|
26
27
|
packageJson ?? {
|
|
27
28
|
name: "fixture",
|
|
28
|
-
scripts: {
|
|
29
|
+
scripts: { [`release:patch:${platform}`]: `right-release --platform ${platform} --tier patch` },
|
|
29
30
|
packageManager: versions.packageManager,
|
|
30
31
|
dependencies: {
|
|
31
32
|
"@rightkit/license": versions.npm["@rightkit/license"],
|
|
@@ -45,13 +46,13 @@ function fixture({ signed = true, publish = false, packageJson, cargoManifest, c
|
|
|
45
46
|
packageManager: "pnpm",
|
|
46
47
|
checks: [],
|
|
47
48
|
targets: {
|
|
48
|
-
|
|
49
|
+
[platform]: {
|
|
49
50
|
...(buildInputs ? { buildInputs } : {}),
|
|
50
51
|
signed,
|
|
51
52
|
package: { cmd: "node", args: ["-e", "process.exit(0)"] },
|
|
52
53
|
...(publish ? { publish: { cmd: "node", args: ["publish-update.mjs"] } } : {}),
|
|
53
54
|
artifacts: [],
|
|
54
|
-
sign: { files: ["fixture.exe"] },
|
|
55
|
+
...(platform === "win" ? { sign: { files: ["fixture.exe"] } } : {}),
|
|
55
56
|
updater: { artifacts: [{ file: "fixture.exe", signature: "fixture.exe.sig", platform: "windows-x86_64", key: "fixture/fixture.exe" }] },
|
|
56
57
|
hardening: [],
|
|
57
58
|
},
|
|
@@ -146,7 +147,7 @@ function runRaw(config, ...args) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
function runDoctor(config, ...args) {
|
|
149
|
-
return spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform",
|
|
150
|
+
return spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", hostPlatform, ...args], {
|
|
150
151
|
encoding: "utf8",
|
|
151
152
|
});
|
|
152
153
|
}
|
|
@@ -219,10 +220,11 @@ test("rejects hosted workflow files before release work starts", () => {
|
|
|
219
220
|
|
|
220
221
|
test("doctor accepts an exact crates.io RightKit pin with benign Cargo config", () => {
|
|
221
222
|
const config = fixture({
|
|
223
|
+
platform: hostPlatform,
|
|
222
224
|
cargoManifest: `[package]\nname = "fixture"\nversion = "0.0.0"\nedition = "2021"\n[dependencies]\nrightkit-license = "=0.1.2"`,
|
|
223
225
|
cargoConfig: `[net]\nretry = 2\n[registries.private]\nindex = "https://example.test/index"`,
|
|
224
226
|
});
|
|
225
|
-
const result =
|
|
227
|
+
const result = runDoctor(config);
|
|
226
228
|
assert.equal(result.status, 0, result.stderr);
|
|
227
229
|
assert.match(result.stdout, /right-release/);
|
|
228
230
|
});
|
|
@@ -236,7 +238,7 @@ test("doctor rejects a selected target with missing or empty buildInputs.include
|
|
|
236
238
|
});
|
|
237
239
|
|
|
238
240
|
test("doctor passes a clean selected target", () => {
|
|
239
|
-
const result = runDoctor(fixture());
|
|
241
|
+
const result = runDoctor(fixture({ platform: hostPlatform }));
|
|
240
242
|
assert.equal(result.status, 0, result.stderr);
|
|
241
243
|
// Derived from the version manifest, not hardcoded: a literal here has to be
|
|
242
244
|
// hand-edited on every bump and fails the suite when someone forgets.
|
|
@@ -244,12 +246,20 @@ test("doctor passes a clean selected target", () => {
|
|
|
244
246
|
assert.match(result.stdout, new RegExp(`right-release ${expected.replace(/\./g, "\\.")}`));
|
|
245
247
|
});
|
|
246
248
|
|
|
249
|
+
test("doctor rejects a real src-tauri target before release work", () => {
|
|
250
|
+
const config = fixture({ platform: hostPlatform });
|
|
251
|
+
mkdirSync(path.join(path.dirname(config), "src-tauri", "target"));
|
|
252
|
+
const result = runDoctor(config);
|
|
253
|
+
assert.notEqual(result.status, 0);
|
|
254
|
+
assert.match(`${result.stdout}\n${result.stderr}`, /target-bridge[\s\S]*real directory[\s\S]*cache migrate/i);
|
|
255
|
+
});
|
|
256
|
+
|
|
247
257
|
test("doctor permits unrelated dirt", () => {
|
|
248
|
-
const config = fixture();
|
|
258
|
+
const config = fixture({ platform: hostPlatform });
|
|
249
259
|
const root = path.dirname(config);
|
|
250
260
|
mkdirSync(path.join(root, "notes"));
|
|
251
261
|
writeFileSync(path.join(root, "notes", "scratch.md"), "unrelated\n");
|
|
252
|
-
const result =
|
|
262
|
+
const result = runDoctor(config);
|
|
253
263
|
assert.equal(result.status, 0, result.stderr);
|
|
254
264
|
});
|
|
255
265
|
|
|
@@ -302,11 +312,12 @@ test("doctor rejects linked and detached checkouts like build runtime", () => {
|
|
|
302
312
|
|
|
303
313
|
test("doctor scopes Cargo inspection to the configured app inside a monorepo", () => {
|
|
304
314
|
const config = fixture({
|
|
315
|
+
platform: hostPlatform,
|
|
305
316
|
cargoManifest: `[package]\nname = "fixture"\nversion = "0.0.0"\nedition = "2021"\n[dependencies]\nrightkit-license = "=0.1.2"`,
|
|
306
317
|
repoCargoConfig: `[net]\nretry = 2`,
|
|
307
318
|
siblingCargoManifest: `[package]\nname = "archived-copy"\nversion = "0.0.0"\n[dependencies]\nrightkit-license = { path = "../../rightkit-license" }`,
|
|
308
319
|
});
|
|
309
|
-
const result =
|
|
320
|
+
const result = runDoctor(config);
|
|
310
321
|
assert.equal(result.status, 0, result.stderr);
|
|
311
322
|
});
|
|
312
323
|
|
|
@@ -442,6 +442,7 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
442
442
|
"@rightkit/legal": "0.3.0",
|
|
443
443
|
"@rightkit/legal-ui": "0.1.0",
|
|
444
444
|
"@rightkit/license": "0.1.6",
|
|
445
|
+
"@rightkit/release": "0.2.48",
|
|
445
446
|
});
|
|
446
447
|
assert.deepEqual(versions.legacyNpm, {
|
|
447
448
|
"@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"],
|
|
@@ -499,7 +500,14 @@ for (const app of apps) {
|
|
|
499
500
|
);
|
|
500
501
|
if (allowed.size) assert.ok(allowed.has(specifier), `${app.key} ${name} must match a published rollout version`);
|
|
501
502
|
}
|
|
502
|
-
|
|
503
|
+
const releaseVersion = pkg.devDependencies?.["@rightkit/release"];
|
|
504
|
+
const acceptedPackageManagers = releaseVersion === versions.stagedNpm?.["@rightkit/release"]
|
|
505
|
+
? [versions.packageManager]
|
|
506
|
+
: [versions.packageManager, "pnpm@11.12.0"];
|
|
507
|
+
assert.ok(
|
|
508
|
+
acceptedPackageManagers.includes(pkg.packageManager),
|
|
509
|
+
`${app.key} packageManager must match its @rightkit/release rollout lane`,
|
|
510
|
+
);
|
|
503
511
|
assert.ok(
|
|
504
512
|
new Set([
|
|
505
513
|
versions.npm["@rightkit/release"],
|
package/rightkit-versions.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
|
-
"packageManager": "pnpm@11.
|
|
3
|
+
"packageManager": "pnpm@11.17.0",
|
|
4
4
|
"npm": {
|
|
5
5
|
"@rightkit/legal": "0.2.0",
|
|
6
6
|
"@rightkit/license": "0.1.5",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"stagedNpm": {
|
|
15
15
|
"@rightkit/legal": "0.3.0",
|
|
16
16
|
"@rightkit/legal-ui": "0.1.0",
|
|
17
|
-
"@rightkit/license": "0.1.6"
|
|
17
|
+
"@rightkit/license": "0.1.6",
|
|
18
|
+
"@rightkit/release": "0.2.48"
|
|
18
19
|
},
|
|
19
20
|
"legacyNpm": {
|
|
20
21
|
"@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"]
|
|
@@ -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.17.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
|
@@ -27,8 +27,29 @@ export function createTargetBridge({
|
|
|
27
27
|
|
|
28
28
|
const bridge = {
|
|
29
29
|
ensure() {
|
|
30
|
-
mkdir(target, { recursive: true });
|
|
31
30
|
let entry = readEntry(link, lstat);
|
|
31
|
+
if (entry && !entry.isSymbolicLink()) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`primary checkout target is not a symbolic link; refusing to replace it: ${link}\n` +
|
|
34
|
+
` It is a real directory holding build output. Run right-release cache migrate before building.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
let pointsAtTarget = false;
|
|
38
|
+
if (entry) {
|
|
39
|
+
try {
|
|
40
|
+
pointsAtTarget = sameRealPath(link, target, realpath, platform);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error?.code !== "ENOENT") throw error;
|
|
43
|
+
}
|
|
44
|
+
if (!pointsAtTarget && !isReclaimableLink(link, ownedRoot, realpath, platform)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`primary checkout target is not the owned shared cache target; refusing to replace it: ${link}\n` +
|
|
47
|
+
` It is a symlink pointing outside this app's release cache, so it was not created by RightKit.\n` +
|
|
48
|
+
` Repoint or remove it yourself, then re-run.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
mkdir(target, { recursive: true });
|
|
32
53
|
if (!entry) {
|
|
33
54
|
symlink(target, link, platform === "win32" ? "junction" : "dir");
|
|
34
55
|
entry = lstat(link);
|
|
@@ -39,40 +60,17 @@ export function createTargetBridge({
|
|
|
39
60
|
ownership = entryIdentity(entry);
|
|
40
61
|
created = true;
|
|
41
62
|
} else {
|
|
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
63
|
if (!pointsAtTarget) {
|
|
58
64
|
// Stale link from an earlier fingerprint, or one whose cache entry a
|
|
59
65
|
// prune already removed. Both are ours to repoint.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
);
|
|
66
|
+
unlink(link);
|
|
67
|
+
symlink(target, link, platform === "win32" ? "junction" : "dir");
|
|
68
|
+
entry = lstat(link);
|
|
69
|
+
if (!sameRealPath(link, target, realpath, platform)) {
|
|
70
|
+
throw new Error(`RightKit target bridge did not repoint to the owned shared cache target: ${link}`);
|
|
75
71
|
}
|
|
72
|
+
ownership = entryIdentity(entry);
|
|
73
|
+
created = true;
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
acquired = true;
|
package/target-bridge.test.mjs
CHANGED
|
@@ -133,6 +133,16 @@ test("fails closed and preserves a real target directory with user content", asy
|
|
|
133
133
|
assert.equal(readFileSync(path.join(fx.link, "user.txt"), "utf8"), "keep\n");
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
test("rejecting a real target does not create the shared cache destination", () => {
|
|
137
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "rightkit-target-bridge-pure-"));
|
|
138
|
+
const target = path.join(root, "shared", "target");
|
|
139
|
+
const link = path.join(root, "app", "src-tauri", "target");
|
|
140
|
+
mkdirSync(link, { recursive: true });
|
|
141
|
+
|
|
142
|
+
assert.throws(() => createTargetBridge({ link, target }).ensure(), /not a symbolic link/i);
|
|
143
|
+
assert.equal(existsSync(target), false);
|
|
144
|
+
});
|
|
145
|
+
|
|
136
146
|
test("cleanup is idempotent and reports process errors without masking a build failure", async () => {
|
|
137
147
|
const fx = fixture();
|
|
138
148
|
const bridge = createTargetBridge(fx);
|
|
@@ -185,6 +195,7 @@ test("a link pointing outside the owned root is still refused", () => {
|
|
|
185
195
|
|
|
186
196
|
const bridge = createTargetBridge({ link, target: newTarget, ownedRoot });
|
|
187
197
|
assert.throws(() => bridge.ensure(), /refusing to replace it/);
|
|
198
|
+
assert.equal(existsSync(newTarget), false);
|
|
188
199
|
assert.equal(realpathSync(link), realpathSync(foreign));
|
|
189
200
|
rmSync(dir, { recursive: true, force: true });
|
|
190
201
|
});
|