@rightkit/release 0.2.2 → 0.2.3

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.2",
3
+ "version": "0.2.3",
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": {
@@ -33,24 +33,80 @@ const root = path.dirname(configPath);
33
33
  const config = (await import(pathToFileURL(configPath))).default;
34
34
  const target = config?.targets?.[platform];
35
35
  const artifacts = target?.updater?.artifacts ?? [];
36
- if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} has no updater.artifacts`);
37
36
  const installers = target?.installer?.artifacts ?? [];
38
37
 
39
38
  if (tier === "patch") {
40
39
  if (!installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
40
+ if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} patch has no updater.artifacts for the free app-update manifest`);
41
41
  for (const installer of installers) {
42
42
  assertCurrentKey(installer.key, "installer");
43
43
  const file = path.resolve(root, installer.file);
44
44
  if (!dryRun) await access(file).catch(() => fail(`missing signed installer: ${installer.file}`));
45
45
  }
46
+ const platforms = {};
47
+ const registrations = new Map();
48
+ const uploadedKeys = new Set();
49
+ for (const artifact of artifacts) {
50
+ assertCurrentKey(artifact.key, "updater");
51
+ const installerKey = patchInstallerKey(artifact);
52
+ assertCurrentKey(installerKey, "installer");
53
+ const file = path.resolve(root, artifact.file);
54
+ const signatureFile = path.resolve(root, artifact.signature);
55
+ if (!dryRun) {
56
+ await access(file).catch(() => fail(`missing patch updater payload: ${artifact.file}`));
57
+ await access(signatureFile).catch(() => fail(`missing patch updater signature: ${artifact.signature}`));
58
+ }
59
+ if (!uploadedKeys.has(installerKey)) {
60
+ console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installerKey}`);
61
+ if (!dryRun) await run(process.execPath, [UPLOAD, file, installerKey, "public"], root);
62
+ uploadedKeys.add(installerKey);
63
+ }
64
+ const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
65
+ const artifactKey = artifact.artifactKey || installerKey.replaceAll("/", "__");
66
+ const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
67
+ platforms[artifact.platform] = {
68
+ signature,
69
+ url: `${PUBLIC_BASE}/${installerKey}`,
70
+ };
71
+ registrations.set(installerKey, { artifactKey, path: installerKey, ...metadata });
72
+ }
73
+ for (const installer of installers) {
74
+ const file = path.resolve(root, installer.file);
75
+ if (!uploadedKeys.has(installer.key)) {
76
+ console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.key}`);
77
+ if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
78
+ uploadedKeys.add(installer.key);
79
+ }
80
+ }
81
+ const version = config.version || target.updater.version;
82
+ if (!version) fail(`${config.app} config must expose version or targets.${platform}.updater.version`);
83
+ const body = {
84
+ appKey: config.app,
85
+ version,
86
+ channel: config.channel || "stable",
87
+ platform: null,
88
+ manifest: {
89
+ version,
90
+ tier,
91
+ notes: process.env.RIGHT_RELEASE_NOTES || `${config.app} ${version}`,
92
+ pub_date: new Date().toISOString(),
93
+ platforms,
94
+ },
95
+ artifacts: [...registrations.values()],
96
+ };
97
+ console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}/v1/admin/apps/patches`);
98
+ console.log(JSON.stringify(body));
99
+ if (!dryRun) await register("/v1/admin/apps/patches", body);
100
+ process.exit(0);
46
101
  }
47
102
 
103
+ if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no updater.artifacts`);
104
+
48
105
  const platforms = {};
49
106
  const registrations = new Map();
50
107
  const uploadedKeys = new Set();
51
108
  for (const artifact of artifacts) {
52
109
  assertCurrentKey(artifact.key, "updater");
53
- const bucket = tier === "patch" ? "public" : "private";
54
110
  const file = path.resolve(root, artifact.file);
55
111
  const signatureFile = path.resolve(root, artifact.signature);
56
112
  if (!dryRun) {
@@ -58,8 +114,8 @@ for (const artifact of artifacts) {
58
114
  await access(signatureFile).catch(() => fail(`missing updater signature: ${artifact.signature}`));
59
115
  }
60
116
  if (!uploadedKeys.has(artifact.key)) {
61
- console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD ${bucket}/${artifact.key}`);
62
- if (!dryRun) await run(process.execPath, [UPLOAD, file, artifact.key, bucket], root);
117
+ console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD private/${artifact.key}`);
118
+ if (!dryRun) await run(process.execPath, [UPLOAD, file, artifact.key, "private"], root);
63
119
  uploadedKeys.add(artifact.key);
64
120
  }
65
121
 
@@ -68,19 +124,11 @@ for (const artifact of artifacts) {
68
124
  const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
69
125
  platforms[artifact.platform] = {
70
126
  signature,
71
- url: tier === "patch" ? `${PUBLIC_BASE}/${artifact.key}` : `${DOWNLOAD_BASE}/${artifact.key}`,
127
+ url: `${DOWNLOAD_BASE}/${artifact.key}`,
72
128
  };
73
129
  registrations.set(artifact.key, { artifactKey, path: artifact.key, ...metadata });
74
130
  }
75
131
 
76
- if (tier === "patch") {
77
- for (const installer of installers) {
78
- const file = path.resolve(root, installer.file);
79
- console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.key}`);
80
- if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
81
- }
82
- }
83
-
84
132
  const version = config.version || target.updater.version;
85
133
  if (!version) fail(`${config.app} config must expose version or targets.${platform}.updater.version`);
86
134
  const body = {
@@ -97,25 +145,11 @@ const body = {
97
145
  },
98
146
  artifacts: [...registrations.values()],
99
147
  };
100
- const route = tier === "patch" ? "/v1/admin/apps/patches" : "/v1/admin/apps/releases";
148
+ const route = "/v1/admin/apps/releases";
101
149
  console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}${route}`);
102
150
  console.log(JSON.stringify(body));
103
151
 
104
- if (!dryRun) {
105
- const token = process.env.RIGHTAPPS_ADMIN_TOKEN;
106
- if (!token) fail("RIGHTAPPS_ADMIN_TOKEN is required to register an update");
107
- const response = await fetch(`${API_BASE}${route}`, {
108
- method: "POST",
109
- headers: {
110
- authorization: `Bearer ${token}`,
111
- "content-type": "application/json",
112
- "x-store-slug": process.env.RIGHTAPPS_STORE_SLUG || "rightapps",
113
- },
114
- body: JSON.stringify(body),
115
- });
116
- if (!response.ok) fail(`registration failed: ${response.status} ${await response.text()}`);
117
- console.log(await response.text());
118
- }
152
+ if (!dryRun) await register(route, body);
119
153
 
120
154
  async function fileMetadata(file) {
121
155
  const bytes = await readFile(file);
@@ -142,6 +176,28 @@ function assertCurrentKey(key, kind) {
142
176
  }
143
177
  }
144
178
 
179
+ function patchInstallerKey(artifact) {
180
+ if (artifact.patchKey) return artifact.patchKey;
181
+ if (!artifact.key.includes("/updates/")) fail(`patch updater key must be convertible to installers/current, got: ${artifact.key}`);
182
+ return artifact.key.replace("/updates/", "/installers/");
183
+ }
184
+
185
+ async function register(route, body) {
186
+ const token = process.env.RIGHTAPPS_ADMIN_TOKEN;
187
+ if (!token) fail("RIGHTAPPS_ADMIN_TOKEN is required to register an update");
188
+ const response = await fetch(`${API_BASE}${route}`, {
189
+ method: "POST",
190
+ headers: {
191
+ authorization: `Bearer ${token}`,
192
+ "content-type": "application/json",
193
+ "x-store-slug": process.env.RIGHTAPPS_STORE_SLUG || "rightapps",
194
+ },
195
+ body: JSON.stringify(body),
196
+ });
197
+ if (!response.ok) fail(`registration failed: ${response.status} ${await response.text()}`);
198
+ console.log(await response.text());
199
+ }
200
+
145
201
  function fail(message) {
146
202
  console.error(`publish-update: ${message}`);
147
203
  process.exit(1);
@@ -59,18 +59,19 @@ function run(tier) {
59
59
  });
60
60
  }
61
61
 
