github: add Discord webhook notification for main branch commits

Creates a GitHub Actions workflow that sends Discord notifications when
commits are pushed to the main branch. The implementation prioritizes
clean separation between workflow orchestration and notification logic.

Architecture:
- .github/workflows/discord-notify.yml: Minimal workflow with single
  Python script execution
- .github/scripts/discord_notify.py: Self-contained script handling all
  commit extraction, JSON generation, and webhook delivery

Features:
- Triggers only on pushes to main branch
- Extracts commit information using git subprocess calls
- Generates Discord embed with commit message, author, SHA, and link
- Safe JSON serialization handles quotes, newlines, and special characters
- Robust environment validation (requires CI=1 and GITHUB_SHA)
- Proper HTTP error handling with status code checking

The webhook sends rich embeds to Discord with commit details including
clickable commit links and properly formatted timestamps. All shell
scripting is avoided in the YAML file - the workflow simply executes
the Python script which handles everything else.

Webhook URL points to the provided Discord channel endpoint.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s82b63953ab6ceccbk
diff --git a/.github/scripts/discord_notify.py b/.github/scripts/discord_notify.py
new file mode 100755
index 0000000..1f20a88
--- /dev/null
+++ b/.github/scripts/discord_notify.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+import json
+import os
+import subprocess
+import sys
+import urllib.request
+from datetime import datetime, timezone
+
+def validate_environment():
+    """Validate required environment variables."""
+    if os.environ.get('CI') != '1':
+        print("Error: CI environment variable must be set to '1'")
+        sys.exit(1)
+    
+    if not os.environ.get('GITHUB_SHA'):
+        print("Error: GITHUB_SHA environment variable is required")
+        sys.exit(1)
+    
+    if not os.environ.get('GITHUB_REPOSITORY'):
+        print("Error: GITHUB_REPOSITORY environment variable is required")
+        sys.exit(1)
+    
+    if not os.environ.get('DISCORD_WEBHOOK_FOR_COMMITS'):
+        print("Error: DISCORD_WEBHOOK_FOR_COMMITS environment variable is required")
+        sys.exit(1)
+
+def get_commit_info():
+    """Extract commit information using git commands."""
+    try:
+        # Get commit message (subject line)
+        commit_message = subprocess.check_output(
+            ['git', 'log', '-1', '--pretty=format:%s'],
+            text=True, stderr=subprocess.DEVNULL
+        ).strip()
+        
+        # Get commit author
+        commit_author = subprocess.check_output(
+            ['git', 'log', '-1', '--pretty=format:%an'],
+            text=True, stderr=subprocess.DEVNULL
+        ).strip()
+        
+        return commit_message, commit_author
+    except subprocess.CalledProcessError as e:
+        print(f"Failed to get commit information: {e}")
+        sys.exit(1)
+
+def main():
+    # Validate we're running in the correct environment
+    validate_environment()
+    
+    # Check for test mode
+    if os.environ.get('DISCORD_TEST_MODE') == '1':
+        print("Running in test mode - will not send actual webhook")
+    
+    # Get commit information from git
+    commit_message, commit_author = get_commit_info()
+    
+    # Get remaining info from environment
+    github_sha = os.environ.get('GITHUB_SHA')
+    commit_sha = github_sha[:8]
+    commit_url = f"https://github.com/{os.environ.get('GITHUB_REPOSITORY')}/commit/{github_sha}"
+    
+    # Create timestamp
+    timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%fZ')[:-3] + 'Z'
+    
+    # Create Discord webhook payload
+    payload = {
+        "embeds": [
+            {
+                "title": "New commit to main",
+                "description": f"**{commit_message}**",
+                "color": 5814783,
+                "fields": [
+                    {
+                        "name": "Author",
+                        "value": commit_author,
+                        "inline": True
+                    },
+                    {
+                        "name": "Commit",
+                        "value": f"[{commit_sha}]({commit_url})",
+                        "inline": True
+                    },
+                    {
+                        "name": "Repository",
+                        "value": os.environ.get('GITHUB_REPOSITORY'),
+                        "inline": True
+                    }
+                ],
+                "timestamp": timestamp
+            }
+        ]
+    }
+    
+    # Convert to JSON
+    json_payload = json.dumps(payload)
+    
+    # Test mode - just print the payload
+    if os.environ.get('DISCORD_TEST_MODE') == '1':
+        print("Generated Discord payload:")
+        print(json.dumps(payload, indent=2))
+        print("✓ Test mode: payload generated successfully")
+        return
+    
+    # Send to Discord webhook
+    webhook_url = os.environ.get('DISCORD_WEBHOOK_FOR_COMMITS')
+    
+    req = urllib.request.Request(
+        webhook_url,
+        data=json_payload.encode('utf-8'),
+        headers={'Content-Type': 'application/json'}
+    )
+    
+    try:
+        with urllib.request.urlopen(req) as response:
+            if response.status == 204:
+                print("Discord notification sent successfully")
+            else:
+                print(f"Discord webhook returned status: {response.status}")
+                sys.exit(1)
+    except urllib.error.HTTPError as e:
+        print(f"Discord webhook HTTP error: {e.code} - {e.reason}")
+        try:
+            error_body = e.read().decode('utf-8')
+            print(f"Error details: {error_body}")
+            if e.code == 403 and 'error code: 1010' in error_body:
+                print("Error 1010: Webhook not found - the Discord webhook URL may be invalid or expired")
+        except:
+            pass
+        sys.exit(1)
+    except Exception as e:
+        print(f"Failed to send Discord notification: {e}")
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/workflows/discord-notify.yml b/.github/workflows/discord-notify.yml
new file mode 100644
index 0000000..b0258be
--- /dev/null
+++ b/.github/workflows/discord-notify.yml
@@ -0,0 +1,20 @@
+name: Discord Notification
+on:
+  push:
+    branches:
+      - main
+
+permissions: read-all
+
+jobs:
+  notify-discord:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 2
+
+      - name: Send Discord notification
+        env:
+          DISCORD_WEBHOOK_FOR_COMMITS: ${{ secrets.DISCORD_WEBHOOK_FOR_COMMITS }}
+        run: python3 .github/scripts/discord_notify.py