@rightkit/release 0.2.9 → 0.2.10
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/cli/right-release.mjs +3 -0
- package/deps.mjs +0 -0
- package/generate-dmg-background.py +53 -0
- package/lsclean.sh +0 -0
- package/notary-auth.mjs +21 -0
- package/notary-auth.test.mjs +30 -0
- package/package.json +9 -3
- package/prune-r2.mjs +56 -0
- package/prune-r2.test.mjs +12 -0
- package/publish-update.mjs +42 -2
- package/publish-update.test.mjs +10 -1
- package/release.mjs +3 -3
- package/release.test.mjs +2 -2
- package/right-suite-contract.test.mjs +15 -9
- package/upload-large.mjs +0 -0
package/cli/right-release.mjs
CHANGED
|
@@ -39,6 +39,8 @@ if (first === "--version" || first === "-v") {
|
|
|
39
39
|
run("release.mjs", rest.includes("--upload") || rest.includes("--publish") ? rest : [...rest, "--upload"]);
|
|
40
40
|
} else if (first === "lsclean") {
|
|
41
41
|
runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
|
|
42
|
+
} else if (first === "generate-dmg-background") {
|
|
43
|
+
runBinary("python3", [path.join(packageRoot, "generate-dmg-background.py"), ...args.slice(1)]);
|
|
42
44
|
} else if (commands.has(first)) {
|
|
43
45
|
run(commands.get(first), args.slice(1));
|
|
44
46
|
} else if (first.startsWith("-")) {
|
|
@@ -102,6 +104,7 @@ Commands:
|
|
|
102
104
|
deps --check|--audit|--update Shared dependency lane
|
|
103
105
|
hardening <artifact...> Run the Right Suite hardening scan
|
|
104
106
|
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
|
107
|
+
generate-dmg-background <options> Generate the branded multi-resolution DMG background
|
|
105
108
|
|
|
106
109
|
Direct flags are treated as: right-release release <flags>.
|
|
107
110
|
Publishing requires an explicit tier. Unsigned/local smoke builds stay app-local unless declared in right-release.config.mjs.`);
|
package/deps.mjs
CHANGED
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from PIL import Image, ImageDraw, ImageFilter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def rgb(value: str) -> tuple[int, int, int]:
|
|
11
|
+
value = value.removeprefix("#")
|
|
12
|
+
if len(value) != 6:
|
|
13
|
+
raise argparse.ArgumentTypeError("colors must be six-digit hex values")
|
|
14
|
+
return tuple(int(value[i:i + 2], 16) for i in (0, 2, 4))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def make(width: int, height: int, scale: int, background: tuple[int, int, int], header: tuple[int, int, int], accent: tuple[int, int, int], icon_path: Path) -> Image.Image:
|
|
18
|
+
image = Image.new("RGBA", (width, height), (*background, 255))
|
|
19
|
+
draw = ImageDraw.Draw(image)
|
|
20
|
+
band = 86 * scale
|
|
21
|
+
draw.rectangle((0, 0, width, band), fill=(*header, 255))
|
|
22
|
+
draw.rectangle((0, band - 2 * scale, width, band), fill=(*accent, 255))
|
|
23
|
+
arrow = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
|
24
|
+
ad = ImageDraw.Draw(arrow)
|
|
25
|
+
stroke = 4 * scale
|
|
26
|
+
ad.line([(315 * scale, 231 * scale), (463 * scale, 231 * scale)], fill=(*accent, 255), width=stroke)
|
|
27
|
+
ad.line([(438 * scale, 205 * scale), (466 * scale, 231 * scale), (438 * scale, 257 * scale)], fill=(*accent, 255), width=stroke, joint="curve")
|
|
28
|
+
image.alpha_composite(arrow.filter(ImageFilter.GaussianBlur(scale)))
|
|
29
|
+
image.alpha_composite(arrow)
|
|
30
|
+
if icon_path.exists():
|
|
31
|
+
icon = Image.open(icon_path).convert("RGBA").resize((48 * scale, 48 * scale), Image.Resampling.LANCZOS)
|
|
32
|
+
image.alpha_composite(icon, (34 * scale, 20 * scale))
|
|
33
|
+
return image.convert("RGB")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main() -> None:
|
|
37
|
+
p = argparse.ArgumentParser()
|
|
38
|
+
p.add_argument("--output-dir", required=True, type=Path)
|
|
39
|
+
p.add_argument("--icon", required=True, type=Path)
|
|
40
|
+
p.add_argument("--background", required=True, type=rgb)
|
|
41
|
+
p.add_argument("--header", required=True, type=rgb)
|
|
42
|
+
p.add_argument("--accent", required=True, type=rgb)
|
|
43
|
+
a = p.parse_args()
|
|
44
|
+
a.output_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
low, high, tiff = (a.output_dir / name for name in ("background.png", "background@2x.png", "background.tiff"))
|
|
46
|
+
make(660, 400, 1, a.background, a.header, a.accent, a.icon).save(low)
|
|
47
|
+
make(1320, 800, 2, a.background, a.header, a.accent, a.icon).save(high)
|
|
48
|
+
subprocess.run(["tiffutil", "-cathidpicheck", str(low), str(high), "-out", str(tiff)], check=True)
|
|
49
|
+
print(tiff)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
package/lsclean.sh
CHANGED
|
File without changes
|
package/notary-auth.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function notarytoolAuthArgs({ profile = "apple-dev-notary" } = {}) {
|
|
5
|
+
const keyPath = process.env.APPLE_API_KEY_PATH?.trim();
|
|
6
|
+
const keyId = process.env.APPLE_API_KEY?.trim();
|
|
7
|
+
const issuer = process.env.APPLE_API_ISSUER?.trim();
|
|
8
|
+
const apiValues = [keyPath, keyId, issuer];
|
|
9
|
+
|
|
10
|
+
if (apiValues.every(Boolean)) {
|
|
11
|
+
return ["--key", expandHome(keyPath), "--key-id", keyId, "--issuer", issuer];
|
|
12
|
+
}
|
|
13
|
+
if (apiValues.some(Boolean)) {
|
|
14
|
+
throw new Error("Notarization API auth requires APPLE_API_KEY_PATH, APPLE_API_KEY, and APPLE_API_ISSUER together");
|
|
15
|
+
}
|
|
16
|
+
return ["--keychain-profile", profile];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function expandHome(value) {
|
|
20
|
+
return value === "~" ? os.homedir() : value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
5
|
+
|
|
6
|
+
test("prefers a complete App Store Connect API credential set", () => {
|
|
7
|
+
const before = { ...process.env };
|
|
8
|
+
try {
|
|
9
|
+
process.env.APPLE_API_KEY_PATH = "~/AuthKey.p8";
|
|
10
|
+
process.env.APPLE_API_KEY = "KEY123";
|
|
11
|
+
process.env.APPLE_API_ISSUER = "ISSUER123";
|
|
12
|
+
assert.deepEqual(notarytoolAuthArgs(), ["--key", `${os.homedir()}/AuthKey.p8`, "--key-id", "KEY123", "--issuer", "ISSUER123"]);
|
|
13
|
+
} finally {
|
|
14
|
+
process.env = before;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("falls back to the device keychain profile and rejects partial API auth", () => {
|
|
19
|
+
const before = { ...process.env };
|
|
20
|
+
try {
|
|
21
|
+
delete process.env.APPLE_API_KEY_PATH;
|
|
22
|
+
delete process.env.APPLE_API_KEY;
|
|
23
|
+
delete process.env.APPLE_API_ISSUER;
|
|
24
|
+
assert.deepEqual(notarytoolAuthArgs(), ["--keychain-profile", "apple-dev-notary"]);
|
|
25
|
+
process.env.APPLE_API_KEY = "KEY123";
|
|
26
|
+
assert.throws(() => notarytoolAuthArgs(), /requires APPLE_API_KEY_PATH.*APPLE_API_ISSUER/);
|
|
27
|
+
} finally {
|
|
28
|
+
process.env = before;
|
|
29
|
+
}
|
|
30
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
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": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"cli",
|
|
11
11
|
"*.mjs",
|
|
12
|
-
"*.sh"
|
|
12
|
+
"*.sh",
|
|
13
|
+
"*.py"
|
|
13
14
|
],
|
|
14
15
|
"sideEffects": false,
|
|
15
16
|
"scripts": {
|
|
@@ -20,5 +21,10 @@
|
|
|
20
21
|
"registry": "https://registry.npmjs.org/",
|
|
21
22
|
"access": "public"
|
|
22
23
|
},
|
|
23
|
-
"
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/adrdsouza/claude.git",
|
|
27
|
+
"directory": "tools/rightkit/packages/release"
|
|
28
|
+
},
|
|
29
|
+
"packageManager": "pnpm@11.12.0"
|
|
24
30
|
}
|
package/prune-r2.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID || "03ae77ccd7a07bcbb2dcfde47fa7ba3a";
|
|
2
|
+
const BUCKETS = { public: "rightapps-downloads", private: "rightapps-updates" };
|
|
3
|
+
|
|
4
|
+
export async function pruneReleaseObjects({ app, platform, keep, dryRun = false }) {
|
|
5
|
+
if (!new Set(["mac", "windows"]).has(platform)) throw new Error(`invalid R2 prune platform: ${platform}`);
|
|
6
|
+
for (const [alias, bucket] of Object.entries(BUCKETS)) {
|
|
7
|
+
const retained = new Set(keep[alias] ?? []);
|
|
8
|
+
if (dryRun) {
|
|
9
|
+
console.log(`[dry-run] PRUNE ${bucket}/${app}/${platform} keep=${[...retained].sort().join(",") || "<none>"}`);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
13
|
+
if (!token) throw new Error("CLOUDFLARE_API_TOKEN is required to prune stale R2 release objects");
|
|
14
|
+
for (const key of await listObjects(bucket, app, token)) {
|
|
15
|
+
if (!isReleaseObject(app, platform, key) || retained.has(key)) continue;
|
|
16
|
+
console.log(`DELETE ${bucket}/${key}`);
|
|
17
|
+
await deleteObject(bucket, key, token);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isReleaseObject(app, platform, key) {
|
|
23
|
+
if (!key.startsWith(`${app}/`)) return false;
|
|
24
|
+
const relative = key.slice(app.length + 1);
|
|
25
|
+
const segments = relative.split("/");
|
|
26
|
+
if (!segments.includes(platform)) return false;
|
|
27
|
+
if (/^(?:installers|updates|updaters|releases)\//.test(relative)) return true;
|
|
28
|
+
return /^v?\d+\.\d+\.\d+(?:[-+][^/]+)?\//.test(relative);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function listObjects(bucket, app, token) {
|
|
32
|
+
const objects = [];
|
|
33
|
+
let cursor = "";
|
|
34
|
+
do {
|
|
35
|
+
const url = new URL(`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/r2/buckets/${bucket}/objects`);
|
|
36
|
+
url.searchParams.set("prefix", `${app}/`);
|
|
37
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
38
|
+
const response = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
|
|
39
|
+
const body = await response.json().catch(() => ({}));
|
|
40
|
+
if (!response.ok || body.success === false) throw new Error(`R2 list failed for ${bucket}: ${response.status}`);
|
|
41
|
+
const page = Array.isArray(body.result) ? body.result : body.result?.objects ?? [];
|
|
42
|
+
objects.push(...page.map((entry) => entry.key).filter(Boolean));
|
|
43
|
+
cursor = body.result_info?.cursor || body.result?.cursor || "";
|
|
44
|
+
} while (cursor);
|
|
45
|
+
return objects;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function deleteObject(bucket, key, token) {
|
|
49
|
+
const encoded = key.split("/").map(encodeURIComponent).join("/");
|
|
50
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/r2/buckets/${bucket}/objects/${encoded}`, {
|
|
51
|
+
method: "DELETE",
|
|
52
|
+
headers: { authorization: `Bearer ${token}` },
|
|
53
|
+
});
|
|
54
|
+
const body = await response.json().catch(() => ({}));
|
|
55
|
+
if (!response.ok || body.success === false) throw new Error(`R2 delete failed for ${bucket}/${key}: ${response.status}`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { isReleaseObject } from "./prune-r2.mjs";
|
|
4
|
+
|
|
5
|
+
test("pruning only targets release objects for the active OS", () => {
|
|
6
|
+
assert.equal(isReleaseObject("viewright", "mac", "viewright/installers/mac/ViewRight.dmg"), true);
|
|
7
|
+
assert.equal(isReleaseObject("viewright", "mac", "viewright/updates/mac/current/ViewRight.app.tar.gz"), true);
|
|
8
|
+
assert.equal(isReleaseObject("viewright", "mac", "viewright/installers/windows/ViewRight-Setup.exe"), false);
|
|
9
|
+
assert.equal(isReleaseObject("viewright", "windows", "viewright/installers/mac/ViewRight.dmg"), false);
|
|
10
|
+
assert.equal(isReleaseObject("viewright", "mac", "viewright/models/mac/model.bin"), false);
|
|
11
|
+
assert.equal(isReleaseObject("viewright", "mac", "other/installers/mac/Other.dmg"), false);
|
|
12
|
+
});
|
package/publish-update.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { access, readFile, stat } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { pruneReleaseObjects } from "./prune-r2.mjs";
|
|
7
8
|
|
|
8
9
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
|
|
@@ -45,6 +46,7 @@ if (tier === "patch") {
|
|
|
45
46
|
const platforms = {};
|
|
46
47
|
const registrations = new Map();
|
|
47
48
|
const uploadedKeys = new Set();
|
|
49
|
+
const uploadedSignatures = new Set();
|
|
48
50
|
for (const artifact of artifacts) {
|
|
49
51
|
assertCurrentKey(artifact.key, "updater");
|
|
50
52
|
const installerKey = patchInstallerKey(artifact);
|
|
@@ -60,6 +62,11 @@ if (tier === "patch") {
|
|
|
60
62
|
if (!dryRun) await run(process.execPath, [UPLOAD, file, installerKey, "public"], root);
|
|
61
63
|
uploadedKeys.add(installerKey);
|
|
62
64
|
}
|
|
65
|
+
if (!uploadedSignatures.has(`${installerKey}.sig`)) {
|
|
66
|
+
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installerKey}.sig`);
|
|
67
|
+
if (!dryRun) await run(process.execPath, [UPLOAD, signatureFile, `${installerKey}.sig`, "public"], root);
|
|
68
|
+
uploadedSignatures.add(`${installerKey}.sig`);
|
|
69
|
+
}
|
|
63
70
|
const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
|
|
64
71
|
const artifactKey = artifact.artifactKey || installerKey.replaceAll("/", "__");
|
|
65
72
|
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
@@ -79,6 +86,15 @@ if (tier === "patch") {
|
|
|
79
86
|
}
|
|
80
87
|
if (!artifacts.length) {
|
|
81
88
|
console.log(`${dryRun ? "[dry-run] " : ""}NO patch updater artifacts; skipping patch manifest registration`);
|
|
89
|
+
await pruneReleaseObjects({
|
|
90
|
+
app: config.app,
|
|
91
|
+
platform: platform === "mac" ? "mac" : "windows",
|
|
92
|
+
dryRun,
|
|
93
|
+
keep: {
|
|
94
|
+
public: installers.map((artifact) => artifact.key),
|
|
95
|
+
private: target.updater.artifacts.flatMap((artifact) => [artifact.key, `${artifact.key}.sig`]),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
82
98
|
process.exit(0);
|
|
83
99
|
}
|
|
84
100
|
const version = config.version || target.updater.version;
|
|
@@ -87,7 +103,7 @@ if (tier === "patch") {
|
|
|
87
103
|
appKey: config.app,
|
|
88
104
|
version,
|
|
89
105
|
channel: config.channel || "stable",
|
|
90
|
-
platform:
|
|
106
|
+
platform: platform === "mac" ? "darwin" : "windows",
|
|
91
107
|
manifest: {
|
|
92
108
|
version,
|
|
93
109
|
tier,
|
|
@@ -100,6 +116,15 @@ if (tier === "patch") {
|
|
|
100
116
|
console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}/v1/admin/apps/patches`);
|
|
101
117
|
console.log(JSON.stringify(body));
|
|
102
118
|
if (!dryRun) await register("/v1/admin/apps/patches", body);
|
|
119
|
+
await pruneReleaseObjects({
|
|
120
|
+
app: config.app,
|
|
121
|
+
platform: platform === "mac" ? "mac" : "windows",
|
|
122
|
+
dryRun,
|
|
123
|
+
keep: {
|
|
124
|
+
public: [...installers.map((artifact) => artifact.key), ...registrations.keys(), ...[...registrations.keys()].map((key) => `${key}.sig`)],
|
|
125
|
+
private: [],
|
|
126
|
+
},
|
|
127
|
+
});
|
|
103
128
|
process.exit(0);
|
|
104
129
|
}
|
|
105
130
|
|
|
@@ -108,6 +133,7 @@ if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no u
|
|
|
108
133
|
const platforms = {};
|
|
109
134
|
const registrations = new Map();
|
|
110
135
|
const uploadedKeys = new Set();
|
|
136
|
+
const uploadedSignatures = new Set();
|
|
111
137
|
for (const artifact of artifacts) {
|
|
112
138
|
assertCurrentKey(artifact.key, "updater");
|
|
113
139
|
const file = path.resolve(root, artifact.file);
|
|
@@ -121,6 +147,11 @@ for (const artifact of artifacts) {
|
|
|
121
147
|
if (!dryRun) await run(process.execPath, [UPLOAD, file, artifact.key, "private"], root);
|
|
122
148
|
uploadedKeys.add(artifact.key);
|
|
123
149
|
}
|
|
150
|
+
if (!uploadedSignatures.has(`${artifact.key}.sig`)) {
|
|
151
|
+
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD private/${artifact.key}.sig`);
|
|
152
|
+
if (!dryRun) await run(process.execPath, [UPLOAD, signatureFile, `${artifact.key}.sig`, "private"], root);
|
|
153
|
+
uploadedSignatures.add(`${artifact.key}.sig`);
|
|
154
|
+
}
|
|
124
155
|
|
|
125
156
|
const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
|
|
126
157
|
const artifactKey = artifact.artifactKey || artifact.key.replaceAll("/", "__");
|
|
@@ -138,7 +169,7 @@ const body = {
|
|
|
138
169
|
appKey: config.app,
|
|
139
170
|
version,
|
|
140
171
|
channel: config.channel || "stable",
|
|
141
|
-
platform:
|
|
172
|
+
platform: platform === "mac" ? "darwin" : "windows",
|
|
142
173
|
manifest: {
|
|
143
174
|
version,
|
|
144
175
|
tier,
|
|
@@ -153,6 +184,15 @@ console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}${route}`);
|
|
|
153
184
|
console.log(JSON.stringify(body));
|
|
154
185
|
|
|
155
186
|
if (!dryRun) await register(route, body);
|
|
187
|
+
await pruneReleaseObjects({
|
|
188
|
+
app: config.app,
|
|
189
|
+
platform: platform === "mac" ? "mac" : "windows",
|
|
190
|
+
dryRun,
|
|
191
|
+
keep: {
|
|
192
|
+
public: installers.map((artifact) => artifact.key),
|
|
193
|
+
private: [...registrations.keys(), ...[...registrations.keys()].map((key) => `${key}.sig`)],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
156
196
|
|
|
157
197
|
async function fileMetadata(file) {
|
|
158
198
|
const bytes = await readFile(file);
|
package/publish-update.test.mjs
CHANGED
|
@@ -91,6 +91,10 @@ test("patch registers a free updater manifest backed by public installer-path ob
|
|
|
91
91
|
assert.match(result.stdout, /pub-6c73208d46c245a9b4881d5e02f6b618\.r2\.dev\/fixture\/installers\/windows\/current\/Fixture\.exe/);
|
|
92
92
|
assert.match(result.stdout, /POST .*\/v1\/admin\/apps\/patches/);
|
|
93
93
|
assert.match(result.stdout, /"tier":"patch"/);
|
|
94
|
+
assert.match(result.stdout, /"platform":"windows"/);
|
|
95
|
+
assert.equal((result.stdout.match(/UPLOAD public\/fixture\/installers\/windows\/current\/Fixture\.exe\.sig/g) ?? []).length, 1);
|
|
96
|
+
assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
|
|
97
|
+
assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows keep=<none>/);
|
|
94
98
|
});
|
|
95
99
|
|
|
96
100
|
test("routes feature updates to the Pro-gated release endpoint", () => {
|
|
@@ -98,8 +102,12 @@ test("routes feature updates to the Pro-gated release endpoint", () => {
|
|
|
98
102
|
assert.equal(result.status, 0, result.stderr);
|
|
99
103
|
assert.match(result.stdout, /POST .*\/v1\/admin\/apps\/releases/);
|
|
100
104
|
assert.match(result.stdout, /"tier":"update"/);
|
|
105
|
+
assert.match(result.stdout, /"platform":"windows"/);
|
|
101
106
|
assert.match(result.stdout, /UPLOAD private\/fixture\/updates\/windows\/current\/Fixture\.exe/);
|
|
102
|
-
assert.
|
|
107
|
+
assert.equal((result.stdout.match(/UPLOAD private\/fixture\/updates\/windows\/current\/Fixture\.exe\.sig/g) ?? []).length, 1);
|
|
108
|
+
assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
|
|
109
|
+
assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows/);
|
|
110
|
+
assert.doesNotMatch(result.stdout, /UPLOAD .*Fixture-Setup\.exe/);
|
|
103
111
|
});
|
|
104
112
|
|
|
105
113
|
test("allows an installer-only patch without registering an updater manifest", () => {
|
|
@@ -113,6 +121,7 @@ test("allows an installer-only patch without registering an updater manifest", (
|
|
|
113
121
|
assert.match(result.stdout, /NO patch updater artifacts/);
|
|
114
122
|
assert.doesNotMatch(result.stdout, /\/v1\/admin\/apps\/patches/);
|
|
115
123
|
assert.doesNotMatch(result.stdout, /UPLOAD private\//);
|
|
124
|
+
assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
|
|
116
125
|
});
|
|
117
126
|
|
|
118
127
|
test("refuses to publish without a valid tier", () => {
|
package/release.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
|
10
10
|
const UPLOAD_LARGE = path.resolve(TOOL_ROOT, "upload-large.mjs");
|
|
11
11
|
const SIGN_WINDOWS = path.resolve(TOOL_ROOT, "sign-windows.mjs");
|
|
12
12
|
const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
|
|
13
|
-
const VERSION = "0.2.
|
|
13
|
+
const VERSION = "0.2.10";
|
|
14
14
|
const TIERS = new Set(["patch", "update"]);
|
|
15
15
|
const RIGHTKIT_EXPECTED = new Map([
|
|
16
16
|
["@rightkit/license", "^0.1.5"],
|
|
@@ -181,8 +181,8 @@ async function validateRightKitPackageContract(root, appName) {
|
|
|
181
181
|
const packageJsonPath = path.join(root, "package.json");
|
|
182
182
|
const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
183
183
|
const scripts = pkg.scripts ?? {};
|
|
184
|
-
if (JSON.stringify(scripts).includes("../tools/right-release")) {
|
|
185
|
-
fail(`${appName ?? pkg.name ?? "app"} package.json must call the right-release bin, not
|
|
184
|
+
if (JSON.stringify(scripts).includes("../tools/right-release") || JSON.stringify(scripts).includes("../tools/rightkit/packages/release")) {
|
|
185
|
+
fail(`${appName ?? pkg.name ?? "app"} package.json must call the right-release bin, not parent-workspace release source`);
|
|
186
186
|
}
|
|
187
187
|
const rightKitDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
188
188
|
for (const [name, specifier] of Object.entries(rightKitDeps).filter(([name]) => name.startsWith("@rightkit/"))) {
|
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.5", "@rightkit/logs": "^0.1.3" },
|
|
21
|
-
devDependencies: { "@rightkit/release": "^0.2.
|
|
21
|
+
devDependencies: { "@rightkit/release": "^0.2.10" },
|
|
22
22
|
},
|
|
23
23
|
null,
|
|
24
24
|
2,
|
|
@@ -57,7 +57,7 @@ function lockFixture() {
|
|
|
57
57
|
name: "fixture-lock",
|
|
58
58
|
scripts: { "release:patch:mac": "right-release --platform mac --tier patch" },
|
|
59
59
|
dependencies: { "@rightkit/license": "^0.1.5", "@rightkit/logs": "^0.1.3" },
|
|
60
|
-
devDependencies: { "@rightkit/release": "^0.2.
|
|
60
|
+
devDependencies: { "@rightkit/release": "^0.2.10" },
|
|
61
61
|
},
|
|
62
62
|
null,
|
|
63
63
|
2,
|
|
@@ -4,17 +4,17 @@ import path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
|
-
const workspace = path.resolve(new URL("
|
|
7
|
+
const workspace = path.resolve(new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
|
|
8
8
|
const pubkey = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI5Mzk1RjlGRjQ2NjI2MUQKUldRZEptYjBuMTg1S1VSUXlBdFM4WmtzaHArYko0U2hRMDVlSDJmSExVZG82Q0hoQ2srUlhqanAK";
|
|
9
9
|
const licensePackage = "^0.1.5";
|
|
10
10
|
const logsPackage = "^0.1.3";
|
|
11
|
-
const releasePackage = "^0.2.
|
|
11
|
+
const releasePackage = "^0.2.10";
|
|
12
12
|
const apps = [
|
|
13
|
-
{ key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json",
|
|
14
|
-
{ key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json",
|
|
15
|
-
{ key: "heardright", root: "heardright/tauri-app-next", tauri: "src-tauri/tauri.conf.json",
|
|
16
|
-
{ key: "mailright", root: "mailright", tauri: "src-tauri/tauri.conf.json",
|
|
17
|
-
{ key: "coderight", root: "coderight/apps/coderight-tauri", tauri: "src-tauri/tauri.conf.json",
|
|
13
|
+
{ key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/build-mac-notarized.sh"] },
|
|
14
|
+
{ key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["package.sh"] },
|
|
15
|
+
{ key: "heardright", root: "heardright/tauri-app-next", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs", "scripts/publish-release.mjs"] },
|
|
16
|
+
{ key: "mailright", root: "mailright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
|
|
17
|
+
{ key: "coderight", root: "coderight/apps/coderight-tauri", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
for (const app of apps) {
|
|
@@ -29,6 +29,7 @@ for (const app of apps) {
|
|
|
29
29
|
if (rightKitDeps["@rightkit/logs"]) assert.equal(rightKitDeps["@rightkit/logs"], logsPackage);
|
|
30
30
|
assert.equal(pkg.devDependencies?.["@rightkit/release"], releasePackage);
|
|
31
31
|
assert(!JSON.stringify(pkg).includes("github:adrdsouza/claude#main&path:/tools/right-release"));
|
|
32
|
+
assert(!JSON.stringify(pkg).includes("github:adrdsouza/claude#main&path:/tools/rightkit/packages/release"));
|
|
32
33
|
assert(!JSON.stringify(pkg).includes("git+https://github.com/adrdsouza/rightkit.git"));
|
|
33
34
|
assert.equal(pkg.scripts["release:doctor"], "right-release doctor");
|
|
34
35
|
assert.equal(pkg.scripts["release:mac"], "right-release --platform mac");
|
|
@@ -44,6 +45,7 @@ for (const app of apps) {
|
|
|
44
45
|
assert.equal(pkg.scripts["deps:check"], "right-release deps --check");
|
|
45
46
|
assert.equal(pkg.scripts["deps:update"], "right-release deps --update");
|
|
46
47
|
assert.ok(!JSON.stringify(pkg.scripts).includes("../tools/right-release"), `${app.key} scripts must not depend on the parent Claude workspace`);
|
|
48
|
+
assert.ok(!JSON.stringify(pkg.scripts).includes("../tools/rightkit/packages/release"), `${app.key} scripts must not depend on the parent Claude workspace`);
|
|
47
49
|
|
|
48
50
|
const config = (await import(`${pathToFileURL(path.join(root, "right-release.config.mjs"))}?contract=${Date.now()}`)).default;
|
|
49
51
|
assert.equal(config.app, app.key);
|
|
@@ -63,6 +65,7 @@ for (const app of apps) {
|
|
|
63
65
|
assert.match(updater.key, /\/updates\/(mac|windows)\/current\//, `${app.key} ${platform} updaters must replace the stable current R2 object`);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
68
|
+
assert.match(config.targets.mac.package.args.join(" "), /mac:dmg:notarized|--notarize/, `${app.key} publish packaging must use the notarized macOS lane`);
|
|
66
69
|
assert.ok(config.targets.win.sign.files.length);
|
|
67
70
|
const winUpdaterFiles = new Set(config.targets.win.updater.artifacts.map((artifact) => artifact.file));
|
|
68
71
|
for (const signed of config.targets.win.sign.files) assert.ok(winUpdaterFiles.has(signed), `${signed} must be both Azure-signed and updater-signed`);
|
|
@@ -70,10 +73,13 @@ for (const app of apps) {
|
|
|
70
73
|
const tauri = JSON.parse(readFileSync(path.join(root, app.tauri), "utf8"));
|
|
71
74
|
assert.equal(tauri.plugins.updater.pubkey, pubkey);
|
|
72
75
|
assert.deepEqual(tauri.plugins.updater.endpoints, [
|
|
73
|
-
`https
|
|
74
|
-
`https://${app.host}/releases/latest.json`,
|
|
76
|
+
`https://api.spoares.com/v1/apps/${app.key}/releases/latest.json?platform={{target}}`,
|
|
75
77
|
]);
|
|
76
78
|
assert.equal(tauri.bundle.createUpdaterArtifacts, true);
|
|
79
|
+
for (const releaseFile of app.releaseFiles) {
|
|
80
|
+
const source = readFileSync(path.join(root, releaseFile), "utf8");
|
|
81
|
+
assert.doesNotMatch(source, /(?:\.\.\/)+tools\/right-release|tools\/right-release\//, `${app.key} ${releaseFile} must consume the installed package`);
|
|
82
|
+
}
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
|
package/upload-large.mjs
CHANGED
|
File without changes
|