Adds changelog gen script

This commit is contained in:
Alicia Sykes committed 2026-07-21 22:40:57 +01:00
1 parent 43f08c2bc1
commit 18ef525524
3 files changed
+296

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
{
"entries": [
{
"version": "v1.0.6",
"previous": "v1.0.5",
"date": "2026-07-18",
"templateCount": 676,
"added": [
"PodFetch"
],
"removed": [],
"updated": []
},
{
"version": "v1.0.5",
"previous": "v1.0.4",
"date": "2026-07-18",
"templateCount": 675,
"added": [
"systemprompt"
],
"removed": [],
"updated": []
},
{
"version": "v1.0.4",
"previous": "v1.0.3",
"date": "2026-07-17",
"templateCount": 674,
"added": [],
"removed": [
"Openvscode-server"
],
"updated": []
},
{
"version": "v1.0.3",
"previous": "v1.0.2",
"date": "2026-07-13",
"templateCount": 675,
"added": [
"Reach Me Out"
],
"removed": [],
"updated": []
},
{
"version": "v1.0.2",
"previous": "v1.0.1",
"date": "2026-07-11",
"templateCount": 674,
"added": [
"Doplarr_rs"
],
"removed": [
"Doplarr"
],
"updated": [
{
"title": "Swag",
"fields": [
"env"
]
}
]
},
{
"version": "v1.0.1",
"previous": "v1.0.0",
"date": "2026-07-10",
"templateCount": 674,
"added": [
"Cloudflared-web"
],
"removed": [],
"updated": [
{
"title": "Kimai",
"fields": [
"env"
]
},
{
"title": "Nginx",
"fields": [
"description"
]
}
]
},
{
"version": "v1.0.0",
"previous": null,
"date": "2026-07-06",
"templateCount": 673,
"added": [],
"removed": [],
"updated": []
}
]
}
+83
View File
@@ -0,0 +1,83 @@
# Re-generates the changelog.json whenever a new tag is created
# To track template changes (added/removed/updated)
name: 📋 Changelog
on:
push:
tags: ['v[0-9]+.[0-9]+.[0-9]+']
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
changelog:
name: 📋 Update changelog.json
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: main
fetch-depth: 0
persist-credentials: false
- name: Set up Python 🐍
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.x'
- name: Generate changelog 🪵
run: python3 lib/changelog.py
- name: Commit and push ⤴️
env:
GH_TOKEN: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
set -euo pipefail
git config user.name "Liss-Bot"
git config user.email "alicia-gh-bot@mail.as93.net"
remote="https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
git add .github/changelog.json
if git diff --staged --quiet; then
echo "changelog.json unchanged, nothing to commit"
exit 0
fi
git commit -m "Updates changelog${TAG:+ for ${TAG}} (auto-generated)"
# Replay onto latest main in case it moved since checkout (our commit
# only touches .github/changelog.json, so this can't conflict)
git pull --rebase "$remote" main
git push "$remote" HEAD:main
- name: Summary
if: always()
env:
TAG: ${{ github.ref_type == 'tag' && github.ref_name || 'n/a (manual run)' }}
run: |
set -euo pipefail
{
echo "## Changelog"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| Trigger | \`${TAG}\` |"
if [ -f .github/changelog.json ]; then
versions=$(jq '.entries | length' .github/changelog.json)
latest=$(jq -r '.entries[0]
| "\(.version): ✨ \(.added | length) added · 🗑️ \(.removed | length) removed · ♻️ \(.updated | length) updated"
' .github/changelog.json)
echo "| Versions | ${versions} |"
echo "| Latest | ${latest} |"
else
echo "| Result | ❌ changelog.json was not generated |"
fi
} >> "$GITHUB_STEP_SUMMARY"
+112
View File
@@ -0,0 +1,112 @@
"""Generates .github/changelog.json - what changed between each version tag.
Walks every vMAJOR.MINOR.PATCH tag in version order, diffs the compiled
templates.json between consecutive tags, and records the templates added,
removed and updated in each version. Rebuilt from scratch on every run, so
the output is deterministic (safe to re-run, self-heals, no commit churn).
Matches the diff semantics of the release notes in release.yml: templates
are keyed by title, with `id` ignored (it renumbers on every regeneration,
which would otherwise flag everything as updated).
"""
import json
import os
import re
import subprocess
import sys
from log import get_logger, banner
log = get_logger()
SEMVER_TAG = re.compile(r'^v\d+\.\d+\.\d+$')
# Pre-v1.0.0 tags predate the current release cadence, so their diffs are
# just a giant re-listing of everything. Start the changelog at v1.0.0
FIRST_VERSION = (1, 0, 0)
script_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.join(script_dir, '..')
output_path = os.path.join(root_dir, '.github', 'changelog.json')
def git(*args):
"""Run a git command in the repo root and return its stdout"""
result = subprocess.run(
['git', *args], capture_output=True, text=True, check=True, cwd=root_dir)
return result.stdout
def version(tag):
"""Numeric (major, minor, patch) tuple for a vX.Y.Z tag"""
return tuple(int(p) for p in tag[1:].split('.'))
def semver_tags():
"""All vMAJOR.MINOR.PATCH tags from FIRST_VERSION onwards, oldest first"""
tags = [t for t in git('tag').splitlines() if SEMVER_TAG.match(t)]
return sorted((t for t in tags if version(t) >= FIRST_VERSION), key=version)
def tag_dates():
"""Map of tag -> creation date (YYYY-MM-DD)"""
lines = git('for-each-ref', '--format=%(refname:short) %(creatordate:short)',
'refs/tags').splitlines()
return dict(line.split(' ', 1) for line in lines if ' ' in line)
def templates_at(tag):
"""Templates in templates.json at a tag, keyed by title with `id` dropped.
Returns None if the file doesn't exist at that tag."""
try:
raw = git('show', f'{tag}:templates.json')
except subprocess.CalledProcessError:
return None
templates = json.loads(raw).get('templates', [])
return {t['title']: {k: v for k, v in t.items() if k != 'id'}
for t in templates if isinstance(t, dict) and 'title' in t}
def diff(old, new):
"""Added / removed / updated templates between two title-keyed maps"""
added = sorted(new.keys() - old.keys())
removed = sorted(old.keys() - new.keys())
updated = []
for title in sorted(new.keys() & old.keys()):
if new[title] != old[title]:
fields = sorted(k for k in new[title].keys() | old[title].keys()
if new[title].get(k) != old[title].get(k))
updated.append({'title': title, 'fields': fields})
return added, removed, updated
def main():
banner('Changelog', 'Diff templates.json between version tags')
tags = semver_tags()
if not tags:
log.error('No version tags found (needs full git history, not a shallow clone)')
sys.exit(1)
dates = tag_dates()
entries = []
previous_tag, previous_map = None, None
for tag in tags:
current = templates_at(tag)
if current is None:
log.warning(f'{tag}: no templates.json at this tag, skipping')
continue
first = previous_map is None
added, removed, updated = ([], [], []) if first else diff(previous_map, current)
entries.append({
'version': tag,
'previous': previous_tag,
'date': dates.get(tag),
'templateCount': len(current),
'added': added, 'removed': removed, 'updated': updated,
})
log.info(f'{tag}: first version ({len(current)} total)' if first else
f'{tag}: +{len(added)} added, -{len(removed)} removed, '
f'~{len(updated)} updated ({len(current)} total)')
previous_tag, previous_map = tag, current
entries.reverse() # newest first
with open(output_path, 'w') as file:
json.dump({'entries': entries}, file, indent=2, ensure_ascii=False)
file.write('\n')
log.info(f'Wrote {len(entries)} versions to {os.path.relpath(output_path, root_dir)}')
if __name__ == '__main__':
main()