@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 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
- const immutableSuffix = `/${manifest.addon}/addons/${manifest.platform}/sha256/${entry.manifestSha256}/addon-manifest.json`;
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}`;
@@ -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
  });
@@ -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) => {
@@ -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
- verifyPlatformTrust(plan);
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 = `${plan.assets[1]}.notes.md`;
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 (!options.releaseId || !options.repo) usage(2);
159
+ if (Boolean(options.releaseId) === Boolean(options.addon)) usage(2);
107
160
  const repoRoot = runCommand("git", ["rev-parse", "--show-toplevel"]);
108
- const plan = prepareGitHubRelease({ repoRoot, ...options });
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
  }
@@ -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 { prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
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.58",
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.58",
467
+ "@rightkit/release": "0.2.59",
468
468
  "@rightkit/qa": "0.2.0",
469
469
  });
470
470
  assert.deepEqual(versions.legacyNpm, {
@@ -18,7 +18,7 @@
18
18
  "@rightkit/legal": "0.3.0",
19
19
  "@rightkit/legal-ui": "0.1.1",
20
20
  "@rightkit/license": "0.1.6",
21
- "@rightkit/release": "0.2.58",
21
+ "@rightkit/release": "0.2.59",
22
22
  "@rightkit/qa": "0.2.0"
23
23
  },
24
24
  "legacyNpm": {