@rightkit/release 0.2.43 → 0.2.45
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 +2 -0
- package/nsis-upgrade-contract.mjs +60 -0
- package/nsis-upgrade-contract.test.mjs +57 -0
- package/package.json +1 -1
- package/release-state.mjs +19 -1
- package/release-state.test.mjs +25 -0
- package/release.test.mjs +1 -1
- package/right-suite-contract.test.mjs +3 -3
- package/rightkit-versions.json +2 -2
package/build-release.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInpu
|
|
|
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
26
|
import { createTargetBridge } from "./target-bridge.mjs";
|
|
27
|
+
import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
|
|
27
28
|
|
|
28
29
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
const WORKER = path.join(TOOL_ROOT, "release.mjs");
|
|
@@ -80,6 +81,7 @@ try {
|
|
|
80
81
|
}
|
|
81
82
|
const target = config.targets?.[platform];
|
|
82
83
|
if (!target?.package) fail(`${config.app} has no ${platform} package command`);
|
|
84
|
+
if (platform === "win") assertNsisInPlaceUpgradeContract(appRoot, target.nsisUpgradeContract);
|
|
83
85
|
const { requiredInputs, buildInputs } = resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target });
|
|
84
86
|
const dirtyInputs = dirtyBuildInputs(repoRoot, buildInputs, commit);
|
|
85
87
|
if (dirtyInputs.length > 0) fail(`dirty files can change the packaged app; resolve them before release:\n${dirtyInputs.map((file) => `- ${file}`).join("\n")}`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function assertNsisInPlaceUpgradeContract(appRoot, options = {}) {
|
|
5
|
+
const baseConfigPath = path.resolve(appRoot, options.tauriConfig ?? "src-tauri/tauri.conf.json");
|
|
6
|
+
const windowsConfigPath = path.resolve(
|
|
7
|
+
appRoot,
|
|
8
|
+
options.windowsTauriConfig ?? "src-tauri/tauri.windows.conf.json",
|
|
9
|
+
);
|
|
10
|
+
const base = readJson(baseConfigPath, "base Tauri config");
|
|
11
|
+
const windows = existsSync(windowsConfigPath)
|
|
12
|
+
? readJson(windowsConfigPath, "Windows Tauri config")
|
|
13
|
+
: {};
|
|
14
|
+
const template =
|
|
15
|
+
windows.bundle?.windows?.nsis?.template ?? base.bundle?.windows?.nsis?.template;
|
|
16
|
+
if (typeof template !== "string" || !template.trim()) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Windows releases require a custom NSIS template with the RightKit automatic in-place upgrade contract; Tauri's stock maintenance choice page is not allowed",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const declaringConfig = windows.bundle?.windows?.nsis?.template
|
|
23
|
+
? windowsConfigPath
|
|
24
|
+
: baseConfigPath;
|
|
25
|
+
const templatePath = path.resolve(path.dirname(declaringConfig), template);
|
|
26
|
+
if (!within(appRoot, templatePath) || !existsSync(templatePath)) {
|
|
27
|
+
throw new Error(`Windows NSIS template is missing or outside the app root: ${template}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const source = readFileSync(templatePath, "utf8");
|
|
31
|
+
if (!/!define\s+RIGHTKIT_AUTOMATIC_IN_PLACE_UPGRADE\b/.test(source)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"Windows NSIS template must declare the RightKit automatic in-place upgrade policy; uninstall/reinstall maintenance choices add unnecessary friction",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
!/\$\{If\}\s+\$WixMode\s*=\s*0[\s\S]{0,160}\$\{AndIf\}\s+\$R0\s*>=\s*0[\s\S]{0,80}\bAbort\b/.test(
|
|
38
|
+
source,
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Windows NSIS template is missing skip-page logic for same-version repair and in-place upgrades",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return { templatePath };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readJson(file, label) {
|
|
49
|
+
if (!existsSync(file)) throw new Error(`missing ${label}: ${file}`);
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`invalid ${label}: ${file} (${String(error)})`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function within(root, candidate) {
|
|
58
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
59
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
60
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
|
|
7
|
+
|
|
8
|
+
function fixture(template) {
|
|
9
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "right-nsis-upgrade-"));
|
|
10
|
+
const tauriDir = path.join(root, "src-tauri");
|
|
11
|
+
const windowsDir = path.join(tauriDir, "windows");
|
|
12
|
+
mkdirSync(windowsDir, { recursive: true });
|
|
13
|
+
writeFileSync(
|
|
14
|
+
path.join(tauriDir, "tauri.conf.json"),
|
|
15
|
+
`${JSON.stringify({ bundle: { windows: { nsis: { template: "windows/installer.nsi" } } } })}\n`,
|
|
16
|
+
);
|
|
17
|
+
writeFileSync(path.join(windowsDir, "installer.nsi"), template);
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("rejects an NSIS template that exposes uninstall-or-reinstall maintenance choices", () => {
|
|
22
|
+
const root = fixture(`
|
|
23
|
+
Page custom PageReinstall PageLeaveReinstall
|
|
24
|
+
Function PageReinstall
|
|
25
|
+
nsDialogs::Show
|
|
26
|
+
FunctionEnd
|
|
27
|
+
`);
|
|
28
|
+
assert.throws(
|
|
29
|
+
() => assertNsisInPlaceUpgradeContract(root),
|
|
30
|
+
/automatic in-place upgrade|maintenance choice/i,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("accepts a template that skips maintenance UI for same-version repair and upgrades", () => {
|
|
35
|
+
const root = fixture(`
|
|
36
|
+
!define RIGHTKIT_AUTOMATIC_IN_PLACE_UPGRADE
|
|
37
|
+
Function PageReinstall
|
|
38
|
+
$\{If\} $WixMode = 0
|
|
39
|
+
$\{AndIf\} $R0 >= 0
|
|
40
|
+
Abort
|
|
41
|
+
$\{EndIf\}
|
|
42
|
+
nsDialogs::Show
|
|
43
|
+
FunctionEnd
|
|
44
|
+
`);
|
|
45
|
+
const result = assertNsisInPlaceUpgradeContract(root);
|
|
46
|
+
assert.match(result.templatePath, /installer\.nsi$/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("rejects a marker without executable skip-page logic", () => {
|
|
50
|
+
const root = fixture(`
|
|
51
|
+
!define RIGHTKIT_AUTOMATIC_IN_PLACE_UPGRADE
|
|
52
|
+
Function PageReinstall
|
|
53
|
+
nsDialogs::Show
|
|
54
|
+
FunctionEnd
|
|
55
|
+
`);
|
|
56
|
+
assert.throws(() => assertNsisInPlaceUpgradeContract(root), /skip.*logic/i);
|
|
57
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.45",
|
|
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/release-state.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, watch } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { DEFAULT_SCCACHE_MAX_BYTES, resolveCacheLayout } from "./cache-policy.mjs";
|
|
6
6
|
|
|
@@ -118,6 +118,24 @@ export async function runBuildStateMachine({
|
|
|
118
118
|
const releaseId = `${app}-${version}-${commit.slice(0, 8)}`;
|
|
119
119
|
const platformDir = platform === "win" ? "windows" : platform === "mac" ? "mac" : platform;
|
|
120
120
|
const sealedDir = path.join(root, ".right-release", "sealed", releaseId, platformDir);
|
|
121
|
+
const sealedRoot = path.join(root, ".right-release", "sealed");
|
|
122
|
+
if (existsSync(sealedRoot)) {
|
|
123
|
+
for (const releaseEntry of readdirSync(sealedRoot, { withFileTypes: true })) {
|
|
124
|
+
if (!releaseEntry.isDirectory()) continue;
|
|
125
|
+
const releaseRoot = path.join(sealedRoot, releaseEntry.name);
|
|
126
|
+
for (const platformEntry of readdirSync(releaseRoot, { withFileTypes: true })) {
|
|
127
|
+
if (!platformEntry.isDirectory()) continue;
|
|
128
|
+
const manifestPath = path.join(releaseRoot, platformEntry.name, "release-manifest.json");
|
|
129
|
+
if (!existsSync(manifestPath)) continue;
|
|
130
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
131
|
+
if (manifest.app === app && manifest.version === version && manifest.commit !== commit) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`${app} version ${version} is already sealed from commit ${manifest.commit} (${manifest.releaseId}); bump the version before rebuilding`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
121
139
|
if (existsSync(path.join(sealedDir, "release-manifest.json"))) {
|
|
122
140
|
const sealed = verifySealedRelease(sealedDir);
|
|
123
141
|
if (sealed.manifest.app !== app || sealed.manifest.version !== version || sealed.manifest.commit !== commit) {
|
package/release-state.test.mjs
CHANGED
|
@@ -150,6 +150,31 @@ test("missing runtime input fails before build preparation and cannot touch an e
|
|
|
150
150
|
assert.equal(readFileSync(fx.installer, "utf8"), "signed-installer");
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
test("a version already sealed from another commit cannot be rebuilt", async () => {
|
|
154
|
+
const fx = fixture();
|
|
155
|
+
let prepared = false;
|
|
156
|
+
await assert.rejects(
|
|
157
|
+
runBuildStateMachine({
|
|
158
|
+
root: fx.root,
|
|
159
|
+
app: "fixture",
|
|
160
|
+
version: fx.manifest.version,
|
|
161
|
+
commit: "1234567890abcdef",
|
|
162
|
+
platform: "win",
|
|
163
|
+
requiredInputs: [],
|
|
164
|
+
ops: {
|
|
165
|
+
preflight: async () => {},
|
|
166
|
+
prepare: async () => { prepared = true; },
|
|
167
|
+
build: async () => {},
|
|
168
|
+
sign: async () => {},
|
|
169
|
+
harden: async () => {},
|
|
170
|
+
seal: async () => {},
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
/version 1\.2\.3 is already sealed from commit abcdef1234567890/,
|
|
174
|
+
);
|
|
175
|
+
assert.equal(prepared, false);
|
|
176
|
+
});
|
|
177
|
+
|
|
153
178
|
test("an interrupted build leaves every previously sealed release untouched", async () => {
|
|
154
179
|
const fx = fixture();
|
|
155
180
|
await assert.rejects(
|
package/release.test.mjs
CHANGED
|
@@ -238,7 +238,7 @@ test("doctor rejects a selected target with missing or empty buildInputs.include
|
|
|
238
238
|
test("doctor passes a clean selected target", () => {
|
|
239
239
|
const result = runDoctor(fixture());
|
|
240
240
|
assert.equal(result.status, 0, result.stderr);
|
|
241
|
-
assert.match(result.stdout, /right-release 0\.2\.
|
|
241
|
+
assert.match(result.stdout, /right-release 0\.2\.45/);
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
test("doctor permits unrelated dirt", () => {
|
|
@@ -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.45");
|
|
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,11 +444,11 @@ 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"],
|
|
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"],
|
|
448
448
|
});
|
|
449
449
|
assert.ok(
|
|
450
450
|
new Set([versions.npm["@rightkit/release"], ...versions.legacyNpm["@rightkit/release"]]).has("0.2.42"),
|
|
451
|
-
"
|
|
451
|
+
"previously published @rightkit/release versions must remain accepted during the 0.2.45 rollout",
|
|
452
452
|
);
|
|
453
453
|
const licensePackage = JSON.parse(readFileSync(
|
|
454
454
|
path.join(workspace, "tools/rightkit/packages/license/package.json"),
|
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.45",
|
|
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"]
|
|
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"]
|
|
21
21
|
},
|
|
22
22
|
"cargo": {
|
|
23
23
|
"rightkit-license": "0.1.2",
|