#!/usr/bin/env python3 import argparse import json import hmac import hashlib import httpx import sqlite3 import os import time def sign(secret: str, body: bytes) -> str: return "sha256=" + hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest() def seed_state(state_path: str, pr_key: str, bug_id: int = 1): # Create/seed the PR->Bug mapping so the server skips Bugzilla and allows synchronize to run mirroring. os.makedirs(os.path.dirname(state_path) or ".", exist_ok=True) conn = sqlite3.connect(state_path) cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS pr_map ( pr_key TEXT PRIMARY KEY, bug_id INTEGER NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) """) now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) cur.execute(""" INSERT INTO pr_map (pr_key, bug_id, created_at, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(pr_key) DO UPDATE SET bug_id=excluded.bug_id, updated_at=excluded.updated_at """, (pr_key, bug_id, now, now)) conn.commit() conn.close() def build_payload(owner_repo: str, pr_number: int, title: str, body: str, base_branch: str, base_sha: str, head_branch: str, head_sha: str, gh_user: str): owner, repo = owner_repo.split("/", 1) return { "action": "synchronize", "repository": { "full_name": owner_repo, "owner": {"login": owner}, "name": repo, }, "pull_request": { "number": pr_number, "html_url": f"https://github.com/{owner_repo}/pull/{pr_number}", "title": title, "body": body, "user": {"login": gh_user, "html_url": f"https://github.com/{gh_user}"}, "base": {"ref": base_branch, "sha": base_sha}, "head": {"ref": head_branch, "sha": head_sha, "repo": {"owner": {"login": gh_user}}}, "created_at": "2025-09-20T12:34:56Z", "labels": [], }, } def main(): ap = argparse.ArgumentParser(description="Post a signed GitHub PR webhook to a running webhook_endpoint server") ap.add_argument("--server", required=True, help="Base URL of running server, e.g. http://127.0.0.1:8081") ap.add_argument("--secret", required=True, help="Webhook secret (must match server WEBHOOK_SECRET)") ap.add_argument("--repo-full", required=True, help="GitHub owner/repo (must match real repo hosting the PR)") ap.add_argument("--pr", type=int, required=True, help="GitHub PR number to mirror") ap.add_argument("--title", default="Test PR from harness") ap.add_argument("--body", default="Harness test body.") ap.add_argument("--base-branch", default="master") ap.add_argument("--base-sha", default="9f8e7d6cafebabe0000deadbeef0000000000000") ap.add_argument("--head-branch", default="feature/test") ap.add_argument("--head-sha", default="a1b2c3ddeeddbb0000deadbeef0000000000000") ap.add_argument("--gh-user", default="octocat", help="GitHub login of PR author") ap.add_argument("--state-path", required=True, help="Path to the server's STATE_PATH SQLite file") ap.add_argument("--seed", action="store_true", help="Seed the state DB so Bugzilla is skipped") args = ap.parse_args() pr_key = f"{args.repo_full}#{args.pr}" if args.seed: print(f"Seeding PR mapping in {args.state_path}: {pr_key} -> bug_id=1") seed_state(args.state_path, pr_key, bug_id=1) payload = build_payload( owner_repo=args.repo_full, pr_number=args.pr, title=args.title, body=args.body, base_branch=args.base_branch, base_sha=args.base_sha, head_branch=args.head_branch, head_sha=args.head_sha, gh_user=args.gh_user, ) body_bytes = json.dumps(payload).encode("utf-8") sig = sign(args.secret, body_bytes) headers = { "X-GitHub-Event": "pull_request", "X-Hub-Signature-256": sig, "Content-Type": "application/json", } url = args.server.rstrip("/") + "/webhook/github" print(f"POST {url} for {args.repo_full} PR #{args.pr}") r = httpx.post(url, data=body_bytes, headers=headers, timeout=30) print("Response:", r.status_code, r.text) if __name__ == "__main__": main()