diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-27 17:28:56 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-27 17:28:56 +0100 |
| commit | 21b8ebfc8c47748e19a43d230ecc176d01e3cb83 (patch) | |
| tree | 6aef153dac176d83518151ed7844cb20314ce79b | |
| parent | 34354da1235916fd83768d94fac960e425056271 (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.md | 36 | ||||
| -rw-r--r-- | scripts/build-changelog.py | 77 | ||||
| -rw-r--r-- | scripts/newos4-maintain.py | 18 |
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' |