返回 CodeWhale
sync-latest-release.mjs
根目录 / web / scripts / sync-latest-release.mjs
1 #!/usr/bin/env node
2 // Refresh the checked-in "latest published release" fact from the real GitHub
3 // release. The fact is mirrored in two places and BOTH must move together:
4 //
5 // web/data/latest-published-release.json (read by derive-facts.mjs)
6 // docs/public-surface-facts.json (latestPublishedRelease, which
7 // names the file above as its
8 // `sources`)
9 //
10 // web/lib/public-surface-contract.test.ts asserts the two agree, so updating
11 // only the first turns a stale marketing fact into a red Lint & Type Check.
12 //
13 // Facts must be derivable from the repo with no network (derive-facts.mjs reads
14 // this file, it does not call GitHub), so the file is checked in. Nothing wrote
15 // it, which is why it drifted: the marketing deploy's post-deploy comparison
16 // failed on latestPublishedRelease.tag because this said v0.9.10 while the
17 // published release was v0.9.11.
18 //
19 // node web/scripts/sync-latest-release.mjs # write if changed
20 // node web/scripts/sync-latest-release.mjs --check # exit 1 if stale
21 //
22 // --check is the CI form: it makes drift a failing gate at PR time instead of a
23 // surprise after a production deploy.
24
25 import { readFileSync, writeFileSync } from "node:fs";
26 import { fileURLToPath } from "node:url";
27 import { dirname, resolve } from "node:path";
28
29 const REPO = "Hmbown/CodeWhale";
30 const here = dirname(fileURLToPath(import.meta.url));
31 const target = resolve(here, "..", "data", "latest-published-release.json");
32 const mirror = resolve(here, "..", "..", "docs", "public-surface-facts.json");
33 const checkOnly = process.argv.includes("--check");
34
35 const headers = {
36 accept: "application/vnd.github+json",
37 "user-agent": "codewhale-facts-sync",
38 };
39 if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
40
41 const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { headers });
42 if (!response.ok) {
43 console.error(`[sync-latest-release] GitHub returned ${response.status}; leaving the file alone.`);
44 process.exit(checkOnly ? 0 : 1);
45 }
46 const release = await response.json();
47
48 const tag = String(release.tag_name || "");
49 const version = tag.startsWith("v") ? tag.slice(1) : "";
50 const next = {
51 tag,
52 version,
53 publishedAt: String(release.published_at || ""),
54 url: `https://github.com/${REPO}/releases/tag/${tag}`,
55 };
56
57 // deriveLatestPublishedRelease() silently returns null on any shape violation,
58 // which would drop the fact entirely rather than report a bad one. Fail loudly.
59 if (!tag || !version || tag !== `v${version}` || !Number.isFinite(Date.parse(next.publishedAt))) {
60 console.error(`[sync-latest-release] refusing to write an unusable release fact: ${JSON.stringify(next)}`);
61 process.exit(1);
62 }
63
64 const readJson = (path) => {
65 try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
66 };
67
68 const current = readJson(target);
69 const matrix = readJson(mirror);
70 const currentMirror = matrix?.latestPublishedRelease ?? null;
71
72 const isCurrent = (fact) =>
73 Boolean(fact) && fact.tag === next.tag && fact.publishedAt === next.publishedAt;
74
75 if (isCurrent(current) && isCurrent(currentMirror)) {
76 console.log(`[sync-latest-release] already current at ${next.tag}`);
77 process.exit(0);
78 }
79
80 if (checkOnly) {
81 if (!isCurrent(current)) {
82 console.error(
83 `[sync-latest-release] stale: ${target} says ${current?.tag ?? "(missing)"}, GitHub says ${next.tag}`,
84 );
85 }
86 if (!isCurrent(currentMirror)) {
87 console.error(
88 `[sync-latest-release] stale: docs/public-surface-facts.json says ${currentMirror?.tag ?? "(missing)"}, GitHub says ${next.tag}`,
89 );
90 }
91 console.error("Run: npm --prefix web run sync:latest-release && npm --prefix web run build");
92 process.exit(1);
93 }
94
95 writeFileSync(target, `${JSON.stringify(next, null, 2)}\n`);
96
97 if (!matrix) {
98 console.error(`[sync-latest-release] could not read ${mirror}; the mirror is now stale.`);
99 process.exit(1);
100 }
101
102 // Preserve every key the matrix carries beyond the four synced fields (notably
103 // `sources`), so this stays a fact refresh and not a schema rewrite.
104 matrix.latestPublishedRelease = { ...currentMirror, ...next };
105 writeFileSync(mirror, `${JSON.stringify(matrix, null, 2)}\n`);
106
107 console.log(`[sync-latest-release] wrote ${next.tag} (${next.publishedAt}) to both facts`);
108
108 lines Plain Text