@rightkit/release 0.2.58 → 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 +3 -3
- package/addon-command.test.mjs +2 -1
- package/addon-contract.mjs +10 -0
- package/github-release.mjs +76 -12
- package/github-release.test.mjs +44 -1
- package/package.json +1 -1
- package/right-suite-contract.test.mjs +1 -1
- package/rightkit-versions.json +1 -1
package/addon-command.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { addonManifestSha256, addonRoutes, canonicalAddonManifest, createAddonManifest, downloadImmutableAddOn, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
|
|
8
|
+
import { addonManifestSha256, addonRoutes, canonicalAddonManifest, createAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
|
|
9
9
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
10
10
|
import { assertCleanSource } from "./source-gate.mjs";
|
|
11
11
|
|
|
@@ -80,6 +80,7 @@ function signExecutableRoles(config, root, platform) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
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>");
|
|
83
84
|
if (!new Set(["patch", "update"]).has(options.tier)) fail("add-on upload requires --tier patch|update");
|
|
84
85
|
const commit = git(repoRoot, ["rev-parse", "HEAD"]);
|
|
85
86
|
const sealed = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), options.platform);
|
|
@@ -124,8 +125,7 @@ async function adopt(options) {
|
|
|
124
125
|
const manifest = JSON.parse(readFileSync(path.join(source, "addon-manifest.json"), "utf8"));
|
|
125
126
|
validateAddonManifest(manifest);
|
|
126
127
|
if (addonManifestSha256(manifest) !== entry.manifestSha256) fail("add-on lock manifest SHA mismatch");
|
|
127
|
-
|
|
128
|
-
if (!entry.manifestUrl.endsWith(immutableSuffix) || /[?#]/.test(entry.manifestUrl)) fail("add-on lock manifestUrl is not immutable");
|
|
128
|
+
if (!immutableAddonManifestUrl(manifest, entry.manifestUrl, entry.manifestSha256)) fail("add-on lock manifestUrl is not immutable");
|
|
129
129
|
const output = path.resolve(options.output);
|
|
130
130
|
const destination = path.join(output, manifest.addon);
|
|
131
131
|
const stage = `${destination}.tmp-${process.pid}`;
|
package/addon-command.test.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import test from "node:test";
|
|
8
|
-
import { createAddonManifest, addonManifestSha256, canonicalAddonManifest, downloadImmutableAddOn } from "./addon-contract.mjs";
|
|
8
|
+
import { createAddonManifest, addonManifestSha256, canonicalAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl } from "./addon-contract.mjs";
|
|
9
9
|
|
|
10
10
|
const command = fileURLToPath(new URL("./addon-command.mjs", import.meta.url));
|
|
11
11
|
function fixture() {
|
|
@@ -50,4 +50,5 @@ test("remote downloader accepts only immutable manifest plus file routes", async
|
|
|
50
50
|
const destination = path.join(root, "remote"); mkdirSync(destination);
|
|
51
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
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);
|
|
53
54
|
});
|
package/addon-contract.mjs
CHANGED
|
@@ -6,12 +6,14 @@ const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
6
6
|
const ROLES = new Set(["command", "service", "icon", "license", "eula", "privacy", "third-party-notices"]);
|
|
7
7
|
const LEGAL_ROLES = new Set(["license", "eula", "privacy", "third-party-notices"]);
|
|
8
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}$/;
|
|
9
10
|
|
|
10
11
|
export function validateAddonConfig(config, { root, platform, allowMissingExecutables = false }) {
|
|
11
12
|
if (!PLATFORM.has(platform)) throw new Error("add-on platform must be mac or win");
|
|
12
13
|
if (!config || config.schema !== 1 || config.kind !== "headless-addon") throw new Error("add-on config must use schema 1 headless-addon");
|
|
13
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}`);
|
|
14
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");
|
|
15
17
|
validateBuildInputs(config.buildInputs);
|
|
16
18
|
const target = config.targets?.[platform];
|
|
17
19
|
if (!target?.targetTriple || !target?.build?.cmd || !Array.isArray(target.build.args)) throw new Error(`add-on ${platform} target requires targetTriple and build command`);
|
|
@@ -79,6 +81,14 @@ export function addonRoutes(manifest) {
|
|
|
79
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 }];
|
|
80
82
|
}
|
|
81
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
|
+
|
|
82
92
|
export async function downloadImmutableAddOn(manifestUrl, destination, { fetchImpl = fetch } = {}) {
|
|
83
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");
|
|
84
94
|
const download = async (url, file) => {
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.59",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -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, {
|