|
| 1 | +"""Stamp date/lastmod in Hugo front matter for added/modified blog posts. |
| 2 | +
|
| 3 | +Reads newline-separated paths from ADDED_FILES and MODIFIED_FILES env vars, |
| 4 | +rewrites only the date/lastmod lines in the front matter (preserving the rest |
| 5 | +verbatim), and signals whether anything changed via the `changed` output. |
| 6 | +""" |
| 7 | + |
| 8 | +import os |
| 9 | +import re |
| 10 | +import sys |
| 11 | +from datetime import datetime, timezone, timedelta |
| 12 | +from pathlib import Path |
| 13 | + |
| 14 | +KST = timezone(timedelta(hours=9)) |
| 15 | +INDEX_NAME = "_index.md" |
| 16 | +BLOG_PREFIX = "content/blog/" |
| 17 | +DATE_RE = re.compile(r"^date:\s*.*$", re.MULTILINE) |
| 18 | +LASTMOD_RE = re.compile(r"^lastmod:\s*.*$", re.MULTILINE) |
| 19 | + |
| 20 | + |
| 21 | +def kst_now_iso() -> str: |
| 22 | + s = datetime.now(KST).strftime("%Y-%m-%dT%H:%M:%S%z") |
| 23 | + return s[:-2] + ":" + s[-2:] |
| 24 | + |
| 25 | + |
| 26 | +def split_front_matter(text: str) -> tuple[str, str] | None: |
| 27 | + if not text.startswith("---"): |
| 28 | + return None |
| 29 | + rest = text[3:] |
| 30 | + if rest.startswith("\n"): |
| 31 | + rest = rest[1:] |
| 32 | + sep = rest.find("\n---") |
| 33 | + if sep == -1: |
| 34 | + return None |
| 35 | + fm = rest[:sep] |
| 36 | + body = rest[sep + 4:] |
| 37 | + return fm, body |
| 38 | + |
| 39 | + |
| 40 | +def replace_or_insert(fm: str, key_re: re.Pattern, key: str, value: str) -> str: |
| 41 | + line = f"{key}: {value}" |
| 42 | + if key_re.search(fm): |
| 43 | + return key_re.sub(line, fm, count=1) |
| 44 | + if not fm.endswith("\n"): |
| 45 | + fm += "\n" |
| 46 | + return fm + line + "\n" |
| 47 | + |
| 48 | + |
| 49 | +def stamp(path: Path, set_date: bool, now: str) -> bool: |
| 50 | + original = path.read_text(encoding="utf-8") |
| 51 | + parts = split_front_matter(original) |
| 52 | + if parts is None: |
| 53 | + print(f" SKIP {path}: no front matter", file=sys.stderr) |
| 54 | + return False |
| 55 | + fm, body = parts |
| 56 | + |
| 57 | + new_fm = fm |
| 58 | + if set_date: |
| 59 | + new_fm = replace_or_insert(new_fm, DATE_RE, "date", now) |
| 60 | + new_fm = replace_or_insert(new_fm, LASTMOD_RE, "lastmod", now) |
| 61 | + |
| 62 | + if new_fm == fm: |
| 63 | + return False |
| 64 | + |
| 65 | + new_text = "---\n" + new_fm |
| 66 | + if not new_text.endswith("\n"): |
| 67 | + new_text += "\n" |
| 68 | + new_text += "---" + body |
| 69 | + path.write_text(new_text, encoding="utf-8") |
| 70 | + return True |
| 71 | + |
| 72 | + |
| 73 | +def collect(env_name: str) -> list[Path]: |
| 74 | + raw_lines = [ |
| 75 | + line.strip() |
| 76 | + for line in os.environ.get(env_name, "").splitlines() |
| 77 | + if line.strip() |
| 78 | + ] |
| 79 | + paths = [] |
| 80 | + for raw in raw_lines: |
| 81 | + if not raw.startswith(BLOG_PREFIX): |
| 82 | + continue |
| 83 | + p = Path(raw) |
| 84 | + if p.name == INDEX_NAME or p.suffix != ".md" or not p.exists(): |
| 85 | + continue |
| 86 | + paths.append(p) |
| 87 | + return paths |
| 88 | + |
| 89 | + |
| 90 | +def write_output(name: str, value: str) -> None: |
| 91 | + out = os.environ.get("GITHUB_OUTPUT") |
| 92 | + if not out: |
| 93 | + return |
| 94 | + with open(out, "a", encoding="utf-8") as f: |
| 95 | + f.write(f"{name}={value}\n") |
| 96 | + |
| 97 | + |
| 98 | +def main() -> int: |
| 99 | + now = kst_now_iso() |
| 100 | + print(f"Stamping with {now}") |
| 101 | + |
| 102 | + added = collect("ADDED_FILES") |
| 103 | + modified = [p for p in collect("MODIFIED_FILES") if p not in added] |
| 104 | + |
| 105 | + changed = False |
| 106 | + for p in added: |
| 107 | + if stamp(p, set_date=True, now=now): |
| 108 | + print(f" stamped (new): {p}") |
| 109 | + changed = True |
| 110 | + for p in modified: |
| 111 | + if stamp(p, set_date=False, now=now): |
| 112 | + print(f" stamped (modified): {p}") |
| 113 | + changed = True |
| 114 | + |
| 115 | + write_output("changed", "true" if changed else "false") |
| 116 | + return 0 |
| 117 | + |
| 118 | + |
| 119 | +if __name__ == "__main__": |
| 120 | + sys.exit(main()) |
0 commit comments