Skip to content

Commit 2f7db89

Browse files
committed
feat(roadmap): /roadmap generated from the engine repo's GitHub milestones
Takes the page and not the mechanism from Jesse's Resources menu: the keel repo's milestones already are the real roadmap and are actively maintained, so the site reads them — no voting, no accounts, no sponsorship queue, none of which exist for a 3-star project whose pitch is that it tells you the truth. - scripts/fetch-milestones.mjs reads open + recently closed milestones and their issues/PRs via the public REST endpoints (GITHUB_TOKEN honoured for rate-limit headroom), writing gitignored data/milestones.json; chained into the fetch script. Same failure policy as fetch-release/fetch-discussions: a failed fetch never breaks the build — it degrades to last-known data and finally to a plain link to the GitHub milestones page. - /{en,ar,fr}/roadmap/ follows the page+component+i18n structure; nav entry after Community in all three locales. Milestones render open → recently shipped, every item links to its issue, closed milestones marked shipped with GitHub's own closed date. - Honesty: an open milestone is an intention, not a commitment, and a due date is a target, not a promise (FR-9); undated milestones say "no date set" instead of inventing one; a doc-meta line notes the roadmap is read from the repo and refreshes hourly. Closes #117
1 parent b8c8e50 commit 2f7db89

11 files changed

