From 7b13231f2930646a315e23a1aa53c82f2399c168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1=C4=8Dek?= Date: Wed, 14 Oct 2026 19:48:29 +0200 Subject: [PATCH] Add changelog-only Discord notifications after successful pushes --- AGENTS.md | 23 +++++++++++++ tools/discord_changelog.py | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 AGENTS.md create mode 100644 tools/discord_changelog.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e8a482a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Push changelogs + +The user authorized Discord changelogs after new changes are successfully pushed. +After each successful push to `origin/main` from this checkout: + +1. Prepare a concise, player-facing changelog covering the commits in that push. + Include only the changelog: describe fixes/features accurately, without server, + deployment, or Git push status. Do not add operational disclaimers or footers. + Phrase gameplay fixes without mentioning the server (for example, "Fixed radio + station generation after an empty-session wait"). +2. Save the message to a temporary file and run + `python3 tools/discord_changelog.py --commit --message-file `. +3. Report a delivery failure honestly. Do not blindly retry a pending delivery; + check Discord first to avoid duplicates. + +The webhook credential is in `.local/discord-webhook.url` (ignored, owner-only). +Never print it, commit it, or put it into changelogs or command-line arguments. +The helper verifies the remote commit, disables mentions, and records delivery in +`.local/discord-changelog-state.json`. Do not backfill old commits unless requested. + +This is an assistant workflow, not a server-side Git hook. Notifications for +pushes from other checkouts require a Discord push webhook in Gitea repository +settings; that server-side automation has not been configured. diff --git a/tools/discord_changelog.py b/tools/discord_changelog.py new file mode 100644 index 0000000..0422701 --- /dev/null +++ b/tools/discord_changelog.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Post a prepared changelog after verifying a successful push to origin/main. + +The webhook is local configuration, never a command-line argument or tracked file. +API: https://docs.discord.com/developers/resources/webhook#execute-webhook +""" +import argparse +import json +import shlex +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--commit', required=True, help='Commit just pushed to origin/main') + parser.add_argument('--message-file', required=True, type=Path, help='Prepared player-facing changelog, at most 2000 characters') + args = parser.parse_args() + commit = subprocess.check_output(['git', 'rev-parse', '--verify', args.commit + '^{commit}'], cwd=ROOT, text=True).strip() + ssh = 'ssh -F ' + shlex.quote(str(Path.home() / '.ssh/config')) + remote = subprocess.check_output(['git', '-c', 'core.sshCommand=' + ssh, 'ls-remote', '--exit-code', 'origin', 'refs/heads/main'], cwd=ROOT, text=True).split()[0] + if commit != remote: + parser.error('Requested commit is not the current origin/main tip; no message sent.') + message = args.message_file.read_text().strip() + if not message or len(message) > 2000: + parser.error('Changelog must contain 1..2000 characters.') + state_path = ROOT / '.local/discord-changelog-state.json' + state = json.loads(state_path.read_text()) if state_path.exists() else {} + if state.get('commit') == commit: + print('This commit is already recorded; no duplicate sent. Status: ' + state.get('status', 'unknown')) + return + url = (ROOT / '.local/discord-webhook.url').read_text().strip() + parsed = urllib.parse.urlsplit(url) + if parsed.scheme != 'https' or parsed.netloc != 'discord.com' or not parsed.path.startswith('/api/webhooks/') or parsed.query or parsed.fragment: + parser.error('Local configuration must contain a Discord HTTPS webhook URL.') + payload = json.dumps({'content': message, 'allowed_mentions': {'parse': []}}).encode() + request = urllib.request.Request(url + '?wait=true', data=payload, headers={ + 'Content-Type': 'application/json', 'User-Agent': 'EscapistsReintroduced-Changelog/1.0'}) + # Preserve an uncertain attempt so a timeout cannot silently cause duplicates. + state_path.write_text(json.dumps({'commit': commit, 'status': 'pending'}) + '\n') + try: + with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response: + result = json.load(response) + if not result.get('id'): + raise ValueError('Missing message confirmation') + except urllib.error.HTTPError as error: + print('Discord returned HTTP ' + str(error.code) + '; inspect delivery before retrying.', file=sys.stderr) + return 1 + except (urllib.error.URLError, TimeoutError, ValueError): + print('Discord delivery could not be confirmed; inspect delivery before retrying.', file=sys.stderr) + return 1 + state_path.write_text(json.dumps({'commit': commit, 'status': 'sent', 'message_id': result['id']}) + '\n') + print('Discord changelog delivered for ' + commit[:7]) + return 0 + + +if __name__ == '__main__': + sys.exit(main())