diff --git a/.github/scripts/inject_metadata.py b/.github/scripts/inject_metadata.py new file mode 100644 index 0000000..a12d89d --- /dev/null +++ b/.github/scripts/inject_metadata.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Generates _data/page_metadata.json with git metadata for every Markdown file. +Uses the GitHub API when GITHUB_TOKEN is set, otherwise falls back to git log. +Source .md files are never modified. +""" +import json +import os +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +REPO = "codeday/docs" +TOKEN = os.environ.get("GITHUB_TOKEN") + + +def github_commits(file_path: str): + if not TOKEN: + return None + url = f"https://api.github.com/repos/{REPO}/commits?path={urllib.parse.quote(file_path)}&per_page=100" + req = urllib.request.Request( + url, + headers={ + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github.v3+json", + }, + ) + try: + with urllib.request.urlopen(req) as r: + return json.loads(r.read()) + except urllib.error.HTTPError as e: + print(f" GitHub API {e.code} for {file_path}", file=sys.stderr) + return None + + +def get_metadata(file_path: str) -> dict: + commits = github_commits(file_path) + if commits: + last = commits[0]["commit"]["committer"]["date"].split("T")[0] + latest = commits[0] + last_login = (latest.get("author") or {}).get("login") + last_name = latest["commit"]["author"]["name"] + + seen, authors = set(), [] + for c in commits: + login = (c.get("author") or {}).get("login") + name = c["commit"]["author"]["name"] + key = login or name + if key not in seen: + seen.add(key) + authors.append({"login": login, "name": name}) + + return { + "last_modified_date": last, + "last_modified_by": {"login": last_login, "name": last_name}, + "authors": authors, + } + + # Fallback: local git log + last_commit = subprocess.run( + ["git", "log", "-1", "--format=%ai\t%an", "--", file_path], + capture_output=True, text=True, + ).stdout.strip() + last_date, last_author_name = (last_commit.split("\t") + ["", ""])[:2] + last_date = last_date.split(" ")[0] or None + + authors_out = subprocess.run( + ["git", "log", "--format=%an", "--", file_path], + capture_output=True, text=True, + ).stdout.strip().split("\n") + seen, authors = set(), [] + for name in authors_out: + if name and name not in seen: + seen.add(name) + authors.append({"login": None, "name": name}) + + return { + "last_modified_date": last_date, + "last_modified_by": {"login": None, "name": last_author_name} if last_author_name else None, + "authors": authors, + } + + +# ---- main --------------------------------------------------------------- +source = Path(__file__).parent.parent.parent +md_files = [ + p for p in source.rglob("*.md") + if not any(part.startswith(".") or part in ("_site", "node_modules") for part in p.parts) +] + +metadata = {} +for filepath in sorted(md_files): + rel = str(filepath.relative_to(source)) + print(f"Processing {rel} …", end=" ", flush=True) + metadata[rel] = get_metadata(rel) + print("done") + +out = source / "_data" / "page_metadata.json" +out.parent.mkdir(exist_ok=True) +out.write_text(json.dumps(metadata, indent=2), encoding="utf-8") +print(f"\nWrote {out.relative_to(source)} ({len(metadata)} entries)") diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml index 61f2d80..756ca31 100644 --- a/.github/workflows/jekyll.yml +++ b/.github/workflows/jekyll.yml @@ -33,6 +33,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Ruby # https://github.com/ruby/setup-ruby/releases/tag/v1.207.0 uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4 @@ -43,6 +45,10 @@ jobs: - name: Setup Pages id: pages uses: actions/configure-pages@v5 + - name: Inject page metadata + run: python3 .github/scripts/inject_metadata.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build with Jekyll # Outputs to the './_site' directory by default run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..9c25013 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.6 diff --git a/_includes/footer_custom.html b/_includes/footer_custom.html new file mode 100644 index 0000000..bda6d9c --- /dev/null +++ b/_includes/footer_custom.html @@ -0,0 +1,49 @@ +{% assign meta = site.data.page_metadata[page.path] %} +{% if meta %} +
+ {% if meta.authors and meta.authors.size > 0 %} + + {% if meta.authors.size > 1 %}Authors:{% else %}Author:{% endif %} + {% for author in meta.authors %} + {% if author.login %} + {{ author.name | default: author.login }} + {% else %} + {{ author.name }} + {% endif %} + {% unless forloop.last %}, {% endunless %} + {% endfor %} + + {% endif %} + {% if meta.last_modified_date %} + + Last modified {{ meta.last_modified_date | date: "%b %-d, %Y" }} + {% if meta.last_modified_by %} + by + {% if meta.last_modified_by.login %} + {{ meta.last_modified_by.name | default: meta.last_modified_by.login }} + {% else %} + {{ meta.last_modified_by.name }} + {% endif %} + {% endif %} + + {% endif %} +
+{% endif %} + +