Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .beads/interactions.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@
{"id":"int-1d38de5ecfd3c7c248a7e3699b5cf56e","kind":"field_change","created_at":"2026-08-25T22:43:44.2927458Z","actor":"Tom Scott","issue_id":"mpc-4s2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Updated opacity reduction to 20% (scale alpha to 80%) in compositor when transparent_text_boxes is enabled, and updated unit tests."}}
{"id":"int-aa5fcf9aa2c9cd767706cab670fa0c11","kind":"field_change","created_at":"2026-08-25T22:51:07.6651874Z","actor":"Tom Scott","issue_id":"mpc-que","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Updated Sample Deck button with requested decklist, expanded the Format block with all supported card and prompt formats, and removed the generation text indicators from preview cards."}}
{"id":"int-27375766f3aa10fd6238234470cf4bf7","kind":"field_change","created_at":"2026-08-31T00:50:48.1740011Z","actor":"Tom Scott","issue_id":"mpc-zmy","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented custom card rename feature with SLX chevron, infilled title plate, and ' @ ' syntax support in deck lines across parser, compositor, backend, and frontend."}}
{"id":"int-08f4cb0e3190dcaceca1ac469a7736ea","kind":"field_change","created_at":"2026-09-19T04:03:55.1802074Z","actor":"Tom Scott","issue_id":"mpc-5om","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed dependency review, upgrades, testing, and PR creation (#1)"}}
{"id":"int-1cf927def3f90c9e8db539798571dabb","kind":"field_change","created_at":"2026-09-24T18:59:54.2466597Z","actor":"Tom Scott","issue_id":"mpc-08e","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added Discord webhook notification step to GitHub Actions CI workflow for PRs upon test success, along with helper script and unit tests."}}
{"id":"int-c515ef7217366424cc713c3e567b5532","kind":"field_change","created_at":"2026-09-24T21:27:14.9849328Z","actor":"Tom Scott","issue_id":"mpc-e8l","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Removed hardcoded webhook URL from ci.yml and notify_discord.py. Configured DISCORD_WEBHOOK_URL to use GitHub Secret."}}
{"id":"int-15c589bc2bfe31dca72f14017480c1a4","kind":"field_change","created_at":"2026-09-24T21:32:50.0423656Z","actor":"Tom Scott","issue_id":"mpc-y2q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Verified notify_discord.py writes to log and cleanly skips the step when DISCORD_WEBHOOK_URL is not present."}}
{"id":"int-96331b1d07884bf004c7d64c3462a372","kind":"field_change","created_at":"2026-09-25T16:18:41.5000114Z","actor":"Tom Scott","issue_id":"mpc-9fn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Reviewed dependencies, updated transitive dependencies to safe minor/patch releases, verified 0 vulnerabilities and 78/78 tests passing, and included Discord CI integration."}}
173 changes: 173 additions & 0 deletions .github/scripts/notify_discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Send a notification to Discord on successful PR CI test completion.

This script constructs a formatted Discord embed and payload containing
the pull request title, description, and link, then posts it to the configured
webhook URL.
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from typing import Any, Dict, Optional

EMBED_COLOR_SUCCESS = 3066993 # 0x2ecc71 (Green)
MAX_TITLE_LENGTH = 256
MAX_DESCRIPTION_LENGTH = 4000


def truncate_text(text: str, max_length: int) -> str:
"""Truncate text to max_length with an ellipsis if needed."""
if len(text) <= max_length:
return text
return text[: max_length - 3] + "..."


def build_discord_payload(
pr_title: str,
pr_body: Optional[str],
pr_url: Optional[str],
pr_number: Optional[str] = None,
pr_author: Optional[str] = None,
) -> Dict[str, Any]:
"""Construct a Discord message payload with a rich embed."""
raw_title = pr_title.strip() if pr_title else "Pull Request"
if pr_number and str(pr_number).strip():
display_title = f"PR #{str(pr_number).strip()}: {raw_title}"
else:
display_title = raw_title
display_title = truncate_text(display_title, MAX_TITLE_LENGTH)

clean_body = (pr_body or "").strip()
if not clean_body:
clean_body = "No description provided."
clean_body = truncate_text(clean_body, MAX_DESCRIPTION_LENGTH)

content_line = f"CI tests passed for PR: **{display_title}**"
clean_url = (pr_url or "").strip()
if clean_url and clean_url.startswith(("http://", "https://")):
content_line += f"\n<{clean_url}>"

embed: Dict[str, Any] = {
"title": display_title,
"description": clean_body,
"color": EMBED_COLOR_SUCCESS,
"footer": {
"text": "MPCWithGenerativeArt CI"
},
}

if clean_url and clean_url.startswith(("http://", "https://")):
embed["url"] = clean_url
embed["fields"] = [
{
"name": "PR Link",
"value": clean_url,
"inline": False,
}
]

if pr_author and pr_author.strip():
embed["author"] = {
"name": pr_author.strip()
}

return {
"content": content_line,
"embeds": [embed],
}


def send_discord_notification(
webhook_url: str,
payload: Dict[str, Any],
timeout: float = 15.0,
) -> bool:
"""Send payload to Discord webhook URL via HTTP POST."""
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
webhook_url,
data=data,
headers={
"Content-Type": "application/json",
"User-Agent": "GitHub-Actions-CI/1.0",
},
)

try:
with urllib.request.urlopen(req, timeout=timeout) as response:
status_code = getattr(response, "status", 200)
print(f"[Discord CI] Notification sent successfully (HTTP {status_code}).")
return True
except urllib.error.HTTPError as exc:
err_msg = exc.read().decode("utf-8", errors="replace")
print(f"[Discord CI] HTTP Error {exc.code}: {err_msg}", file=sys.stderr)
return False
except Exception as exc:
print(f"[Discord CI] Error sending webhook: {exc}", file=sys.stderr)
return False


def parse_args() -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description="Send Discord PR CI notification")
parser.add_argument("--webhook-url", default=None, help="Discord Webhook URL")
parser.add_argument("--title", default=None, help="PR Title")
parser.add_argument("--body", default=None, help="PR Description")
parser.add_argument("--url", default=None, help="PR URL")
parser.add_argument("--number", default=None, help="PR Number")
parser.add_argument("--author", default=None, help="PR Author")
parser.add_argument(
"--dry-run",
action="store_true",
help="Print payload without sending HTTP request",
)
return parser.parse_args()


def main() -> int:
"""Main execution entrypoint."""
args = parse_args()

webhook_url = (
args.webhook_url
or os.environ.get("DISCORD_WEBHOOK_URL")
or ""
).strip()

if not webhook_url and not args.dry_run:
print(
"[Discord CI] DISCORD_WEBHOOK_URL secret is not present. Skipping step."
)
return 0

pr_title = args.title or os.environ.get("PR_TITLE", "Pull Request")
pr_body = args.body if args.body is not None else os.environ.get("PR_BODY")
pr_url = args.url or os.environ.get("PR_URL", "")
pr_number = args.number or os.environ.get("PR_NUMBER", "")
pr_author = args.author or os.environ.get("PR_AUTHOR", "")

payload = build_discord_payload(
pr_title=pr_title,
pr_body=pr_body,
pr_url=pr_url,
pr_number=pr_number,
pr_author=pr_author,
)

if args.dry_run:
print("[Discord CI] Dry run enabled. Payload:")
print(json.dumps(payload, indent=2))
return 0

success = send_discord_notification(webhook_url=webhook_url, payload=payload)
return 0 if success else 1


if __name__ == "__main__":
sys.exit(main())
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,17 @@ jobs:
TESTING: "true"
ENV_FILE: "none"
run: |
python -m unittest tests/test_grok.py tests/test_janus.py tests/test_api.py tests/test_pipeline.py tests/test_samples.py
python -m unittest tests/test_grok.py tests/test_janus.py tests/test_api.py tests/test_pipeline.py tests/test_samples.py tests/test_notify_discord.py

- name: Notify Discord on PR test success
if: success() && github.event_name == 'pull_request'
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
python .github/scripts/notify_discord.py

Loading
Loading