Skip to content

Commit f6151e7

Browse files
committed
add Github Action
- auto update readme (Solvesql)
1 parent e0afd7d commit f6151e7

2 files changed

Lines changed: 213 additions & 0 deletions

File tree

.github/workflows/auto-update-readme.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
paths:
77
- "BaekJoon/**"
88
- "LeetCode/**"
9+
- "Solvesql/**"
910

1011
permissions:
1112
contents: write # Actions가 commit/push 가능
@@ -58,6 +59,14 @@ jobs:
5859
run: |
5960
python scripts/update_leetcode_readme.py --before "$BEFORE" --after "$AFTER"
6061
62+
- name: Update Solvesql README
63+
if: steps.changes.outputs.bj == 'true'
64+
env:
65+
BEFORE: ${{ github.event.before }}
66+
AFTER: ${{ github.sha }}
67+
run: |
68+
python scripts/update_solvesql_readme.py --before "$BEFORE" --after "$AFTER"
69+
6170
- name: Commit & push if changed
6271
run: |
6372
if [ -n "$(git status --porcelain)" ]; then # README가 바뀌었을 때만 커밋

scripts/update_solvesql_readme.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import argparse
2+
import re
3+
import subprocess
4+
from dataclasses import dataclass
5+
from pathlib import Path
6+
from typing import Dict, List, Tuple
7+
8+
9+
REPO_ROOT = Path(__file__).resolve().parents[1]
10+
SS_README = REPO_ROOT / "Solvesql" / "README.md"
11+
12+
13+
SECTION_RE = re.compile(r"^##\s*난이도\s*(\d+)\s*$", re.MULTILINE)
14+
15+
16+
@dataclass(frozen=True)
17+
class SsMeta:
18+
idx: int
19+
level: int
20+
title: str
21+
slug: str
22+
category: str
23+
note: str
24+
solution_relpath: str # e.g. Solvesql/Solutions/xxx.sql
25+
26+
27+
def git_changed_files(before: str, after: str) -> List[str]:
28+
out = subprocess.check_output(
29+
["git", "diff", "--name-only", f"{before}..{after}"],
30+
text=True
31+
).strip()
32+
return [x for x in out.splitlines() if x]
33+
34+
35+
def is_solvesql_solution(path: str) -> bool:
36+
return path.startswith("Solvesql/Solutions/") and path.endswith(".sql")
37+
38+
39+
def parse_meta_from_file(abs_path: Path, rel_path: str) -> SsMeta:
40+
text = abs_path.read_text(encoding="utf-8", errors="ignore")
41+
42+
# 한 줄 단위로만 파싱 (*/ 같은 토큰이 섞이는 문제 방지)
43+
def pick(key: str) -> str:
44+
for line in text.splitlines():
45+
line = line.strip()
46+
if line.startswith(f"@ss.{key}:"):
47+
return line.split(":", 1)[1].strip()
48+
if line.startswith(f"-- @ss.{key}:"):
49+
return line.split(":", 1)[1].strip()
50+
return ""
51+
52+
idx_s = pick("idx")
53+
level_s = pick("level")
54+
slug = pick("slug")
55+
56+
if not idx_s.isdigit():
57+
raise ValueError(f"Missing or invalid @ss.idx in {rel_path}")
58+
if not level_s.isdigit():
59+
raise ValueError(f"Missing or invalid @ss.level in {rel_path}")
60+
if not slug:
61+
raise ValueError(f"Missing @ss.slug in {rel_path}")
62+
63+
title = pick("title") or "(Unknown Title)"
64+
category = pick("category") or ""
65+
note = pick("note") or ""
66+
67+
return SsMeta(
68+
idx=int(idx_s),
69+
level=int(level_s),
70+
title=title,
71+
slug=slug,
72+
category=category,
73+
note=note,
74+
solution_relpath=rel_path.replace("\\", "/"),
75+
)
76+
77+
78+
def build_row(m: SsMeta) -> str:
79+
q_url = f"https://solvesql.com/problems/{m.slug}/"
80+
gh_url = f"https://github.com/SubAkBa/Algorithm_Solution/blob/master/{m.solution_relpath}"
81+
# README 예시 포맷과 동일하게: Solution 링크 텍스트는 "Solution"
82+
return (
83+
f"| {m.idx} | "
84+
f"[{m.title}]({q_url}) | "
85+
f"{m.category} | "
86+
f"[Solution]({gh_url}) | "
87+
f"{m.note} |"
88+
)
89+
90+
91+
def extract_table_parts(section_text: str) -> Tuple[str, str, str]:
92+
"""
93+
section_text에서:
94+
- prefix: 테이블 헤더(정렬선 포함)까지
95+
- rows_block: 행들만
96+
- suffix: </details> 이후 나머지
97+
"""
98+
# 테이블 헤더는 "| Idx | Question"으로 시작한다고 가정
99+
header_m = re.search(r"(\| Idx \|.*\n\|:---:.*\n)", section_text)
100+
if not header_m:
101+
raise RuntimeError("Table header not found in Solvesql section.")
102+
103+
header_end = header_m.end(1)
104+
after = section_text[header_end:]
105+
106+
end_m = re.search(r"\n</details>\s*\n", after)
107+
if not end_m:
108+
raise RuntimeError("</details> not found after table in Solvesql section.")
109+
110+
rows_area = after[:end_m.start()]
111+
suffix = after[end_m.start():]
112+
113+
rows = []
114+
for line in rows_area.splitlines():
115+
line = line.rstrip()
116+
if line.startswith("|") and re.match(r"^\|\s*\d+\s*\|", line):
117+
rows.append(line)
118+
119+
prefix = section_text[:header_end]
120+
return prefix, "\n".join(rows), suffix
121+
122+
123+
def parse_existing_rows(rows_block: str) -> Dict[int, str]:
124+
rows: Dict[int, str] = {}
125+
for line in rows_block.splitlines():
126+
m = re.match(r"^\|\s*(\d+)\s*\|", line)
127+
if m:
128+
rows[int(m.group(1))] = line
129+
return rows
130+
131+
132+
def update_readme(readme_text: str, metas: List[SsMeta]) -> str:
133+
# 난이도별로 메타 묶기
134+
by_level: Dict[int, List[SsMeta]] = {}
135+
for m in metas:
136+
by_level.setdefault(m.level, []).append(m)
137+
138+
matches = list(SECTION_RE.finditer(readme_text))
139+
if not matches:
140+
raise RuntimeError("No sections like '## 난이도 N' found in Solvesql/README.md")
141+
142+
out = []
143+
last = 0
144+
145+
for i, s in enumerate(matches):
146+
start_pos = s.start()
147+
end_pos = matches[i + 1].start() if i + 1 < len(matches) else len(readme_text)
148+
149+
out.append(readme_text[last:start_pos])
150+
sec_text = readme_text[start_pos:end_pos]
151+
152+
level = int(s.group(1))
153+
if level not in by_level:
154+
out.append(sec_text)
155+
last = end_pos
156+
continue
157+
158+
prefix, rows_block, suffix = extract_table_parts(sec_text)
159+
existing = parse_existing_rows(rows_block)
160+
161+
# 신규/갱신 반영 (idx 같으면 덮어씀)
162+
for meta in by_level[level]:
163+
existing[meta.idx] = build_row(meta)
164+
165+
# idx 오름차순 정렬
166+
new_rows = "\n".join(existing[k] for k in sorted(existing.keys()))
167+
new_sec = prefix + new_rows + "\n" + suffix
168+
169+
out.append(new_sec)
170+
last = end_pos
171+
172+
out.append(readme_text[last:])
173+
return "".join(out)
174+
175+
176+
def main():
177+
ap = argparse.ArgumentParser()
178+
ap.add_argument("--before", required=True)
179+
ap.add_argument("--after", required=True)
180+
args = ap.parse_args()
181+
182+
changed = git_changed_files(args.before, args.after)
183+
targets = [p for p in changed if is_solvesql_solution(p)]
184+
if not targets:
185+
return
186+
187+
metas: List[SsMeta] = []
188+
for rel in targets:
189+
abs_path = REPO_ROOT / rel
190+
if abs_path.exists():
191+
metas.append(parse_meta_from_file(abs_path, rel))
192+
193+
if not metas:
194+
return
195+
196+
text = SS_README.read_text(encoding="utf-8")
197+
new_text = update_readme(text, metas)
198+
199+
if new_text != text:
200+
SS_README.write_text(new_text, encoding="utf-8")
201+
202+
203+
if __name__ == "__main__":
204+
main()

0 commit comments

Comments
 (0)