Skip to content

Commit 16ddd9b

Browse files
committed
Add per-package roadmap page with GitHub issues integration
1 parent 9a05aac commit 16ddd9b

19 files changed

Lines changed: 632 additions & 10 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ jobs:
5555
fi
5656
done
5757
58-
# Build all versions (API, notebooks, outputs, figures)
58+
# Build all versions (API, notebooks, outputs, figures, roadmaps)
5959
# Uses smart caching: historical versions only built once, latest always rebuilt
6060
- name: Build documentation
61+
env:
62+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6163
run: python scripts/build.py
6264

6365
# Build global search and crossref indexes

scripts/build-roadmaps.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Fetch roadmap issues from GitHub and rebuild package manifests.
4+
5+
Standalone script for CI scheduled runs — only updates roadmap.json
6+
and package manifests without rebuilding API docs or notebooks.
7+
8+
Usage:
9+
python scripts/build-roadmaps.py
10+
python scripts/build-roadmaps.py --package pathsim
11+
python scripts/build-roadmaps.py --dry-run
12+
"""
13+
14+
import argparse
15+
import json
16+
import sys
17+
from pathlib import Path
18+
19+
sys.path.insert(0, str(Path(__file__).parent))
20+
21+
from lib.config import PACKAGES, STATIC_DIR
22+
from lib.roadmap import build_roadmap
23+
from lib.git import get_tag_date, parse_version
24+
25+
26+
def rebuild_package_manifest(package_id: str) -> None:
27+
"""Regenerate package manifest with updated hasRoadmap flag."""
28+
output_dir = STATIC_DIR / package_id
29+
manifest_path = output_dir / "manifest.json"
30+
31+
if not manifest_path.exists():
32+
return
33+
34+
with open(manifest_path, "r", encoding="utf-8") as f:
35+
manifest = json.load(f)
36+
37+
# Update hasRoadmap flag
38+
roadmap_path = output_dir / "roadmap.json"
39+
has_roadmap = False
40+
if roadmap_path.exists():
41+
try:
42+
with open(roadmap_path, "r", encoding="utf-8") as f:
43+
roadmap_data = json.load(f)
44+
has_roadmap = len(roadmap_data.get("issues", [])) > 0
45+
except Exception:
46+
pass
47+
48+
manifest["hasRoadmap"] = has_roadmap
49+
50+
with open(manifest_path, "w", encoding="utf-8") as f:
51+
json.dump(manifest, f, indent=2, ensure_ascii=False)
52+
53+
print(f" Updated manifest: hasRoadmap={has_roadmap}")
54+
55+
56+
def main():
57+
parser = argparse.ArgumentParser(description="Fetch roadmap issues from GitHub")
58+
parser.add_argument(
59+
"--package", "-p",
60+
choices=list(PACKAGES.keys()),
61+
help="Single package (default: all)",
62+
)
63+
parser.add_argument(
64+
"--dry-run", "-n",
65+
action="store_true",
66+
help="Preview without writing",
67+
)
68+
args = parser.parse_args()
69+
70+
packages = [args.package] if args.package else list(PACKAGES.keys())
71+
72+
print("PathSim Roadmap Builder")
73+
print("=" * 50)
74+
if args.dry_run:
75+
print("DRY RUN")
76+
77+
for pkg_id in packages:
78+
display = PACKAGES[pkg_id]["display_name"]
79+
print(f"\n{display}:")
80+
build_roadmap(pkg_id, args.dry_run)
81+
if not args.dry_run:
82+
rebuild_package_manifest(pkg_id)
83+
84+
print(f"\nDone.")
85+
86+
87+
if __name__ == "__main__":
88+
main()

scripts/build.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
generate_version_manifest,
4646
)
4747
from lib.executor import execute_notebooks
48+
from lib.roadmap import build_roadmap
4849

4950
# Optional: embeddings for semantic search
5051
try:
@@ -402,10 +403,22 @@ def generate_package_manifest(package_id: str, dry_run: bool = False):
402403
"hasExamples": has_examples,
403404
})
404405

406+
# Check if roadmap exists
407+
roadmap_path = output_dir / "roadmap.json"
408+
has_roadmap = False
409+
if roadmap_path.exists():
410+
try:
411+
with open(roadmap_path, "r", encoding="utf-8") as f:
412+
roadmap_data = json.load(f)
413+
has_roadmap = len(roadmap_data.get("issues", [])) > 0
414+
except Exception:
415+
pass
416+
405417
manifest = {
406418
"package": package_id,
407419
"latestTag": latest_tag,
408420
"versions": versions,
421+
"hasRoadmap": has_roadmap,
409422
}
410423

411424
if dry_run:
@@ -480,6 +493,10 @@ def build_package(
480493
if not dry_run:
481494
checkout_main(repo_path)
482495

496+
# Fetch roadmap issues from GitHub
497+
print(f"\n Fetching roadmap...")
498+
build_roadmap(package_id, dry_run)
499+
483500
# Generate package manifest
484501
if not dry_run:
485502
generate_package_manifest(package_id, dry_run)

scripts/lib/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"figures": ROOT_DIR / "pathsim" / "docs" / "source" / "examples" / "figures",
2121
"display_name": "PathSim",
2222
"griffe_package": "pathsim",
23+
"github_repo": "pathsim/pathsim",
2324
"root_modules": [
2425
"pathsim",
2526
],
@@ -31,6 +32,7 @@
3132
"figures": ROOT_DIR / "pathsim-chem" / "docs" / "source" / "examples" / "figures",
3233
"display_name": "PathSim-Chem",
3334
"griffe_package": "pathsim_chem",
35+
"github_repo": "pathsim/pathsim-chem",
3436
"root_modules": [
3537
"pathsim_chem",
3638
],
@@ -42,6 +44,7 @@
4244
"figures": ROOT_DIR / "pathsim-vehicle" / "docs" / "source" / "examples" / "figures",
4345
"display_name": "PathSim-Vehicle",
4446
"griffe_package": "pathsim_vehicle",
47+
"github_repo": "pathsim/pathsim-vehicle",
4548
"root_modules": [
4649
"pathsim_vehicle",
4750
],
@@ -53,6 +56,7 @@
5356
"figures": ROOT_DIR / "pathsim-flight" / "docs" / "source" / "examples" / "figures",
5457
"display_name": "PathSim-Flight",
5558
"griffe_package": "pathsim_flight",
59+
"github_repo": "pathsim/pathsim-flight",
5660
"root_modules": [
5761
"pathsim_flight",
5862
],
@@ -64,6 +68,7 @@
6468
"figures": ROOT_DIR / "pathsim-rf" / "docs" / "source" / "examples" / "figures",
6569
"display_name": "PathSim-RF",
6670
"griffe_package": "pathsim_rf",
71+
"github_repo": "pathsim/pathsim-rf",
6772
"root_modules": [
6873
"pathsim_rf",
6974
],

scripts/lib/roadmap.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Fetch roadmap issues from GitHub for each package.
3+
4+
Writes static/{package}/roadmap.json with open issues labeled 'roadmap'.
5+
"""
6+
7+
import json
8+
import os
9+
from datetime import datetime, timezone
10+
from pathlib import Path
11+
12+
import requests
13+
14+
from .config import PACKAGES, STATIC_DIR
15+
16+
17+
def fetch_roadmap_issues(github_repo: str) -> list[dict]:
18+
"""
19+
Fetch open issues with the 'roadmap' label from a GitHub repo.
20+
21+
Args:
22+
github_repo: GitHub repo in "owner/name" format.
23+
24+
Returns:
25+
List of issue dicts with relevant fields.
26+
"""
27+
token = os.environ.get("GITHUB_TOKEN", "")
28+
headers = {"Authorization": f"token {token}"} if token else {}
29+
30+
url = f"https://api.github.com/repos/{github_repo}/issues"
31+
params = {
32+
"state": "open",
33+
"labels": "roadmap",
34+
"sort": "created",
35+
"direction": "desc",
36+
"per_page": 100,
37+
}
38+
39+
response = requests.get(url, headers=headers, params=params, timeout=30)
40+
response.raise_for_status()
41+
raw_issues = response.json()
42+
43+
issues = []
44+
for issue in raw_issues:
45+
# Skip pull requests
46+
if "pull_request" in issue:
47+
continue
48+
49+
body = (issue.get("body") or "").strip()
50+
51+
labels = [
52+
label["name"]
53+
for label in issue.get("labels", [])
54+
if label["name"] != "roadmap"
55+
]
56+
57+
issues.append({
58+
"number": issue["number"],
59+
"title": issue["title"],
60+
"body": body,
61+
"labels": labels,
62+
"url": issue["html_url"],
63+
"created": issue["created_at"],
64+
})
65+
66+
return issues
67+
68+
69+
def build_roadmap(package_id: str, dry_run: bool = False) -> bool:
70+
"""
71+
Fetch and write roadmap.json for a single package.
72+
73+
Returns True if roadmap items were found.
74+
"""
75+
pkg_config = PACKAGES.get(package_id)
76+
if not pkg_config:
77+
print(f" Unknown package: {package_id}")
78+
return False
79+
80+
github_repo = pkg_config.get("github_repo")
81+
if not github_repo:
82+
print(f" No github_repo configured for {package_id}")
83+
return False
84+
85+
output_dir = STATIC_DIR / package_id
86+
output_path = output_dir / "roadmap.json"
87+
88+
try:
89+
issues = fetch_roadmap_issues(github_repo)
90+
except Exception as e:
91+
print(f" Failed to fetch roadmap for {package_id}: {e}")
92+
return False
93+
94+
if dry_run:
95+
print(f" Would write {len(issues)} roadmap items to {output_path}")
96+
return len(issues) > 0
97+
98+
output_dir.mkdir(parents=True, exist_ok=True)
99+
100+
roadmap = {
101+
"package": package_id,
102+
"repo": github_repo,
103+
"updated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
104+
"issues": issues,
105+
}
106+
107+
with open(output_path, "w", encoding="utf-8") as f:
108+
json.dump(roadmap, f, indent=2, ensure_ascii=False)
109+
110+
print(f" {len(issues)} roadmap items")
111+
112+
# Remove roadmap.json if empty (so hasRoadmap stays false)
113+
if not issues:
114+
output_path.unlink(missing_ok=True)
115+
116+
return len(issues) > 0
117+
118+
119+
def build_all_roadmaps(dry_run: bool = False) -> None:
120+
"""Fetch roadmaps for all configured packages."""
121+
print("\nFetching roadmap issues")
122+
print("=" * 50)
123+
for package_id in PACKAGES:
124+
print(f" {PACKAGES[package_id]['display_name']}:")
125+
build_roadmap(package_id, dry_run)

