@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.71",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: native-host signed installers, updater artifacts, hardening, immutable GitHub Release upload, and add-on adoption.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,16 +12,11 @@
|
|
|
12
12
|
"*.mjs",
|
|
13
13
|
"*.json",
|
|
14
14
|
"*.sh",
|
|
15
|
-
"*.py"
|
|
15
|
+
"*.py",
|
|
16
|
+
"!*.test.mjs",
|
|
17
|
+
"!standalone-clone-evidence.json"
|
|
16
18
|
],
|
|
17
19
|
"sideEffects": false,
|
|
18
|
-
"scripts": {
|
|
19
|
-
"test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
20
|
-
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
21
|
-
"prepublishOnly": "pnpm test && pnpm test:registry-parity",
|
|
22
|
-
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
23
|
-
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
24
|
-
},
|
|
25
20
|
"publishConfig": {
|
|
26
21
|
"registry": "https://registry.npmjs.org/",
|
|
27
22
|
"access": "public"
|
|
@@ -31,5 +26,10 @@
|
|
|
31
26
|
"url": "git+https://github.com/bogusyogi/claude.git",
|
|
32
27
|
"directory": "tools/rightkit/packages/release"
|
|
33
28
|
},
|
|
34
|
-
"
|
|
35
|
-
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
31
|
+
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
32
|
+
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
33
|
+
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/preflight.mjs
CHANGED
|
@@ -170,6 +170,8 @@ export function collectPreflight({
|
|
|
170
170
|
version,
|
|
171
171
|
configPath,
|
|
172
172
|
cargoLockPaths = [],
|
|
173
|
+
nativeLayout,
|
|
174
|
+
targetLink,
|
|
173
175
|
minFreeGb = 25,
|
|
174
176
|
env = process.env,
|
|
175
177
|
} = {}) {
|
|
@@ -177,7 +179,8 @@ export function collectPreflight({
|
|
|
177
179
|
const windows = platform === "win" || platform === "win32";
|
|
178
180
|
|
|
179
181
|
if (appRoot) {
|
|
180
|
-
const target = path.join(appRoot, "src-tauri", "target");
|
|
182
|
+
const target = targetLink ?? nativeLayout?.targetLink ?? path.join(appRoot, "src-tauri", "target");
|
|
183
|
+
const targetLabel = nativeLayout?.targetPrefix || "src-tauri/target/";
|
|
181
184
|
let entry = null;
|
|
182
185
|
try {
|
|
183
186
|
entry = lstatSync(target);
|
|
@@ -186,7 +189,7 @@ export function collectPreflight({
|
|
|
186
189
|
}
|
|
187
190
|
checks.push(
|
|
188
191
|
!entry || entry.isSymbolicLink()
|
|
189
|
-
? ok("target-bridge", entry ?
|
|
192
|
+
? ok("target-bridge", entry ? `${targetLabel} is a symbolic link` : `${targetLabel} is ready for the shared cache bridge`)
|
|
190
193
|
// A real directory here is not corruption on a broker-managed host: the
|
|
191
194
|
// build broker owns CARGO_TARGET_DIR and this is its own output (or a
|
|
192
195
|
// leftover from before it took ownership), not Cache V2's bridge target.
|
|
@@ -210,7 +213,7 @@ export function collectPreflight({
|
|
|
210
213
|
? fail(
|
|
211
214
|
"version",
|
|
212
215
|
`${app} ${version} is already sealed at ${sealedDir}; right-release will refuse to rebuild it`,
|
|
213
|
-
`bump to ${sealed.suggestion} in package.json
|
|
216
|
+
`bump to ${sealed.suggestion} in package.json and ${nativeLayout?.manifestPrefix ?? "src-tauri/"}Cargo.toml, then re-run the legal notices generator`,
|
|
214
217
|
)
|
|
215
218
|
: ok("version", `${version} is free to build`),
|
|
216
219
|
);
|
package/registry-parity.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { basename, join } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
|
|
9
9
|
const packageRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
10
10
|
|
|
@@ -72,7 +72,7 @@ export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
if (import.meta.url ===
|
|
75
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
76
76
|
verifyRegistryParity({ allowUnpublished: process.argv.includes('--allow-unpublished') })
|
|
77
77
|
.then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
|
|
78
78
|
.catch((error) => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; });
|
package/release-invocation.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
4
5
|
|
|
5
6
|
function git(cwd, args) {
|
|
6
7
|
const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
|
|
@@ -53,12 +54,18 @@ export function resolveConfiguredBuildInputs(config, target, label = "release co
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target, env = process.env }) {
|
|
57
|
+
const nativeLayout = resolveNativeCargoLayout({
|
|
58
|
+
appRoot,
|
|
59
|
+
config,
|
|
60
|
+
requireManifest: true,
|
|
61
|
+
requireLock: true,
|
|
62
|
+
});
|
|
56
63
|
const requiredInputs = [
|
|
57
64
|
configPath,
|
|
58
65
|
path.join(appRoot, "package.json"),
|
|
59
66
|
path.join(appRoot, "pnpm-lock.yaml"),
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
nativeLayout.manifestPath,
|
|
68
|
+
nativeLayout.lockPath,
|
|
62
69
|
...(target?.preflight?.files ?? []).map((file) => path.resolve(appRoot, expandEnv(file, env))),
|
|
63
70
|
].filter((file, index, files) => files.indexOf(file) === index);
|
|
64
71
|
for (const file of requiredInputs) {
|
|
@@ -77,6 +84,7 @@ export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, confi
|
|
|
77
84
|
|
|
78
85
|
return {
|
|
79
86
|
requiredInputs,
|
|
87
|
+
nativeLayout,
|
|
80
88
|
buildInputs: {
|
|
81
89
|
include: configured.include.map(qualify),
|
|
82
90
|
exclude: (configured.exclude ?? []).map(qualify),
|
package/release-state.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { readCachePolicy, resolveCacheLayout } from "./cache-policy.mjs";
|
|
6
|
+
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
6
7
|
|
|
7
8
|
export function cacheFingerprint({ cargoLockSha256, rustc, target, architecture, features = [] }) {
|
|
8
9
|
const payload = JSON.stringify({
|
|
@@ -15,14 +16,16 @@ export function cacheFingerprint({ cargoLockSha256, rustc, target, architecture,
|
|
|
15
16
|
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function resolveReleaseLayout({ repoRoot, configPath }) {
|
|
19
|
+
export function resolveReleaseLayout({ repoRoot, configPath, config, nativeLayout }) {
|
|
19
20
|
const resolvedRepoRoot = path.resolve(repoRoot);
|
|
20
21
|
const appRoot = path.dirname(path.resolve(configPath));
|
|
22
|
+
const resolvedNativeLayout = nativeLayout ?? resolveNativeCargoLayout({ appRoot, config });
|
|
21
23
|
return {
|
|
22
24
|
repoRoot: resolvedRepoRoot,
|
|
23
25
|
appRoot,
|
|
24
26
|
vaultRoot: path.join(resolvedRepoRoot, ".right-release"),
|
|
25
|
-
targetLink:
|
|
27
|
+
targetLink: resolvedNativeLayout.targetLink,
|
|
28
|
+
nativeLayout: resolvedNativeLayout,
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|
package/release.mjs
CHANGED
|
@@ -9,6 +9,14 @@ import { validateRightKitCargoContract } from "./cargo-contract.mjs";
|
|
|
9
9
|
import { assertQaBackdoorContract } from "./qa-contract.mjs";
|
|
10
10
|
import { assertLegalReleaseContract } from "./legal-contract.mjs";
|
|
11
11
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
12
|
+
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
13
|
+
import {
|
|
14
|
+
canonicalPackageIdentity,
|
|
15
|
+
createNativeFinalizationReceipt,
|
|
16
|
+
mintNativeProvenance,
|
|
17
|
+
resolveNativeFinalizerConfig,
|
|
18
|
+
validateNativeFinalizationOutput,
|
|
19
|
+
} from "./native-release-finalization.mjs";
|
|
12
20
|
import { collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
|
|
13
21
|
import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
|
|
14
22
|
import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
@@ -92,6 +100,9 @@ if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<m
|
|
|
92
100
|
|
|
93
101
|
const root = path.dirname(configPath);
|
|
94
102
|
const workdir = path.resolve(root, config.workdir ?? ".");
|
|
103
|
+
const nativeLayout = resolveNativeCargoLayout({ appRoot: root, config });
|
|
104
|
+
const nativeAssembly = config.nativeAssembly;
|
|
105
|
+
const nativeFinalizer = resolveNativeFinalizerConfig(nativeAssembly);
|
|
95
106
|
await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
|
|
96
107
|
validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
|
|
97
108
|
const target = config.targets?.[opts.platform];
|
|
@@ -118,7 +129,7 @@ if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.pl
|
|
|
118
129
|
* Assemble preflight inputs from the app's own files. Kept here (not in
|
|
119
130
|
* preflight.mjs) so the check module stays free of release.mjs's config shape.
|
|
120
131
|
*/
|
|
121
|
-
function releasePreflight({ config, configPath, root, repoRoot, platform }) {
|
|
132
|
+
function releasePreflight({ config, configPath, root, repoRoot, platform, nativeLayout: suppliedNativeLayout = nativeLayout }) {
|
|
122
133
|
let version;
|
|
123
134
|
try {
|
|
124
135
|
version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
|
|
@@ -128,10 +139,9 @@ function releasePreflight({ config, configPath, root, repoRoot, platform }) {
|
|
|
128
139
|
// Every Cargo.lock the build compiles from; a vendored-OpenSSL edge in any of
|
|
129
140
|
// them means the build needs a real Windows Perl.
|
|
130
141
|
const cargoLockPaths = [
|
|
131
|
-
|
|
132
|
-
path.join(root, "Cargo.lock"),
|
|
142
|
+
...(suppliedNativeLayout?.lockCandidates ?? []),
|
|
133
143
|
...(config.cargoLocks ?? []).map((entry) => path.resolve(root, entry)),
|
|
134
|
-
];
|
|
144
|
+
].filter((entry, index, entries) => entries.indexOf(entry) === index);
|
|
135
145
|
return collectPreflight({
|
|
136
146
|
platform,
|
|
137
147
|
appRoot: root,
|
|
@@ -140,11 +150,13 @@ function releasePreflight({ config, configPath, root, repoRoot, platform }) {
|
|
|
140
150
|
version,
|
|
141
151
|
configPath,
|
|
142
152
|
cargoLockPaths,
|
|
153
|
+
nativeLayout: suppliedNativeLayout,
|
|
154
|
+
targetLink: suppliedNativeLayout?.targetLink,
|
|
143
155
|
});
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
if (opts.doctor) {
|
|
147
|
-
const { buildInputs } = resolveReleaseBuildInputs({
|
|
159
|
+
const { buildInputs, nativeLayout: resolvedNativeLayout } = resolveReleaseBuildInputs({
|
|
148
160
|
repoRoot: doctorInvocation.repoRoot,
|
|
149
161
|
appRoot: root,
|
|
150
162
|
configPath,
|
|
@@ -174,7 +186,7 @@ if (opts.doctor) {
|
|
|
174
186
|
|
|
175
187
|
// Config above, MACHINE below. Printing config never told anyone whether this
|
|
176
188
|
// box could finish a build; these checks do.
|
|
177
|
-
const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform });
|
|
189
|
+
const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform, nativeLayout: resolvedNativeLayout });
|
|
178
190
|
console.log("preflight:");
|
|
179
191
|
console.log(formatPreflight(checks));
|
|
180
192
|
const failures = preflightFailures(checks);
|
|
@@ -200,6 +212,7 @@ if (!opts.skipChecks) {
|
|
|
200
212
|
for (const script of config.checks ?? []) await runPackageScript(config.packageManager, script, workdir);
|
|
201
213
|
}
|
|
202
214
|
|
|
215
|
+
let nativeFinalization = null;
|
|
203
216
|
let bundleMarkerReceipts = [];
|
|
204
217
|
if (opts.platform === "win") {
|
|
205
218
|
const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
|
|
@@ -215,6 +228,29 @@ if (opts.platform === "win") {
|
|
|
215
228
|
console.log(`right-release: bundle marker ${receipt.bundle} ${receipt.alreadyPatched ? "already applied" : "applied"} at offset ${receipt.offset} in ${path.basename(receipt.file)}`);
|
|
216
229
|
}
|
|
217
230
|
await signWindows(rawFiles, "raw-exe", root);
|
|
231
|
+
if (nativeAssembly?.packageHook && !hasNativeTargetSpecificPrePackage(target, nativeAssembly.packageHook)) {
|
|
232
|
+
await runCommand(nativeAssembly.packageHook, root);
|
|
233
|
+
}
|
|
234
|
+
if (nativeFinalizer) {
|
|
235
|
+
nativeFinalization = await runNativeFinalizer({
|
|
236
|
+
finalizer: nativeFinalizer,
|
|
237
|
+
nativeAssembly,
|
|
238
|
+
target,
|
|
239
|
+
root,
|
|
240
|
+
signedFiles: rawFiles,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
} else if (nativeAssembly) {
|
|
244
|
+
if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
|
|
245
|
+
if (nativeFinalizer) {
|
|
246
|
+
nativeFinalization = await runNativeFinalizer({
|
|
247
|
+
finalizer: nativeFinalizer,
|
|
248
|
+
nativeAssembly,
|
|
249
|
+
target,
|
|
250
|
+
root,
|
|
251
|
+
signedFiles: target.sign?.prePackageFiles ?? target.sign?.files ?? [],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
218
254
|
}
|
|
219
255
|
|
|
220
256
|
await runCommand(command, root);
|
|
@@ -223,6 +259,8 @@ for (const rel of target.artifacts ?? []) {
|
|
|
223
259
|
await mustExist(path.resolve(root, rel), `missing release artifact: ${rel}`);
|
|
224
260
|
}
|
|
225
261
|
|
|
262
|
+
if (nativeFinalization) await completeNativeFinalization(nativeFinalization, { root, target, nativeAssembly });
|
|
263
|
+
|
|
226
264
|
if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT) {
|
|
227
265
|
const files = target.sign.files.map((p) => path.resolve(root, p));
|
|
228
266
|
for (const file of files) await mustExist(file, `missing signing artifact: ${file}`);
|
|
@@ -311,13 +349,180 @@ async function mustExist(file, message) {
|
|
|
311
349
|
await access(file).catch(() => fail(message));
|
|
312
350
|
}
|
|
313
351
|
|
|
352
|
+
async function runNativeFinalizer({ finalizer, nativeAssembly, target, root, signedFiles }) {
|
|
353
|
+
if (!finalizer?.cmd) fail(`${config.app} ${opts.platform} nativeAssembly.finalizer must declare a command`);
|
|
354
|
+
const signedArtifacts = nativeSignedArtifacts(signedFiles, root);
|
|
355
|
+
const packageIdentity = nativePackageIdentity({ nativeAssembly, target });
|
|
356
|
+
const provenance = mintNativeProvenance({
|
|
357
|
+
app: config.app,
|
|
358
|
+
version: config.version,
|
|
359
|
+
platform: opts.platform,
|
|
360
|
+
architecture: target.selectedArchitecture ?? target.architecture ?? nativeAssembly?.architecture ?? process.arch,
|
|
361
|
+
targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
362
|
+
signedArtifacts,
|
|
363
|
+
packageIdentity,
|
|
364
|
+
});
|
|
365
|
+
const outputPath = nativeFinalizerOutputPath({ finalizer, target, root });
|
|
366
|
+
const finalizerCommand = {
|
|
367
|
+
...finalizer,
|
|
368
|
+
args: finalizer.args.map((arg) => expandFinalizerToken(arg, { provenance, outputPath, packageIdentity })),
|
|
369
|
+
};
|
|
370
|
+
const stdout = await runCommand(finalizerCommand, root, {
|
|
371
|
+
captureStdout: true,
|
|
372
|
+
env: {
|
|
373
|
+
RIGHT_RELEASE_NATIVE_PROVENANCE: provenance,
|
|
374
|
+
RIGHT_RELEASE_NATIVE_FINALIZATION_RECEIPT: outputPath,
|
|
375
|
+
RIGHT_RELEASE_NATIVE_PACKAGE_IDENTITY: JSON.stringify(packageIdentity),
|
|
376
|
+
RIGHT_RELEASE_NATIVE_SIGNED_ARTIFACTS: JSON.stringify(signedArtifacts),
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
if (opts.dryRun) return { outputPath, provenance, signedArtifacts, packageIdentity, output: null };
|
|
380
|
+
|
|
381
|
+
let output = null;
|
|
382
|
+
if (existsSync(outputPath)) {
|
|
383
|
+
try { output = JSON.parse(readFileSync(outputPath, "utf8")); } catch (error) { fail(`native finalizer output is not valid JSON: ${outputPath}: ${error.message}`); }
|
|
384
|
+
} else if (stdout?.trim()) {
|
|
385
|
+
try { output = JSON.parse(stdout); } catch { fail(`native finalizer output missing: ${outputPath}`); }
|
|
386
|
+
} else {
|
|
387
|
+
fail(`native finalizer output missing: ${outputPath}`);
|
|
388
|
+
}
|
|
389
|
+
output = enrichNativeFinalizerOutput(output, root);
|
|
390
|
+
try {
|
|
391
|
+
output = validateNativeFinalizationOutput(output, { expectedProvenance: provenance });
|
|
392
|
+
} catch (error) {
|
|
393
|
+
fail(`invalid native finalizer output: ${error.message}`);
|
|
394
|
+
}
|
|
395
|
+
return { outputPath, provenance, signedArtifacts, packageIdentity, output };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function enrichNativeFinalizerOutput(output, root) {
|
|
399
|
+
if (output?.provenance) return output;
|
|
400
|
+
if (output?.runtime?.provenance) return { ...output, provenance: output.runtime.provenance };
|
|
401
|
+
if (!output?.output) return output;
|
|
402
|
+
const manifestPath = path.join(path.resolve(root, output.output), "share", "legion", "release.json");
|
|
403
|
+
if (!existsSync(manifestPath)) return output;
|
|
404
|
+
try {
|
|
405
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
406
|
+
const provenance = manifest.runtime?.provenance;
|
|
407
|
+
if (provenance) return { ...output, provenance };
|
|
408
|
+
} catch {
|
|
409
|
+
return output;
|
|
410
|
+
}
|
|
411
|
+
return output;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function completeNativeFinalization(finalization, { root, target, nativeAssembly }) {
|
|
415
|
+
if (opts.dryRun) {
|
|
416
|
+
console.log(`dry-run: native finalization receipt binds archive identity at ${finalization.outputPath}`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const archivePath = nativeArchiveCandidates({ root, target, nativeAssembly }).find((file) => existsSync(file));
|
|
420
|
+
if (!archivePath) fail(`${config.app} ${opts.platform} native finalization cannot find final archive`);
|
|
421
|
+
let receipt;
|
|
422
|
+
try {
|
|
423
|
+
receipt = createNativeFinalizationReceipt({
|
|
424
|
+
app: config.app,
|
|
425
|
+
version: config.version,
|
|
426
|
+
platform: opts.platform,
|
|
427
|
+
architecture: target.selectedArchitecture ?? target.architecture ?? nativeAssembly?.architecture ?? process.arch,
|
|
428
|
+
targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
429
|
+
signedArtifacts: finalization.signedArtifacts,
|
|
430
|
+
packageIdentity: finalization.packageIdentity,
|
|
431
|
+
archivePath,
|
|
432
|
+
finalizerOutput: finalization.output,
|
|
433
|
+
root,
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
fail(`native finalization archive binding failed: ${error.message}`);
|
|
437
|
+
}
|
|
438
|
+
mkdirSync(path.dirname(finalization.outputPath), { recursive: true });
|
|
439
|
+
writeFileSync(finalization.outputPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
440
|
+
console.log(`right-release: native finalization verified ${receipt.provenance} archive=${receipt.archiveSha256}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function nativeSignedArtifacts(files, root) {
|
|
444
|
+
const values = Array.isArray(files) ? files : [];
|
|
445
|
+
return values.map((value) => {
|
|
446
|
+
const declared = typeof value === "string" ? value : value?.path ?? value?.file;
|
|
447
|
+
if (!declared) fail("native finalizer signed artifact path is missing");
|
|
448
|
+
const absolute = path.resolve(root, declared);
|
|
449
|
+
if (opts.dryRun) return { path: path.relative(root, absolute).replaceAll("\\", "/") };
|
|
450
|
+
if (!existsSync(absolute)) fail(`native finalizer signed artifact missing: ${absolute}`);
|
|
451
|
+
return {
|
|
452
|
+
path: path.relative(root, absolute).replaceAll("\\", "/"),
|
|
453
|
+
sha256: createHash("sha256").update(readFileSync(absolute)).digest("hex"),
|
|
454
|
+
sizeBytes: readFileSync(absolute).length,
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function nativePackageIdentity({ nativeAssembly, target }) {
|
|
460
|
+
const declared = nativeAssembly?.packageIdentity ?? target.packageIdentity;
|
|
461
|
+
if (declared) return canonicalPackageIdentity(declared);
|
|
462
|
+
const artifacts = [
|
|
463
|
+
...(target.artifacts ?? []),
|
|
464
|
+
...(target.installer?.artifacts ?? []).map((artifact) => artifact.file),
|
|
465
|
+
...(target.updater?.artifacts ?? []).map((artifact) => artifact.file),
|
|
466
|
+
].filter(Boolean);
|
|
467
|
+
return canonicalPackageIdentity({
|
|
468
|
+
kind: target.packageKind ?? "release",
|
|
469
|
+
platform: opts.platform,
|
|
470
|
+
architecture: target.selectedArchitecture ?? target.architecture ?? nativeAssembly?.architecture ?? process.arch,
|
|
471
|
+
...(artifacts.length ? { artifacts } : {}),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function nativeArchiveCandidates({ root, target, nativeAssembly }) {
|
|
476
|
+
const declared = [
|
|
477
|
+
nativeAssembly?.archive,
|
|
478
|
+
nativeAssembly?.archivePath,
|
|
479
|
+
nativeAssembly?.packageIdentity?.path,
|
|
480
|
+
target.archive,
|
|
481
|
+
target.archivePath,
|
|
482
|
+
...(target.artifacts ?? []),
|
|
483
|
+
...(target.installer?.artifacts ?? []).map((artifact) => artifact.file),
|
|
484
|
+
...(target.updater?.artifacts ?? []).map((artifact) => artifact.file),
|
|
485
|
+
].filter((value) => typeof value === "string" && value.trim());
|
|
486
|
+
return [...new Set(declared.map((value) => path.resolve(root, value)))];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function nativeFinalizerOutputPath({ finalizer, target, root }) {
|
|
490
|
+
const declared = finalizer.output
|
|
491
|
+
?? target.evidence?.provenance
|
|
492
|
+
?? target.provenance
|
|
493
|
+
?? `.right-release/receipts/native-finalization-${opts.platform}.json`;
|
|
494
|
+
return path.resolve(root, declared);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function expandFinalizerToken(value, { provenance, outputPath, packageIdentity }) {
|
|
498
|
+
const replacements = {
|
|
499
|
+
"{{provenance}}": provenance,
|
|
500
|
+
"{provenance}": provenance,
|
|
501
|
+
"${RIGHT_RELEASE_NATIVE_PROVENANCE}": provenance,
|
|
502
|
+
"$RIGHT_RELEASE_NATIVE_PROVENANCE": provenance,
|
|
503
|
+
"{{receipt}}": outputPath,
|
|
504
|
+
"{receipt}": outputPath,
|
|
505
|
+
"${RIGHT_RELEASE_NATIVE_FINALIZATION_RECEIPT}": outputPath,
|
|
506
|
+
"$RIGHT_RELEASE_NATIVE_FINALIZATION_RECEIPT": outputPath,
|
|
507
|
+
"{{packageIdentity}}": JSON.stringify(packageIdentity),
|
|
508
|
+
"{packageIdentity}": JSON.stringify(packageIdentity),
|
|
509
|
+
};
|
|
510
|
+
return Object.entries(replacements).reduce((result, [token, replacement]) => result.replaceAll(token, replacement), String(value));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function hasNativeTargetSpecificPrePackage(target, packageHook) {
|
|
514
|
+
if (!target?.prePackage || !packageHook || target.prePackage.cmd !== packageHook.cmd) return false;
|
|
515
|
+
const args = target.prePackage.args ?? [];
|
|
516
|
+
return args.some((arg) => ["--platform", "--architecture", "--target", "--out"].includes(arg));
|
|
517
|
+
}
|
|
518
|
+
|
|
314
519
|
// Post-release target/ hygiene: `cargo sweep --installed` deletes artifacts left
|
|
315
520
|
// by toolchains rustup no longer has (each update orphans a multi-GB pile per
|
|
316
521
|
// app) and never touches anything the current toolchains produced. Runs after
|
|
317
522
|
// publish so it cannot race an artifact, and is best-effort: a missing
|
|
318
523
|
// cargo-sweep or a sweep failure must never fail a release.
|
|
319
524
|
function sweepStaleRustArtifacts() {
|
|
320
|
-
const candidates = [...new Set([root, workdir
|
|
525
|
+
const candidates = [...new Set([root, workdir, nativeLayout.manifestDir, nativeLayout.workspaceRoot])];
|
|
321
526
|
const projects = candidates.filter((dir) => existsSync(path.join(dir, "target")));
|
|
322
527
|
for (const dir of projects) {
|
|
323
528
|
if (opts.dryRun) {
|
|
@@ -348,10 +553,11 @@ async function runPackageScript(pm, script, cwd) {
|
|
|
348
553
|
await run(pm, ["run", script], cwd);
|
|
349
554
|
}
|
|
350
555
|
|
|
351
|
-
async function runCommand(command, root) {
|
|
556
|
+
async function runCommand(command, root, { env = {}, captureStdout = false } = {}) {
|
|
352
557
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
353
|
-
|
|
558
|
+
return run(command.cmd, command.args ?? [], cwd, { ...(await commandEnv(command, root)), ...env }, {
|
|
354
559
|
...command,
|
|
560
|
+
captureStdout,
|
|
355
561
|
timeoutMs: commandTimeoutMs(command),
|
|
356
562
|
});
|
|
357
563
|
}
|
|
@@ -550,7 +756,7 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
550
756
|
const timeout = options.timeoutMs ? ` timeout=${options.timeoutMs}ms` : "";
|
|
551
757
|
const tier = opts.tier ? `RIGHT_RELEASE_TIER=${opts.tier} ` : "";
|
|
552
758
|
console.log(`dry-run: (${cwd}) ${tier}${printable}${timeout}`);
|
|
553
|
-
return Promise.resolve();
|
|
759
|
+
return Promise.resolve("");
|
|
554
760
|
}
|
|
555
761
|
const started = Date.now();
|
|
556
762
|
return new Promise((resolve) => {
|
|
@@ -558,10 +764,12 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
558
764
|
const child = spawn(cmd, runArgs, {
|
|
559
765
|
cwd,
|
|
560
766
|
env: { ...process.env, ...releaseEnv, ...env },
|
|
561
|
-
stdio: "inherit",
|
|
767
|
+
stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
562
768
|
shell: useShell,
|
|
563
769
|
windowsHide: true,
|
|
564
770
|
});
|
|
771
|
+
let stdout = "";
|
|
772
|
+
if (options.captureStdout) child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
|
|
565
773
|
const timer = options.timeoutMs
|
|
566
774
|
? setTimeout(() => {
|
|
567
775
|
killProcessTree(child.pid);
|
|
@@ -579,7 +787,7 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
579
787
|
if (timer) clearTimeout(timer);
|
|
580
788
|
console.log(`right-release step: ${printable} (${Date.now() - started}ms)`);
|
|
581
789
|
if (code !== 0) process.exit(code ?? 1);
|
|
582
|
-
resolve();
|
|
790
|
+
resolve(options.captureStdout ? stdout : "");
|
|
583
791
|
});
|
|
584
792
|
});
|
|
585
793
|
}
|
package/rightkit-versions.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@rightkit/logs": "0.1.4",
|
|
23
23
|
"@rightkit/platform-ui": "0.1.1",
|
|
24
24
|
"@rightkit/qa": "0.2.1",
|
|
25
|
-
"@rightkit/release": "0.2.
|
|
25
|
+
"@rightkit/release": "0.2.71",
|
|
26
26
|
"@rightkit/tauri": "0.1.1",
|
|
27
27
|
"@rightkit/updates": "0.2.4"
|
|
28
28
|
},
|
|
@@ -64,7 +64,9 @@
|
|
|
64
64
|
"0.2.65",
|
|
65
65
|
"0.2.66",
|
|
66
66
|
"0.2.67",
|
|
67
|
-
"0.2.68"
|
|
67
|
+
"0.2.68",
|
|
68
|
+
"0.2.69",
|
|
69
|
+
"0.2.70"
|
|
68
70
|
],
|
|
69
71
|
"@rightkit/qa": [
|
|
70
72
|
"0.1.0",
|
package/addon-command.test.mjs
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import { createAddonManifest, addonManifestSha256, canonicalAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl } from "./addon-contract.mjs";
|
|
9
|
-
|
|
10
|
-
const command = fileURLToPath(new URL("./addon-command.mjs", import.meta.url));
|
|
11
|
-
function fixture() {
|
|
12
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "right-addon-command-"));
|
|
13
|
-
mkdirSync(path.join(root, "legal")); mkdirSync(path.join(root, "assets"));
|
|
14
|
-
for (const name of ["LICENSE", "EULA.txt", "PRIVACY.md", "THIRD-PARTY-NOTICES.txt"]) writeFileSync(path.join(root, "legal", name), name);
|
|
15
|
-
writeFileSync(path.join(root, "assets", "tab.png"), "icon");
|
|
16
|
-
const files = [["command", "membrane", "out/membrane", true], ["service", "membrane-service", "out/membrane-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false], ["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false]].map(([role, name, source, executable]) => ({ role, name, source, executable }));
|
|
17
|
-
writeFileSync(path.join(root, "right-addon.config.mjs"), `export default ${JSON.stringify({ schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: [], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "membrane-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "false", args: [] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "false", args: [] }, signing: { contract: "azure-artifact-signing-v1" }, files } } }, null, 2)};\n`);
|
|
18
|
-
execFileSync("git", ["init", "--initial-branch", "main"], { cwd: root }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: root }); execFileSync("git", ["config", "user.name", "Test"], { cwd: root }); execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-m", "init"], { cwd: root });
|
|
19
|
-
return root;
|
|
20
|
-
}
|
|
21
|
-
test("add-on dry-run creates no release state and never executes build", () => {
|
|
22
|
-
const root = fixture(); const result = spawnSync(process.execPath, [command, "build", "--config", "right-addon.config.mjs", "--platform", "mac", "--dry-run"], { cwd: root, encoding: "utf8" });
|
|
23
|
-
assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /would run: false/); assert.equal(existsSync(path.join(root, ".right-release")), false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("local adoption verifies lock and stages portable plus Tauri-qualified binaries", async () => {
|
|
27
|
-
const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?test=${Date.now()}`)).default;
|
|
28
|
-
mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "membrane-service"), "service");
|
|
29
|
-
const manifest = createAddonManifest({ config, root, platform: "mac", commit: "d".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
|
|
30
|
-
const sealed = path.join(root, "sealed"); mkdirSync(sealed);
|
|
31
|
-
for (const file of manifest.files) writeFileSync(path.join(sealed, file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source)));
|
|
32
|
-
writeFileSync(path.join(sealed, "addon-manifest.json"), canonicalAddonManifest(manifest));
|
|
33
|
-
const digest = addonManifestSha256(manifest); const lock = path.join(root, "membrane.lock.json"); writeFileSync(lock, JSON.stringify({ targets: { mac: { manifestUrl: `https://example.test/membrane/addons/mac/sha256/${digest}/addon-manifest.json`, manifestSha256: digest } } }));
|
|
34
|
-
const output = path.join(root, "addons"); const unsigned = spawnSync(process.execPath, [command, "adopt", "--platform", "mac", "--lock", lock, "--output", `${output}-unsigned`, "--source", sealed], { cwd: root, encoding: "utf8", env: { ...process.env, RIGHT_RELEASE_TEST_FIXTURE: "" } });
|
|
35
|
-
assert.notEqual(unsigned.status, 0); assert.match(unsigned.stderr, /unexpected mac signing contract/);
|
|
36
|
-
const result = spawnSync(process.execPath, [command, "adopt", "--platform", "mac", "--lock", lock, "--output", output, "--source", sealed], { cwd: root, encoding: "utf8", env: { ...process.env, RIGHT_RELEASE_TEST_FIXTURE: "1" } });
|
|
37
|
-
assert.equal(result.status, 0, result.stderr);
|
|
38
|
-
assert.equal(readFileSync(path.join(output, "membrane", "bin", "membrane-aarch64-apple-darwin"), "utf8"), "command");
|
|
39
|
-
assert.equal(readFileSync(path.join(output, "membrane", "resources", "LICENSE"), "utf8"), "LICENSE");
|
|
40
|
-
const receipt = JSON.parse(readFileSync(path.join(root, ".right-release", "addons", "adoptions", "membrane-mac.json"), "utf8"));
|
|
41
|
-
assert.equal(receipt.manifestSha256, digest);
|
|
42
|
-
assert.deepEqual(receipt.files.map((file) => [file.role, file.sealedSha256, file.adoptedSha256]), [["command", manifest.files.find((file) => file.role === "command").sha256, manifest.files.find((file) => file.role === "command").sha256], ["service", manifest.files.find((file) => file.role === "service").sha256, manifest.files.find((file) => file.role === "service").sha256]]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("remote downloader accepts only immutable manifest plus file routes", async () => {
|
|
46
|
-
const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?remote=${Date.now()}`)).default;
|
|
47
|
-
mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "membrane-service"), "service");
|
|
48
|
-
const manifest = createAddonManifest({ config, root, platform: "mac", commit: "e".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
|
|
49
|
-
const digest = addonManifestSha256(manifest); const base = `https://example.test/membrane/addons/mac/sha256/${digest}/`; const bytes = new Map([[`${base}addon-manifest.json`, Buffer.from(canonicalAddonManifest(manifest))], ...manifest.files.map((file) => [base + encodeURIComponent(file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source))])]);
|
|
50
|
-
const destination = path.join(root, "remote"); mkdirSync(destination);
|
|
51
|
-
await downloadImmutableAddOn(`${base}addon-manifest.json`, destination, { fetchImpl: async (url) => ({ ok: bytes.has(url), status: bytes.has(url) ? 200 : 404, arrayBuffer: async () => bytes.get(url) }) });
|
|
52
|
-
assert.equal(readFileSync(path.join(destination, "membrane-service"), "utf8"), "service");
|
|
53
|
-
assert.equal(immutableAddonManifestUrl(manifest, `https://github.com/Orthic-Labs/Membrane/releases/download/addon-membrane-v0.1.0-mac-sha256-${digest}/addon-manifest.json`, digest), true);
|
|
54
|
-
});
|
package/addon-contract.test.mjs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import { addonManifestSha256, addonRoutes, createAddonManifest, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
|
|
7
|
-
|
|
8
|
-
function fixture() {
|
|
9
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "right-addon-contract-"));
|
|
10
|
-
mkdirSync(path.join(root, "bin")); mkdirSync(path.join(root, "assets")); mkdirSync(path.join(root, "legal"));
|
|
11
|
-
for (const [file, bytes] of [["bin/membrane", "membrane"], ["bin/membrane-service", "service"], ["assets/tab.png", "icon"], ["legal/LICENSE", "license"], ["legal/EULA.txt", "eula"], ["legal/PRIVACY.md", "privacy"], ["legal/THIRD-PARTY-NOTICES.txt", "notice"]]) writeFileSync(path.join(root, file), bytes);
|
|
12
|
-
const files = [
|
|
13
|
-
["command", "membrane", "bin/membrane", true], ["service", "membrane-service", "bin/membrane-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false],
|
|
14
|
-
["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false],
|
|
15
|
-
].map(([role, name, source, executable]) => ({ role, name, source, executable }));
|
|
16
|
-
return { root, config: { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: ["test"], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "membrane-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "azure-artifact-signing-v1" }, files } } } };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
test("add-on manifest has portable immutable identity and routes", () => {
|
|
20
|
-
const { root, config } = fixture();
|
|
21
|
-
const signing = { command: { contract: "test", status: "verified" }, service: { contract: "test", status: "verified" } };
|
|
22
|
-
const manifest = createAddonManifest({ config, root, platform: "mac", commit: "a".repeat(40), signedAt: "2026-08-11T00:00:00.000Z", signing });
|
|
23
|
-
assert.deepEqual(Object.keys(manifest), ["schema", "kind", "addon", "version", "commit", "platform", "targetTriple", "consumer", "files", "signedAt"]);
|
|
24
|
-
const digest = addonManifestSha256(manifest); const routes = addonRoutes(manifest);
|
|
25
|
-
assert.equal(routes.at(-1).manifest, true);
|
|
26
|
-
assert.match(routes[0].key, new RegExp(`^membrane/addons/mac/sha256/${digest}/`));
|
|
27
|
-
assert.ok(routes.slice(0, -1).every((route) => !route.manifest));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("add-on contract rejects unsafe, incomplete, duplicate, and unsigned inputs", () => {
|
|
31
|
-
const { root, config } = fixture();
|
|
32
|
-
const broken = structuredClone(config); broken.targets.mac.files[0].source = "../escape";
|
|
33
|
-
assert.throws(() => validateAddonConfig(broken, { root, platform: "mac" }), /unsafe add-on source/);
|
|
34
|
-
const duplicate = structuredClone(config); duplicate.targets.mac.files[1].role = "command";
|
|
35
|
-
assert.throws(() => validateAddonConfig(duplicate, { root, platform: "mac" }), /duplicate/);
|
|
36
|
-
const signing = { command: { contract: "test", status: "verified" }, service: { contract: "test", status: "verified" } };
|
|
37
|
-
const manifest = createAddonManifest({ config, root, platform: "mac", commit: "b".repeat(40), signing });
|
|
38
|
-
manifest.files.find((file) => file.role === "command").signing = null;
|
|
39
|
-
assert.throws(() => validateAddonManifest(manifest), /unsigned executable/);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("manifest excludes source and mutable transport fields", () => {
|
|
43
|
-
const { root, config } = fixture();
|
|
44
|
-
const manifest = createAddonManifest({ config, root, platform: "win", commit: "c".repeat(40), signing: { command: { contract: "test", status: "verified" }, service: { contract: "test", status: "verified" } } });
|
|
45
|
-
const text = JSON.stringify(manifest);
|
|
46
|
-
assert.doesNotMatch(text, /source|path|url|token/i);
|
|
47
|
-
assert.equal(readFileSync(path.join(root, "bin", "membrane"), "utf8"), "membrane");
|
|
48
|
-
});
|