Skip to content

Commit c04deeb

Browse files
committed
add Github Action
- auto update readme (BaekJoon, LeetCode)
1 parent e1c4f46 commit c04deeb

5 files changed

Lines changed: 467 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Auto Update README
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
paths:
7+
- "BaekJoon/**"
8+
- "LeetCode/**"
9+
10+
permissions:
11+
contents: write # Actions가 commit/push 가능
12+
13+
jobs:
14+
update: # Job 이름
15+
if: github.actor != 'github-actions[bot]' # Actions가 만든 커밋은 무시되도록 하여 무한루프 방지
16+
runs-on: ubuntu-latest # GitHub가 제공하는 리눅스 VM (Python, git 기본 내장)
17+
steps:
18+
- uses: actions/checkout@v4 # 레포 체크아웃
19+
with:
20+
fetch-depth: 0 # before..after diff가 정확히 계산
21+
22+
- uses: actions/setup-python@v5 # Python 세팅
23+
with:
24+
python-version: "3.11"
25+
26+
- name: Detect changed areas
27+
id: changes
28+
env:
29+
BEFORE: ${{ github.event.before }}
30+
AFTER: ${{ github.sha }}
31+
run: |
32+
echo "lc=false" >> $GITHUB_OUTPUT
33+
echo "bj=false" >> $GITHUB_OUTPUT
34+
35+
files=$(git diff --name-only "$BEFORE..$AFTER")
36+
37+
if echo "$files" | grep -q '^LeetCode/'; then
38+
echo "lc=true" >> $GITHUB_OUTPUT
39+
fi
40+
41+
if echo "$files" | grep -q '^BaekJoon/'; then
42+
echo "bj=true" >> $GITHUB_OUTPUT
43+
fi
44+
45+
- name: Update BaekJoon README
46+
if: steps.changes.outputs.bj == 'true'
47+
env:
48+
BEFORE: ${{ github.event.before }}
49+
AFTER: ${{ github.sha }}
50+
run: |
51+
python scripts/update_baekjoon_readme.py --before "$BEFORE" --after "$AFTER"
52+
53+
- name: Update LeetCode README
54+
if: steps.changes.outputs.lc == 'true'
55+
env:
56+
BEFORE: ${{ github.event.before }}
57+
AFTER: ${{ github.sha }}
58+
run: |
59+
python scripts/update_leetcode_readme.py --before "$BEFORE" --after "$AFTER"
60+
61+
- name: Commit & push if changed
62+
run: |
63+
if [ -n "$(git status --porcelain)" ]; then # README가 바뀌었을 때만 커밋
64+
git config user.name "github-actions[bot]" # GitHub 공식 bot 계정으로 커밋
65+
git config user.email "github-actions[bot]@users.noreply.github.com"
66+
git add LeetCode/README.md BaekJoon/README.md
67+
git commit -m "chore: auto update READMEs"
68+
git push
69+
fi

BaekJoon/Solutions/Solution_1062.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/*
2+
@boj.idx: 1062
3+
@boj.tier: gold
4+
@boj.title: 가르침
5+
@boj.level: IV
6+
@boj.tags: Brute Force, Bit Masking, Backtracking
7+
@boj.complexity: O()/O()
8+
@boj.note: 백트래킹은 항상 예외 케이스들을 잘 생각하고 조건 처리하자.
9+
*/
10+
111
import java.util.*;
212
import java.io.*;
313

