@rightkit/release 0.2.57 → 0.2.59
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/addon-command.mjs +199 -0
- package/addon-command.test.mjs +54 -0
- package/addon-contract.mjs +110 -0
- package/addon-contract.test.mjs +48 -0
- package/build-release.mjs +17 -0
- package/build-release.test.mjs +15 -1
- package/cargo-contract.mjs +9 -0
- package/cli/right-release.mjs +3 -0
- package/github-release.mjs +76 -12
- package/github-release.test.mjs +44 -1
- package/package.json +31 -32
- package/release-state.mjs +1 -0
- package/right-suite-contract.test.mjs +1 -1
- package/rightkit-versions.json +67 -67
- package/standalone-clone-evidence.json +0 -32
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { addonManifestSha256, addonRoutes, canonicalAddonManifest, createAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
|
|
9
|
+
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
10
|
+
import { assertCleanSource } from "./source-gate.mjs";
|
|
11
|
+
|
|
12
|
+
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const SIGN_WINDOWS = path.join(ROOT, "sign-windows.mjs");
|
|
14
|
+
const HARDEN = path.join(ROOT, "hardeningscan.mjs");
|
|
15
|
+
const [operation, ...args] = process.argv.slice(2);
|
|
16
|
+
const options = parse(args);
|
|
17
|
+
if (!new Set(["doctor", "build", "upload", "adopt"]).has(operation)) usage(2);
|
|
18
|
+
if (!new Set(["mac", "win"]).has(options.platform)) fail("--platform must be mac or win");
|
|
19
|
+
|
|
20
|
+
if (operation === "adopt") await adopt(options);
|
|
21
|
+
else {
|
|
22
|
+
if (!options.config) fail("--config is required");
|
|
23
|
+
const configPath = path.resolve(options.config);
|
|
24
|
+
const root = path.dirname(configPath);
|
|
25
|
+
const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
|
|
26
|
+
assertPrimaryReleaseCheckout(repoRoot);
|
|
27
|
+
const config = (await import(`${pathToFileURL(configPath).href}?addon=${Date.now()}`)).default;
|
|
28
|
+
if (operation === "doctor") doctor({ config, configPath, root, repoRoot, options });
|
|
29
|
+
else if (operation === "build") await build({ config, configPath, root, repoRoot, options });
|
|
30
|
+
else await upload({ config, root, repoRoot, options });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function doctor({ config, configPath, root, repoRoot, options }) {
|
|
34
|
+
validateAddonConfig(config, { root, platform: options.platform, allowMissingExecutables: true });
|
|
35
|
+
console.log(`right-release addon doctor: ${config.addon} ${config.version} ${options.platform}`);
|
|
36
|
+
console.log(`config: ${configPath}`);
|
|
37
|
+
console.log(`repo: ${repoRoot}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function build({ config, configPath, root, repoRoot, options }) {
|
|
41
|
+
const commit = git(repoRoot, ["rev-parse", "HEAD"]);
|
|
42
|
+
const { target } = validateAddonConfig(config, { root, platform: options.platform, allowMissingExecutables: true });
|
|
43
|
+
if (options.dryRun) {
|
|
44
|
+
console.log(`right-release addon build dry-run: ${config.addon} ${config.version} ${options.platform}`);
|
|
45
|
+
console.log(`config: ${configPath}`);
|
|
46
|
+
console.log(`commit: ${commit}`);
|
|
47
|
+
console.log(`would run: ${target.build.cmd} ${target.build.args.join(" ")}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
assertCleanSource({ status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]), commandId: "right-release addon build" });
|
|
51
|
+
run(target.build.cmd, target.build.args, root);
|
|
52
|
+
const signing = signExecutableRoles(config, root, options.platform);
|
|
53
|
+
const manifest = createAddonManifest({ config, root, platform: options.platform, commit, signing });
|
|
54
|
+
const digest = addonManifestSha256(manifest);
|
|
55
|
+
const sealed = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), options.platform);
|
|
56
|
+
if (existsSync(sealed)) fail(`add-on seal already exists: ${sealed}`);
|
|
57
|
+
const temp = `${sealed}.tmp-${process.pid}`;
|
|
58
|
+
mkdirSync(temp, { recursive: true });
|
|
59
|
+
for (const file of config.targets[options.platform].files ?? config.files) copyFileSync(path.resolve(root, file.source), path.join(temp, file.name));
|
|
60
|
+
writeFileSync(path.join(temp, "addon-manifest.json"), canonicalAddonManifest(manifest));
|
|
61
|
+
run(process.execPath, [HARDEN, ...manifest.files.map((file) => path.join(temp, file.name))], root);
|
|
62
|
+
mkdirSync(path.dirname(sealed), { recursive: true });
|
|
63
|
+
renameSync(temp, sealed);
|
|
64
|
+
console.log(`right-release addon build: sealed ${sealed}`);
|
|
65
|
+
console.log(`manifestSha256: ${digest}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function signExecutableRoles(config, root, platform) {
|
|
69
|
+
const files = config.targets[platform].files ?? config.files;
|
|
70
|
+
const executable = files.filter((file) => file.executable === true);
|
|
71
|
+
if (platform === "win") run(process.execPath, [SIGN_WINDOWS, ...executable.map((file) => path.resolve(root, file.source))], root);
|
|
72
|
+
else for (const file of executable) {
|
|
73
|
+
const absolute = path.resolve(root, file.source);
|
|
74
|
+
run("codesign", ["--force", "--options", "runtime", "--sign", "Developer ID Application", absolute], root);
|
|
75
|
+
const output = command("codesign", ["-dv", "--verbose=4", absolute], root);
|
|
76
|
+
if (!new RegExp(`TeamIdentifier=${config.targets.mac.signing.teamId}`).test(output)) fail(`codesign team mismatch: ${file.name}`);
|
|
77
|
+
run("codesign", ["--verify", "--strict", "--verbose=2", absolute], root);
|
|
78
|
+
}
|
|
79
|
+
return Object.fromEntries(executable.map((file) => [file.role, { contract: config.targets[platform].signing.contract, status: "verified" }]));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function upload({ config, root, repoRoot, options }) {
|
|
83
|
+
if (config.distribution?.provider === "github-releases") fail("this add-on publishes with right-release github --addon <config> --platform <platform>");
|
|
84
|
+
if (!new Set(["patch", "update"]).has(options.tier)) fail("add-on upload requires --tier patch|update");
|
|
85
|
+
const commit = git(repoRoot, ["rev-parse", "HEAD"]);
|
|
86
|
+
const sealed = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), options.platform);
|
|
87
|
+
const manifest = JSON.parse(readFileSync(path.join(sealed, "addon-manifest.json"), "utf8"));
|
|
88
|
+
validateAddonManifest(manifest);
|
|
89
|
+
const routes = addonRoutes(manifest);
|
|
90
|
+
if (options.dryRun) {
|
|
91
|
+
for (const route of routes) console.log(`would upload ${route.key}${route.manifest ? " (last)" : ""}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!process.env.CLOUDFLARE_API_TOKEN) fail("CLOUDFLARE_API_TOKEN is required before R2 mutation");
|
|
95
|
+
for (const route of routes) {
|
|
96
|
+
const source = route.manifest ? path.join(sealed, "addon-manifest.json") : path.join(sealed, route.name);
|
|
97
|
+
await putImmutable({ source, route, repoRoot });
|
|
98
|
+
}
|
|
99
|
+
console.log(`right-release addon upload: verified ${config.addon} ${options.platform} tier=${options.tier}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function putImmutable({ source, route, repoRoot }) {
|
|
103
|
+
const target = `rightapps-downloads/${route.key}`;
|
|
104
|
+
const temp = path.join(os.tmpdir(), `right-addon-r2-${process.pid}-${createHash("sha256").update(route.key).digest("hex").slice(0, 12)}`);
|
|
105
|
+
const existing = wrangler(["r2", "object", "get", target, "--file", temp, "--remote"], repoRoot);
|
|
106
|
+
if (existing.status === 0) {
|
|
107
|
+
const actual = sha256(temp); rmSync(temp, { force: true });
|
|
108
|
+
if (actual !== route.sha256) fail(`immutable object exists with different bytes: ${route.key}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!/not found|does not exist|404/i.test(`${existing.stdout}\n${existing.stderr}`)) fail(`cannot inspect immutable object: ${route.key}`);
|
|
112
|
+
const uploaded = wrangler(["r2", "object", "put", target, "--file", source, "--remote"], repoRoot);
|
|
113
|
+
if (uploaded.status !== 0) fail(`immutable upload failed: ${route.key}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function adopt(options) {
|
|
117
|
+
if (!options.lock || !options.output) fail("adopt requires --lock and --output");
|
|
118
|
+
const lockPath = path.resolve(options.lock);
|
|
119
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
120
|
+
const entry = lock.targets?.[options.platform] ?? lock[options.platform];
|
|
121
|
+
if (!entry?.manifestSha256 || !entry?.manifestUrl) fail("add-on lock needs platform manifestUrl and manifestSha256");
|
|
122
|
+
const temporary = options.source ? null : mkdtempSync(path.join(os.tmpdir(), "right-addon-adopt-"));
|
|
123
|
+
const source = options.source ? path.resolve(options.source) : temporary;
|
|
124
|
+
if (!options.source) await downloadImmutableAddOn(entry.manifestUrl, source);
|
|
125
|
+
const manifest = JSON.parse(readFileSync(path.join(source, "addon-manifest.json"), "utf8"));
|
|
126
|
+
validateAddonManifest(manifest);
|
|
127
|
+
if (addonManifestSha256(manifest) !== entry.manifestSha256) fail("add-on lock manifest SHA mismatch");
|
|
128
|
+
if (!immutableAddonManifestUrl(manifest, entry.manifestUrl, entry.manifestSha256)) fail("add-on lock manifestUrl is not immutable");
|
|
129
|
+
const output = path.resolve(options.output);
|
|
130
|
+
const destination = path.join(output, manifest.addon);
|
|
131
|
+
const stage = `${destination}.tmp-${process.pid}`;
|
|
132
|
+
const adoptedFiles = [];
|
|
133
|
+
rmSync(stage, { recursive: true, force: true }); mkdirSync(stage, { recursive: true });
|
|
134
|
+
for (const file of manifest.files) {
|
|
135
|
+
const sourceFile = path.join(source, file.name);
|
|
136
|
+
if (!existsSync(sourceFile) || statSync(sourceFile).size !== file.sizeBytes || sha256(sourceFile) !== file.sha256) fail(`add-on file verification failed: ${file.name}`);
|
|
137
|
+
verifyAdoptedSignature({ file, sourceFile, platform: manifest.platform, sourceFixture: Boolean(options.source) });
|
|
138
|
+
const subdir = file.role === "command" || file.role === "service" ? "bin" : file.role === "icon" || LEGAL(file.role) ? "resources" : "resources";
|
|
139
|
+
mkdirSync(path.join(stage, subdir), { recursive: true });
|
|
140
|
+
const installed = path.join(stage, subdir, file.name);
|
|
141
|
+
copyFileSync(sourceFile, installed);
|
|
142
|
+
// Tauri resolves externalBin artifacts by Rust target triple. Keep the
|
|
143
|
+
// portable role name too, then provide the required target-qualified alias.
|
|
144
|
+
if (file.role === "command" || file.role === "service") {
|
|
145
|
+
const base = file.name.replace(/\.exe$/i, "");
|
|
146
|
+
const suffix = manifest.platform === "win" ? `${base}-${manifest.targetTriple}.exe` : `${base}-${manifest.targetTriple}`;
|
|
147
|
+
const alias = path.join(stage, "bin", suffix);
|
|
148
|
+
copyFileSync(sourceFile, alias);
|
|
149
|
+
const adoptedSha256 = sha256(alias);
|
|
150
|
+
if (adoptedSha256 !== file.sha256) fail(`adopted alias hash mismatch: ${file.name}`);
|
|
151
|
+
adoptedFiles.push({ role: file.role, name: file.name, sealedSha256: file.sha256, adoptedName: `bin/${suffix}`, adoptedSha256 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
copyFileSync(path.join(source, "addon-manifest.json"), path.join(stage, "addon-manifest.json"));
|
|
155
|
+
// Directory rename is atomic on one filesystem. Move any previous add-on
|
|
156
|
+
// aside first, restore it if replacement fails, then clean it only after the
|
|
157
|
+
// new verified tree is in place.
|
|
158
|
+
const previous = `${destination}.previous-${process.pid}`;
|
|
159
|
+
mkdirSync(output, { recursive: true });
|
|
160
|
+
if (existsSync(destination)) renameSync(destination, previous);
|
|
161
|
+
try {
|
|
162
|
+
renameSync(stage, destination);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (existsSync(previous)) renameSync(previous, destination);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
rmSync(previous, { recursive: true, force: true });
|
|
168
|
+
if (temporary) rmSync(temporary, { recursive: true, force: true });
|
|
169
|
+
const repoRoot = git(path.dirname(lockPath), ["rev-parse", "--show-toplevel"]);
|
|
170
|
+
const receipt = path.join(repoRoot, ".right-release", "addons", "adoptions", `${manifest.addon}-${manifest.platform}.json`);
|
|
171
|
+
mkdirSync(path.dirname(receipt), { recursive: true });
|
|
172
|
+
writeFileSync(receipt, `${JSON.stringify({ schema: 1, addon: manifest.addon, platform: manifest.platform, manifestSha256: entry.manifestSha256, manifestUrl: entry.manifestUrl, output: destination, files: adoptedFiles, adoptedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
173
|
+
console.log(`right-release addon adopt: ${destination}`);
|
|
174
|
+
console.log(`receipt: ${receipt}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function verifyAdoptedSignature({ file, sourceFile, platform, sourceFixture }) {
|
|
178
|
+
if (!file.executable) return;
|
|
179
|
+
if (file.signing.contract === "test-fixture-v1" && sourceFixture && process.env.RIGHT_RELEASE_TEST_FIXTURE === "1") return;
|
|
180
|
+
if (platform === "mac") {
|
|
181
|
+
if (file.signing.contract !== "apple-developer-id-executable-v1") fail(`unexpected mac signing contract: ${file.name}`);
|
|
182
|
+
const output = command("codesign", ["-dv", "--verbose=4", sourceFile], path.dirname(sourceFile));
|
|
183
|
+
if (!/TeamIdentifier=6KLGD3LLKF/.test(output)) fail(`codesign team mismatch: ${file.name}`);
|
|
184
|
+
run("codesign", ["--verify", "--strict", "--verbose=2", sourceFile], path.dirname(sourceFile));
|
|
185
|
+
} else {
|
|
186
|
+
if (file.signing.contract !== "azure-artifact-signing-v1") fail(`unexpected Windows signing contract: ${file.name}`);
|
|
187
|
+
run(process.execPath, [SIGN_WINDOWS, "--verify-only", sourceFile], path.dirname(sourceFile));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function LEGAL(role) { return new Set(["license", "eula", "privacy", "third-party-notices"]).has(role); }
|
|
192
|
+
function parse(args) { const value = { platform: process.platform === "win32" ? "win" : "mac", dryRun: false }; for (let i = 0; i < args.length; i += 1) { const key = args[i]; if (key === "--dry-run") value.dryRun = true; else if (["--config", "--platform", "--tier", "--lock", "--output", "--source"].includes(key)) value[key.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = args[++i]; else fail(`unknown argument: ${key}`); } return value; }
|
|
193
|
+
function git(cwd, args) { return command("git", args, cwd).trim(); }
|
|
194
|
+
function command(cmd, args, cwd) { const result = spawnSync(cmd, args, { cwd, encoding: "utf8", windowsHide: true }); if (result.status !== 0) throw new Error(`${cmd} ${args.join(" ")} failed: ${result.stderr || result.stdout}`); return `${result.stdout ?? ""}${result.stderr ?? ""}`; }
|
|
195
|
+
function run(cmd, args, cwd) { command(cmd, args, cwd); }
|
|
196
|
+
function wrangler(args, cwd) { return spawnSync("pnpm", ["dlx", "wrangler@4", ...args], { cwd, encoding: "utf8", windowsHide: true }); }
|
|
197
|
+
function sha256(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); }
|
|
198
|
+
function usage(code) { console.log("usage: right-release addon doctor|build|upload|adopt --platform mac|win ..."); process.exit(code); }
|
|
199
|
+
function fail(message) { console.error(`right-release addon: ${message}`); process.exit(1); }
|
|
@@ -0,0 +1,54 @@
|
|
|
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", "crypt-service", "out/crypt-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: "orthic-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", "crypt-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", "crypt-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, "crypt-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
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
6
|
+
const ROLES = new Set(["command", "service", "icon", "license", "eula", "privacy", "third-party-notices"]);
|
|
7
|
+
const LEGAL_ROLES = new Set(["license", "eula", "privacy", "third-party-notices"]);
|
|
8
|
+
const PLATFORM = new Set(["mac", "win"]);
|
|
9
|
+
const GITHUB_REPOSITORY = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
10
|
+
|
|
11
|
+
export function validateAddonConfig(config, { root, platform, allowMissingExecutables = false }) {
|
|
12
|
+
if (!PLATFORM.has(platform)) throw new Error("add-on platform must be mac or win");
|
|
13
|
+
if (!config || config.schema !== 1 || config.kind !== "headless-addon") throw new Error("add-on config must use schema 1 headless-addon");
|
|
14
|
+
for (const key of ["addon", "version", "packageManager"]) if (typeof config[key] !== "string" || !SAFE_NAME.test(config[key])) throw new Error(`unsafe add-on ${key}`);
|
|
15
|
+
if (!Array.isArray(config.checks)) throw new Error("add-on checks must be an array");
|
|
16
|
+
if (config.distribution && (config.distribution.provider !== "github-releases" || !GITHUB_REPOSITORY.test(config.distribution.repository ?? ""))) throw new Error("add-on distribution must name a GitHub Releases repository");
|
|
17
|
+
validateBuildInputs(config.buildInputs);
|
|
18
|
+
const target = config.targets?.[platform];
|
|
19
|
+
if (!target?.targetTriple || !target?.build?.cmd || !Array.isArray(target.build.args)) throw new Error(`add-on ${platform} target requires targetTriple and build command`);
|
|
20
|
+
if (!target.signing?.contract) throw new Error(`add-on ${platform} target requires signing contract`);
|
|
21
|
+
if (platform === "mac" && target.signing.contract === "apple-developer-id-executable-v1" && !/^[A-Z0-9]{10}$/.test(target.signing.teamId ?? "")) throw new Error("mac add-on requires Developer ID teamId");
|
|
22
|
+
const files = resolveAddonFiles(config, root, platform);
|
|
23
|
+
const seenRoles = new Set();
|
|
24
|
+
const seenNames = new Set();
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
if (!ROLES.has(file.role) || seenRoles.has(file.role)) throw new Error(`duplicate or unsafe add-on role: ${file.role}`);
|
|
27
|
+
if (!SAFE_NAME.test(file.name) || file.name.includes("..")) throw new Error(`unsafe add-on file name: ${file.name}`);
|
|
28
|
+
if (path.isAbsolute(file.source) || file.source.split(/[\\/]/).includes("..")) throw new Error(`unsafe add-on source: ${file.source}`);
|
|
29
|
+
if (seenNames.has(file.name)) throw new Error(`duplicate add-on file name: ${file.name}`);
|
|
30
|
+
if (!existsSync(path.resolve(root, file.source)) && !(allowMissingExecutables && file.executable === true)) throw new Error(`missing add-on file: ${file.source}`);
|
|
31
|
+
seenRoles.add(file.role); seenNames.add(file.name);
|
|
32
|
+
}
|
|
33
|
+
for (const role of ["command", "service", "icon", ...LEGAL_ROLES]) if (!seenRoles.has(role)) throw new Error(`missing required add-on role: ${role}`);
|
|
34
|
+
return { target, files };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveAddonFiles(config, root, platform) {
|
|
38
|
+
const configured = config.targets?.[platform]?.files ?? config.files;
|
|
39
|
+
if (!Array.isArray(configured)) throw new Error("add-on config requires files with role, name, and source");
|
|
40
|
+
return configured.map((file) => ({ ...file, executable: file.executable === true, source: String(file.source ?? "") }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createAddonManifest({ config, root, platform, commit, signedAt = new Date().toISOString(), signing = {} }) {
|
|
44
|
+
const { target, files } = validateAddonConfig(config, { root, platform });
|
|
45
|
+
if (!/^[0-9a-f]{40}$/i.test(commit)) throw new Error("add-on commit must be a full Git SHA");
|
|
46
|
+
const manifestFiles = files.map((file) => {
|
|
47
|
+
const source = path.resolve(root, file.source);
|
|
48
|
+
const executable = file.executable === true;
|
|
49
|
+
if ((file.role === "command" || file.role === "service") && !executable) throw new Error(`executable role must declare executable: ${file.role}`);
|
|
50
|
+
const signature = signing[file.role] ?? (executable ? { contract: target.signing.contract, status: "verified" } : null);
|
|
51
|
+
if (executable && (!signature || signature.status !== "verified")) throw new Error(`unsigned executable role: ${file.role}`);
|
|
52
|
+
return { role: file.role, name: file.name, sha256: sha256(source), sizeBytes: statSync(source).size, executable, signing: signature };
|
|
53
|
+
});
|
|
54
|
+
const manifest = { schema: 1, kind: "headless-addon", addon: config.addon, version: config.version, commit, platform, targetTriple: target.targetTriple, consumer: config.consumer, files: manifestFiles, signedAt };
|
|
55
|
+
validateAddonManifest(manifest);
|
|
56
|
+
return manifest;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateAddonManifest(manifest) {
|
|
60
|
+
const expected = ["schema", "kind", "addon", "version", "commit", "platform", "targetTriple", "consumer", "files", "signedAt"];
|
|
61
|
+
assertExactKeys(manifest, expected, "add-on manifest");
|
|
62
|
+
if (manifest.schema !== 1 || manifest.kind !== "headless-addon" || !SAFE_NAME.test(manifest.addon) || !SAFE_NAME.test(manifest.version)) throw new Error("invalid add-on manifest identity");
|
|
63
|
+
if (!PLATFORM.has(manifest.platform) || !/^[0-9a-f]{40}$/i.test(manifest.commit) || !Array.isArray(manifest.files)) throw new Error("invalid add-on manifest platform or files");
|
|
64
|
+
const roles = new Set(); const names = new Set();
|
|
65
|
+
for (const file of manifest.files) {
|
|
66
|
+
assertExactKeys(file, ["role", "name", "sha256", "sizeBytes", "executable", "signing"], "add-on file");
|
|
67
|
+
if (!ROLES.has(file.role) || roles.has(file.role) || !SAFE_NAME.test(file.name) || names.has(file.name)) throw new Error("duplicate or unsafe add-on manifest file");
|
|
68
|
+
if (!/^[0-9a-f]{64}$/i.test(file.sha256) || !Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0 || typeof file.executable !== "boolean") throw new Error("invalid add-on file integrity");
|
|
69
|
+
if (file.executable && (!file.signing || file.signing.status !== "verified")) throw new Error(`unsigned executable role: ${file.role}`);
|
|
70
|
+
roles.add(file.role); names.add(file.name);
|
|
71
|
+
}
|
|
72
|
+
for (const role of ["command", "service", "icon", ...LEGAL_ROLES]) if (!roles.has(role)) throw new Error(`missing required add-on role: ${role}`);
|
|
73
|
+
return manifest;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function canonicalAddonManifest(manifest) { validateAddonManifest(manifest); return `${JSON.stringify(manifest)}\n`; }
|
|
77
|
+
export function addonManifestSha256(manifest) { return createHash("sha256").update(canonicalAddonManifest(manifest)).digest("hex"); }
|
|
78
|
+
export function addonRoutes(manifest) {
|
|
79
|
+
const digest = addonManifestSha256(manifest);
|
|
80
|
+
const root = `${manifest.addon}/addons/${manifest.platform}/sha256/${digest}`;
|
|
81
|
+
return [...manifest.files.map((file) => ({ name: file.name, key: `${root}/${file.name}`, sha256: file.sha256, manifest: false })), { name: "addon-manifest.json", key: `${root}/addon-manifest.json`, sha256: digest, manifest: true }];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function immutableAddonManifestUrl(manifest, manifestUrl, digest = addonManifestSha256(manifest)) {
|
|
85
|
+
if (/[?#]/.test(manifestUrl)) return false;
|
|
86
|
+
const r2Suffix = `/${manifest.addon}/addons/${manifest.platform}/sha256/${digest}/addon-manifest.json`;
|
|
87
|
+
const githubTag = `addon-${manifest.addon}-v${manifest.version}-${manifest.platform}-sha256-${digest}`;
|
|
88
|
+
const githubSuffix = `/releases/download/${githubTag}/addon-manifest.json`;
|
|
89
|
+
return manifestUrl.endsWith(r2Suffix) || (/^https:\/\/github\.com\//.test(manifestUrl) && manifestUrl.endsWith(githubSuffix));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function downloadImmutableAddOn(manifestUrl, destination, { fetchImpl = fetch } = {}) {
|
|
93
|
+
if (!/^https:\/\//.test(manifestUrl) || !manifestUrl.endsWith("/addon-manifest.json") || /[?#]/.test(manifestUrl)) throw new Error("add-on lock manifestUrl must be an immutable HTTPS manifest URL");
|
|
94
|
+
const download = async (url, file) => {
|
|
95
|
+
const response = await fetchImpl(url, { cache: "no-store" });
|
|
96
|
+
if (!response.ok) throw new Error(`immutable add-on download failed: ${response.status} ${url}`);
|
|
97
|
+
writeFileSync(file, Buffer.from(await response.arrayBuffer()));
|
|
98
|
+
};
|
|
99
|
+
await download(manifestUrl, path.join(destination, "addon-manifest.json"));
|
|
100
|
+
const manifest = JSON.parse(readFileSync(path.join(destination, "addon-manifest.json"), "utf8"));
|
|
101
|
+
validateAddonManifest(manifest);
|
|
102
|
+
const base = manifestUrl.slice(0, -"addon-manifest.json".length);
|
|
103
|
+
for (const file of manifest.files) await download(`${base}${encodeURIComponent(file.name)}`, path.join(destination, file.name));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateBuildInputs(inputs) {
|
|
107
|
+
if (!Array.isArray(inputs?.include) || !inputs.include.length || inputs.include.some((entry) => typeof entry !== "string" || path.isAbsolute(entry) || entry.split(/[\\/]/).includes(".."))) throw new Error("add-on buildInputs.include must be safe and non-empty");
|
|
108
|
+
}
|
|
109
|
+
function sha256(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); }
|
|
110
|
+
function assertExactKeys(value, keys, label) { if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join("\0") !== [...keys].sort().join("\0")) throw new Error(`${label} has unsupported fields`); }
|
|
@@ -0,0 +1,48 @@
|
|
|
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/crypt-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", "crypt-service", "bin/crypt-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: "orthic-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
|
+
});
|
package/build-release.mjs
CHANGED
|
@@ -99,6 +99,23 @@ let cacheLease;
|
|
|
99
99
|
let heavySlot;
|
|
100
100
|
let interrupted;
|
|
101
101
|
|
|
102
|
+
// A dry-run is an inspection contract, not a partial build. It must finish
|
|
103
|
+
// before locks, cache directories, receipts, dependency installation, or any
|
|
104
|
+
// child build/sign process can exist.
|
|
105
|
+
if (dryRun) {
|
|
106
|
+
const commit = git(invocationRoot, ["rev-parse", "HEAD"]);
|
|
107
|
+
const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
|
|
108
|
+
if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
|
|
109
|
+
const config = (await import(`${pathToFileURL(configPath).href}?dry-run=${commit}`)).default;
|
|
110
|
+
if (!config?.app || !config?.version) fail("release config must expose app and version");
|
|
111
|
+
const target = config.targets?.[platform];
|
|
112
|
+
if (!target?.package) fail(`${config.app} has no ${platform} package command`);
|
|
113
|
+
console.log(`right-release build dry-run: ${config.app} ${config.version} ${platform}`);
|
|
114
|
+
console.log(`config: ${configPath}`);
|
|
115
|
+
console.log(`commit: ${commit}`);
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
103
120
|
process.once(signal, () => {
|
|
104
121
|
if (child?.pid) killTree(child.pid);
|
package/build-release.test.mjs
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
6
|
import test from "node:test";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
|
|
7
9
|
const source = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs"), "utf8");
|
|
10
|
+
const build = path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs");
|
|
11
|
+
|
|
12
|
+
test("general build dry-run is filesystem-pure and never starts its package command", () => {
|
|
13
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "right-build-dry-run-"));
|
|
14
|
+
writeFileSync(path.join(root, "right-release.config.mjs"), "export default { app: 'fixture', version: '1.0.0', targets: { mac: { package: { cmd: 'false' } } } };\n");
|
|
15
|
+
for (const args of [["init", "--initial-branch", "main"], ["config", "user.email", "test@example.com"], ["config", "user.name", "Test"], ["add", "."], ["commit", "-m", "init"]]) execFileSync("git", args, { cwd: root });
|
|
16
|
+
const before = readdirSync(root).sort();
|
|
17
|
+
const result = spawnSync(process.execPath, [build, "--config", "right-release.config.mjs", "--platform", "mac", "--dry-run"], { cwd: root, encoding: "utf8" });
|
|
18
|
+
assert.equal(result.status, 0, result.stderr);
|
|
19
|
+
assert.match(result.stdout, /build dry-run/);
|
|
20
|
+
assert.deepEqual(readdirSync(root).sort(), before);
|
|
21
|
+
});
|
|
8
22
|
|
|
9
23
|
test("macOS and Windows builds default to Cache V2 while explicit legacy mode remains available", () => {
|
|
10
24
|
assert.match(source, /assertCleanSource/);
|
package/cargo-contract.mjs
CHANGED
|
@@ -85,6 +85,15 @@ function readCargoManifestDependencies(manifestPath, cargoHome, label) {
|
|
|
85
85
|
windowsHide: true,
|
|
86
86
|
},
|
|
87
87
|
);
|
|
88
|
+
if (result.error) {
|
|
89
|
+
// spawnSync failed to launch the process at all (no exit code, no stderr) -
|
|
90
|
+
// a missing/unspawnable binary, not cargo rejecting the manifest. Reporting
|
|
91
|
+
// this as a version-contract violation sent a real diagnosis (a Windows
|
|
92
|
+
// rustup-proxy binary that can't be spawned from a non-console session)
|
|
93
|
+
// down the wrong path: it read as "fix your Cargo.toml" instead of "cargo
|
|
94
|
+
// could not be launched here".
|
|
95
|
+
throw new Error(`${label} could not launch ${cargoExecutable()} to read Cargo metadata: ${result.error.message}`);
|
|
96
|
+
}
|
|
88
97
|
if (result.status !== 0) {
|
|
89
98
|
throw new Error(`${label} Cargo metadata rejected manifest; RightKit dependencies must use exact crates.io versions: ${String(result.stderr ?? "").trim()}`);
|
|
90
99
|
}
|
package/cli/right-release.mjs
CHANGED
|
@@ -61,6 +61,8 @@ if (first === "--version" || first === "-v") {
|
|
|
61
61
|
process.exit(2);
|
|
62
62
|
}
|
|
63
63
|
run("model-promote.mjs", rest.slice(1));
|
|
64
|
+
} else if (first === "addon") {
|
|
65
|
+
run("addon-command.mjs", args.slice(1));
|
|
64
66
|
} else if (first === "lsclean") {
|
|
65
67
|
runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
|
|
66
68
|
} else if (first === "generate-dmg-background") {
|
|
@@ -133,6 +135,7 @@ Commands:
|
|
|
133
135
|
publish swift [--scope <s>] [--url <u>] [--dry-run] Test, archive, scan, and publish RightKitSwift to a Swift registry
|
|
134
136
|
model promote --authority heardright --config <file> [--dry-run]
|
|
135
137
|
Sign and promote one runtime/model artifact
|
|
138
|
+
addon doctor|build|upload|adopt <options> Manage a signed headless add-on
|
|
136
139
|
doctor [--platform mac|win] Inspect one app's release config
|
|
137
140
|
doctor --all Verify all Right Suite app release contracts
|
|
138
141
|
suite-doctor Verify all local Right Suite repositories
|
package/github-release.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { verifySealedRelease } from "./release-state.mjs";
|
|
8
|
+
import { addonManifestSha256, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
|
|
8
9
|
|
|
9
10
|
const REPO_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
10
11
|
const RELEASE_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
|
|
@@ -37,24 +38,69 @@ export function prepareGitHubRelease({ repoRoot, releaseId, platform, repo, stat
|
|
|
37
38
|
`Build commit: \`${sealed.manifest.commit}\``,
|
|
38
39
|
`SHA-256: \`${installers[0].sha256}\``,
|
|
39
40
|
].join("\n"),
|
|
41
|
+
notesFile: path.join(releaseState, "release-notes.md"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function prepareGitHubAddonRelease({ repoRoot, config, configRoot = repoRoot, platform, repo }) {
|
|
46
|
+
if (platform !== "mac" && platform !== "win") throw new Error("platform must be mac or win");
|
|
47
|
+
if (!REPO_RE.test(repo)) throw new Error(`invalid GitHub repository: ${repo}`);
|
|
48
|
+
validateAddonConfig(config, { root: configRoot, platform, allowMissingExecutables: true });
|
|
49
|
+
const commit = runCommand("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
|
|
50
|
+
const sealedDir = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), platform);
|
|
51
|
+
const manifestPath = path.join(sealedDir, "addon-manifest.json");
|
|
52
|
+
if (!existsSync(manifestPath)) throw new Error(`sealed add-on manifest not found: ${manifestPath}`);
|
|
53
|
+
const manifest = validateAddonManifest(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
54
|
+
if (manifest.addon !== config.addon || manifest.version !== config.version || manifest.platform !== platform || manifest.commit !== commit) throw new Error("sealed add-on identity mismatch");
|
|
55
|
+
const assets = manifest.files.map((file) => {
|
|
56
|
+
const asset = path.join(sealedDir, file.name);
|
|
57
|
+
if (!existsSync(asset) || statSync(asset).size !== file.sizeBytes || sha256(asset) !== file.sha256) throw new Error(`sealed add-on file mismatch: ${file.name}`);
|
|
58
|
+
return asset;
|
|
59
|
+
});
|
|
60
|
+
const digest = addonManifestSha256(manifest);
|
|
61
|
+
const tag = `addon-${manifest.addon}-v${manifest.version}-${platform}-sha256-${digest}`;
|
|
62
|
+
const stateDir = path.join(repoRoot, ".right-release", "state", "addons", manifest.addon, manifest.version, commit.slice(0, 8), platform, "github");
|
|
63
|
+
mkdirSync(stateDir, { recursive: true });
|
|
64
|
+
return {
|
|
65
|
+
kind: "addon", manifest, manifestPath, sealedDir, assets: [...assets, manifestPath], tag,
|
|
66
|
+
title: `${manifest.addon} ${manifest.version} ${platform} add-on`,
|
|
67
|
+
notes: [`Signed ${platform === "mac" ? "macOS" : "Windows"} add-on component set.`, "", `Build commit: \`${manifest.commit}\``, `Manifest SHA-256: \`${digest}\``].join("\n"),
|
|
68
|
+
notesFile: path.join(stateDir, "release-notes.md"),
|
|
69
|
+
manifestUrl: `https://github.com/${repo}/releases/download/${tag}/addon-manifest.json`,
|
|
40
70
|
};
|
|
41
71
|
}
|
|
42
72
|
|
|
43
73
|
export function publishGitHubRelease(plan, { repo, dryRun = false, run = runCommand } = {}) {
|
|
44
74
|
const visibility = JSON.parse(run("gh", ["repo", "view", repo, "--json", "visibility"]));
|
|
45
75
|
if (visibility.visibility !== "PUBLIC") throw new Error(`GitHub releases require a public repository: ${repo}`);
|
|
46
|
-
|
|
76
|
+
if (plan.kind === "addon") verifyAddonTrust(plan);
|
|
77
|
+
else verifyPlatformTrust(plan);
|
|
47
78
|
const existing = run("gh", ["release", "view", plan.tag, "--repo", repo, "--json", "tagName"], { allowFailure: true });
|
|
48
|
-
if (dryRun) return { status: existing.ok ? "would-update" : "would-create", tag: plan.tag, assets: plan.assets };
|
|
79
|
+
if (dryRun) return { status: existing.ok ? plan.kind === "addon" ? "would-verify" : "would-update" : "would-create", tag: plan.tag, assets: plan.assets };
|
|
80
|
+
if (plan.kind === "addon" && existing.ok) {
|
|
81
|
+
for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
|
|
82
|
+
return { status: "already-verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
|
|
83
|
+
}
|
|
49
84
|
if (!existing.ok) {
|
|
50
|
-
const notesFile =
|
|
85
|
+
const notesFile = plan.notesFile;
|
|
51
86
|
writeFileSync(notesFile, `${plan.notes}\n`);
|
|
52
87
|
run("gh", ["release", "create", plan.tag, "--repo", repo, "--title", plan.title, "--notes-file", notesFile]);
|
|
53
88
|
rmSync(notesFile, { force: true });
|
|
54
89
|
}
|
|
55
90
|
run("gh", ["release", "upload", plan.tag, ...plan.assets, "--repo", repo, "--clobber"]);
|
|
56
91
|
for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
|
|
57
|
-
return { status: "verified", tag: plan.tag, assets: plan.assets };
|
|
92
|
+
return { status: "verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function verifyAddonTrust(plan) {
|
|
96
|
+
for (const file of plan.manifest.files.filter((entry) => entry.executable)) {
|
|
97
|
+
const executable = path.join(plan.sealedDir, file.name);
|
|
98
|
+
if (plan.manifest.platform === "mac") {
|
|
99
|
+
const output = runCombined("codesign", ["-dv", "--verbose=4", executable]);
|
|
100
|
+
if (!/TeamIdentifier=6KLGD3LLKF/.test(output)) throw new Error(`codesign team mismatch: ${file.name}`);
|
|
101
|
+
runCommand("codesign", ["--verify", "--strict", "--verbose=2", executable]);
|
|
102
|
+
} else runCommand(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), "sign-windows.mjs"), "--verify-only", executable]);
|
|
103
|
+
}
|
|
58
104
|
}
|
|
59
105
|
|
|
60
106
|
function verifyPlatformTrust(plan) {
|
|
@@ -77,8 +123,8 @@ function sha256(file) {
|
|
|
77
123
|
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
78
124
|
}
|
|
79
125
|
|
|
80
|
-
function runCommand(command, args, { allowFailure = false } = {}) {
|
|
81
|
-
const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
|
|
126
|
+
function runCommand(command, args, { allowFailure = false, cwd } = {}) {
|
|
127
|
+
const result = spawnSync(command, args, { cwd, encoding: "utf8", windowsHide: true });
|
|
82
128
|
const output = result.stdout?.trim() ?? "";
|
|
83
129
|
if (result.status !== 0) {
|
|
84
130
|
if (allowFailure) return { ok: false, output, error: result.stderr?.trim() ?? "" };
|
|
@@ -87,25 +133,43 @@ function runCommand(command, args, { allowFailure = false } = {}) {
|
|
|
87
133
|
return allowFailure ? { ok: true, output } : output;
|
|
88
134
|
}
|
|
89
135
|
|
|
136
|
+
function runCombined(command, args) {
|
|
137
|
+
const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
|
|
138
|
+
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
|
|
139
|
+
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
90
142
|
function usage(code) {
|
|
91
|
-
console.log("usage: right-release github --release <sealed-id> --platform mac|win --repo owner/repo [--dry-run]");
|
|
143
|
+
console.log("usage: right-release github (--release <sealed-id> | --addon <config>) --platform mac|win [--repo owner/repo] [--dry-run]");
|
|
92
144
|
process.exit(code);
|
|
93
145
|
}
|
|
94
146
|
|
|
95
147
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
96
148
|
const args = process.argv.slice(2);
|
|
97
|
-
const options = { platform: process.platform === "win32" ? "win" : "mac", releaseId: "", repo: "", dryRun: false };
|
|
149
|
+
const options = { platform: process.platform === "win32" ? "win" : "mac", releaseId: "", addon: "", repo: "", dryRun: false };
|
|
98
150
|
for (let index = 0; index < args.length; index += 1) {
|
|
99
151
|
if (args[index] === "--platform") options.platform = args[++index];
|
|
100
152
|
else if (args[index] === "--release") options.releaseId = args[++index];
|
|
153
|
+
else if (args[index] === "--addon") options.addon = args[++index];
|
|
101
154
|
else if (args[index] === "--repo") options.repo = args[++index];
|
|
102
155
|
else if (args[index] === "--dry-run") options.dryRun = true;
|
|
103
156
|
else if (args[index] === "-h" || args[index] === "--help") usage(0);
|
|
104
157
|
else throw new Error(`unknown argument: ${args[index]}`);
|
|
105
158
|
}
|
|
106
|
-
if (
|
|
159
|
+
if (Boolean(options.releaseId) === Boolean(options.addon)) usage(2);
|
|
107
160
|
const repoRoot = runCommand("git", ["rev-parse", "--show-toplevel"]);
|
|
108
|
-
|
|
161
|
+
let plan;
|
|
162
|
+
if (options.addon) {
|
|
163
|
+
const configPath = path.resolve(options.addon);
|
|
164
|
+
const config = (await import(`${pathToFileURL(configPath).href}?github=${Date.now()}`)).default;
|
|
165
|
+
options.repo ||= config.distribution?.repository ?? "";
|
|
166
|
+
if (config.distribution?.provider !== "github-releases") throw new Error("add-on config must select github-releases distribution");
|
|
167
|
+
plan = prepareGitHubAddonRelease({ repoRoot, config, configRoot: path.dirname(configPath), ...options });
|
|
168
|
+
} else {
|
|
169
|
+
if (!options.repo) usage(2);
|
|
170
|
+
plan = prepareGitHubRelease({ repoRoot, ...options });
|
|
171
|
+
}
|
|
109
172
|
const result = publishGitHubRelease(plan, options);
|
|
110
173
|
console.log(`right-release github: ${result.status} ${result.tag}`);
|
|
174
|
+
if (result.manifestUrl) console.log(`manifestUrl: ${result.manifestUrl}`);
|
|
111
175
|
}
|
package/github-release.test.mjs
CHANGED
|
@@ -4,7 +4,9 @@ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
|
-
import {
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { canonicalAddonManifest, createAddonManifest } from "./addon-contract.mjs";
|
|
9
|
+
import { prepareGitHubAddonRelease, prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
|
|
8
10
|
|
|
9
11
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
10
12
|
|
|
@@ -22,6 +24,24 @@ function fixture() {
|
|
|
22
24
|
return { root, releaseId };
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
function addonFixture() {
|
|
28
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "right-release-github-addon-"));
|
|
29
|
+
mkdirSync(path.join(root, "out"));
|
|
30
|
+
for (const [name, bytes] of [["membrane", "command"], ["crypt-service", "service"], ["icon.png", "icon"], ["LICENSE", "license"], ["EULA.txt", "eula"], ["PRIVACY.md", "privacy"], ["THIRD-PARTY-NOTICES.txt", "notices"]]) writeFileSync(path.join(root, "out", name), bytes);
|
|
31
|
+
execFileSync("git", ["init", "--initial-branch", "main"], { cwd: root });
|
|
32
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: root });
|
|
33
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: root });
|
|
34
|
+
writeFileSync(path.join(root, "tracked"), "source"); execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-m", "source"], { cwd: root });
|
|
35
|
+
const files = [["command", "membrane", true], ["service", "crypt-service", true], ["icon", "icon.png", false], ["license", "LICENSE", false], ["eula", "EULA.txt", false], ["privacy", "PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", false]].map(([role, name, executable]) => ({ role, name, source: `out/${name}`, executable }));
|
|
36
|
+
const config = { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", distribution: { provider: "github-releases", repository: "Orthic-Labs/Membrane" }, checks: [], buildInputs: { include: ["tracked"] }, consumer: { contract: "orthic-product-v1" }, targets: { win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "false", args: [] }, signing: { contract: "azure-artifact-signing-v1" }, files } } };
|
|
37
|
+
const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
|
|
38
|
+
const manifest = createAddonManifest({ config, root, platform: "win", commit, signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
|
|
39
|
+
const sealed = path.join(root, ".right-release", "addons", "membrane", "0.1.0", commit.slice(0, 8), "win"); mkdirSync(sealed, { recursive: true });
|
|
40
|
+
for (const file of manifest.files) writeFileSync(path.join(sealed, file.name), readFileSync(path.join(root, "out", file.name)));
|
|
41
|
+
writeFileSync(path.join(sealed, "addon-manifest.json"), canonicalAddonManifest(manifest));
|
|
42
|
+
return { root, config };
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
test("GitHub plan derives tag, notes, installer, manifest & checksums only from sealed bytes", () => {
|
|
26
46
|
const fx = fixture();
|
|
27
47
|
const plan = prepareGitHubRelease({ repoRoot: fx.root, releaseId: fx.releaseId, platform: "win", repo: "Orthic-Labs/CutRight" });
|
|
@@ -44,3 +64,26 @@ test("dry-run checks public visibility without creating or uploading a release",
|
|
|
44
64
|
assert.equal(publishGitHubRelease(plan, { repo: "Orthic-Labs/CutRight", dryRun: true, run }).status, "would-create");
|
|
45
65
|
assert.equal(calls.length, 2);
|
|
46
66
|
});
|
|
67
|
+
|
|
68
|
+
test("GitHub add-on plan uses a content-addressed platform tag and manifest URL", () => {
|
|
69
|
+
const fx = addonFixture();
|
|
70
|
+
const plan = prepareGitHubAddonRelease({ repoRoot: fx.root, config: fx.config, platform: "win", repo: "Orthic-Labs/Membrane" });
|
|
71
|
+
assert.match(plan.tag, /^addon-membrane-v0\.1\.0-win-sha256-[0-9a-f]{64}$/);
|
|
72
|
+
assert.equal(plan.assets.at(-1), plan.manifestPath);
|
|
73
|
+
assert.equal(plan.manifestUrl, `https://github.com/Orthic-Labs/Membrane/releases/download/${plan.tag}/addon-manifest.json`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("existing GitHub add-on release is verified without upload mutation", () => {
|
|
77
|
+
const asset = path.join(mkdtempSync(path.join(os.tmpdir(), "right-release-existing-")), "addon-manifest.json"); writeFileSync(asset, "sealed");
|
|
78
|
+
const calls = [];
|
|
79
|
+
const run = (_command, args, options = {}) => {
|
|
80
|
+
calls.push(args);
|
|
81
|
+
if (args[0] === "repo") return JSON.stringify({ visibility: "PUBLIC" });
|
|
82
|
+
if (args[0] === "release" && args[1] === "view") return options.allowFailure ? { ok: true } : "";
|
|
83
|
+
if (args[0] === "release" && args[1] === "download") { mkdirSync(args[args.indexOf("--dir") + 1], { recursive: true }); writeFileSync(path.join(args[args.indexOf("--dir") + 1], path.basename(asset)), "sealed"); return ""; }
|
|
84
|
+
throw new Error(`unexpected mutation: ${args.join(" ")}`);
|
|
85
|
+
};
|
|
86
|
+
const result = publishGitHubRelease({ kind: "addon", manifest: { files: [] }, sealedDir: path.dirname(asset), tag: "immutable", assets: [asset], manifestUrl: "https://example.test/manifest" }, { repo: "Orthic-Labs/Membrane", run });
|
|
87
|
+
assert.equal(result.status, "already-verified");
|
|
88
|
+
assert.equal(calls.some((args) => args[0] === "release" && args[1] === "upload"), false);
|
|
89
|
+
});
|
package/package.json
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"right-release": "cli/right-release.mjs"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"cli",
|
|
11
|
-
"*.mjs",
|
|
12
|
-
"*.json",
|
|
13
|
-
"*.sh",
|
|
14
|
-
"*.py"
|
|
15
|
-
],
|
|
16
|
-
"sideEffects": false,
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@rightkit/release",
|
|
3
|
+
"version": "0.2.59",
|
|
4
|
+
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"right-release": "cli/right-release.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli",
|
|
11
|
+
"*.mjs",
|
|
12
|
+
"*.json",
|
|
13
|
+
"*.sh",
|
|
14
|
+
"*.py"
|
|
15
|
+
],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"registry": "https://registry.npmjs.org/",
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/adrdsouza/claude.git",
|
|
24
|
+
"directory": "tools/rightkit/packages/release"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test *.test.mjs",
|
|
28
|
+
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
29
|
+
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/release-state.mjs
CHANGED
|
@@ -93,6 +93,7 @@ export function verifySealedRelease(sealedDir) {
|
|
|
93
93
|
if (!existsSync(manifestPath)) throw new Error(`sealed manifest missing: ${manifestPath}`);
|
|
94
94
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
95
95
|
if (manifest.schema !== 1) throw new Error(`unsupported sealed manifest schema: ${manifest.schema}`);
|
|
96
|
+
if (manifest.kind != null && typeof manifest.kind !== "string") throw new Error("sealed manifest kind must be a string when present");
|
|
96
97
|
if (!manifest.checkpoints?.includes("sealed")) throw new Error("release is not sealed");
|
|
97
98
|
for (const item of manifest.files ?? []) {
|
|
98
99
|
const file = path.join(sealedDir, item.name);
|
|
@@ -464,7 +464,7 @@ test("RightKit exposes one current version manifest", () => {
|
|
|
464
464
|
"@rightkit/legal": "0.3.0",
|
|
465
465
|
"@rightkit/legal-ui": "0.1.1",
|
|
466
466
|
"@rightkit/license": "0.1.6",
|
|
467
|
-
"@rightkit/release": "0.2.
|
|
467
|
+
"@rightkit/release": "0.2.59",
|
|
468
468
|
"@rightkit/qa": "0.2.0",
|
|
469
469
|
});
|
|
470
470
|
assert.deepEqual(versions.legacyNpm, {
|
package/rightkit-versions.json
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema": 2,
|
|
3
|
-
"packageManager": "pnpm@11.18.0",
|
|
4
|
-
"npm": {
|
|
5
|
-
"@rightkit/git": "0.2.0",
|
|
6
|
-
"@rightkit/legal": "0.2.0",
|
|
7
|
-
"@rightkit/license": "0.1.5",
|
|
8
|
-
"@rightkit/logs": "0.1.3",
|
|
9
|
-
"@rightkit/platform-ui": "0.1.0",
|
|
10
|
-
"@rightkit/qa": "0.1.0",
|
|
11
|
-
"@rightkit/release": "0.2.47",
|
|
12
|
-
"@rightkit/tauri": "0.1.0",
|
|
13
|
-
"@rightkit/updates": "0.2.3"
|
|
14
|
-
},
|
|
15
|
-
"stagedNpm": {
|
|
16
|
-
"@rightkit/ax": "0.2.0",
|
|
17
|
-
"@rightkit/git": "0.2.0",
|
|
18
|
-
"@rightkit/legal": "0.3.0",
|
|
19
|
-
"@rightkit/legal-ui": "0.1.1",
|
|
20
|
-
"@rightkit/license": "0.1.6",
|
|
21
|
-
"@rightkit/release": "0.2.
|
|
22
|
-
"@rightkit/qa": "0.2.0"
|
|
23
|
-
},
|
|
24
|
-
"legacyNpm": {
|
|
25
|
-
"@rightkit/legal-ui": [
|
|
26
|
-
"0.1.0"
|
|
27
|
-
],
|
|
28
|
-
"@rightkit/release": [
|
|
29
|
-
"0.2.22",
|
|
30
|
-
"0.2.29",
|
|
31
|
-
"0.2.30",
|
|
32
|
-
"0.2.31",
|
|
33
|
-
"0.2.41",
|
|
34
|
-
"0.2.42",
|
|
35
|
-
"0.2.43",
|
|
36
|
-
"0.2.44",
|
|
37
|
-
"0.2.45",
|
|
38
|
-
"0.2.46",
|
|
39
|
-
"0.2.49",
|
|
40
|
-
"0.2.50",
|
|
41
|
-
"0.2.51",
|
|
42
|
-
"0.2.53",
|
|
43
|
-
"0.2.54",
|
|
44
|
-
"0.2.55",
|
|
45
|
-
"0.2.56"
|
|
46
|
-
],
|
|
47
|
-
"@rightkit/qa": [
|
|
48
|
-
"0.1.0"
|
|
49
|
-
]
|
|
50
|
-
},
|
|
51
|
-
"cargo": {
|
|
52
|
-
"rightkit-license": "0.1.2",
|
|
53
|
-
"rightkit-logs": "0.1.0",
|
|
54
|
-
"rightkit-process": "0.1.0",
|
|
55
|
-
"rightkit-tauri": "0.1.0"
|
|
56
|
-
},
|
|
57
|
-
"stagedCargo": {
|
|
58
|
-
"rightkit-license": "0.1.3",
|
|
59
|
-
"rightkit-tauri": "0.1.1"
|
|
60
|
-
},
|
|
61
|
-
"swift": {
|
|
62
|
-
"rightkit-swift": {
|
|
63
|
-
"url": "https://github.com/bogusyogi/rightkit-swift.git",
|
|
64
|
-
"version": "0.1.0"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"schema": 2,
|
|
3
|
+
"packageManager": "pnpm@11.18.0",
|
|
4
|
+
"npm": {
|
|
5
|
+
"@rightkit/git": "0.2.0",
|
|
6
|
+
"@rightkit/legal": "0.2.0",
|
|
7
|
+
"@rightkit/license": "0.1.5",
|
|
8
|
+
"@rightkit/logs": "0.1.3",
|
|
9
|
+
"@rightkit/platform-ui": "0.1.0",
|
|
10
|
+
"@rightkit/qa": "0.1.0",
|
|
11
|
+
"@rightkit/release": "0.2.47",
|
|
12
|
+
"@rightkit/tauri": "0.1.0",
|
|
13
|
+
"@rightkit/updates": "0.2.3"
|
|
14
|
+
},
|
|
15
|
+
"stagedNpm": {
|
|
16
|
+
"@rightkit/ax": "0.2.0",
|
|
17
|
+
"@rightkit/git": "0.2.0",
|
|
18
|
+
"@rightkit/legal": "0.3.0",
|
|
19
|
+
"@rightkit/legal-ui": "0.1.1",
|
|
20
|
+
"@rightkit/license": "0.1.6",
|
|
21
|
+
"@rightkit/release": "0.2.59",
|
|
22
|
+
"@rightkit/qa": "0.2.0"
|
|
23
|
+
},
|
|
24
|
+
"legacyNpm": {
|
|
25
|
+
"@rightkit/legal-ui": [
|
|
26
|
+
"0.1.0"
|
|
27
|
+
],
|
|
28
|
+
"@rightkit/release": [
|
|
29
|
+
"0.2.22",
|
|
30
|
+
"0.2.29",
|
|
31
|
+
"0.2.30",
|
|
32
|
+
"0.2.31",
|
|
33
|
+
"0.2.41",
|
|
34
|
+
"0.2.42",
|
|
35
|
+
"0.2.43",
|
|
36
|
+
"0.2.44",
|
|
37
|
+
"0.2.45",
|
|
38
|
+
"0.2.46",
|
|
39
|
+
"0.2.49",
|
|
40
|
+
"0.2.50",
|
|
41
|
+
"0.2.51",
|
|
42
|
+
"0.2.53",
|
|
43
|
+
"0.2.54",
|
|
44
|
+
"0.2.55",
|
|
45
|
+
"0.2.56"
|
|
46
|
+
],
|
|
47
|
+
"@rightkit/qa": [
|
|
48
|
+
"0.1.0"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"cargo": {
|
|
52
|
+
"rightkit-license": "0.1.2",
|
|
53
|
+
"rightkit-logs": "0.1.0",
|
|
54
|
+
"rightkit-process": "0.1.0",
|
|
55
|
+
"rightkit-tauri": "0.1.0"
|
|
56
|
+
},
|
|
57
|
+
"stagedCargo": {
|
|
58
|
+
"rightkit-license": "0.1.3",
|
|
59
|
+
"rightkit-tauri": "0.1.1"
|
|
60
|
+
},
|
|
61
|
+
"swift": {
|
|
62
|
+
"rightkit-swift": {
|
|
63
|
+
"url": "https://github.com/bogusyogi/rightkit-swift.git",
|
|
64
|
+
"version": "0.1.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-10T04:42:36.444Z",
|
|
4
|
-
"workRoot": "C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR",
|
|
5
|
-
"apps": [
|
|
6
|
-
{
|
|
7
|
-
"key": "viewright",
|
|
8
|
-
"remote": "https://github.com/bogusyogi/viewright.git",
|
|
9
|
-
"appDir": ".",
|
|
10
|
-
"revision": "21a4171fa8add2d8114bc5f07498b60d7e8eafe5",
|
|
11
|
-
"packageManager": "pnpm@11.18.0",
|
|
12
|
-
"clone": {
|
|
13
|
-
"command": "git clone --depth 1 --single-branch https://github.com/bogusyogi/viewright.git C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright",
|
|
14
|
-
"status": 0,
|
|
15
|
-
"stdout": "",
|
|
16
|
-
"stderr": "Cloning into 'C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright'...\nUpdating files: 91% (1902/2080)\rUpdating files: 92% (1914/2080)\rUpdating files: 93% (1935/2080)\rUpdating files: 94% (1956/2080)\rUpdating files: 95% (1976/2080)\rUpdating files: 96% (1997/2080)\rUpdating files: 97% (2018/2080)\rUpdating files: 98% (2039/2080)\rUpdating files: 99% (2060/2080)\rUpdating files: 100% (2080/2080)\rUpdating files: 100% (2080/2080), done."
|
|
17
|
-
},
|
|
18
|
-
"install": {
|
|
19
|
-
"command": "C:\\nvm4w\\nodejs\\node.exe C:\\nvm4w\\nodejs\\node_modules\\pnpm\\bin\\pnpm.mjs install --frozen-lockfile",
|
|
20
|
-
"status": 0,
|
|
21
|
-
"stdout": "✓ Lockfile passes supply-chain policies (verified 8h ago)\nLockfile is up to date, resolution step is skipped\nProgress: resolved 1, reused 0, downloaded 0, added 0\nPackages: +619\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 619, reused 0, downloaded 0, added 0\nProgress: resolved 619, reused 60, downloaded 0, added 0\nPackages are hard linked from the content-addressable store to the virtual store.\n Content-addressable store is at: C:\\Users\\adrds\\AppData\\Local\\pnpm\\store\\v11\n Virtual store is at: node_modules/.pnpm\nProgress: resolved 619, reused 573, downloaded 7, added 7\nProgress: resolved 619, reused 574, downloaded 9, added 10\nProgress: resolved 619, reused 574, downloaded 22, added 11\nProgress: resolved 619, reused 574, downloaded 23, added 11\nProgress: resolved 619, reused 574, downloaded 26, added 14\nProgress: resolved 619, reused 574, downloaded 30, added 24\nProgress: resolved 619, reused 574, downloaded 31, added 57\nProgress: resolved 619, reused 574, downloaded 34, added 128\nProgress: resolved 619, reused 574, downloaded 38, added 135\nProgress: resolved 619, reused 574, downloaded 38, added 136\nProgress: resolved 619, reused 574, downloaded 39, added 141\nProgress: resolved 619, reused 574, downloaded 39, added 148\nProgress: resolved 619, reused 574, downloaded 39, added 182\nProgress: resolved 619, reused 574, downloaded 40, added 201\nProgress: resolved 619, reused 574, downloaded 41, added 218\nProgress: resolved 619, reused 574, downloaded 41, added 242\nProgress: resolved 619, reused 574, downloaded 42, added 312\nProgress: resolved 619, reused 574, downloaded 42, added 371\nProgress: resolved 619, reused 574, downloaded 42, added 430\nProgress: resolved 619, reused 574, downloaded 42, added 486\nProgress: resolved 619, reused 574, downloaded 43, added 543\nProgress: resolved 619, reused 574, downloaded 43, added 578\nProgress: resolved 619, reused 574, downloaded 44, added 596\nProgress: resolved 619, reused 574, downloaded 44, added 611\nProgress: resolved 619, reused 574, downloaded 44, added 612\nProgress: resolved 619, reused 574, downloaded 44, added 613\nProgress: resolved 619, reused 574, downloaded 44, added 615\nProgress: resolved 619, reused 574, downloaded 44, added 616\nProgress: resolved 619, reused 574, downloaded 44, added 617\nProgress: resolved 619, reused 574, downloaded 44, added 618\nProgress: resolved 619, reused 574, downloaded 45, added 618\nProgress: resolved 619, reused 574, downloaded 45, added 619\nProgress: resolved 619, reused 574, downloaded 45, added 619, done\n\ndependencies:\n+ @codemirror/autocomplete 6.20.3\n+ @codemirror/commands 6.10.4\n+ @codemirror/lang-html 6.4.11\n+ @codemirror/lang-javascript 6.2.5\n+ @codemirror/lang-markdown 6.5.0\n+ @codemirror/language 6.12.4\n+ @codemirror/lint 6.9.7\n+ @codemirror/search 6.7.1\n+ @codemirror/state 6.7.1\n+ @codemirror/view 6.43.6\n+ @eigenpal/docx-editor-agents @eigenpal/docx-editor-agents@file:vendor/docx-editor/packages/agents(react@19.2.7)\n+ @eigenpal/docx-editor-core @eigenpal/docx-editor-core@file:vendor/docx-editor/packages/core(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.10)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.42.0)\n+ @eigenpal/docx-editor-i18n @eigenpal/docx-editor-i18n@file:vendor/docx-editor/packages/i18n\n+ @eigenpal/docx-editor-react @eigenpal/docx-editor-react@file:vendor/docx-editor/packages/react(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.10)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.42.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)\n+ @lezer/highlight 1.2.3\n+ @mdx-js/mdx 3.1.1\n+ @phosphor-icons/react 2.1.10\n+ @radix-ui/react-select 2.3.2\n+ @rightkit/legal-ui 0.1.0\n+ @rightkit/license 0.1.6\n+ @rightkit/logs 0.1.3\n+ @rightkit/tauri 0.1.0\n+ @rightkit/updates 0.2.3\n+ @tauri-apps/api 2.11.1\n+ @tauri-apps/plugin-http 2.5.9\n+ @tauri-apps/plugin-process 2.3.1\n+ @tauri-apps/plugin-updater 2.10.1\n+ clsx 2.1.1\n+ docxtemplater 3.69.0\n+ dompurify 3.4.13\n+ fabric 7.4.0\n+ github-slugger 2.0.0\n+ jszip 3.10.1\n+ katex 0.17.0\n+ mermaid 11.16.1\n+ pdfjs-dist 6.2.108\n+ pizzip 3.2.0\n+ prosemirror-commands 1.7.1\n+ prosemirror-dropcursor 1.8.2\n+ prosemirror-history 1.5.0\n+ prosemirror-keymap 1.2.3\n+ prosemirror-model 1.25.10\n+ prosemirror-state 1.4.4\n+ prosemirror-tables 1.8.5\n+ prosemirror-transform 1.12.0\n+ prosemirror-view 1.42.0\n+ react 19.2.7\n+ react-dom 19.2.7\n+ react-image-crop 11.1.2\n+ rehype-stringify 10.0.1\n+ remark-frontmatter 5.0.0\n+ remark-gfm 4.0.1\n+ remark-math 6.0.0\n+ remark-parse 11.0.0\n+ remark-rehype 11.1.2\n+ remark-smartypants 3.0.2\n+ shiki 4.3.1\n+ sonner 2.0.7\n+ sucrase 3.35.1\n+ unified 11.0.5\n+ xml-js 1.6.11\n+ yaml 2.9.0\n\ndevDependencies:\n+ @biomejs/biome 2.5.3\n+ @rightkit/legal 0.3.0\n+ @rightkit/release 0.2.50\n+ @tailwindcss/vite 4.3.2\n+ @tauri-apps/cli 2.11.4\n+ @testing-library/dom 10.4.1\n+ @testing-library/jest-dom 6.9.1\n+ @testing-library/react 16.3.2\n+ @types/mdast 4.0.4\n+ @types/react 19.2.17\n+ @types/react-dom 19.2.3\n+ @types/ws 8.18.1\n+ @vitejs/plugin-react 6.0.3\n+ happy-dom 20.10.6\n+ jscpd 5.0.12\n+ jsdom 29.1.1\n+ knip 6.25.0\n+ tailwindcss 4.3.2\n+ typescript 6.0.3\n+ vite 8.1.4\n+ vitest 4.1.10\n+ ws 8.21.0\n\nDone in 40.8s using pnpm v11.18.0",
|
|
22
|
-
"stderr": ""
|
|
23
|
-
},
|
|
24
|
-
"doctor": {
|
|
25
|
-
"command": "C:\\nvm4w\\nodejs\\node.exe C:\\nvm4w\\nodejs\\node_modules\\pnpm\\bin\\pnpm.mjs release:doctor",
|
|
26
|
-
"status": 0,
|
|
27
|
-
"stdout": "right-release 0.2.50\nconfig: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\right-release.config.mjs\napp: viewright\nplatform: win\ntier: <required for release/publish>\npackageManager: pnpm\nworkdir: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\nhardeningscan: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\node_modules\\.pnpm\\@rightkit+release@0.2.50\\node_modules\\@rightkit\\release\\hardeningscan.mjs\nlegal: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\legal\\legal-manifest.json\nlegalAcceptance: viewright-2026-07-17-v3\nlegalManifestSha256: 035ea7cbd040ef001dbdc1385f114c8bb126178fea9a9f705016a726f96a7830\nsign: src-tauri/target/release/bundle/nsis/ViewRight_0.1.60_x64-setup.exe\npreflight:\n [ok ] target-bridge: src-tauri/target is ready for the shared cache bridge\n [ok ] version: 0.1.60 is free to build\n [ok ] windows-sdk: makeappx.exe from SDK 10.0.26100.0\n [ok ] signtool: signtool.exe from SDK 10.0.26100.0\n [ok ] sccache: sccache 0.17.0\n [ok ] disk: 628.7GB free",
|
|
28
|
-
"stderr": "$ right-release doctor"
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
}
|