62
- test("routes patch manifests to the public patch registration endpoint", () => {
62
+ test("patch registers a free updater manifest backed by public installer-path objects", () => {
63
63
  const result = run("patch");
64
64
  assert.equal(result.status, 0, result.stderr);
65
- const updaterUpload = result.stdout.indexOf("UPLOAD public/fixture/updates/windows/current/Fixture.exe");
65
+ const patchPayloadUpload = result.stdout.indexOf("UPLOAD public/fixture/installers/windows/current/Fixture.exe");
66
66
  const installerUpload = result.stdout.indexOf("UPLOAD public/fixture/installers/windows/current/Fixture-Setup.exe");
67
67
  const registration = result.stdout.indexOf("/v1/admin/apps/patches");
68
- assert.ok(updaterUpload >= 0, "patch must upload a public updater");
69
- assert.ok(installerUpload >= 0, "patch must upload a public installer");
70
- assert.ok(updaterUpload < installerUpload, "patch should upload the updater before replacing the public installer");
68
+ assert.ok(patchPayloadUpload >= 0, "patch updater payload must upload to the public installer path");
69
+ assert.ok(installerUpload >= 0, "patch download installer must upload to the public installer path");
70
+ assert.ok(patchPayloadUpload < installerUpload, "patch should upload app-update payloads before replacing the public download installer");
71
71
  assert.ok(installerUpload < registration, "patch should register only after uploads complete");
72
- assert.equal(result.stdout.match(/UPLOAD public\/fixture\/updates\/windows\/current\/Fixture\.exe/g)?.length, 1);
73
- assert.match(result.stdout, /pub-6c73208d46c245a9b4881d5e02f6b618\.r2\.dev\/fixture\/updates\/windows\/current\/Fixture\.exe/);
72
+ assert.doesNotMatch(result.stdout, /UPLOAD public\/fixture\/updates\//);
73
+ assert.doesNotMatch(result.stdout, /UPLOAD private\//);
74
+ assert.match(result.stdout, /pub-6c73208d46c245a9b4881d5e02f6b618\.r2\.dev\/fixture\/installers\/windows\/current\/Fixture\.exe/);
74
75
  assert.match(result.stdout, /POST .*\/v1\/admin\/apps\/patches/);
75
76
  assert.match(result.stdout, /"tier":"patch"/);
76
77
  });
@@ -99,3 +100,13 @@ test("refuses versioned R2 object keys that would accumulate old releases", () =
99
100
  assert.notEqual(result.status, 0);
100
101
  assert.match(result.stderr, /current R2 object/);
101
102
  });
103
+
104
+ test("refuses versioned updater object keys for gated feature updates", () => {
105
+ const config = versionedFixture();
106
+ const result = spawnSync(process.execPath, [publisher, "--config", config, "--platform", "win", "--dry-run"], {
107
+ encoding: "utf8",
108
+ env: { ...process.env, RIGHT_RELEASE_TIER: "update" },
109
+ });
110
+ assert.notEqual(result.status, 0);
111
+ assert.match(result.stderr, /current R2 object/);
112
+ });
package/release.mjs CHANGED
@@ -9,7 +9,7 @@ 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.2";
12
+ const VERSION = "0.2.3";
13
13
  const TIERS = new Set(["patch", "update"]);
14
14
  const RIGHTKIT_EXPECTED = new Map([
15
15
  ["@rightkit/license", "^0.1.3"],
package/release.test.mjs CHANGED
@@ -18,7 +18,7 @@ function fixture({ signed = true, publish = false, packageJson } = {}) {
18
18
  name: "fixture",
19
19
  scripts: { "release:patch:win": "right-release --platform win --tier patch" },
20
20
  dependencies: { "@rightkit/license": "^0.1.3", "@rightkit/logs": "^0.1.1" },
21
- devDependencies: { "@rightkit/release": "^0.2.2" },
21
+ devDependencies: { "@rightkit/release": "^0.2.3" },
22
22
  },
23
23
  null,
24
24
  2,
@@ -115,7 +115,7 @@ test("publish runs the signed package step before the updater publication step",
115
115
  assert.ok(publishAt > packageAt, result.stdout);
116
116
  });
117
117
 
118
- test("Windows re-signs the updater artifact after Azure code signing", () => {
118
+ test("Windows patch signs the installer then minisigns the free updater payload", () => {
119
119
  const result = run(fixture({ publish: true }), "--tier=patch", "--upload");
120
120
  assert.equal(result.status, 0, result.stderr);
121
121
  const azureAt = result.stdout.indexOf("sign-windows.mjs");
@@ -125,3 +125,14 @@ test("Windows re-signs the updater artifact after Azure code signing", () => {
125
125
  assert.ok(updaterAt > azureAt, result.stdout);
126
126
  assert.ok(publishAt > updaterAt, result.stdout);
127
127
  });
128
+
129
+ test("Windows update re-signs the updater artifact after Azure code signing", () => {
130
+ const result = run(fixture({ publish: true }), "--tier=update", "--upload");
131
+ assert.equal(result.status, 0, result.stderr);
132
+ const azureAt = result.stdout.indexOf("sign-windows.mjs");
133
+ const updaterAt = result.stdout.indexOf("sign-updater.mjs");
134
+ const publishAt = result.stdout.indexOf("publish-update.mjs");
135
+ assert.ok(azureAt >= 0, result.stdout);
136
+ assert.ok(updaterAt > azureAt, result.stdout);
137
+ assert.ok(publishAt > updaterAt, result.stdout);
138
+ });
@@ -8,7 +8,7 @@ const workspace = path.resolve(new URL("../..", import.meta.url).pathname.replac
8
8
  const pubkey = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI5Mzk1RjlGRjQ2NjI2MUQKUldRZEptYjBuMTg1S1VSUXlBdFM4WmtzaHArYko0U2hRMDVlSDJmSExVZG82Q0hoQ2srUlhqanAK";
9
9
  const licensePackage = "^0.1.3";
10
10
  const logsPackage = "^0.1.1";
11
- const releasePackage = "^0.2.2";
11
+ const releasePackage = "^0.2.3";
12
12
  const apps = [
13
13
  { key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json", host: "viewright.cc" },
14
14
  { key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json", host: "scraperight.cc" },