@rightkit/release 0.2.69 → 0.2.71
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 +19 -14
- package/cache-command.mjs +7 -4
- package/cli/right-release.mjs +0 -0
- package/native-cargo-layout.mjs +239 -0
- package/native-release-finalization.mjs +259 -0
- package/package.json +11 -11
- package/preflight.mjs +6 -3
- package/registry-parity.mjs +2 -2
- package/release-invocation.mjs +10 -2
- package/release-state.mjs +5 -2
- package/release.mjs +220 -12
- package/rightkit-versions.json +4 -2
- package/addon-command.test.mjs +0 -54
- package/addon-contract.test.mjs +0 -48
- package/asr-artifact-adoption.test.mjs +0 -122
- package/build-invocation-contract.test.mjs +0 -124
- package/build-release.test.mjs +0 -242
- package/cache-command.test.mjs +0 -118
- package/cache-policy.test.mjs +0 -346
- package/cargo-guard.test.mjs +0 -195
- package/cargo-target.test.mjs +0 -82
- package/create-mac-updater.test.mjs +0 -14
- package/github-release.test.mjs +0 -103
- package/heavy-command.test.mjs +0 -221
- package/legal-contract.test.mjs +0 -151
- package/mirror-root-artifact.test.mjs +0 -58
- package/model-promote.test.mjs +0 -284
- package/notary-auth.test.mjs +0 -31
- package/nsis-payload.test.mjs +0 -139
- package/nsis-upgrade-contract.test.mjs +0 -57
- package/pipeline-normalization-contract.test.mjs +0 -140
- package/preflight.test.mjs +0 -123
- package/progress-control.test.mjs +0 -56
- package/prune-r2.test.mjs +0 -12
- package/publish-cargo.test.mjs +0 -43
- package/publish-swift.test.mjs +0 -44
- package/publish-update.test.mjs +0 -207
- package/qa-contract.test.mjs +0 -47
- package/registry-parity.test.mjs +0 -30
- package/release-cli-contract.test.mjs +0 -119
- package/release-invocation.test.mjs +0 -22
- package/release-state.test.mjs +0 -395
- package/release-token.test.mjs +0 -21
- package/release.test.mjs +0 -533
- package/right-suite-contract.test.mjs +0 -1011
- package/rightapps-register.test.mjs +0 -28
- package/runtime-artifact-manifest.test.mjs +0 -128
- package/sign-updater.test.mjs +0 -12
- package/source-gate.test.mjs +0 -25
- package/standalone-clone-evidence.json +0 -32
- package/standalone-clone-verify.test.mjs +0 -76
- package/suite-doctor.test.mjs +0 -19
- package/target-bridge.test.mjs +0 -269
- package/tauri-bundle-marker.test.mjs +0 -81
- package/upload-large.test.mjs +0 -70
package/model-promote.test.mjs
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { createHash, generateKeyPairSync, sign } from "node:crypto";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
RUNTIME_ARTIFACT_TRUSTED_KEY_ID,
|
|
12
|
-
defaultWindowsRuntimeArtifactSigningKeyFile,
|
|
13
|
-
loadRuntimeArtifactSigningKey,
|
|
14
|
-
promoteRuntimeArtifact,
|
|
15
|
-
runtimeArtifactUploadEnv,
|
|
16
|
-
} from "./model-promote.mjs";
|
|
17
|
-
const { privateKey } = generateKeyPairSync("ed25519");
|
|
18
|
-
const artifactDigest = "a".repeat(64);
|
|
19
|
-
const evidenceDigest = "b".repeat(64);
|
|
20
|
-
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const candidateCommit = "a".repeat(40);
|
|
22
|
-
|
|
23
|
-
// Source identity as build/upload/promote read it: the exact commit, plus the
|
|
24
|
-
// porcelain status that must be empty for the promotion to run.
|
|
25
|
-
function verificationFixture(commit = candidateCommit, status = "") {
|
|
26
|
-
return { candidateCommit: commit, status };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function request(overrides = {}) {
|
|
30
|
-
return {
|
|
31
|
-
authority: "heardright",
|
|
32
|
-
privateKey,
|
|
33
|
-
keyId: RUNTIME_ARTIFACT_TRUSTED_KEY_ID,
|
|
34
|
-
artifactFile: "C:/fixtures/parakeet.onnx",
|
|
35
|
-
evidenceFile: "C:/fixtures/eval.json",
|
|
36
|
-
config: {
|
|
37
|
-
artifact: {
|
|
38
|
-
filename: "parakeet.onnx",
|
|
39
|
-
sha256: artifactDigest,
|
|
40
|
-
sizeBytes: 42,
|
|
41
|
-
},
|
|
42
|
-
evidence: { sha256: evidenceDigest },
|
|
43
|
-
manifest: {
|
|
44
|
-
artifactKind: "asr-model",
|
|
45
|
-
entitlement: { appKey: "scraperight", tier: "pro" },
|
|
46
|
-
distribution: { delivery: "private-r2", bucket: "rightapps-updates" },
|
|
47
|
-
target: { os: "windows", arch: "x86_64" },
|
|
48
|
-
versions: {
|
|
49
|
-
runtime: "rightkit-asr-0.1.0",
|
|
50
|
-
model: "parakeet-tdt-0.6b-v3",
|
|
51
|
-
tokenizer: "sentencepiece-2026-07-14",
|
|
52
|
-
preprocessing: "rightkit-asr-0.1.0",
|
|
53
|
-
license: "cc-by-4.0",
|
|
54
|
-
provenance: "heardright-approved-2026-07-14",
|
|
55
|
-
},
|
|
56
|
-
provenance: {
|
|
57
|
-
source: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
|
|
58
|
-
sourceRevision: "0123456789abcdef",
|
|
59
|
-
licenseId: "CC-BY-4.0",
|
|
60
|
-
noticeSha256: "c".repeat(64),
|
|
61
|
-
},
|
|
62
|
-
promotion: {
|
|
63
|
-
promotionId: "hr-asr-2026-07-14-001",
|
|
64
|
-
promotedAt: "2026-07-14T12:00:00.000Z",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
verification: verificationFixture(),
|
|
69
|
-
...overrides,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function boundaries(events) {
|
|
74
|
-
return {
|
|
75
|
-
metadata: async (file) => file.endsWith("eval.json")
|
|
76
|
-
? { sha256: evidenceDigest, sizeBytes: 10 }
|
|
77
|
-
: { sha256: artifactDigest, sizeBytes: 42 },
|
|
78
|
-
upload: async ({ purpose }) => { events.push(`upload:${purpose}`); },
|
|
79
|
-
register: async () => { events.push("register"); },
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
test("uploads immutable object, registers signed envelope, then replaces stable pointer", async () => {
|
|
84
|
-
const events = [];
|
|
85
|
-
const result = await promoteRuntimeArtifact(request(), boundaries(events));
|
|
86
|
-
|
|
87
|
-
assert.deepEqual(events, ["upload:object", "register", "upload:pointer"]);
|
|
88
|
-
assert.equal(result.envelope.signature.keyId, RUNTIME_ARTIFACT_TRUSTED_KEY_ID);
|
|
89
|
-
assert.equal(result.envelope.manifest.object.sha256, artifactDigest);
|
|
90
|
-
assert.equal(result.envelope.manifest.promotion.evidenceSha256, evidenceDigest);
|
|
91
|
-
assert.match(result.envelope.manifest.pointerKey, /\/current\/manifest\.json$/);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("dry-run validates and signs without invoking process or network boundaries", async () => {
|
|
95
|
-
const events = [];
|
|
96
|
-
const result = await promoteRuntimeArtifact(request({ dryRun: true }), boundaries(events));
|
|
97
|
-
assert.deepEqual(events, []);
|
|
98
|
-
assert.deepEqual(result.steps, ["upload immutable object", "register signed envelope", "replace stable pointer"]);
|
|
99
|
-
assert.equal(result.envelope.signature.algorithm, "Ed25519");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("model promotion refuses a dirty working tree", async () => {
|
|
103
|
-
const events = [];
|
|
104
|
-
const verification = verificationFixture(candidateCommit, " M packages/release/model-promote.mjs\0");
|
|
105
|
-
await assert.rejects(
|
|
106
|
-
promoteRuntimeArtifact(request({ verification, dryRun: true }), boundaries(events)),
|
|
107
|
-
/clean working tree/,
|
|
108
|
-
);
|
|
109
|
-
assert.deepEqual(events, []);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("model promotion requires source identity", async () => {
|
|
113
|
-
const events = [];
|
|
114
|
-
await assert.rejects(
|
|
115
|
-
promoteRuntimeArtifact(request({ verification: undefined, dryRun: true }), boundaries(events)),
|
|
116
|
-
/source identity is required/,
|
|
117
|
-
);
|
|
118
|
-
assert.deepEqual(events, []);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("never registers or publishes a pointer after immutable upload failure", async () => {
|
|
122
|
-
const events = [];
|
|
123
|
-
const deps = boundaries(events);
|
|
124
|
-
deps.upload = async ({ purpose }) => {
|
|
125
|
-
events.push(`upload:${purpose}`);
|
|
126
|
-
throw new Error("R2 failed");
|
|
127
|
-
};
|
|
128
|
-
await assert.rejects(promoteRuntimeArtifact(request(), deps), /R2 failed/);
|
|
129
|
-
assert.deepEqual(events, ["upload:object"]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("never publishes a pointer when RightApps registration fails", async () => {
|
|
133
|
-
const events = [];
|
|
134
|
-
const deps = boundaries(events);
|
|
135
|
-
deps.register = async () => {
|
|
136
|
-
events.push("register");
|
|
137
|
-
throw new Error("registration failed");
|
|
138
|
-
};
|
|
139
|
-
await assert.rejects(promoteRuntimeArtifact(request(), deps), /registration failed/);
|
|
140
|
-
assert.deepEqual(events, ["upload:object", "register"]);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("fails before mutation on digest, authority, key-id, or lane contradiction", async () => {
|
|
144
|
-
const badRequests = [
|
|
145
|
-
request({ authority: "scraperight" }),
|
|
146
|
-
request({ keyId: "untrusted-key" }),
|
|
147
|
-
request({ config: { ...request().config, artifact: { ...request().config.artifact, sha256: "d".repeat(64) } } }),
|
|
148
|
-
request({ config: {
|
|
149
|
-
...request().config,
|
|
150
|
-
manifest: {
|
|
151
|
-
...request().config.manifest,
|
|
152
|
-
entitlement: { appKey: "scraperight", tier: "public" },
|
|
153
|
-
distribution: { delivery: "public-r2", bucket: "rightapps-downloads" },
|
|
154
|
-
},
|
|
155
|
-
} }),
|
|
156
|
-
request({ config: {
|
|
157
|
-
...request().config,
|
|
158
|
-
manifest: {
|
|
159
|
-
...request().config.manifest,
|
|
160
|
-
promotion: {
|
|
161
|
-
...request().config.manifest.promotion,
|
|
162
|
-
authorityAppKey: "scraperight",
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
} }),
|
|
166
|
-
];
|
|
167
|
-
for (const input of badRequests) {
|
|
168
|
-
const events = [];
|
|
169
|
-
await assert.rejects(promoteRuntimeArtifact(input, boundaries(events)));
|
|
170
|
-
assert.deepEqual(events, []);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("right-release routes model promote to the portable promotion lane", () => {
|
|
175
|
-
const result = spawnSync(process.execPath, [path.join(packageRoot, "cli/right-release.mjs"), "model", "promote", "--authority", "heardright"], {
|
|
176
|
-
cwd: packageRoot,
|
|
177
|
-
encoding: "utf8",
|
|
178
|
-
windowsHide: true,
|
|
179
|
-
});
|
|
180
|
-
assert.equal(result.status, 1);
|
|
181
|
-
assert.match(result.stderr, /model promote requires --config/);
|
|
182
|
-
assert.doesNotMatch(result.stderr, /unknown command/);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("model promote help documents the portable config and secure key inputs", () => {
|
|
186
|
-
const result = spawnSync(process.execPath, [path.join(packageRoot, "cli/right-release.mjs"), "model", "promote", "--help"], {
|
|
187
|
-
cwd: packageRoot,
|
|
188
|
-
encoding: "utf8",
|
|
189
|
-
windowsHide: true,
|
|
190
|
-
});
|
|
191
|
-
assert.equal(result.status, 0, result.stderr);
|
|
192
|
-
assert.match(result.stdout, /artifact\.file/);
|
|
193
|
-
assert.match(result.stdout, /evidence\.sha256/);
|
|
194
|
-
assert.match(result.stdout, /RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE/);
|
|
195
|
-
assert.match(result.stdout, /RIGHTAPPS_RELEASE_TOKEN/);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("portable CLI promotes from a clean checkout, refuses a dirty one, and rejects injected trust flags", async () => {
|
|
199
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "rightkit-model-promote-test-"));
|
|
200
|
-
try {
|
|
201
|
-
const artifact = Buffer.from("tiny model fixture");
|
|
202
|
-
const evidence = Buffer.from('{"certified":true}');
|
|
203
|
-
const testKeys = generateKeyPairSync("ed25519");
|
|
204
|
-
const keyPem = testKeys.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
205
|
-
await Promise.all([
|
|
206
|
-
writeFile(path.join(root, "model.onnx"), artifact),
|
|
207
|
-
writeFile(path.join(root, "evidence.json"), evidence),
|
|
208
|
-
writeFile(path.join(root, "key.pem"), keyPem, { mode: 0o600 }),
|
|
209
|
-
]);
|
|
210
|
-
spawnSync("git", ["init"], { cwd: root, encoding: "utf8" });
|
|
211
|
-
spawnSync("git", ["add", "."], { cwd: root, encoding: "utf8" });
|
|
212
|
-
const committed = spawnSync("git", ["-c", "user.name=fixture", "-c", "user.email=fixture@example.invalid", "commit", "-m", "fixture"], { cwd: root, encoding: "utf8" });
|
|
213
|
-
assert.equal(committed.status, 0, committed.stderr);
|
|
214
|
-
const config = {
|
|
215
|
-
artifact: {
|
|
216
|
-
file: "model.onnx",
|
|
217
|
-
filename: "model.onnx",
|
|
218
|
-
sha256: createHash("sha256").update(artifact).digest("hex"),
|
|
219
|
-
sizeBytes: artifact.length,
|
|
220
|
-
},
|
|
221
|
-
evidence: {
|
|
222
|
-
file: "evidence.json",
|
|
223
|
-
sha256: createHash("sha256").update(evidence).digest("hex"),
|
|
224
|
-
},
|
|
225
|
-
manifest: request().config.manifest,
|
|
226
|
-
};
|
|
227
|
-
await writeFile(path.join(root, "promotion.json"), JSON.stringify(config));
|
|
228
|
-
spawnSync("git", ["add", "."], { cwd: root, encoding: "utf8" });
|
|
229
|
-
spawnSync("git", ["-c", "user.name=fixture", "-c", "user.email=fixture@example.invalid", "commit", "-m", "config"], { cwd: root, encoding: "utf8" });
|
|
230
|
-
|
|
231
|
-
const promoteArgs = [
|
|
232
|
-
path.join(packageRoot, "cli/right-release.mjs"), "model", "promote",
|
|
233
|
-
"--authority", "heardright",
|
|
234
|
-
"--config", path.join(root, "promotion.json"),
|
|
235
|
-
"--signing-key-file", path.join(root, "key.pem"),
|
|
236
|
-
"--dry-run",
|
|
237
|
-
];
|
|
238
|
-
const result = spawnSync(process.execPath, promoteArgs, { cwd: root, encoding: "utf8", windowsHide: true });
|
|
239
|
-
assert.equal(result.status, 0, result.stderr);
|
|
240
|
-
|
|
241
|
-
await writeFile(path.join(root, "untracked.txt"), "dirty");
|
|
242
|
-
const dirty = spawnSync(process.execPath, promoteArgs, { cwd: root, encoding: "utf8", windowsHide: true });
|
|
243
|
-
assert.equal(dirty.status, 1);
|
|
244
|
-
assert.match(dirty.stderr, /clean working tree/);
|
|
245
|
-
|
|
246
|
-
const attacker = spawnSync(process.execPath, [
|
|
247
|
-
path.join(packageRoot, "cli/right-release.mjs"), "model", "promote",
|
|
248
|
-
"--authority", "heardright", "--config", path.join(root, "promotion.json"),
|
|
249
|
-
"--verification-public-key", path.join(root, "verification-public.pem"),
|
|
250
|
-
], { cwd: root, encoding: "utf8", windowsHide: true });
|
|
251
|
-
assert.equal(attacker.status, 1);
|
|
252
|
-
assert.match(attacker.stderr, /unknown model promote argument: --verification-public-key/);
|
|
253
|
-
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /BEGIN PRIVATE KEY/);
|
|
254
|
-
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /Uploading|POST https:/);
|
|
255
|
-
} finally {
|
|
256
|
-
await rm(root, { recursive: true, force: true });
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("uses the canonical Windows signing-key path and supports a Keychain-fed PEM environment value", async () => {
|
|
261
|
-
assert.equal(
|
|
262
|
-
defaultWindowsRuntimeArtifactSigningKeyFile({ APPDATA: "C:/Users/test/AppData/Roaming" }),
|
|
263
|
-
path.join("C:/Users/test/AppData/Roaming", "RightKit", "runtime-artifact-signing-key.pem"),
|
|
264
|
-
);
|
|
265
|
-
assert.equal(
|
|
266
|
-
await loadRuntimeArtifactSigningKey({ env: { RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY: "line1\\nline2" }, platform: "darwin" }),
|
|
267
|
-
"line1\nline2",
|
|
268
|
-
);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("does not forward signing or RightApps credentials into the Wrangler uploader process", () => {
|
|
272
|
-
const env = runtimeArtifactUploadEnv({
|
|
273
|
-
CLOUDFLARE_API_TOKEN: "cloudflare-token",
|
|
274
|
-
RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY: "private-pem",
|
|
275
|
-
RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE: "key.pem",
|
|
276
|
-
RIGHTAPPS_RELEASE_TOKEN: "release-token",
|
|
277
|
-
RIGHTAPPS_RELEASE_TOKEN_FILE: "release.token",
|
|
278
|
-
});
|
|
279
|
-
assert.equal(env.CLOUDFLARE_API_TOKEN, "cloudflare-token");
|
|
280
|
-
assert.equal(env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY, undefined);
|
|
281
|
-
assert.equal(env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE, undefined);
|
|
282
|
-
assert.equal(env.RIGHTAPPS_RELEASE_TOKEN, undefined);
|
|
283
|
-
assert.equal(env.RIGHTAPPS_RELEASE_TOKEN_FILE, undefined);
|
|
284
|
-
});
|
package/notary-auth.test.mjs
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import test from "node:test";
|
|
5
|
-
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
6
|
-
|
|
7
|
-
test("prefers a complete App Store Connect API credential set", () => {
|
|
8
|
-
const before = { ...process.env };
|
|
9
|
-
try {
|
|
10
|
-
process.env.APPLE_API_KEY_PATH = "~/AuthKey.p8";
|
|
11
|
-
process.env.APPLE_API_KEY = "KEY123";
|
|
12
|
-
process.env.APPLE_API_ISSUER = "ISSUER123";
|
|
13
|
-
assert.deepEqual(notarytoolAuthArgs(), ["--key", path.join(os.homedir(), "AuthKey.p8"), "--key-id", "KEY123", "--issuer", "ISSUER123"]);
|
|
14
|
-
} finally {
|
|
15
|
-
process.env = before;
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("falls back to the device keychain profile and rejects partial API auth", () => {
|
|
20
|
-
const before = { ...process.env };
|
|
21
|
-
try {
|
|
22
|
-
delete process.env.APPLE_API_KEY_PATH;
|
|
23
|
-
delete process.env.APPLE_API_KEY;
|
|
24
|
-
delete process.env.APPLE_API_ISSUER;
|
|
25
|
-
assert.deepEqual(notarytoolAuthArgs(), ["--keychain-profile", "apple-dev-notary"]);
|
|
26
|
-
process.env.APPLE_API_KEY = "KEY123";
|
|
27
|
-
assert.throws(() => notarytoolAuthArgs(), /requires APPLE_API_KEY_PATH.*APPLE_API_ISSUER/);
|
|
28
|
-
} finally {
|
|
29
|
-
process.env = before;
|
|
30
|
-
}
|
|
31
|
-
});
|
package/nsis-payload.test.mjs
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import test, { after } from "node:test";
|
|
7
|
-
import { listNsisPayload, parseSevenZipListing, resolveSevenZip, verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
8
|
-
|
|
9
|
-
const workspace = mkdtempSync(path.join(tmpdir(), "rightkit-nsis-"));
|
|
10
|
-
after(() => rmSync(workspace, { recursive: true, force: true }));
|
|
11
|
-
|
|
12
|
-
// 7-Zip's technical listing: an archive record, a rule, then one record per member.
|
|
13
|
-
// The blank `Attributes` and absent timestamps are what the column-aligned listing
|
|
14
|
-
// gets wrong, so they are reproduced here deliberately.
|
|
15
|
-
const LISTING = [
|
|
16
|
-
"Path = D:\\out\\App_1.2.3_x64-setup.exe",
|
|
17
|
-
"Type = Nsis",
|
|
18
|
-
"",
|
|
19
|
-
"----------",
|
|
20
|
-
"Path = $PLUGINSDIR\\System.dll",
|
|
21
|
-
"Size = 12288",
|
|
22
|
-
"Attributes = ",
|
|
23
|
-
"",
|
|
24
|
-
"Path = app with spaces.exe",
|
|
25
|
-
"Size = 13750736",
|
|
26
|
-
"Attributes = ",
|
|
27
|
-
"",
|
|
28
|
-
"Path = legal",
|
|
29
|
-
"Size = 0",
|
|
30
|
-
"Folder = +",
|
|
31
|
-
"Attributes = D",
|
|
32
|
-
"",
|
|
33
|
-
"Path = legal\\EULA.md",
|
|
34
|
-
"Size = 16723",
|
|
35
|
-
"Attributes = A",
|
|
36
|
-
"",
|
|
37
|
-
].join("\n");
|
|
38
|
-
|
|
39
|
-
test("parses 7-Zip technical listings, including blank dates and spaced names", () => {
|
|
40
|
-
const entries = parseSevenZipListing(LISTING);
|
|
41
|
-
assert.deepEqual(entries.map((entry) => entry.name), [
|
|
42
|
-
"$PLUGINSDIR/System.dll",
|
|
43
|
-
"app with spaces.exe",
|
|
44
|
-
"legal/EULA.md",
|
|
45
|
-
]);
|
|
46
|
-
assert.equal(entries[1].sizeBytes, 13_750_736);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("ignores the archive's own record above the rule", () => {
|
|
50
|
-
const entries = parseSevenZipListing(LISTING);
|
|
51
|
-
assert.equal(entries.some((entry) => entry.name.endsWith("setup.exe")), false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("listNsisPayload surfaces the 7-Zip failure instead of reporting an empty payload", () => {
|
|
55
|
-
assert.throws(
|
|
56
|
-
() => listNsisPayload("installer.exe", {
|
|
57
|
-
sevenZip: "7z",
|
|
58
|
-
run: () => ({ status: 2, stdout: "", stderr: "Cannot open the file as archive" }),
|
|
59
|
-
}),
|
|
60
|
-
/could not read the NSIS payload.*Cannot open the file as archive/s,
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("verifyNsisEmbeddedBinary accepts a payload whose bytes match the signed EXE", () => {
|
|
65
|
-
const installer = path.join(workspace, "setup.exe");
|
|
66
|
-
writeFileSync(installer, "installer-container");
|
|
67
|
-
const payload = Buffer.from("signed-executable-bytes");
|
|
68
|
-
const sha256 = createHash("sha256").update(payload).digest("hex");
|
|
69
|
-
|
|
70
|
-
const receipt = verifyNsisEmbeddedBinary({
|
|
71
|
-
installer,
|
|
72
|
-
entryName: "app.exe",
|
|
73
|
-
expectedSha256: sha256,
|
|
74
|
-
sevenZip: "7z",
|
|
75
|
-
run: (_command, args) => {
|
|
76
|
-
const destination = args.find((arg) => arg.startsWith("-o")).slice(2);
|
|
77
|
-
writeFileSync(path.join(destination, "app.exe"), payload);
|
|
78
|
-
return { status: 0, stdout: "Everything is Ok", stderr: "" };
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
assert.equal(receipt.sha256, sha256);
|
|
83
|
-
assert.equal(receipt.entry, "app.exe");
|
|
84
|
-
assert.equal(receipt.sizeBytes, payload.length);
|
|
85
|
-
assert.equal(receipt.installerSha256, createHash("sha256").update(readFileSync(installer)).digest("hex"));
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("verifyNsisEmbeddedBinary rejects a payload that drifted from the signed EXE", () => {
|
|
89
|
-
const installer = path.join(workspace, "setup-drifted.exe");
|
|
90
|
-
writeFileSync(installer, "installer-container");
|
|
91
|
-
assert.throws(
|
|
92
|
-
() => verifyNsisEmbeddedBinary({
|
|
93
|
-
installer,
|
|
94
|
-
entryName: "app.exe",
|
|
95
|
-
expectedSha256: createHash("sha256").update("signed-executable-bytes").digest("hex"),
|
|
96
|
-
sevenZip: "7z",
|
|
97
|
-
run: (_command, args) => {
|
|
98
|
-
const destination = args.find((arg) => arg.startsWith("-o")).slice(2);
|
|
99
|
-
writeFileSync(path.join(destination, "app.exe"), "rewritten-after-signing");
|
|
100
|
-
return { status: 0, stdout: "Everything is Ok", stderr: "" };
|
|
101
|
-
},
|
|
102
|
-
}),
|
|
103
|
-
/does not embed the signed app\.exe.*Authenticode signature is invalid/s,
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("verifyNsisEmbeddedBinary fails when the entry is absent rather than passing vacuously", () => {
|
|
108
|
-
const installer = path.join(workspace, "setup-missing.exe");
|
|
109
|
-
writeFileSync(installer, "installer-container");
|
|
110
|
-
assert.throws(
|
|
111
|
-
() => verifyNsisEmbeddedBinary({
|
|
112
|
-
installer,
|
|
113
|
-
entryName: "app.exe",
|
|
114
|
-
expectedSha256: "a".repeat(64),
|
|
115
|
-
sevenZip: "7z",
|
|
116
|
-
run: () => ({ status: 2, stdout: "", stderr: "No files to process" }),
|
|
117
|
-
}),
|
|
118
|
-
/could not extract app\.exe/,
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("verifyNsisEmbeddedBinary refuses a caller that has no digest to compare against", () => {
|
|
123
|
-
assert.throws(
|
|
124
|
-
() => verifyNsisEmbeddedBinary({ installer: "setup.exe", entryName: "app.exe", expectedSha256: undefined, sevenZip: "7z", run: () => ({ status: 0 }) }),
|
|
125
|
-
/expected a sha256 digest/,
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("resolveSevenZip names the fix when 7-Zip is absent", () => {
|
|
130
|
-
assert.throws(
|
|
131
|
-
() => resolveSevenZip({ env: {}, exists: () => false, which: () => null }),
|
|
132
|
-
/install 7-Zip.*RIGHT_RELEASE_SEVENZIP/s,
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("resolveSevenZip honours an explicit override and rejects a bad one", () => {
|
|
137
|
-
assert.equal(resolveSevenZip({ env: { RIGHT_RELEASE_SEVENZIP: "C:\\tools\\7z.exe" }, exists: () => true }), "C:\\tools\\7z.exe");
|
|
138
|
-
assert.throws(() => resolveSevenZip({ env: { RIGHT_RELEASE_SEVENZIP: "C:\\gone\\7z.exe" }, exists: () => false }), /does not exist/);
|
|
139
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
|
|
7
|
-
const workspace = path.resolve(
|
|
8
|
-
process.env.RIGHT_SUITE_CONTRACT_WORKSPACE
|
|
9
|
-
?? new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"),
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
const packageRoots = [
|
|
13
|
-
"coderight/apps/coderight-tauri",
|
|
14
|
-
"cutright/apps/studio",
|
|
15
|
-
"genright",
|
|
16
|
-
"heardright/tauri-app-next",
|
|
17
|
-
"mailright",
|
|
18
|
-
"membrane/apps/membrane-hub",
|
|
19
|
-
"scraperight",
|
|
20
|
-
"viewright",
|
|
21
|
-
"voiceright",
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const releaseConfigRoots = [
|
|
25
|
-
...packageRoots,
|
|
26
|
-
"tools/screenright",
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
const rightGitRepos = ["cutright", "legion", "membrane"];
|
|
30
|
-
const privateRepos = ["rightsites", "sellright", "workright"];
|
|
31
|
-
|
|
32
|
-
function executableSource(source) {
|
|
33
|
-
return source
|
|
34
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
35
|
-
.split("\n")
|
|
36
|
-
.filter((line) => !/^\s*(?:\/\/|#)/.test(line))
|
|
37
|
-
.join("\n");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function pipelineScripts(root) {
|
|
41
|
-
const repo = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8", windowsHide: true }).trim();
|
|
42
|
-
const prefix = path.relative(repo, root).replaceAll(path.sep, "/");
|
|
43
|
-
const tracked = execFileSync("git", ["-C", repo, "ls-files", "-z"], { encoding: "utf8", windowsHide: true })
|
|
44
|
-
.split("\0")
|
|
45
|
-
.filter(Boolean);
|
|
46
|
-
return tracked
|
|
47
|
-
.filter((entry) => {
|
|
48
|
-
const local = prefix ? entry.slice(prefix.length + 1) : entry;
|
|
49
|
-
if (prefix && !entry.startsWith(`${prefix}/`)) return false;
|
|
50
|
-
return (
|
|
51
|
-
local.startsWith("scripts/")
|
|
52
|
-
|| local === "package.ps1"
|
|
53
|
-
|| local === "package.sh"
|
|
54
|
-
) && /\.(?:mjs|cjs|js|ts|ps1|sh|py)$/i.test(local)
|
|
55
|
-
&& !/(?:^|[.-])test(?:[.-]|$)/i.test(path.basename(local));
|
|
56
|
-
})
|
|
57
|
-
.map((entry) => path.join(repo, entry))
|
|
58
|
-
.filter((entry) => existsSync(entry));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
test("product package pipelines use installed RightKit commands", () => {
|
|
62
|
-
const violations = [];
|
|
63
|
-
for (const root of packageRoots) {
|
|
64
|
-
const manifestPath = path.join(workspace, root, "package.json");
|
|
65
|
-
if (!existsSync(manifestPath)) continue;
|
|
66
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
67
|
-
for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
|
|
68
|
-
if (!/^(?:build|package|publish|release|deps|verify:rust|test:rust|lint:rust)/.test(name)) continue;
|
|
69
|
-
if (/(?:\.\.\/[\w./-]*)?tools\/right-release|tools\\right-release/i.test(command)) violations.push(`${root} ${name}: retired workspace-local right-release source`);
|
|
70
|
-
if (/(?:^|&&|\|\||;)\s*cargo\s+/i.test(command)) violations.push(`${root} ${name}: bare cargo`);
|
|
71
|
-
if (/(?:^|&&|\|\||;)\s*npx\s+/i.test(command)) violations.push(`${root} ${name}: npx`);
|
|
72
|
-
if (/(?:^|&&|\|\||;)\s*npm\s+(?:ci|install)\b/i.test(command)) violations.push(`${root} ${name}: npm install`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
assert.deepEqual(violations, []);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("release configs resolve Cargo output through RightKit", () => {
|
|
79
|
-
const violations = [];
|
|
80
|
-
for (const root of releaseConfigRoots) {
|
|
81
|
-
const configPath = path.join(workspace, root, "right-release.config.mjs");
|
|
82
|
-
if (!existsSync(configPath)) continue;
|
|
83
|
-
const source = executableSource(readFileSync(configPath, "utf8"));
|
|
84
|
-
const hardcodedTargetRoot = /(?:const|let|var)\s+\w+\s*=\s*["'`]src-tauri[\\/]target/i.test(source);
|
|
85
|
-
const hardcodedTargetArtifact = /(?:file|signature)\s*:\s*["'`]src-tauri[\\/]target/i.test(source);
|
|
86
|
-
if (!hardcodedTargetRoot && !hardcodedTargetArtifact) continue;
|
|
87
|
-
if (!/@rightkit\/release\/cargo-target\.mjs|\.\/scripts\/lib\/target-root\.mjs/.test(source)) violations.push(`${root}: no RightKit target resolver`);
|
|
88
|
-
if (!/resolve(?:TargetRoot|WindowsReleaseDir)\s*\(/.test(source)) violations.push(`${root}: resolver not invoked`);
|
|
89
|
-
if (hardcodedTargetRoot) violations.push(`${root}: hardcoded target root`);
|
|
90
|
-
if (hardcodedTargetArtifact) violations.push(`${root}: hardcoded target artifact`);
|
|
91
|
-
}
|
|
92
|
-
assert.deepEqual(violations, []);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("product pipeline children use managed Rust commands", () => {
|
|
96
|
-
const violations = [];
|
|
97
|
-
for (const root of packageRoots) {
|
|
98
|
-
for (const file of pipelineScripts(path.join(workspace, root))) {
|
|
99
|
-
const source = executableSource(readFileSync(file, "utf8"));
|
|
100
|
-
const label = path.relative(workspace, file);
|
|
101
|
-
if (/(?:spawnSync|spawn|execFileSync|execFile)\s*\(\s*["'`](?:cargo|rustc|rustdoc)["'`]/.test(source)) violations.push(`${label}: unmanaged JS child`);
|
|
102
|
-
if (/subprocess\.(?:run|Popen)\s*\(\s*\[\s*["'](?:cargo|rustc|rustdoc)["']/.test(source)) violations.push(`${label}: unmanaged Python child`);
|
|
103
|
-
if (/(?:^|\n)\s*(?:&\s*)?(?:cargo|rustc|rustdoc)\s+(?:build|check|clippy|metadata|test|-vV)\b/m.test(source)) violations.push(`${label}: unmanaged shell command`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
assert.deepEqual(violations, []);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("hosted workflows are generated by right-git", () => {
|
|
110
|
-
const violations = [];
|
|
111
|
-
for (const root of rightGitRepos) {
|
|
112
|
-
const workflowRoot = path.join(workspace, root, ".github", "workflows");
|
|
113
|
-
if (!existsSync(workflowRoot)) continue;
|
|
114
|
-
for (const entry of readdirSync(workflowRoot, { withFileTypes: true })) {
|
|
115
|
-
if (!entry.isFile() || !/\.ya?ml$/i.test(entry.name)) continue;
|
|
116
|
-
const source = readFileSync(path.join(workflowRoot, entry.name), "utf8");
|
|
117
|
-
if (!/^# Managed by right-git\b/.test(source)) violations.push(`${root}/.github/workflows/${entry.name}: not right-git managed`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
for (const root of privateRepos) {
|
|
121
|
-
const workflowRoot = path.join(workspace, root, ".github", "workflows");
|
|
122
|
-
const workflows = existsSync(workflowRoot)
|
|
123
|
-
? readdirSync(workflowRoot).filter((entry) => /\.ya?ml$/i.test(entry))
|
|
124
|
-
: [];
|
|
125
|
-
if (workflows.length) violations.push(`${root}: private repo has ${workflows.join(", ")}`);
|
|
126
|
-
}
|
|
127
|
-
assert.deepEqual(violations, []);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("workspace memory pipelines route Rust through RightKit", () => {
|
|
131
|
-
for (const relative of [
|
|
132
|
-
"tools/pipelines/memory/dispatch/gate.py",
|
|
133
|
-
"tools/pipelines/memory/install-cortex.py",
|
|
134
|
-
"tools/pipelines/memory/verify-source-owner.py",
|
|
135
|
-
]) {
|
|
136
|
-
const source = executableSource(readFileSync(path.join(workspace, relative), "utf8"));
|
|
137
|
-
assert.doesNotMatch(source, /(?:^|&&|\|\||;)\s*cargo\s+(?:metadata|build|check|clippy|test)\b/m, `${relative} invokes bare Cargo shell command`);
|
|
138
|
-
assert.doesNotMatch(source, /subprocess\.run\(\s*\[\s*["']cargo["']/m, `${relative} invokes bare Cargo subprocess`);
|
|
139
|
-
}
|
|
140
|
-
});
|