@rightkit/release 0.2.61 → 0.2.63
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/build-release.mjs +86 -22
- package/build-release.test.mjs +31 -2
- package/cargo-contract.mjs +3 -2
- package/cargo-guard.test.mjs +1 -0
- package/cli/right-release.mjs +2 -2
- package/github-release.mjs +25 -10
- package/heavy-command.mjs +27 -11
- package/heavy-command.test.mjs +49 -0
- package/package.json +4 -3
- package/process-liveness.mjs +39 -0
- package/release-cli-contract.test.mjs +17 -6
- package/release-state.mjs +27 -3
- package/release-state.test.mjs +48 -6
- package/release.mjs +15 -15
- package/release.test.mjs +24 -2
- package/right-suite-contract.test.mjs +22 -6
- package/rightkit-versions.json +5 -2
- package/standalone-clone-evidence.json +32 -0
- package/target-bridge.mjs +2 -2
- package/target-bridge.test.mjs +17 -0
- package/upload-release.mjs +36 -170
package/upload-release.mjs
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import {
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { prepareGitHubRelease, publishGitHubRelease, repositoryFromRemote } from "./github-release.mjs";
|
|
7
7
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { verifySealedRelease } from "./release-state.mjs";
|
|
9
9
|
import { assertCleanSource } from "./source-gate.mjs";
|
|
10
10
|
|
|
11
|
-
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const PUBLISH_UPDATE = path.join(TOOL_ROOT, "publish-update.mjs");
|
|
13
|
-
const HARDENING = path.join(TOOL_ROOT, "hardeningscan.mjs");
|
|
14
|
-
const SIGN_WINDOWS = path.join(TOOL_ROOT, "sign-windows.mjs");
|
|
15
|
-
const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
|
|
16
|
-
const PUBLIC_BASE = (process.env.RIGHTAPPS_PUBLIC_DOWNLOAD_BASE || "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev").replace(/\/$/, "");
|
|
17
|
-
const API_BASE = (process.env.RIGHTAPPS_API_URL || "https://api.spoares.com").replace(/\/$/, "");
|
|
18
11
|
const args = process.argv.slice(2);
|
|
19
12
|
let platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : process.platform;
|
|
20
13
|
let releaseId = "";
|
|
21
14
|
let tier = "";
|
|
15
|
+
let repo = "";
|
|
16
|
+
let configName = "right-release.config.mjs";
|
|
22
17
|
let dryRun = false;
|
|
23
18
|
|
|
24
|
-
for (let
|
|
25
|
-
const arg = args[
|
|
26
|
-
if (arg === "--platform") platform = args[++
|
|
27
|
-
else if (arg === "--release") releaseId = args[++
|
|
28
|
-
else if (arg === "--tier") tier = args[++
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const arg = args[index];
|
|
21
|
+
if (arg === "--platform") platform = args[++index];
|
|
22
|
+
else if (arg === "--release") releaseId = args[++index];
|
|
23
|
+
else if (arg === "--tier") tier = args[++index];
|
|
29
24
|
else if (arg.startsWith("--tier=")) tier = arg.slice("--tier=".length);
|
|
25
|
+
else if (arg === "--repo") repo = args[++index];
|
|
26
|
+
else if (arg === "--config") configName = args[++index];
|
|
30
27
|
else if (arg === "--dry-run") dryRun = true;
|
|
31
28
|
else if (arg === "-h" || arg === "--help") usage(0);
|
|
32
29
|
else fail(`unknown argument: ${arg}`);
|
|
@@ -43,183 +40,52 @@ assertCleanSource({
|
|
|
43
40
|
commandId: "right-release upload",
|
|
44
41
|
});
|
|
45
42
|
const platformDir = platform === "win" ? "windows" : "mac";
|
|
46
|
-
const
|
|
47
|
-
const sealed = verifySealedRelease(sealedDir);
|
|
43
|
+
const sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", releaseId, platformDir));
|
|
48
44
|
if (sealed.manifest.commit !== candidateCommit) fail("sealed release was built from a different commit than the current checkout");
|
|
45
|
+
|
|
49
46
|
const stateRoot = path.join(repoRoot, ".right-release", "state", releaseId, platformDir);
|
|
50
|
-
const backupRoot = path.join(stateRoot, "rollback", tier);
|
|
51
47
|
const verifiedMarker = path.join(stateRoot, `verified-${tier}.json`);
|
|
52
48
|
if (existsSync(verifiedMarker)) {
|
|
53
49
|
console.log(`right-release upload: already verified ${releaseId} tier=${tier}`);
|
|
54
50
|
process.exit(0);
|
|
55
51
|
}
|
|
56
|
-
if (!dryRun && !process.env.CLOUDFLARE_API_TOKEN) fail("CLOUDFLARE_API_TOKEN is required before R2 mutation");
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
const configPath = path.resolve(configName);
|
|
54
|
+
if (existsSync(configPath)) {
|
|
55
|
+
const releaseConfig = (await import(`${pathToFileURL(configPath).href}?upload=${Date.now()}`)).default;
|
|
56
|
+
if (releaseConfig?.distribution?.provider && releaseConfig.distribution.provider !== "github-releases") {
|
|
57
|
+
fail("release config distribution.provider must be github-releases");
|
|
58
|
+
}
|
|
59
|
+
repo ||= releaseConfig?.distribution?.repository ?? "";
|
|
60
|
+
}
|
|
61
|
+
repo ||= repositoryFromRemote(repoRoot);
|
|
62
|
+
const plan = prepareGitHubRelease({ repoRoot, releaseId, platform, repo });
|
|
63
|
+
const result = publishGitHubRelease(plan, { repo, dryRun });
|
|
64
|
+
if (!dryRun) writeJson(verifiedMarker, {
|
|
65
|
+
schema: 1,
|
|
60
66
|
releaseId,
|
|
61
67
|
platform,
|
|
62
68
|
tier,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
runChecked(process.execPath, [HARDENING, ...files], repoRoot);
|
|
68
|
-
},
|
|
69
|
-
backup: async (release, routes) => backupStableObjects(release, routes),
|
|
70
|
-
upload: async (release) => publishSealed(release),
|
|
71
|
-
register: async () => {},
|
|
72
|
-
verifyRemote: async (release, routes) => verifyUploaded(release, routes),
|
|
73
|
-
restore: async (_release, routes) => restoreStableObjects(routes),
|
|
74
|
-
discardBackup: async () => rmSync(backupRoot, { recursive: true, force: true }),
|
|
75
|
-
},
|
|
69
|
+
provider: "github-releases",
|
|
70
|
+
repository: repo,
|
|
71
|
+
tag: result.tag,
|
|
72
|
+
verifiedAt: new Date().toISOString(),
|
|
76
73
|
});
|
|
77
|
-
|
|
78
|
-
console.log(`right-release upload: ${dryRun ? "dry-run " : ""}verified ${releaseId} tier=${tier}`);
|
|
79
|
-
|
|
80
|
-
function verifyPlatformTrust(release) {
|
|
81
|
-
if (dryRun) return;
|
|
82
|
-
const artifacts = release.manifest.files.filter((file) => file.role !== "updater-signature");
|
|
83
|
-
if (platform === "win") {
|
|
84
|
-
for (const artifact of artifacts) {
|
|
85
|
-
const file = path.join(release.sealedDir, artifact.name);
|
|
86
|
-
runChecked(process.execPath, [SIGN_WINDOWS, "--verify-only", file], repoRoot);
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
for (const artifact of artifacts.filter((file) => /\.dmg$/i.test(file.name))) {
|
|
90
|
-
const file = path.join(release.sealedDir, artifact.name);
|
|
91
|
-
runChecked("codesign", ["--verify", "--strict", "--verbose=2", file], repoRoot);
|
|
92
|
-
runChecked("xcrun", ["stapler", "validate", file], repoRoot);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function backupStableObjects(release, routes) {
|
|
98
|
-
if (dryRun) return;
|
|
99
|
-
if (existsSync(path.join(backupRoot, "backup-manifest.json"))) restoreStableObjects(routes);
|
|
100
|
-
mkdirSync(backupRoot, { recursive: true });
|
|
101
|
-
const entries = [];
|
|
102
|
-
for (const route of uniqueRoutes(routes)) {
|
|
103
|
-
const bucket = bucketName(route.bucket);
|
|
104
|
-
const file = path.join(backupRoot, `${createHash("sha256").update(`${bucket}/${route.key}`).digest("hex").slice(0, 12)}.bin`);
|
|
105
|
-
const result = wrangler(["r2", "object", "get", `${bucket}/${route.key}`, "--file", file, "--remote"]);
|
|
106
|
-
if (result.status === 0) entries.push({ bucket: route.bucket, key: route.key, existed: true, file: path.basename(file), sha256: hashFile(file) });
|
|
107
|
-
else if (/not found|does not exist|404/i.test(`${result.stdout}\n${result.stderr}`)) entries.push({ bucket: route.bucket, key: route.key, existed: false, file: null, sha256: null });
|
|
108
|
-
else throw new Error(`failed to back up R2 object ${bucket}/${route.key}: ${result.stderr || result.stdout}`);
|
|
109
|
-
}
|
|
110
|
-
writeJson(path.join(backupRoot, "backup-manifest.json"), { schema: 1, releaseId: release.manifest.releaseId, entries });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function publishSealed(release) {
|
|
114
|
-
if (dryRun) return;
|
|
115
|
-
const generated = path.join(stateRoot, `sealed-upload-${tier}.config.mjs`);
|
|
116
|
-
const config = sealedUploadConfig(release);
|
|
117
|
-
writeFileSync(generated, `export default ${JSON.stringify(config, null, 2)};\n`);
|
|
118
|
-
runChecked(process.execPath, [PUBLISH_UPDATE, "--config", generated, "--platform", platform], repoRoot, { ...process.env, RIGHT_RELEASE_TIER: tier });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function sealedUploadConfig(release) {
|
|
122
|
-
const fileByName = new Map(release.manifest.files.map((file) => [file.name, path.join(release.sealedDir, file.name)]));
|
|
123
|
-
const patchRoutes = release.manifest.routes.patch ?? [];
|
|
124
|
-
const updateRoutes = release.manifest.routes.update ?? [];
|
|
125
|
-
const installers = patchRoutes.filter((route) => route.role === "installer").map((route) => ({ file: fileByName.get(route.name), key: route.key }));
|
|
126
|
-
const updaterRoutes = updateRoutes.map((route) => {
|
|
127
|
-
const patch = patchRoutes.find((candidate) => candidate.name === route.name && candidate.platform === route.platform);
|
|
128
|
-
return { file: fileByName.get(route.name), signature: fileByName.get(route.signature), platform: route.platform, key: route.key, patchKey: patch?.key };
|
|
129
|
-
});
|
|
130
|
-
return {
|
|
131
|
-
schema: 1,
|
|
132
|
-
app: release.manifest.app,
|
|
133
|
-
version: release.manifest.version,
|
|
134
|
-
channel: "stable",
|
|
135
|
-
targets: { [platform]: { installer: { artifacts: installers }, updater: { artifacts: updaterRoutes } } },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function verifyUploaded(release, routes) {
|
|
140
|
-
if (dryRun) return;
|
|
141
|
-
const files = new Map(release.manifest.files.map((file) => [file.name, file]));
|
|
142
|
-
for (const route of uniqueRoutes(routes)) {
|
|
143
|
-
const expected = files.get(route.name);
|
|
144
|
-
if (!expected) throw new Error(`route references unsealed file: ${route.name}`);
|
|
145
|
-
const verifyFile = path.join(stateRoot, `remote-${createHash("sha256").update(route.key).digest("hex").slice(0, 12)}.bin`);
|
|
146
|
-
if (route.bucket === "public") {
|
|
147
|
-
const response = await fetch(`${PUBLIC_BASE}/${route.key}`, { cache: "no-store" });
|
|
148
|
-
if (!response.ok) throw new Error(`public artifact verification failed: ${response.status} ${route.key}`);
|
|
149
|
-
writeFileSync(verifyFile, Buffer.from(await response.arrayBuffer()));
|
|
150
|
-
} else {
|
|
151
|
-
const result = wrangler(["r2", "object", "get", `${bucketName(route.bucket)}/${route.key}`, "--file", verifyFile, "--remote"]);
|
|
152
|
-
if (result.status !== 0) throw new Error(`private artifact verification failed: ${route.key}`);
|
|
153
|
-
}
|
|
154
|
-
if (statSync(verifyFile).size !== expected.sizeBytes || hashFile(verifyFile) !== expected.sha256) throw new Error(`remote hash mismatch: ${route.key}`);
|
|
155
|
-
rmSync(verifyFile, { force: true });
|
|
156
|
-
}
|
|
157
|
-
if (!routes.some((route) => route.role === "updater")) return;
|
|
158
|
-
const apiPlatform = platform === "win" ? "windows-x86_64" : "darwin-aarch64";
|
|
159
|
-
const response = await fetch(`${API_BASE}/v1/apps/${release.manifest.app}/releases/latest.json?platform=${apiPlatform}`, { cache: "no-store" });
|
|
160
|
-
if (!response.ok) throw new Error(`updater API verification failed: ${response.status}`);
|
|
161
|
-
const body = await response.json();
|
|
162
|
-
if (body.version !== release.manifest.version) throw new Error(`updater API version mismatch: expected ${release.manifest.version}, got ${body.version}`);
|
|
163
|
-
const expectedRoutes = new Set(routes.map((route) => route.key));
|
|
164
|
-
const urls = Object.values(body.platforms ?? {}).map((entry) => entry.url).filter(Boolean);
|
|
165
|
-
if (!urls.some((url) => [...expectedRoutes].some((key) => url.endsWith(key)))) throw new Error("updater API returned the wrong artifact URL");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function restoreStableObjects(routes) {
|
|
169
|
-
const manifestPath = path.join(backupRoot, "backup-manifest.json");
|
|
170
|
-
if (!existsSync(manifestPath)) return;
|
|
171
|
-
const backup = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
172
|
-
for (const entry of backup.entries) {
|
|
173
|
-
if (entry.existed) {
|
|
174
|
-
const file = path.join(backupRoot, entry.file);
|
|
175
|
-
if (hashFile(file) !== entry.sha256) throw new Error(`rollback copy hash mismatch: ${entry.key}`);
|
|
176
|
-
runChecked(process.execPath, [UPLOAD, file, entry.key, entry.bucket], repoRoot);
|
|
177
|
-
} else {
|
|
178
|
-
const result = wrangler(["r2", "object", "delete", `${bucketName(entry.bucket)}/${entry.key}`, "--remote"]);
|
|
179
|
-
if (result.status !== 0 && !/not found|404/i.test(`${result.stdout}\n${result.stderr}`)) throw new Error(`rollback delete failed: ${entry.key}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
rmSync(backupRoot, { recursive: true, force: true });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function uniqueRoutes(routes) {
|
|
186
|
-
return [...new Map(routes.map((route) => [`${route.bucket}/${route.key}`, route])).values()];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function bucketName(bucket) {
|
|
190
|
-
return bucket === "public" ? "rightapps-downloads" : "rightapps-updates";
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function wrangler(runArgs) {
|
|
194
|
-
return spawnSync("pnpm", ["dlx", "wrangler@4", ...runArgs], { cwd: repoRoot, env: process.env, encoding: "utf8", windowsHide: true, shell: process.platform === "win32" });
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function hashFile(file) {
|
|
198
|
-
return createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
199
|
-
}
|
|
74
|
+
console.log(`right-release upload: ${dryRun ? "dry-run " : ""}${result.status} ${releaseId} tier=${tier} repo=${repo}`);
|
|
200
75
|
|
|
201
76
|
function git(cwd, runArgs) {
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function commandOutput(cmd, runArgs, cwd = process.cwd()) {
|
|
206
|
-
const result = spawnSync(cmd, runArgs, { cwd, encoding: "utf8", windowsHide: true });
|
|
207
|
-
if (result.status !== 0) fail(`${cmd} ${runArgs.join(" ")} failed: ${result.stderr}`);
|
|
77
|
+
const result = spawnSync("git", runArgs, { cwd, encoding: "utf8", windowsHide: true });
|
|
78
|
+
if (result.status !== 0) fail(`git ${runArgs.join(" ")} failed: ${result.stderr}`);
|
|
208
79
|
return result.stdout.trim();
|
|
209
80
|
}
|
|
210
81
|
|
|
211
|
-
function runChecked(cmd, runArgs, cwd, env = process.env) {
|
|
212
|
-
const result = spawnSync(cmd, runArgs, { cwd, env, stdio: "inherit", windowsHide: true, shell: false });
|
|
213
|
-
if (result.status !== 0) throw new Error(`${cmd} exited ${result.status}`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
82
|
function writeJson(file, value) {
|
|
217
83
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
218
84
|
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
219
85
|
}
|
|
220
86
|
|
|
221
87
|
function usage(code) {
|
|
222
|
-
console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--dry-run]");
|
|
88
|
+
console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.mjs] [--repo owner/repo] [--dry-run]");
|
|
223
89
|
process.exit(code);
|
|
224
90
|
}
|
|
225
91
|
|