aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-27 17:28:56 +0100
committerAgando <roduccomm@gmail.com>2026-10-27 17:28:56 +0100
commit21b8ebfc8c47748e19a43d230ecc176d01e3cb83 (patch)
tree6aef153dac176d83518151ed7844cb20314ce79b
parent34354da1235916fd83768d94fac960e425056271 (diff)
feat(docs): tag builds, feed the changelog to machines, fail maintenance when it ages
Tasks 3, 4, 5, 6, 7 of the changelog plan. - 67 build tags (v4.6.10 … v4.6.119) now exist, so a build is a ref instead of something that has to be re-inferred: `git log v4.6.118..v4.6.119` finally means something. - The generator prefers tags, but via ONE `git for-each-ref` pass rather than a `git describe` per commit. That matters: describe-per-commit made the build 2.4-2.7 s against a 1.0-1.45 s baseline — i.e. tags made it slower, which the plan's gate said to treat as "tags bought nothing". The right fix was a bulk lookup, not dropping the tags: now 0.80-1.04 s (median 0.84 s) with output byte-identical to the untagged baseline (68 entries, same version order). - changelog.json + feed.xml (Atom) are generated from the same page dict: 36 KB / 21 KB, 68/68 entries, both HTTP 200, item markup stripped so a reader gets text not HTML. - check_changelog() now also asserts the page was GENERATED recently, not merely that its version matches: a version match survives a dead cron forever. Seeded a 26-day-old timestamp -> "FAIL: changelog: generated 640h ago (>48h) - the daily job is not running", maintenance FAIL; restored -> "PASS: changelog age 0.1h", maintenance OK. - The exact cron pre-script was run end to end, not a stand-in: MAINTENANCE (--fix) block prints "PASS: changelog: changelog current: v4.6.119 (68 entries)" and "PASS: changelog age 0.2h", result OK, no second cron created. - Scale trigger written into the generator's docstring (split by year at ~150 builds), not built. - Mobile re-checked after the page gained the deep-link note: changelog + board both ALL VIEWPORTS OK at 360/390 px, light and dark.
-rw-r--r--CHANGELOG.md36
-rw-r--r--scripts/build-changelog.py77
-rw-r--r--scripts/newos4-maintain.py18
3 files changed, 126 insertions, 5 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a15d68a..979d9e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,13 +22,47 @@ Generated from git history by `scripts/build-changelog.py` — do not edit by ha
Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
+# newOS4 changelog
+
+Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers.
+
<!-- generated:start -->
## v4.6.119 — 2026-10-27
-_Touches modules (4), main.html (1) · fix 1_
+_Touches modules (4), scripts (2), main.html (1), CHANGELOG.md (1) · release 2, fix 1_
- `if test …` was always false and `for` items never globbed` `e575ee8`
+- `Generate the changelog from git and keep it current in maintenance` `22bb505`
+- `Link the changelog in and deep-link each board fix to its build` `b26bfb8`
## v4.6.118 — 2026-10-27
diff --git a/scripts/build-changelog.py b/scripts/build-changelog.py
index 71bcb28..ac4e8c1 100644
--- a/scripts/build-changelog.py
+++ b/scripts/build-changelog.py
@@ -11,9 +11,14 @@ the two prose blocks that described the old data source. No design duplication,
no second copy of the CSS or the renderer.
Run:
- python3 scripts/build-changelog.py # write the page + CHANGELOG.md
+ python3 scripts/build-changelog.py # write page + CHANGELOG.md + json + atom
python3 scripts/build-changelog.py --check # exit 1 if stale, write nothing
-"""
+
+Scale: the page is one file while it stays under ~150 builds (measured 2026-10-27: 68 builds =
+63 KB, so ~200 KB lands near 200 builds). Past that, split by calendar year -- changelog.html
+keeps the newest 40 builds, changelog-<year>.html takes the rest, and this script links them.
+Trigger the work when len(page['entries']) >= 150 stops being unusual, not when the page feels
+slow in a browser."""
import html
import json
@@ -62,7 +67,36 @@ def commits():
return out
+_TAGMAP = None
+
+
+def tag_map():
+ """commit -> build tag, in ONE git call.
+
+ `git describe --tags --abbrev=0 <sha>` per commit is correct but costs a history walk each
+ time: measured 2.5 s for 158 commits versus 1.0 s for plain tree reads. One for-each-ref pass
+ gives the same authority (a tag is a ref, not an inference) at no per-commit cost.
+ `%(*objectname)` dereferences annotated tags to the commit they point at.
+ """
+ global _TAGMAP
+ if _TAGMAP is None:
+ _TAGMAP = {}
+ try:
+ out = git('for-each-ref', 'refs/tags', '--format=%(refname:short) %(*objectname)')
+ except subprocess.CalledProcessError:
+ out = ''
+ for line in out.splitlines():
+ parts = line.split()
+ if len(parts) == 2 and re.match(r'^v?\d+(\.\d+)+$', parts[0]):
+ _TAGMAP[parts[1]] = parts[0]
+ return _TAGMAP
+
+
def version_at(sha):
+ # A tag is the authority; the tree read is only needed for commits that predate tagging.
+ tagged = tag_map().get(sha)
+ if tagged:
+ return tagged
try:
src = git('show', f'{sha}:{LOADER}')
except subprocess.CalledProcessError:
@@ -242,6 +276,40 @@ def write_md(page, rows):
return len(body)
+def write_feed(page):
+ """Same data, two machine shapes: JSON for tools, Atom for readers.
+
+ Entry dates are day-precision, so <updated> gets T00:00:00Z appended rather than
+ pretending to a time of day. Item markup is stripped: a feed reader gets text, not HTML.
+ """
+ base = os.path.dirname(PAGE)
+ with open(os.path.join(base, 'changelog.json'), 'w', encoding='utf-8') as fh:
+ json.dump(page, fh, ensure_ascii=False, indent=1)
+ entries = []
+ for e in page['entries']:
+ title = '%s — %s' % (e['version'], e['date'])
+ body = ' '.join(re.sub(r'<[^>]+>', ' ', i).strip() for i in e['items'])
+ entries.append(
+ ' <entry>\n'
+ ' <title>%s</title>\n'
+ ' <id>tag:webx11.duckdns.org,2026:%s</id>\n'
+ ' <updated>%sT00:00:00Z</updated>\n'
+ ' <summary>%s</summary>\n'
+ ' </entry>' % (html.escape(title), html.escape(e['version']),
+ e['date'], html.escape(re.sub(r'\s+', ' ', body)[:900]))
+ )
+ feed = ('<?xml version="1.0" encoding="utf-8"?>\n'
+ '<feed xmlns="http://www.w3.org/2005/Atom">\n'
+ ' <title>newOS4 changelog</title>\n'
+ ' <id>tag:webx11.duckdns.org,2026:changelog</id>\n'
+ ' <updated>%s</updated>\n <author><name>WebX11</name></author>\n%s\n</feed>\n'
+ % (page['generated'], '\n'.join(entries)))
+ path = os.path.join(base, 'feed.xml')
+ with open(path, 'w', encoding='utf-8') as fh:
+ fh.write(feed)
+ return len(feed)
+
+
def main():
page, rows = build()
if '--check' in sys.argv:
@@ -256,8 +324,9 @@ def main():
return 0
size = write_page(page)
mdsize = write_md(page, rows)
- print('changelog: %d entries, %d commits, %s -> page %d bytes, md %d bytes'
- % (len(page['entries']), len(rows), page['stats']['versions'], size, mdsize))
+ fsize = write_feed(page)
+ print('changelog: %d entries, %d commits, %s -> page %d bytes, md %d bytes, feed %d bytes'
+ % (len(page['entries']), len(rows), page['stats']['versions'], size, mdsize, fsize))
return 0
diff --git a/scripts/newos4-maintain.py b/scripts/newos4-maintain.py
index 8cc7c5a..01cc986 100644
--- a/scripts/newos4-maintain.py
+++ b/scripts/newos4-maintain.py
@@ -5,6 +5,7 @@ import glob, json, os, re, subprocess, sys
from datetime import datetime, timezone
ROOT = '/var/www/webx11.duckdns.org/spaces/newos4'
+PAGE = '/var/www/webx11.duckdns.org/spaces/agando/downloads/changelog.html'
WEB = 'https://webx11.duckdns.org/spaces/newos4'
FIX = '--fix' in sys.argv
DEEP = '--deep' in sys.argv
@@ -134,6 +135,23 @@ def check_changelog():
r = run([sys.executable, script, '--check'], cwd=ROOT)
if r.returncode == 0:
add('PASS', 'changelog: ' + (r.stdout.strip() or 'current'))
+ # A version match is not freshness: if the cron dies, the page and the build stay in
+ # agreement forever while the page stops being regenerated.
+ import datetime as _dt
+ m = re.search(r'"generated"\s*:\s*"([^"]+)"', open(PAGE, encoding='utf-8').read())
+ if not m:
+ add('FAIL', 'changelog: page has no generated timestamp')
+ return
+ try:
+ gen = _dt.datetime.strptime(m.group(1), '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=_dt.timezone.utc)
+ except ValueError:
+ add('FAIL', 'changelog: unreadable generated timestamp ' + m.group(1))
+ return
+ age_h = (_dt.datetime.now(_dt.timezone.utc) - gen).total_seconds() / 3600.0
+ if age_h > 48:
+ add('FAIL', 'changelog: generated %.0fh ago (>48h) - the daily job is not running' % age_h)
+ else:
+ add('PASS', 'changelog age %.1fh' % age_h)
return
stale = (r.stdout or r.stderr).strip().splitlines()
stale = stale[-1][:150] if stale else 'stale'