@rightkit/release 0.2.30 → 0.2.32
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 +384 -0
- package/cli/right-release.mjs +14 -9
- package/create-mac-updater.mjs +6 -1
- package/create-mac-updater.test.mjs +1 -1
- package/legal-contract.mjs +9 -2
- package/legal-contract.test.mjs +4 -0
- package/package.json +1 -1
- package/pub-upload.mjs +100 -0
- package/release-cli-contract.test.mjs +43 -0
- package/release-state.mjs +111 -0
- package/release-state.test.mjs +224 -0
- package/release.mjs +3 -3
- package/release.test.mjs +14 -17
- package/right-suite-contract.test.mjs +2 -3
- package/rightkit-versions.json +3 -4
- package/upload-release.mjs +221 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function cacheFingerprint({ cargoLockSha256, rustc, target, features = [] }) {
|
|
6
|
+
const payload = JSON.stringify({
|
|
7
|
+
cargoLockSha256,
|
|
8
|
+
rustc,
|
|
9
|
+
target,
|
|
10
|
+
features: [...features].sort(),
|
|
11
|
+
});
|
|
12
|
+
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
13
|
+
}
|
|
14
|
+
export function releaseEnvironment({ root, platform, cacheKey, kind = "release" }) {
|
|
15
|
+
if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
|
|
16
|
+
const targetKind = kind === "release" ? "cargo-target" : "test-target";
|
|
17
|
+
return {
|
|
18
|
+
CARGO_TARGET_DIR: path.resolve(root, "cache", targetKind, platform, cacheKey),
|
|
19
|
+
CARGO_HOME: path.resolve(root, "cache", "cargo-home"),
|
|
20
|
+
SCCACHE_DIR: path.resolve(root, "cache", "sccache"),
|
|
21
|
+
RUSTC_WRAPPER: "sccache",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function verifySealedRelease(sealedDir) {
|
|
26
|
+
const manifestPath = path.join(sealedDir, "release-manifest.json");
|
|
27
|
+
if (!existsSync(manifestPath)) throw new Error(`sealed manifest missing: ${manifestPath}`);
|
|
28
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
29
|
+
if (manifest.schema !== 1) throw new Error(`unsupported sealed manifest schema: ${manifest.schema}`);
|
|
30
|
+
if (!manifest.checkpoints?.includes("sealed")) throw new Error("release is not sealed");
|
|
31
|
+
for (const item of manifest.files ?? []) {
|
|
32
|
+
const file = path.join(sealedDir, item.name);
|
|
33
|
+
if (!existsSync(file)) throw new Error(`sealed file missing: ${item.name}`);
|
|
34
|
+
const bytes = readFileSync(file);
|
|
35
|
+
const actualHash = createHash("sha256").update(bytes).digest("hex");
|
|
36
|
+
if (actualHash !== item.sha256) throw new Error(`sealed file hash mismatch: ${item.name}`);
|
|
37
|
+
if (bytes.length !== item.sizeBytes) throw new Error(`sealed file size mismatch: ${item.name}`);
|
|
38
|
+
}
|
|
39
|
+
return { manifest, manifestPath, sealedDir };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function runBuildStateMachine({
|
|
43
|
+
root,
|
|
44
|
+
app,
|
|
45
|
+
version,
|
|
46
|
+
commit,
|
|
47
|
+
platform,
|
|
48
|
+
requiredInputs = [],
|
|
49
|
+
ops,
|
|
50
|
+
}) {
|
|
51
|
+
const releaseId = `${app}-${version}-${commit.slice(0, 8)}`;
|
|
52
|
+
const platformDir = platform === "win" ? "windows" : platform === "mac" ? "mac" : platform;
|
|
53
|
+
const sealedDir = path.join(root, ".right-release", "sealed", releaseId, platformDir);
|
|
54
|
+
if (existsSync(path.join(sealedDir, "release-manifest.json"))) {
|
|
55
|
+
const sealed = verifySealedRelease(sealedDir);
|
|
56
|
+
if (sealed.manifest.app !== app || sealed.manifest.version !== version || sealed.manifest.commit !== commit) {
|
|
57
|
+
throw new Error(`sealed release identity mismatch: ${releaseId}`);
|
|
58
|
+
}
|
|
59
|
+
return { status: "sealed", resumed: true, releaseId, sealedDir };
|
|
60
|
+
}
|
|
61
|
+
await ops.preflight?.({ root, app, version, commit, platform, requiredInputs, releaseId });
|
|
62
|
+
await ops.prepare?.({ root, app, version, commit, platform, releaseId });
|
|
63
|
+
await ops.build?.({ root, app, version, commit, platform, releaseId });
|
|
64
|
+
await ops.sign?.({ root, app, version, commit, platform, releaseId });
|
|
65
|
+
await ops.harden?.({ root, app, version, commit, platform, releaseId });
|
|
66
|
+
await ops.seal?.({ root, app, version, commit, platform, releaseId, sealedDir });
|
|
67
|
+
return { status: "sealed", resumed: false, releaseId, sealedDir };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runUploadStateMachine({ root, releaseId, platform, tier, ops = {} }) {
|
|
71
|
+
if (tier !== "patch" && tier !== "update") throw new Error("upload tier is required (patch|update)");
|
|
72
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/.test(String(releaseId))) throw new Error(`invalid release id: ${releaseId}`);
|
|
73
|
+
const platformDir = platform === "win" ? "windows" : platform === "mac" ? "mac" : platform;
|
|
74
|
+
const sealedDir = path.join(root, ".right-release", "sealed", releaseId, platformDir);
|
|
75
|
+
const sealed = verifySealedRelease(sealedDir);
|
|
76
|
+
if (sealed.manifest.platform !== platform) throw new Error("sealed release platform mismatch");
|
|
77
|
+
const routes = sealed.manifest.routes?.[tier] ?? [];
|
|
78
|
+
if (!routes.length) throw new Error(`sealed release has no ${tier} upload route`);
|
|
79
|
+
validateUploadRoutes(routes, tier, platformDir);
|
|
80
|
+
|
|
81
|
+
await ops.verifyAuthenticode?.(sealed);
|
|
82
|
+
await ops.harden?.(sealed);
|
|
83
|
+
let backedUp = false;
|
|
84
|
+
try {
|
|
85
|
+
await ops.backup?.(sealed, routes);
|
|
86
|
+
backedUp = true;
|
|
87
|
+
await ops.upload?.(sealed, routes);
|
|
88
|
+
await ops.register?.(sealed, routes);
|
|
89
|
+
await ops.verifyRemote?.(sealed, routes);
|
|
90
|
+
await ops.discardBackup?.(sealed, routes);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (backedUp) await ops.restore?.(sealed, routes);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
return { status: "verified", releaseId, tier, sealedDir };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validateUploadRoutes(routes, tier, platformDir) {
|
|
99
|
+
const expectedBucket = tier === "patch" ? "public" : "private";
|
|
100
|
+
const expectedLane = tier === "patch" ? "installers" : "updates";
|
|
101
|
+
const marker = `/${expectedLane}/${platformDir}/current/`;
|
|
102
|
+
for (const route of routes) {
|
|
103
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(String(route.key)) || String(route.key).includes("//")) {
|
|
104
|
+
throw new Error(`unsafe R2 key: ${route.key}`);
|
|
105
|
+
}
|
|
106
|
+
if (route.bucket !== expectedBucket || !String(route.key).includes(marker)) {
|
|
107
|
+
throw new Error(`${tier} route must use ${expectedBucket} ${expectedLane}/${platformDir}/current: ${route.key}`);
|
|
108
|
+
}
|
|
109
|
+
if (String(route.key).includes("..")) throw new Error(`${tier} route contains traversal: ${route.key}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
cacheFingerprint,
|
|
10
|
+
releaseEnvironment,
|
|
11
|
+
runBuildStateMachine,
|
|
12
|
+
runUploadStateMachine,
|
|
13
|
+
verifySealedRelease,
|
|
14
|
+
} from "./release-state.mjs";
|
|
15
|
+
|
|
16
|
+
function sha256(value) {
|
|
17
|
+
return createHash("sha256").update(value).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fixture() {
|
|
21
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "right-release-state-"));
|
|
22
|
+
const sealedDir = path.join(root, ".right-release", "sealed", "fixture-1.2.3-abcdef12", "windows");
|
|
23
|
+
mkdirSync(sealedDir, { recursive: true });
|
|
24
|
+
const installer = path.join(sealedDir, "Fixture_x64-setup.exe");
|
|
25
|
+
const signature = `${installer}.sig`;
|
|
26
|
+
writeFileSync(installer, "signed-installer");
|
|
27
|
+
writeFileSync(signature, "updater-signature");
|
|
28
|
+
const manifest = {
|
|
29
|
+
schema: 1,
|
|
30
|
+
releaseId: "fixture-1.2.3-abcdef12",
|
|
31
|
+
app: "fixture",
|
|
32
|
+
version: "1.2.3",
|
|
33
|
+
commit: "abcdef1234567890",
|
|
34
|
+
platform: "win",
|
|
35
|
+
files: [
|
|
36
|
+
{ role: "installer", name: path.basename(installer), sha256: sha256("signed-installer"), sizeBytes: 16 },
|
|
37
|
+
{ role: "updater-signature", name: path.basename(signature), sha256: sha256("updater-signature"), sizeBytes: 17 },
|
|
38
|
+
],
|
|
39
|
+
routes: {
|
|
40
|
+
patch: [{ role: "installer", bucket: "public", key: "fixture/installers/windows/current/Fixture_x64-setup.exe" }],
|
|
41
|
+
update: [{ role: "installer", bucket: "private", key: "fixture/updates/windows/current/Fixture_x64-setup.exe" }],
|
|
42
|
+
},
|
|
43
|
+
checkpoints: ["preflight_complete", "build_complete", "signed", "hardened", "sealed"],
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(path.join(sealedDir, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
46
|
+
return { root, sealedDir, installer, signature, manifest };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("upload has no build operation and restores the previous stable object when registration fails", async () => {
|
|
50
|
+
const fx = fixture();
|
|
51
|
+
const calls = [];
|
|
52
|
+
await assert.rejects(
|
|
53
|
+
runUploadStateMachine({
|
|
54
|
+
root: fx.root,
|
|
55
|
+
releaseId: fx.manifest.releaseId,
|
|
56
|
+
platform: "win",
|
|
57
|
+
tier: "patch",
|
|
58
|
+
ops: {
|
|
59
|
+
verifyAuthenticode: async () => calls.push("verify-authenticode"),
|
|
60
|
+
harden: async () => calls.push("harden"),
|
|
61
|
+
backup: async () => calls.push("backup"),
|
|
62
|
+
upload: async () => calls.push("upload"),
|
|
63
|
+
register: async () => { calls.push("register"); throw new Error("R2/API unavailable"); },
|
|
64
|
+
verifyRemote: async () => calls.push("verify-remote"),
|
|
65
|
+
restore: async () => calls.push("restore"),
|
|
66
|
+
discardBackup: async () => calls.push("discard-backup"),
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
/R2\/API unavailable/,
|
|
70
|
+
);
|
|
71
|
+
assert.deepEqual(calls, ["verify-authenticode", "harden", "backup", "upload", "register", "restore"]);
|
|
72
|
+
});
|
|
73
|
+
test("missing runtime input fails before build preparation and cannot touch an existing sealed installer", async () => {
|
|
74
|
+
const fx = fixture();
|
|
75
|
+
let prepared = false;
|
|
76
|
+
await assert.rejects(
|
|
77
|
+
runBuildStateMachine({
|
|
78
|
+
root: fx.root,
|
|
79
|
+
app: "fixture",
|
|
80
|
+
version: "1.2.4",
|
|
81
|
+
commit: "1234567890abcdef",
|
|
82
|
+
platform: "win",
|
|
83
|
+
requiredInputs: [path.join(fx.root, "missing-directml.dll")],
|
|
84
|
+
ops: {
|
|
85
|
+
preflight: async ({ requiredInputs }) => {
|
|
86
|
+
for (const file of requiredInputs) readFileSync(file);
|
|
87
|
+
},
|
|
88
|
+
prepare: async () => { prepared = true; },
|
|
89
|
+
build: async () => {},
|
|
90
|
+
sign: async () => {},
|
|
91
|
+
harden: async () => {},
|
|
92
|
+
seal: async () => {},
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
/ENOENT/,
|
|
96
|
+
);
|
|
97
|
+
assert.equal(prepared, false);
|
|
98
|
+
assert.equal(readFileSync(fx.installer, "utf8"), "signed-installer");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("an interrupted build leaves every previously sealed release untouched", async () => {
|
|
102
|
+
const fx = fixture();
|
|
103
|
+
await assert.rejects(
|
|
104
|
+
runBuildStateMachine({
|
|
105
|
+
root: fx.root,
|
|
106
|
+
app: "fixture",
|
|
107
|
+
version: "1.2.4",
|
|
108
|
+
commit: "1234567890abcdef",
|
|
109
|
+
platform: "win",
|
|
110
|
+
requiredInputs: [],
|
|
111
|
+
ops: {
|
|
112
|
+
preflight: async () => {},
|
|
113
|
+
prepare: async () => {},
|
|
114
|
+
build: async () => { throw new Error("SIGINT"); },
|
|
115
|
+
sign: async () => {},
|
|
116
|
+
harden: async () => {},
|
|
117
|
+
seal: async () => {},
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
/SIGINT/,
|
|
121
|
+
);
|
|
122
|
+
assert.equal(readFileSync(fx.installer, "utf8"), "signed-installer");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a valid sealed release resumes without rebuilding", async () => {
|
|
126
|
+
const fx = fixture();
|
|
127
|
+
let builds = 0;
|
|
128
|
+
const result = await runBuildStateMachine({
|
|
129
|
+
root: fx.root,
|
|
130
|
+
app: "fixture",
|
|
131
|
+
version: fx.manifest.version,
|
|
132
|
+
commit: fx.manifest.commit,
|
|
133
|
+
platform: "win",
|
|
134
|
+
requiredInputs: [],
|
|
135
|
+
ops: {
|
|
136
|
+
preflight: async () => {},
|
|
137
|
+
prepare: async () => {},
|
|
138
|
+
build: async () => { builds += 1; },
|
|
139
|
+
sign: async () => {},
|
|
140
|
+
harden: async () => {},
|
|
141
|
+
seal: async () => {},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
assert.equal(result.status, "sealed");
|
|
145
|
+
assert.equal(result.resumed, true);
|
|
146
|
+
assert.equal(builds, 0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("manifest or file tampering blocks upload before any mutation", async () => {
|
|
150
|
+
const fx = fixture();
|
|
151
|
+
writeFileSync(fx.installer, "changed");
|
|
152
|
+
assert.throws(() => verifySealedRelease(fx.sealedDir), /hash mismatch/);
|
|
153
|
+
let uploaded = false;
|
|
154
|
+
await assert.rejects(
|
|
155
|
+
runUploadStateMachine({
|
|
156
|
+
root: fx.root,
|
|
157
|
+
releaseId: fx.manifest.releaseId,
|
|
158
|
+
platform: "win",
|
|
159
|
+
tier: "patch",
|
|
160
|
+
ops: {
|
|
161
|
+
upload: async () => { uploaded = true; },
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
/hash mismatch/,
|
|
165
|
+
);
|
|
166
|
+
assert.equal(uploaded, false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("tier or non-current route mismatch fails before R2 backup", async () => {
|
|
170
|
+
const fx = fixture();
|
|
171
|
+
fx.manifest.routes.patch[0].bucket = "private";
|
|
172
|
+
fx.manifest.routes.patch[0].key = "fixture/updates/windows/1.2.3/Fixture.exe";
|
|
173
|
+
writeFileSync(path.join(fx.sealedDir, "release-manifest.json"), `${JSON.stringify(fx.manifest, null, 2)}\n`);
|
|
174
|
+
let backedUp = false;
|
|
175
|
+
await assert.rejects(
|
|
176
|
+
runUploadStateMachine({
|
|
177
|
+
root: fx.root,
|
|
178
|
+
releaseId: fx.manifest.releaseId,
|
|
179
|
+
platform: "win",
|
|
180
|
+
tier: "patch",
|
|
181
|
+
ops: { backup: async () => { backedUp = true; } },
|
|
182
|
+
}),
|
|
183
|
+
/patch route.*public.*installers.*current/i,
|
|
184
|
+
);
|
|
185
|
+
assert.equal(backedUp, false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("release ids and R2 keys reject traversal and shell metacharacters", async () => {
|
|
189
|
+
const fx = fixture();
|
|
190
|
+
await assert.rejects(
|
|
191
|
+
runUploadStateMachine({ root: fx.root, releaseId: "../../outside", platform: "win", tier: "patch" }),
|
|
192
|
+
/invalid release id/i,
|
|
193
|
+
);
|
|
194
|
+
fx.manifest.routes.patch[0].key = "fixture/installers/windows/current/Fixture.exe&whoami";
|
|
195
|
+
writeFileSync(path.join(fx.sealedDir, "release-manifest.json"), `${JSON.stringify(fx.manifest, null, 2)}\n`);
|
|
196
|
+
await assert.rejects(
|
|
197
|
+
runUploadStateMachine({ root: fx.root, releaseId: fx.manifest.releaseId, platform: "win", tier: "patch" }),
|
|
198
|
+
/unsafe R2 key/i,
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("release and test Cargo targets are disjoint", () => {
|
|
203
|
+
const root = "C:/repo/.right-release";
|
|
204
|
+
const build = releaseEnvironment({ root, platform: "win", cacheKey: "deps123", kind: "release" });
|
|
205
|
+
const testEnv = releaseEnvironment({ root, platform: "win", cacheKey: "deps123", kind: "test" });
|
|
206
|
+
assert.notEqual(build.CARGO_TARGET_DIR, testEnv.CARGO_TARGET_DIR);
|
|
207
|
+
assert.match(build.CARGO_TARGET_DIR, /cache[\\/]cargo-target[\\/]win[\\/]deps123/);
|
|
208
|
+
assert.match(testEnv.CARGO_TARGET_DIR, /cache[\\/]test-target[\\/]win[\\/]deps123/);
|
|
209
|
+
assert.match(build.SCCACHE_DIR, /cache[\\/]sccache/);
|
|
210
|
+
assert.equal(build.RUSTC_WRAPPER, "sccache");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("dependency cache survives source commits and invalidates only dependency/toolchain inputs", () => {
|
|
214
|
+
const base = {
|
|
215
|
+
cargoLockSha256: "lock-a",
|
|
216
|
+
rustc: "rustc 1.90.0 host x86_64-pc-windows-msvc",
|
|
217
|
+
target: "x86_64-pc-windows-msvc",
|
|
218
|
+
features: ["bundled-sqlcipher-vendored-openssl"],
|
|
219
|
+
};
|
|
220
|
+
assert.equal(cacheFingerprint({ ...base, commit: "aaa" }), cacheFingerprint({ ...base, commit: "bbb" }));
|
|
221
|
+
assert.notEqual(cacheFingerprint(base), cacheFingerprint({ ...base, cargoLockSha256: "lock-b" }));
|
|
222
|
+
assert.notEqual(cacheFingerprint(base), cacheFingerprint({ ...base, rustc: "rustc 1.91.0" }));
|
|
223
|
+
assert.notEqual(cacheFingerprint(base), cacheFingerprint({ ...base, features: ["bundled-sqlcipher"] }));
|
|
224
|
+
});
|
package/release.mjs
CHANGED
|
@@ -59,7 +59,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
59
59
|
else usage(2, `unknown argument: ${arg}`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
if (
|
|
62
|
+
if (opts.upload) fail("combined build+upload was removed; run right-release build, then right-release upload --release <id> --tier patch|update");
|
|
63
63
|
if (opts.tier && !TIERS.has(opts.tier)) usage(2, `invalid --tier: ${opts.tier} (expected patch|update)`);
|
|
64
64
|
|
|
65
65
|
const configPath = path.resolve(opts.config);
|
|
@@ -103,7 +103,7 @@ if (opts.doctor) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
|
|
106
|
-
const mode =
|
|
106
|
+
const mode = "package";
|
|
107
107
|
const command = target.package;
|
|
108
108
|
if (!command) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
|
|
109
109
|
if (opts.upload && !target.publish && !target.upload) {
|
|
@@ -111,7 +111,7 @@ if (opts.upload && !target.publish && !target.upload) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const started = Date.now();
|
|
114
|
-
console.log(`right-release ${VERSION}: ${config.app ?? path.basename(root)} ${opts.platform} ${mode} tier=${opts.tier}`);
|
|
114
|
+
console.log(`right-release ${VERSION}: ${config.app ?? path.basename(root)} ${opts.platform} ${mode} tier=${opts.tier ?? "neutral"}`);
|
|
115
115
|
|
|
116
116
|
if (opts.install) await runInstall(config.packageManager, workdir);
|
|
117
117
|
if (!opts.skipChecks) {
|
package/release.test.mjs
CHANGED
|
@@ -128,10 +128,11 @@ function runRaw(config, ...args) {
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
test("
|
|
131
|
+
test("accepts a tier-neutral internal build", () => {
|
|
132
132
|
const result = run(fixture());
|
|
133
|
-
assert.
|
|
134
|
-
assert.match(result.
|
|
133
|
+
assert.equal(result.status, 0, result.stderr);
|
|
134
|
+
assert.match(result.stdout, /tier=neutral/);
|
|
135
|
+
assert.doesNotMatch(result.stdout, /RIGHT_RELEASE_TIER=/);
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
test("accepts patch and exposes it to the signed package command", () => {
|
|
@@ -233,35 +234,31 @@ test("doctor rejects a RightKit override at an ancestor repository root", () =>
|
|
|
233
234
|
assert.match(result.stderr, /forbidden RightKit Cargo override/i);
|
|
234
235
|
});
|
|
235
236
|
|
|
236
|
-
test("
|
|
237
|
+
test("combined build and upload is rejected before the package step", () => {
|
|
237
238
|
const result = run(fixture({ publish: true }), "--tier=update", "--upload");
|
|
238
|
-
assert.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
assert.ok(packageAt >= 0, result.stdout);
|
|
242
|
-
assert.ok(publishAt > packageAt, result.stdout);
|
|
239
|
+
assert.notEqual(result.status, 0);
|
|
240
|
+
assert.match(result.stderr, /combined build\+upload was removed/i);
|
|
241
|
+
assert.doesNotMatch(result.stdout, /node -e process\.exit\(0\)/);
|
|
243
242
|
});
|
|
244
243
|
|
|
245
|
-
test("Windows
|
|
246
|
-
const result = run(fixture({ publish: true })
|
|
244
|
+
test("Windows tier-neutral build signs the installer then minisigns the updater payload", () => {
|
|
245
|
+
const result = run(fixture({ publish: true }));
|
|
247
246
|
assert.equal(result.status, 0, result.stderr);
|
|
248
247
|
const azureAt = result.stdout.indexOf("sign-windows.mjs");
|
|
249
248
|
const updaterAt = result.stdout.indexOf("sign-updater.mjs");
|
|
250
|
-
const publishAt = result.stdout.indexOf("publish-update.mjs");
|
|
251
249
|
assert.ok(azureAt >= 0, result.stdout);
|
|
252
250
|
assert.ok(updaterAt > azureAt, result.stdout);
|
|
253
|
-
assert.
|
|
251
|
+
assert.doesNotMatch(result.stdout, /publish-update\.mjs/);
|
|
254
252
|
});
|
|
255
253
|
|
|
256
|
-
test("
|
|
257
|
-
const result = run(fixture({ publish: true }), "--tier=update"
|
|
254
|
+
test("legacy internal tier does not make the build worker upload", () => {
|
|
255
|
+
const result = run(fixture({ publish: true }), "--tier=update");
|
|
258
256
|
assert.equal(result.status, 0, result.stderr);
|
|
259
257
|
const azureAt = result.stdout.indexOf("sign-windows.mjs");
|
|
260
258
|
const updaterAt = result.stdout.indexOf("sign-updater.mjs");
|
|
261
|
-
const publishAt = result.stdout.indexOf("publish-update.mjs");
|
|
262
259
|
assert.ok(azureAt >= 0, result.stdout);
|
|
263
260
|
assert.ok(updaterAt > azureAt, result.stdout);
|
|
264
|
-
assert.
|
|
261
|
+
assert.doesNotMatch(result.stdout, /publish-update\.mjs/);
|
|
265
262
|
});
|
|
266
263
|
|
|
267
264
|
test("release sweeps stale rust artifacts when a target dir exists", () => {
|
|
@@ -317,7 +317,7 @@ test("Cargo version contract rejects staged versions that mismatch canonical man
|
|
|
317
317
|
test("RightKit exposes one current version manifest", () => {
|
|
318
318
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
319
319
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
320
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
320
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.32");
|
|
321
321
|
assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
|
|
322
322
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
323
323
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
@@ -327,10 +327,9 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
327
327
|
"@rightkit/legal": "0.3.0",
|
|
328
328
|
"@rightkit/legal-ui": "0.1.0",
|
|
329
329
|
"@rightkit/license": "0.1.6",
|
|
330
|
-
"@rightkit/release": "0.2.30",
|
|
331
330
|
});
|
|
332
331
|
assert.deepEqual(versions.legacyNpm, {
|
|
333
|
-
"@rightkit/release": ["0.2.22"],
|
|
332
|
+
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31"],
|
|
334
333
|
});
|
|
335
334
|
const licensePackage = JSON.parse(readFileSync(
|
|
336
335
|
path.join(workspace, "tools/rightkit/packages/license/package.json"),
|
package/rightkit-versions.json
CHANGED
|
@@ -7,18 +7,17 @@
|
|
|
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.32",
|
|
11
11
|
"@rightkit/tauri": "0.1.0",
|
|
12
12
|
"@rightkit/updates": "0.2.3"
|
|
13
13
|
},
|
|
14
14
|
"stagedNpm": {
|
|
15
15
|
"@rightkit/legal": "0.3.0",
|
|
16
16
|
"@rightkit/legal-ui": "0.1.0",
|
|
17
|
-
"@rightkit/license": "0.1.6"
|
|
18
|
-
"@rightkit/release": "0.2.30"
|
|
17
|
+
"@rightkit/license": "0.1.6"
|
|
19
18
|
},
|
|
20
19
|
"legacyNpm": {
|
|
21
|
-
"@rightkit/release": ["0.2.22"]
|
|
20
|
+
"@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31"]
|
|
22
21
|
},
|
|
23
22
|
"cargo": {
|
|
24
23
|
"rightkit-license": "0.1.2",
|