Lines changed: 618 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"version": "0.1.0",
66
"private": true,
77
"scripts": {
8-
"fetch": "node scripts/fetch-release.mjs && node scripts/fetch-engine-docs.mjs && node scripts/fetch-discussions.mjs",
8+
"fetch": "node scripts/fetch-release.mjs && node scripts/fetch-engine-docs.mjs && node scripts/fetch-discussions.mjs && node scripts/fetch-milestones.mjs",
99
"dev": "npm run fetch && astro dev",
1010
"build": "npm run fetch && astro build",
1111
"preview": "astro preview",

scripts/fetch-milestones.mjs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env node
2+
/**
3+
* #117 — the Roadmap page's data feed.
4+
*
5+
* Reads the keel repo's GitHub milestones (open + recently closed) and the
6+
* issues/PRs under each, via the public REST endpoints, and writes
7+
* data/milestones.json. The roadmap is a read of the repo's own working
8+
* plan — no voting, no accounts, no subscribe box (#117's non-goals).
9+
*
10+
* Failure policy (same as fetch-release / fetch-discussions): a failed
11+
* fetch never breaks the build. It degrades to the last known data and
12+
* finally to an empty stub, at which point the Roadmap page shows a plain
13+
* link to the GitHub milestones page.
14+
*/
15+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
16+
import { fileURLToPath } from "node:url";
17+
import { dirname, join } from "node:path";
18+
19+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
20+
const OUT = join(root, "data/milestones.json");
21+
mkdirSync(join(root, "data"), { recursive: true });
22+
23+
const REPO_API = "https://api.github.com/repos/CodeGateSoftware/keel";
24+
const MILESTONES_URL = "https://github.com/CodeGateSoftware/keel/milestones";
25+
/** Open milestones first (that's the roadmap), then the recently shipped. */
26+
const OPEN_API = `${REPO_API}/milestones?state=open&per_page=50`;
27+
const CLOSED_API = `${REPO_API}/milestones?state=closed&per_page=10`;
28+
/** Issues *and* PRs under one milestone — the issues endpoint returns both. */
29+
const itemsApi = (number) => `${REPO_API}/issues?milestone=${number}&state=all&per_page=100`;
30+
31+
/** Sane cap: the biggest milestone to date holds ~15 items; 30 leaves room
32+
* without ever writing the whole repo into data/. The milestone's own
33+
* open/closed counts stay in the output, so the page can say "and N more". */
34+
const MAX_ITEMS_PER_MILESTONE = 30;
35+
/** Recently-shipped section: enough to show momentum, not a full history. */
36+
const MAX_CLOSED_MILESTONES = 5;
37+
38+
const headers = {
39+
accept: "application/vnd.github+json",
40+
"user-agent": "keeltrading.com-milestones-fetch",
41+
};
42+
if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
43+
44+
async function getJson(url) {
45+
const response = await fetch(url, { headers });
46+
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
47+
return response.json();
48+
}
49+
50+
/** One milestone, shaped for the page. Items are capped; the full counts
51+
* (openIssues + closedIssues) travel along so the page can point to GitHub
52+
* for the overflow instead of hiding it. */
53+
async function readMilestone(milestone) {
54+
let items = [];
55+
try {
56+
const raw = await getJson(itemsApi(milestone.number));
57+
items = (Array.isArray(raw) ? raw : [])
58+
.slice(0, MAX_ITEMS_PER_MILESTONE)
59+
.map((item) => ({
60+
number: item.number,
61+
title: item.title,
62+
state: item.state,
63+
isPullRequest: item.pull_request !== undefined,
64+
url: item.html_url,
65+
}));
66+
} catch (error) {
67+
// The milestone itself still renders — without its item list.
68+
console.warn(` WARN: items for milestone ${milestone.number} failed (${error.message})`);
69+
}
70+
71+
return {
72+
number: milestone.number,
73+
title: milestone.title,
74+
description: milestone.description ?? "",
75+
state: milestone.state,
76+
url: milestone.html_url,
77+
openIssues: milestone.open_issues ?? 0,
78+
closedIssues: milestone.closed_issues ?? 0,
79+
dueOn: milestone.due_on,
80+
closedAt: milestone.closed_at,
81+
items,
82+
};
83+
}
84+
85+
try {
86+
const [openRaw, closedRaw] = await Promise.all([
87+
getJson(OPEN_API),
88+
getJson(CLOSED_API),
89+
]);
90+
91+
// Reading order (#117): dated milestones by nearest target first, undated
92+
// after them (a milestone without a date is an intention, not a schedule),
93+
// then the recently shipped, newest closure first.
94+
const open = (Array.isArray(openRaw) ? openRaw : [])
95+
.slice()
96+
.sort((a, b) => {
97+
if (a.due_on && b.due_on) return a.due_on.localeCompare(b.due_on);
98+
if (a.due_on) return -1;
99+
if (b.due_on) return 1;
100+
return a.number - b.number;
101+
});
102+
const closed = (Array.isArray(closedRaw) ? closedRaw : [])
103+
.filter((m) => m.closed_at)
104+
.sort((a, b) => b.closed_at.localeCompare(a.closed_at))
105+
.slice(0, MAX_CLOSED_MILESTONES);
106+
107+
const milestones = [];
108+
for (const milestone of [...open, ...closed]) {
109+
milestones.push(await readMilestone(milestone));
110+
}
111+
112+
writeFileSync(
113+
OUT,
114+
JSON.stringify(
115+
{
116+
milestonesUrl: MILESTONES_URL,
117+
fetchedAt: new Date().toISOString(),
118+
milestones,
119+
},
120+
null,
121+
2,
122+
) + "\n",
123+
);
124+
console.log(` roadmap: ${open.length} open + ${closed.length} recently closed -> data/milestones.json`);
125+
} catch (error) {
126+
const previous = existsSync(OUT) ? JSON.parse(readFileSync(OUT, "utf8")) : null;
127+
if (previous?.milestones?.length) {
128+
console.warn(` WARN: milestones fetch failed (${error.message}); keeping last-known ${previous.milestones.length} milestones`);
129+
} else {
130+
writeFileSync(
131+
OUT,
132+
JSON.stringify(
133+
{
134+
milestonesUrl: MILESTONES_URL,
135+
fetchedAt: null,
136+
milestones: [],
137+
},
138+
null,
139+
2,
140+
) + "\n",
141+
);
142+
console.warn(` WARN: milestones fetch failed (${error.message}); Roadmap page will link to GitHub milestones`);
143+
}
144+
}

src/components/Header.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const navItems: { key: Exclude<PageKey, "home">; label: string }[] = [
1818
{ key: "news", label: chrome.nav.news },
1919
{ key: "changelog", label: chrome.nav.changelog },
2020
{ key: "community", label: chrome.nav.community },
21+
{ key: "roadmap", label: chrome.nav.roadmap },
2122
{ key: "compliance", label: chrome.nav.compliance },
2223
{ key: "compare", label: chrome.nav.compare },
2324
{ key: "about", label: chrome.nav.about },

0 commit comments

Comments
 (0)