src/app.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,8 @@ button.ghost:hover {
413413
flex-direction: column;
414414
align-items: center;
415415
gap: 6px;
416-
padding: 6px 12px;
417-
min-width: 56px;
416+
padding: 6px 0;
417+
width: 64px;
418418
background: transparent;
419419
border: none;
420420
border-radius: var(--radius-md);

src/lib/api/roadmap.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { base } from '$app/paths';
2+
3+
export interface RoadmapIssue {
4+
number: number;
5+
title: string;
6+
body: string;
7+
labels: string[];
8+
url: string;
9+
created: string;
10+
}
11+
12+
export interface RoadmapData {
13+
package: string;
14+
repo: string;
15+
updated: string;
16+
issues: RoadmapIssue[];
17+
}
18+
19+
/**
20+
* Fetch roadmap data for a package.
21+
* Path: /{package}/roadmap.json
22+
*/
23+
export async function getRoadmapData(
24+
packageId: string,
25+
fetch: typeof globalThis.fetch
26+
): Promise<RoadmapData> {
27+
const url = `${base}/${packageId}/roadmap.json`;
28+
const response = await fetch(url);
29+
30+
if (!response.ok) {
31+
throw new Error(`Failed to load roadmap for ${packageId}: ${response.status}`);
32+
}
33+
34+
return response.json();
35+
}

src/lib/api/versions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface PackageManifest {
1212
package: string;
1313
latestTag: string;
1414
versions: VersionInfo[];
15+
hasRoadmap?: boolean;
1516
}
1617

1718
/**
@@ -106,3 +107,10 @@ export function versionHasExamples(tag: string, manifest: PackageManifest): bool
106107
const version = manifest.versions.find((v) => v.tag === normalized);
107108
return version?.hasExamples ?? false;
108109
}
110+
111+
/**
112+
* Check if a package has roadmap items
113+
*/
114+
export function packageHasRoadmap(manifest: PackageManifest): boolean {
115+
return manifest.hasRoadmap ?? false;
116+
}

src/lib/components/common/Icon.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@
298298
<rect x="3" y="3" width="18" height="18" rx="2"/>
299299
<rect x="12" y="12" width="7" height="7" rx="1"/>
300300
</svg>
301+
{:else if name === 'roadmap'}
302+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
303+
<line x1="7" y1="3" x2="7" y2="21"/>
304+
<path d="M7 5h10l-3 4 3 4H7"/>
305+
</svg>
301306
{:else if name === 'zoom-in'}
302307
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
303308
<circle cx="11" cy="11" r="8"/>

0 commit comments

Comments
 (0)