From fe0a9ebff7a912ceb300ca27f2c1dc21db395d51 Mon Sep 17 00:00:00 2001 From: developtheweb Date: Tue, 7 Jul 2026 21:58:06 -0400 Subject: [PATCH] ci: nightly feed, stats, and entropy refresh; fix: snake workflow --- .github/workflows/nightly.yml | 37 ++++++ .github/workflows/snake.yml | 12 +- engine/nightly.py | 210 ++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/nightly.yml create mode 100644 engine/nightly.py diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..21cb5f0 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,37 @@ +name: Nightly Telemetry + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +concurrency: + group: demon-state + cancel-in-progress: false + +permissions: + contents: write + +jobs: + refresh: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Refresh feed, stats, featured cards, entropy field + env: + GITHUB_TOKEN: ${{ github.token }} + run: python3 engine/nightly.py + + - name: Commit if changed + run: | + if git diff --quiet; then + echo "no changes" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898470+github-actions[bot]@users.noreply.github.com" + git add README.md state/demon.json assets/entropy.svg + git commit -m "chore: nightly telemetry refresh" + git pull --rebase origin "${GITHUB_REF_NAME}" + git push diff --git a/.github/workflows/snake.yml b/.github/workflows/snake.yml index 81849ab..988375b 100644 --- a/.github/workflows/snake.yml +++ b/.github/workflows/snake.yml @@ -4,19 +4,15 @@ on: schedule: # Runs every 12 hours - cron: "0 */12 * * *" - + workflow_dispatch: - - push: - branches: - - main jobs: generate: runs-on: ubuntu-latest permissions: contents: write - + steps: - name: Generate Snake uses: Platane/snk/svg-only@v3 @@ -25,11 +21,11 @@ jobs: outputs: | dist/github-contribution-grid-snake.svg dist/github-contribution-grid-snake-dark.svg?palette=github-dark - + - name: Push to output branch uses: crazy-max/ghaction-github-pages@v4 with: target_branch: output build_dir: dist env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/engine/nightly.py b/engine/nightly.py new file mode 100644 index 0000000..d4358b2 --- /dev/null +++ b/engine/nightly.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Nightly telemetry refresh for the profile README. + +Updates the marker-fenced sections of README.md: + FEED — 4 newest Strange Quarks articles (RSS if populated, else the + server-rendered blog HTML) + STATS — telemetry row with live star/repo totals + FEATURED — star counts on the featured project cards +and bumps the entropy field's chaos generation so the field differs daily. + +Every section is fail-safe: if a fetch or parse fails, that section is left +untouched and the script still exits 0. Stdlib only. +""" + +import datetime +import html as htmllib +import json +import os +import re +import subprocess +import sys +import urllib.request +import xml.etree.ElementTree as ET + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +README = os.path.join(ROOT, "README.md") +STATE_PATH = os.path.join(ROOT, "state", "demon.json") + +SITE = "https://stevenmilanese.com" +FEED_URL = f"{SITE}/feed.xml" +BLOG_URL = f"{SITE}/blog" + +FEATURED_REPOS = [ + ("anthropic-certs", "Verified Anthropic certifications — Claude, API, Claude Code CLI, MCP."), + ("slTrain", "A steam locomotive for your terminal, smoke trail included."), + ("meowchi-releases", "A desktop pet that evolves when you actually get work done."), + ("mpl", "Mathematics Programming Language — write the equation, run the equation."), +] + + +def fetch(url, accept=None): + req = urllib.request.Request(url, headers={"User-Agent": "developtheweb-nightly/1.0"}) + if accept: + req.add_header("Accept", accept) + token = os.environ.get("GITHUB_TOKEN") + if token and url.startswith("https://api.github.com"): + req.add_header("Authorization", f"Bearer {token}") + with urllib.request.urlopen(req, timeout=30) as resp: + return resp.read().decode("utf-8", errors="replace") + + +def gh_api(path): + return json.loads(fetch(f"https://api.github.com{path}", accept="application/vnd.github+json")) + + +def replace_section(text, marker, body): + start, end = f"", f"" + if start not in text or end not in text: + raise ValueError(f"missing {marker} markers") + pre = text.split(start)[0] + post = text.split(end)[1] + return f"{pre}{start}\n{body}\n{end}{post}" + + +def articles_from_rss(): + root = ET.fromstring(fetch(FEED_URL)) + items = [] + for item in root.iter("item"): + title = (item.findtext("title") or "").strip() + link = (item.findtext("link") or "").strip() + pub = (item.findtext("pubDate") or "").strip() + date = None + try: + date = datetime.datetime.strptime(pub[:16].strip(), "%a, %d %b %Y") + except ValueError: + pass + if title and link: + items.append({"title": title, "url": link, "date": date, "meta": ""}) + return items + + +def articles_from_html(): + page = fetch(BLOG_URL) + skip = ("/blog/tag/", "/blog/author/") + anchors = [ + m for m in re.finditer(r']+href="(/blog/[a-z0-9][a-z0-9-]*)"', page) + if not m.group(1).startswith(skip) + ] + seen, articles = set(), [] + for idx, m in enumerate(anchors): + slug = m.group(1) + if slug in seen: + continue + seen.add(slug) + stop = len(page) + for nxt in anchors[idx + 1:]: + if nxt.group(1) != slug: + stop = nxt.start() + break + window = page[m.start():stop] + title_m = re.search(r'alt="([^"]+)"', window) + stripped = re.sub(r"<[^>]+>", " ", window) + stripped = htmllib.unescape(re.sub(r"", "", stripped)) + date_m = re.search(r"([A-Z][a-z]+ \d{1,2}, \d{4})", stripped) + mins_m = re.search(r"(\d+)\s*min", stripped) + views_m = re.search(r"([\d,]+)\s*views", stripped) + if not (title_m and date_m): + continue + date = datetime.datetime.strptime(date_m.group(1), "%B %d, %Y") + meta = f"{mins_m.group(1)} min read" if mins_m else ( + f"{views_m.group(1)} views" if views_m else "" + ) + articles.append({ + "title": htmllib.unescape(title_m.group(1)), + "url": f"{SITE}{slug}", + "date": date, + "meta": meta, + }) + return articles + + +def update_feed(text): + articles = [] + try: + articles = articles_from_rss() + except Exception as exc: + print(f"feed: rss unusable ({exc}); falling back to html") + if not articles: + articles = articles_from_html() + dated = [a for a in articles if a["date"]] + dated.sort(key=lambda a: a["date"], reverse=True) + newest = dated[:4] + if len(newest) < 4: + raise ValueError(f"only {len(newest)} parseable articles") + rows = ["| Article | |", "|:---|---:|"] + for a in newest: + title = a["title"].replace("|", "\\|") + when = a["date"].strftime("%b %d, %Y").replace(" 0", " ") + meta = f"{when} · {a['meta']}" if a["meta"] else when + rows.append(f"| [{title}]({a['url']}) | {meta} |") + return replace_section(text, "FEED", "\n".join(rows)) + + +def update_stats(text): + repos = gh_api("/users/developtheweb/repos?per_page=100&type=owner") + public = [r for r in repos if not r["private"]] + stars = sum(r["stargazers_count"] for r in public) + line = ( + "
\n\n" + "`10¹⁰⁶ yr until heat death — the deadline` · " + "`2.9 zJ of order per bit sorted (kT ln 2, 300 K)` · " + f"`★ {stars} stars across {len(public)} public repos`\n\n" + "
" + ) + return replace_section(text, "STATS", line) + + +def update_featured(text): + cards = [] + for name, blurb in FEATURED_REPOS: + repo = gh_api(f"/repos/developtheweb/{name}") + cards.append( + f'\n' + f'

{name}

\n' + f'

★ {repo["stargazers_count"]}

\n' + f'

{blurb}

\n' + f'' + ) + table = ( + '
\n\n' + f'\n{cards[0]}\n{cards[1]}\n\n' + f'\n{cards[2]}\n{cards[3]}\n\n' + '
\n
' + ) + return replace_section(text, "FEATURED", table) + + +def bump_entropy(): + with open(STATE_PATH) as fh: + state = json.load(fh) + state["generation"] += 1 + with open(STATE_PATH, "w") as fh: + json.dump(state, fh, indent=2) + fh.write("\n") + subprocess.run( + [sys.executable, os.path.join(ROOT, "engine", "generate_entropy_svg.py")], + check=True, + ) + + +def main(): + with open(README) as fh: + text = fh.read() + for label, fn in (("FEED", update_feed), ("STATS", update_stats), ("FEATURED", update_featured)): + try: + text = fn(text) + print(f"{label}: updated") + except Exception as exc: + print(f"{label}: left untouched ({exc})") + with open(README, "w") as fh: + fh.write(text) + try: + bump_entropy() + print("ENTROPY: regenerated") + except Exception as exc: + print(f"ENTROPY: left untouched ({exc})") + + +if __name__ == "__main__": + main()