Skip to content

Commit 6c718eb

Browse files
committed
feat: add workflow to automatically update README with repository data
1 parent bddfc07 commit 6c718eb

3 files changed

Lines changed: 264 additions & 1 deletion

File tree

.github/workflows/update.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Update Schoolwork README
2+
3+
on:
4+
schedule:
5+
- cron: "0 0 * * 1"
6+
workflow_dispatch:
7+
push:
8+
branches:
9+
- main
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
update-readme:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 5
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Install dependencies
29+
run: pip install requests
30+
31+
- name: Generate README
32+
env:
33+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34+
ORG_NAME: gainsborouo-coursework
35+
run: python scripts/generate.py
36+
37+
- name: Check for changes
38+
id: changes
39+
run: |
40+
if git diff --quiet -- README.md; then
41+
echo "changed=false" >> "$GITHUB_OUTPUT"
42+
else
43+
echo "changed=true" >> "$GITHUB_OUTPUT"
44+
fi
45+
46+
- name: Commit README
47+
if: steps.changes.outputs.changed == 'true'
48+
env:
49+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50+
run: |
51+
SHA=$(gh api repos/${{ github.repository }}/contents/README.md --jq .sha)
52+
CONTENT=$(base64 < README.md | tr -d '\n')
53+
54+
gh api repos/${{ github.repository }}/contents/README.md \
55+
-X PUT \
56+
-f message='chore: regenerate coursework README' \
57+
-f content="$CONTENT" \
58+
-f sha="$SHA"

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,42 @@
1-
# schoolwork
1+
<!-- AUTO-GENERATED: DO NOT EDIT -->
2+
# Coursework
3+
4+
This repository indexes my coursework repositories.
5+
6+
## NYCU
7+
8+
### 113上
9+
10+
| Name | Repository | Language |
11+
|---|---|---|
12+
| 網路程式設計概論 | [`NYCU_Intro-to-Network-Programming`](https://github.com/gainsborouo-coursework/NYCU_Intro-to-Network-Programming) | Python |
13+
14+
### 112下
15+
16+
| Name | Repository | Language |
17+
|---|---|---|
18+
| 人工智慧概論 | [`NYCU_Intro-to-Artificial-Intelligence`](https://github.com/gainsborouo-coursework/NYCU_Intro-to-Artificial-Intelligence) | Jupyter Notebook |
19+
| 人工智慧概論 期末專題 | [`NYCU_AI-Final-Project`](https://github.com/gainsborouo-coursework/NYCU_AI-Final-Project) | Jupyter Notebook |
20+
| 密碼工程 | [`NYCU_Cryptography-Engineering`](https://github.com/gainsborouo-coursework/NYCU_Cryptography-Engineering) | Python |
21+
| 競技程式設計(一) | [`NYCU_Competitive-Programming-I`](https://github.com/gainsborouo-coursework/NYCU_Competitive-Programming-I) | C++ |
22+
| 網路規劃與管理實務 | [`NYCU_Network-Planning-and-Management-Practices`](https://github.com/gainsborouo-coursework/NYCU_Network-Planning-and-Management-Practices) | Shell |
23+
| 計算機組織 | [`NYCU_Computer-Organization`](https://github.com/gainsborouo-coursework/NYCU_Computer-Organization) | Verilog |
24+
| 資料結構與物件導向程式設計 | [`NYCU_Data-Structures-and-OOP`](https://github.com/gainsborouo-coursework/NYCU_Data-Structures-and-OOP) | C++ |
25+
| 資料結構與物件導向程式設計 Dungeon | [`NYCU_OOP-Dungeon`](https://github.com/gainsborouo-coursework/NYCU_OOP-Dungeon) | Makefile |
26+
27+
### 112上
28+
29+
| Name | Repository | Language |
30+
|---|---|---|
31+
| 計算機概論與程式設計 | [`NYCU_Intro-to-Computers-and-Programming`](https://github.com/gainsborouo-coursework/NYCU_Intro-to-Computers-and-Programming) | C++ |
32+
| 資料庫系統概論 | [`NYCU_Intro-to-Database-Systems`](https://github.com/gainsborouo-coursework/NYCU_Intro-to-Database-Systems) | C++ |
33+
| 資料庫系統概論 HW0 | [`NYCU_DBMS-HW0`](https://github.com/gainsborouo-coursework/NYCU_DBMS-HW0) | HTML |
34+
| 資料庫系統概論 期末專題 | [`NYCU_DBMS-Final-Project`](https://github.com/gainsborouo-coursework/NYCU_DBMS-Final-Project) | PHP |
35+
36+
## NEHS
37+
38+
### 110下
39+
40+
| Name | Repository | Language |
41+
|---|---|---|
42+
| 資訊課 期末專題 | [`NEHS_StonePle`](https://github.com/gainsborouo-coursework/NEHS_StonePle) | Java |

scripts/generate.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import os
2+
import re
3+
from collections import defaultdict
4+
from typing import Any
5+
6+
import requests
7+
8+
9+
ORG_NAME = os.environ["ORG_NAME"]
10+
TOKEN = os.environ["GITHUB_TOKEN"]
11+
12+
HEADERS = {
13+
"Accept": "application/vnd.github+json",
14+
"Authorization": f"Bearer {TOKEN}",
15+
}
16+
17+
API_URL = f"https://api.github.com/orgs/{ORG_NAME}/repos?type=public&per_page=100"
18+
19+
SCHOOL_PREFIX_MAP = {
20+
"NYCU_": "NYCU",
21+
"NEHS_": "NEHS",
22+
}
23+
24+
SEMESTER_PATTERN = re.compile(r"^(\d{3})(上|下)\s+")
25+
SCHOOL_NAME_PATTERN = re.compile(r"^(交大|竹科實中)\s+")
26+
27+
28+
def fetch_repos() -> list[dict[str, Any]]:
29+
repos: list[dict[str, Any]] = []
30+
url = API_URL
31+
32+
while url:
33+
response = requests.get(url, headers=HEADERS, timeout=30)
34+
response.raise_for_status()
35+
repos.extend(response.json())
36+
37+
next_url = None
38+
link_header = response.headers.get("Link")
39+
if link_header:
40+
links = requests.utils.parse_header_links(
41+
link_header.rstrip(">").replace(">,", ",<")
42+
)
43+
for link in links:
44+
if link.get("rel") == "next":
45+
next_url = link.get("url")
46+
break
47+
48+
url = next_url
49+
50+
return repos
51+
52+
53+
def detect_school(repo_name: str) -> str:
54+
for prefix, school in SCHOOL_PREFIX_MAP.items():
55+
if repo_name.startswith(prefix):
56+
return school
57+
return "Other"
58+
59+
60+
def parse_description(description: str | None) -> tuple[str | None, str]:
61+
if not description:
62+
return None, ""
63+
64+
desc = description.strip()
65+
66+
semester_match = SEMESTER_PATTERN.match(desc)
67+
if not semester_match:
68+
return None, desc
69+
70+
semester = f"{semester_match.group(1)}{semester_match.group(2)}"
71+
rest = desc[semester_match.end():].strip()
72+
73+
rest = SCHOOL_NAME_PATTERN.sub("", rest).strip()
74+
75+
return semester, rest
76+
77+
78+
def semester_sort_key(semester: str | None) -> tuple[int, int]:
79+
if not semester:
80+
return (-1, -1)
81+
82+
year = int(semester[:-1])
83+
half = semester[-1]
84+
half_order = 1 if half == "下" else 0
85+
return (year, half_order)
86+
87+
88+
def display_title(repo: dict[str, Any]) -> str:
89+
_, parsed_course_name = parse_description(repo.get("description"))
90+
return parsed_course_name or repo["name"]
91+
92+
93+
def repo_sort_key(repo: dict[str, Any]) -> tuple[str, str]:
94+
semester, _ = parse_description(repo.get("description"))
95+
return (semester or "", display_title(repo).lower())
96+
97+
98+
def build_markdown(repos: list[dict[str, Any]]) -> str:
99+
grouped: dict[str, dict[str | None, list[dict[str, Any]]]] = defaultdict(
100+
lambda: defaultdict(list)
101+
)
102+
103+
for repo in repos:
104+
school = detect_school(repo["name"])
105+
semester, _ = parse_description(repo.get("description"))
106+
grouped[school][semester].append(repo)
107+
108+
school_order = ["NYCU", "NEHS", "Other"]
109+
110+
parts: list[str] = [
111+
"<!-- AUTO-GENERATED: DO NOT EDIT -->",
112+
"# Coursework",
113+
"",
114+
"This repository indexes my coursework repositories.",
115+
"",
116+
]
117+
118+
for school in school_order:
119+
semesters = grouped.get(school)
120+
if not semesters:
121+
continue
122+
123+
parts.append(f"## {school}")
124+
parts.append("")
125+
126+
sorted_semesters = sorted(
127+
semesters.keys(),
128+
key=semester_sort_key,
129+
reverse=True,
130+
)
131+
132+
for semester in sorted_semesters:
133+
repos_in_semester = sorted(semesters[semester], key=repo_sort_key)
134+
135+
heading = semester if semester else "Uncategorized"
136+
parts.append(f"### {heading}")
137+
parts.append("")
138+
139+
for repo in repos_in_semester:
140+
title = display_title(repo)
141+
url = repo["html_url"]
142+
parts.append(f"- [{title}]({url})")
143+
144+
parts.append("")
145+
146+
return "\n".join(parts).rstrip() + "\n"
147+
148+
149+
def main() -> None:
150+
repos = fetch_repos()
151+
152+
repos = [repo for repo in repos if not repo.get("private", False)]
153+
repos = [repo for repo in repos if repo["name"].lower() != "schoolwork"]
154+
155+
repos = sorted(repos, key=lambda r: r["name"])
156+
157+
content = build_markdown(repos)
158+
159+
with open("README.md", "w", encoding="utf-8") as file:
160+
file.write(content)
161+
162+
163+
if __name__ == "__main__":
164+
main()

0 commit comments

Comments
 (0)