@rightkit/release 0.2.0 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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": {
package/release.mjs CHANGED
@@ -9,8 +9,14 @@ const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
9
9
  const UPLOAD_LARGE = path.resolve(TOOL_ROOT, "upload-large.mjs");
10
10
  const SIGN_WINDOWS = path.resolve(TOOL_ROOT, "sign-windows.mjs");
11
11
  const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
12
- const VERSION = "0.2.0";
12
+ const VERSION = "0.2.2";
13
13
  const TIERS = new Set(["patch", "update"]);
14
+ const RIGHTKIT_EXPECTED = new Map([
15
+ ["@rightkit/license", "^0.1.3"],
16
+ ["@rightkit/logs", "^0.1.1"],
17
+ ["@rightkit/release", `^${VERSION}`],
18
+ ]);
19
+ const FORBIDDEN_RIGHTKIT_SPEC = /^(?:git|file|link|workspace):|github|github\.com/i;
14
20
 
15
21
  const args = process.argv.slice(2);
16
22
  const opts = {
@@ -57,6 +63,7 @@ if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<m
57
63
 
58
64
  const root = path.dirname(configPath);
59
65
  const workdir = path.resolve(root, config.workdir ?? ".");
66
+ await validateRightKitPackageContract(root, config.app);
60
67
  const target = config.targets?.[opts.platform];
61
68
  if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
62
69
  if (target.signed !== true) {
@@ -167,6 +174,28 @@ async function runCommand(command, root) {
167
174
  await run(command.cmd, command.args ?? [], cwd, await commandEnv(command, root), command);
168
175
  }
169
176
 
177
+ async function validateRightKitPackageContract(root, appName) {
178
+ const packageJsonPath = path.join(root, "package.json");
179
+ const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
180
+ const scripts = pkg.scripts ?? {};
181
+ if (JSON.stringify(scripts).includes("../tools/right-release")) {
182
+ fail(`${appName ?? pkg.name ?? "app"} package.json must call the right-release bin, not ../tools/right-release/*`);
183
+ }
184
+ const rightKitDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
185
+ for (const [name, specifier] of Object.entries(rightKitDeps).filter(([name]) => name.startsWith("@rightkit/"))) {
186
+ if (FORBIDDEN_RIGHTKIT_SPEC.test(String(specifier))) {
187
+ fail(`${appName ?? pkg.name ?? "app"} ${name} must come from the published npm package, not ${specifier}`);
188
+ }
189
+ const expected = RIGHTKIT_EXPECTED.get(name);
190
+ if (expected && specifier !== expected) {
191
+ fail(`${appName ?? pkg.name ?? "app"} ${name} must be ${expected}, got ${specifier}`);
192
+ }
193
+ }
194
+ if (pkg.devDependencies?.["@rightkit/release"] !== RIGHTKIT_EXPECTED.get("@rightkit/release")) {
195
+ fail(`${appName ?? pkg.name ?? "app"} must depend on @rightkit/release ${RIGHTKIT_EXPECTED.get("@rightkit/release")} in devDependencies`);
196
+ }
197
+ }
198
+
170
199
  async function commandEnv(command, root) {
171
200
  const env = { ...(command.env ?? {}) };
172
201
  for (const [name, rel] of Object.entries(command.envFiles ?? {})) {
package/release.test.mjs CHANGED
@@ -8,9 +8,22 @@ import test from "node:test";
8
8
 
9
9
  const release = fileURLToPath(new URL("./release.mjs", import.meta.url));
10
10
 
11
- function fixture({ signed = true, publish = false } = {}) {
11
+ function fixture({ signed = true, publish = false, packageJson } = {}) {
12
12
  const dir = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
13
13
  const config = path.join(dir, "right-release.config.mjs");
14
+ writeFileSync(
15
+ path.join(dir, "package.json"),
16
+ JSON.stringify(
17
+ packageJson ?? {
18
+ name: "fixture",
19
+ scripts: { "release:patch:win": "right-release --platform win --tier patch" },
20
+ dependencies: { "@rightkit/license": "^0.1.3", "@rightkit/logs": "^0.1.1" },
21
+ devDependencies: { "@rightkit/release": "^0.2.2" },
22
+ },
23
+ null,
24
+ 2,
25
+ ),
26
+ );
14
27
  writeFileSync(
15
28
  config,
16
29
  `export default ${JSON.stringify({
@@ -72,6 +85,27 @@ test("rejects targets that do not explicitly declare signed release output", ()
72
85
  assert.match(result.stderr, /signed: true/);
73
86
  });
74
87
 
88
+ test("rejects Git, path, or link RightKit app dependencies before release work starts", () => {
89
+ const result = run(
90
+ fixture({
91
+ packageJson: {
92
+ name: "fixture",
93
+ scripts: { "release:patch:win": "node ../tools/right-release/release.mjs --tier patch" },
94
+ dependencies: {
95
+ "@rightkit/license": "git+https://github.com/adrdsouza/rightkit.git#main",
96
+ "@rightkit/logs": "link:../../../rightkit/packages/logs",
97
+ },
98
+ devDependencies: {
99
+ "@rightkit/release": "github:adrdsouza/claude#main&path:/tools/right-release",
100
+ },
101
+ },
102
+ }),
103
+ "--tier=patch",
104
+ );
105
+ assert.notEqual(result.status, 0);
106
+ assert.match(result.stderr, /right-release bin|published npm package/i);
107
+ });
108
+
75
109
  test("publish runs the signed package step before the updater publication step", () => {
76
110
  const result = run(fixture({ publish: true }), "--tier=update", "--upload");
77
111
  assert.equal(result.status, 0, result.stderr);
@@ -6,7 +6,9 @@ import test from "node:test";
6
6
 
7
7
  const workspace = path.resolve(new URL("../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
8
8
  const pubkey = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI5Mzk1RjlGRjQ2NjI2MUQKUldRZEptYjBuMTg1S1VSUXlBdFM4WmtzaHArYko0U2hRMDVlSDJmSExVZG82Q0hoQ2srUlhqanAK";
9
- const releasePackage = "^0.2.0";
9
+ const licensePackage = "^0.1.3";
10
+ const logsPackage = "^0.1.1";
11
+ const releasePackage = "^0.2.2";
10
12
  const apps = [
11
13
  { key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json", host: "viewright.cc" },
12
14
  { key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json", host: "scraperight.cc" },
@@ -19,6 +21,12 @@ for (const app of apps) {
19
21
  test(`${app.key} follows the signed tiered Right Release contract`, async () => {
20
22
  const root = path.join(workspace, app.root);
21
23
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
24
+ const rightKitDeps = { ...pkg.dependencies, ...pkg.devDependencies };
25
+ for (const [name, specifier] of Object.entries(rightKitDeps).filter(([name]) => name.startsWith("@rightkit/"))) {
26
+ assert.doesNotMatch(specifier, /^(?:git|file|link|workspace):|github/i, `${app.key} ${name} must come from the published npm package`);
27
+ }
28
+ if (rightKitDeps["@rightkit/license"]) assert.equal(rightKitDeps["@rightkit/license"], licensePackage);
29
+ if (rightKitDeps["@rightkit/logs"]) assert.equal(rightKitDeps["@rightkit/logs"], logsPackage);
22
30
  assert.equal(pkg.devDependencies?.["@rightkit/release"], releasePackage);
23
31
  assert(!JSON.stringify(pkg).includes("github:adrdsouza/claude#main&path:/tools/right-release"));
24
32
  assert(!JSON.stringify(pkg).includes("git+https://github.com/adrdsouza/rightkit.git"));
package/upload-large.mjs CHANGED
@@ -1,74 +1,63 @@
1
1
  #!/usr/bin/env node
2
- import { createReadStream, statSync } from "node:fs";
2
+ import { accessSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
 
6
- const WORKER_BASE = process.env.RIGHT_RELEASE_UPLOAD_WORKER_BASE || "https://heardright-models.adrdsouza.workers.dev";
7
- const PART_SIZE = 90 * 1024 * 1024;
6
+ const ACCOUNT_ID = "03ae77ccd7a07bcbb2dcfde47fa7ba3a";
7
+ const BUCKETS = new Map([
8
+ ["public", "rightapps-downloads"],
9
+ ["private", "rightapps-updates"],
10
+ ]);
8
11
 
9
- const [, , localFile, r2Key, bucket = "public"] = process.argv;
12
+ const [, , localFile, r2Key, bucketAlias = "public"] = process.argv;
10
13
  if (!localFile || !r2Key) {
11
14
  console.error("Usage: node upload-large.mjs <localFile> <r2Key> [public|private]");
12
15
  process.exit(1);
13
16
  }
14
17
 
15
- const token = process.env.ADMIN_UPLOAD_TOKEN || readKeychainToken();
16
- if (!token) {
17
- console.error("ADMIN_UPLOAD_TOKEN not set and no Keychain token found");
18
+ const bucket = BUCKETS.get(bucketAlias);
19
+ if (!bucket) {
20
+ console.error(`upload-large: invalid bucket alias ${bucketAlias}; expected public|private`);
21
+ process.exit(1);
22
+ }
23
+
24
+ try {
25
+ accessSync(localFile);
26
+ } catch {
27
+ console.error(`upload-large: missing local file: ${localFile}`);
18
28
  process.exit(1);
19
29
  }
20
30
 
21
- const headers = { Authorization: `Bearer ${token}` };
22
31
  const fileSize = statSync(localFile).size;
32
+ const object = `${bucket}/${r2Key}`;
33
+ const wranglerArgs = ["wrangler@4", "r2", "object", "put", object, "--file", path.resolve(localFile), "--remote"];
34
+ const env = cleanCloudflareEnv(process.env);
23
35
 
24
- async function api(method, pathname, body, isJson = true) {
25
- const opts = {
26
- method,
27
- headers: isJson
28
- ? { ...headers, "Content-Type": "application/json" }
29
- : { ...headers, "Content-Type": "application/octet-stream" },
30
- };
31
- if (body != null) opts.body = isJson ? JSON.stringify(body) : body;
32
- const resp = await fetch(`${WORKER_BASE}${pathname}`, opts);
33
- const text = await resp.text();
34
- if (!resp.ok) throw new Error(`${method} ${pathname} -> ${resp.status}: ${text}`);
35
- return JSON.parse(text);
36
- }
36
+ console.log(`Uploading ${path.basename(localFile)} (${(fileSize / 1024 / 1024).toFixed(1)} MB) -> ${object}`);
37
+ console.log(`wrangler r2 object put ${object} --file ${path.resolve(localFile)} --remote`);
37
38
 
38
- async function readChunk(offset, size) {
39
- return new Promise((resolve, reject) => {
40
- const chunks = [];
41
- const stream = createReadStream(localFile, { start: offset, end: offset + size - 1 });
42
- stream.on("data", (c) => chunks.push(c));
43
- stream.on("end", () => resolve(Buffer.concat(chunks)));
44
- stream.on("error", reject);
45
- });
39
+ if (process.env.RIGHT_RELEASE_UPLOAD_DRY_RUN === "1") {
40
+ process.exit(0);
46
41
  }
47
42
 
48
- console.log(`Uploading ${path.basename(localFile)} (${(fileSize / 1024 / 1024).toFixed(1)} MB) -> ${bucket}/${r2Key}`);
49
- const { uploadId } = await api("POST", "/admin/multipart/init", { key: r2Key, bucket });
50
- const parts = [];
51
- const totalParts = Math.ceil(fileSize / PART_SIZE);
52
- for (let i = 0; i < totalParts; i++) {
53
- const offset = i * PART_SIZE;
54
- const size = Math.min(PART_SIZE, fileSize - offset);
55
- const partNumber = i + 1;
56
- const chunk = await readChunk(offset, size);
57
- const params = `?uploadId=${encodeURIComponent(uploadId)}&key=${encodeURIComponent(r2Key)}&bucket=${bucket}`;
58
- const { etag } = await api("PUT", `/admin/multipart/part/${partNumber}${params}`, chunk, false);
59
- parts.push({ partNumber, etag });
60
- console.log(` part ${partNumber}/${totalParts}`);
61
- }
62
- await api("POST", "/admin/multipart/complete", { uploadId, key: r2Key, bucket, parts });
63
- console.log(`Done. ${r2Key} uploaded to ${bucket}.`);
43
+ const result = spawnSync("npx", wranglerArgs, {
44
+ env,
45
+ stdio: "inherit",
46
+ shell: process.platform === "win32",
47
+ windowsHide: true,
48
+ });
64
49
 
65
- function readKeychainToken() {
66
- if (process.platform !== "darwin") return "";
67
- for (const service of ["rightapps-admin-upload-token", "heardright-admin-upload-token"]) {
68
- const result = spawnSync("security", ["find-generic-password", "-a", process.env.USER || "", "-s", service, "-w"], {
69
- encoding: "utf8",
70
- });
71
- if (result.status === 0 && result.stdout.trim()) return result.stdout.trim();
72
- }
73
- return "";
50
+ if (result.error) {
51
+ console.error(`upload-large: failed to start wrangler: ${result.error.message}`);
52
+ process.exit(1);
53
+ }
54
+ process.exit(result.status ?? 1);
55
+
56
+ function cleanCloudflareEnv(source) {
57
+ const env = { ...source, CLOUDFLARE_ACCOUNT_ID: source.CLOUDFLARE_ACCOUNT_ID || ACCOUNT_ID };
58
+ delete env.CLOUDFLARE_API_KEY;
59
+ delete env.CLOUDFLARE_EMAIL;
60
+ delete env.CF_API_KEY;
61
+ delete env.CF_EMAIL;
62
+ return env;
74
63
  }
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import test from "node:test";
8
+
9
+ const uploader = fileURLToPath(new URL("./upload-large.mjs", import.meta.url));
10
+
11
+ function fixtureFile() {
12
+ const dir = mkdtempSync(path.join(os.tmpdir(), "right-upload-test-"));
13
+ const file = path.join(dir, "Artifact.exe");
14
+ writeFileSync(file, "artifact");
15
+ return file;
16
+ }
17
+
18
+ function run(bucket) {
19
+ return spawnSync(process.execPath, [uploader, fixtureFile(), "fixture/updates/windows/current/Artifact.exe", bucket], {
20
+ encoding: "utf8",
21
+ env: { ...process.env, RIGHT_RELEASE_UPLOAD_DRY_RUN: "1" },
22
+ });
23
+ }
24
+
25
+ test("uploads public artifacts through wrangler remote R2 downloads bucket", () => {
26
+ const result = run("public");
27
+ assert.equal(result.status, 0, result.stderr);
28
+ assert.match(result.stdout, /wrangler r2 object put rightapps-downloads\/fixture\/updates\/windows\/current\/Artifact\.exe/);
29
+ assert.match(result.stdout, /--remote/);
30
+ assert.doesNotMatch(result.stdout, /workers\.dev|multipart|heardright-models/i);
31
+ });
32
+
33
+ test("uploads private artifacts through wrangler remote R2 updates bucket", () => {
34
+ const result = run("private");
35
+ assert.equal(result.status, 0, result.stderr);
36
+ assert.match(result.stdout, /wrangler r2 object put rightapps-updates\/fixture\/updates\/windows\/current\/Artifact\.exe/);
37
+ assert.match(result.stdout, /--remote/);
38
+ });
39
+
40
+ test("refuses unknown bucket aliases", () => {
41
+ const result = run("random");
42
+ assert.notEqual(result.status, 0);
43
+ assert.match(result.stderr, /expected public\|private/);
44
+ });