ci: nightly feed, stats, and entropy refresh; fix: snake workflow
This commit is contained in:
parent
3ebe545f10
commit
fe0a9ebff7
3 changed files with 251 additions and 8 deletions
37
.github/workflows/nightly.yml
vendored
Normal file
37
.github/workflows/nightly.yml
vendored
Normal file
|
|
@ -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
|
||||||
4
.github/workflows/snake.yml
vendored
4
.github/workflows/snake.yml
vendored
|
|
@ -7,10 +7,6 @@ on:
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate:
|
generate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
210
engine/nightly.py
Normal file
210
engine/nightly.py
Normal file
|
|
@ -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"<!-- {marker}:START -->", f"<!-- {marker}:END -->"
|
||||||
|
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'<a[^>]+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 = (
|
||||||
|
"<div align=\"center\">\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"
|
||||||
|
"</div>"
|
||||||
|
)
|
||||||
|
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'<td width="50%" valign="top">\n'
|
||||||
|
f'<h3 align="center"><a href="{repo["html_url"]}">{name}</a></h3>\n'
|
||||||
|
f'<p align="center"><code>★ {repo["stargazers_count"]}</code></p>\n'
|
||||||
|
f'<p align="center">{blurb}</p>\n'
|
||||||
|
f'</td>'
|
||||||
|
)
|
||||||
|
table = (
|
||||||
|
'<div align="center">\n<table>\n'
|
||||||
|
f'<tr>\n{cards[0]}\n{cards[1]}\n</tr>\n'
|
||||||
|
f'<tr>\n{cards[2]}\n{cards[3]}\n</tr>\n'
|
||||||
|
'</table>\n</div>'
|
||||||
|
)
|
||||||
|
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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue