#!/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())