LeetCode/Solutions/Solution_AddBinary.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/*
2+
@lc.idx: 67
3+
@lc.slug: add-binary
4+
@lc.title: add binary
5+
@lc.level: Easy
6+
@lc.tags: Math, String, Bit Manipulation, Simulation
7+
@lc.complexity: O(N)/O(1)
8+
@lc.note:
9+
*/
10+
111
public class Solution_AddBinary {
212
public static String addBinary(String a, String b) {
313
int aIdx = a.length() - 1;

scripts/update_baekjoon_readme.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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
7+
8+
REPO_ROOT = Path(__file__).resolve().parents[1]
9+
BJ_README = REPO_ROOT / "BaekJoon" / "README.md"
10+
11+
TIER_ORDER = ["bronze", "silver", "gold", "platinum", "diamond", "ruby"]
12+
13+
@dataclass(frozen=True)
14+
class BjMeta:
15+
idx: int
16+
tier: str
17+
title: str
18+
level: str
19+
tags: str
20+
complexity: str
21+
note: str
22+
solution_relpath: str
23+
24+
def git_changed_files(before: str, after: str) -> List[str]:
25+
out = subprocess.check_output(
26+
["git", "diff", "--name-only", f"{before}..{after}"],
27+
text=True
28+
).strip()
29+
return [x for x in out.splitlines() if x]
30+
31+
def is_baekjoon_solution(path: str) -> bool:
32+
return path.startswith("BaekJoon/Solutions/") and (
33+
path.endswith(".java") or path.endswith(".py") or path.endswith(".cpp")
34+
)
35+
36+
def parse_meta(abs_path: Path, rel_path: str) -> BjMeta:
37+
text = abs_path.read_text(encoding="utf-8", errors="ignore")
38+
39+
def pick(key: str) -> str:
40+
m = re.search(rf"@boj\.{re.escape(key)}\s*:\s*(.*)\s*$", text, re.MULTILINE)
41+
return (m.group(1).strip() if m else "")
42+
43+
idx_s = pick("idx")
44+
if not idx_s.isdigit():
45+
raise ValueError(f"Missing or invalid @boj.id in {rel_path}")
46+
47+
tier = pick("tier").lower()
48+
if tier not in TIER_ORDER:
49+
raise ValueError(f"Missing/invalid @boj.tier in {rel_path} (expected one of {TIER_ORDER})")
50+
51+
return BjMeta(
52+
idx=int(idx_s),
53+
tier=tier,
54+
title=pick("title") or "(Unknown Title)",
55+
level=pick("level") or "",
56+
tags=pick("tags") or "",
57+
complexity=pick("complexity") or "",
58+
note=pick("note") or "",
59+
solution_relpath=rel_path.replace("\\", "/"),
60+
)
61+
62+
def build_row(m: BjMeta) -> str:
63+
boj_url = f"https://www.acmicpc.net/problem/{m.idx}"
64+
gh_url = f"https://github.com/SubAkBa/Algorithm_Solution/blob/master/{m.solution_relpath}"
65+
return (
66+
f"| {m.idx} | "
67+
f"[{m.title}]({boj_url}) | "
68+
f"{m.level} | "
69+
f"{m.tags} | "
70+
f"[{m.complexity}]({gh_url}) | "
71+
f"{m.note} |"
72+
)
73+
74+
def split_by_tier_sections(readme: str) -> Dict[str, str]:
75+
"""
76+
tier별로 섹션 텍스트를 뽑기.
77+
기준: <img src="../img/{tier}.png">
78+
"""
79+
# 각 티어 img 시작 위치 찾기
80+
positions = []
81+
for tier in TIER_ORDER:
82+
token = f'<img src="../img/{tier}.png">'
83+
idx = readme.find(token)
84+
if idx != -1:
85+
positions.append((idx, tier, token))
86+
87+
if not positions:
88+
raise RuntimeError("No tier image sections found in BaekJoon/README.md")
89+
90+
positions.sort()
91+
sections = {}
92+
for i, (pos, tier, token) in enumerate(positions):
93+
end = positions[i + 1][0] if i + 1 < len(positions) else len(readme)
94+
sections[tier] = readme[pos:end]
95+
return sections
96+
97+
def extract_table_parts(section_text: str) -> tuple[str, str, str]:
98+
"""
99+
section_text 내에서:
100+
- prefix: 테이블 헤더(정렬선 포함)까지
101+
- rows_block: 기존 행들
102+
- suffix: </details> 포함 이후
103+
"""
104+
header_m = re.search(r"(\| Idx.*\n\|.*\n)", section_text)
105+
if not header_m:
106+
raise RuntimeError("Table header not found in tier section.")
107+
108+
header_end = header_m.end(1)
109+
after = section_text[header_end:]
110+
111+
end_m = re.search(r"\n</details>\s*\n", after)
112+
if not end_m:
113+
raise RuntimeError("</details> not found after table in tier section.")
114+
115+
rows_area = after[:end_m.start()]
116+
suffix = after[end_m.start():]
117+
118+
rows = []
119+
for line in rows_area.splitlines():
120+
line = line.rstrip()
121+
if line.startswith("|") and re.match(r"^\|\s*\d+\s*\|", line):
122+
rows.append(line)
123+
124+
prefix = section_text[:header_end]
125+
return prefix, "\n".join(rows), suffix
126+
127+
def parse_existing_rows(rows_block: str) -> Dict[int, str]:
128+
rows = {}
129+
for line in rows_block.splitlines():
130+
m = re.match(r"^\|\s*(\d+)\s*\|", line)
131+
if m:
132+
rows[int(m.group(1))] = line
133+
return rows
134+
135+
def update_readme(readme_text: str, metas: List[BjMeta]) -> str:
136+
sections = split_by_tier_sections(readme_text)
137+
138+
# tier별로 metas 묶기
139+
by_tier: Dict[str, List[BjMeta]] = {}
140+
for m in metas:
141+
by_tier.setdefault(m.tier, []).append(m)
142+
143+
# 원본 텍스트에서 tier section 단위로 replace
144+
new_text = readme_text
145+
for tier, sec_text in sections.items():
146+
if tier not in by_tier:
147+
continue
148+
149+
prefix, rows_block, suffix = extract_table_parts(sec_text)
150+
existing = parse_existing_rows(rows_block)
151+
152+
for meta in by_tier[tier]:
153+
existing[meta.idx] = build_row(meta)
154+
155+
new_rows = "\n".join(existing[k] for k in sorted(existing.keys()))
156+
new_sec = prefix + new_rows + "\n" + suffix
157+
158+
new_text = new_text.replace(sec_text, new_sec, 1)
159+
160+
return new_text
161+
162+
def main():
163+
ap = argparse.ArgumentParser()
164+
ap.add_argument("--before", required=True)
165+
ap.add_argument("--after", required=True)
166+
args = ap.parse_args()
167+
168+
changed = git_changed_files(args.before, args.after)
169+
targets = [p for p in changed if is_baekjoon_solution(p)]
170+
if not targets:
171+
return
172+
173+
metas: List[BjMeta] = []
174+
for rel in targets:
175+
abs_path = REPO_ROOT / rel
176+
if abs_path.exists():
177+
metas.append(parse_meta(abs_path, rel))
178+
179+
if not metas:
180+
return
181+
182+
text = BJ_README.read_text(encoding="utf-8")
183+
new_text = update_readme(text, metas)
184+
185+
if new_text != text:
186+
BJ_README.write_text(new_text, encoding="utf-8")
187+
188+
if __name__ == "__main__":
189+
main()

0 commit comments

Comments
 (0)