Latest version - 19Sept2025

As running on vps.bjsystems.co.uk accessible by: https://githubpr.bjsystems.co.uk

https://githubpr.bjsystems.co.uk/healthz
https://githubpr.bjsystems.co.uk/webhook/github
This commit is contained in:
2025-09-19 17:40:15 +02:00
parent de4bf24f5b
commit 7da658ec0f

View File

@@ -7,13 +7,45 @@ import logging
import hmac import hmac
import hashlib import hashlib
import contextlib import contextlib
from typing import Optional, Tuple, Dict, Any from typing import Optional, Tuple, Dict, Any, List
import httpx import httpx
from fastapi import FastAPI, Header, HTTPException, Request from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse, PlainTextResponse from fastapi.responses import JSONResponse, PlainTextResponse
from github import Github, Auth from github import Github, Auth
#
# .env contents (with keys obscured))
#
# WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxx
# LOG_LEVEL=INFO
# # GitHub (set GITHUB_DRY_RUN=true during early tests to avoid PR writes)
# GITHUB_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# GITHUB_DRY_RUN=false
# # Bugzilla base
# BUGZILLA_BASE_URL=https://bugs.koozali.org
# BUGZILLA_PRODUCT=SME Server 11.x
# BUGZILLA_VERSION=11.beta1
# BUGZILLA_COMPONENT_DEFAULT=Github PR
# # Auth: prefer API key
# BUGZILLA_AUTH_MODE=api_key
# BUGZILLA_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# # (or JSON-RPC login if no API key)
# # BUGZILLA_AUTH_MODE=basic
# BUGZILLA_USER=githubpr
# # BUGZILLA_PASSWORD=your_password
# # Behavior
# BUGZILLA_ATTACH_DIFF=true
# STATE_PATH=/home/githubpr/app/webhook/state/webhook_state.sqlite
# GITEA_BASE_URL=https://src.koozali.org
# GITEA_ORG=smeserver
# BUGZILLA_CC_ENABLE=true
# BUGZILLA_CC_ALLOW_NOREPLY=false
#
# ---------------- Logging (UTC) ---------------- # ---------------- Logging (UTC) ----------------
class UTCFormatter(logging.Formatter): class UTCFormatter(logging.Formatter):
@@ -56,19 +88,24 @@ BUGZILLA_API_KEY = os.environ.get("BUGZILLA_API_KEY", "").strip()
BUGZILLA_USER = os.environ.get("BUGZILLA_USER", "").strip() BUGZILLA_USER = os.environ.get("BUGZILLA_USER", "").strip()
BUGZILLA_PASSWORD = os.environ.get("BUGZILLA_PASSWORD", "").strip() BUGZILLA_PASSWORD = os.environ.get("BUGZILLA_PASSWORD", "").strip()
# REQUIRED fields for bug creation # REQUIRED fields
BUGZILLA_PRODUCT = os.environ.get("BUGZILLA_PRODUCT", "").strip() BUGZILLA_PRODUCT = os.environ.get("BUGZILLA_PRODUCT", "").strip()
BUGZILLA_VERSION = os.environ.get("BUGZILLA_VERSION", "").strip() BUGZILLA_VERSION = os.environ.get("BUGZILLA_VERSION", "").strip()
# Component default (as requested) # Component default
BUGZILLA_COMPONENT_DEFAULT = os.environ.get("BUGZILLA_COMPONENT_DEFAULT", "e-smith-*/smeserver-* packages").strip() BUGZILLA_COMPONENT_DEFAULT = os.environ.get("BUGZILLA_COMPONENT_DEFAULT", "e-smith-*/smeserver-* packages").strip()
# In case we still need a fallback name for unexpected errors
BUGZILLA_COMPONENT_FALLBACK = os.environ.get("BUGZILLA_COMPONENT_FALLBACK", BUGZILLA_COMPONENT_DEFAULT).strip() BUGZILLA_COMPONENT_FALLBACK = os.environ.get("BUGZILLA_COMPONENT_FALLBACK", BUGZILLA_COMPONENT_DEFAULT).strip()
# Custom field settings # Custom field settings
CF_PACKAGE_FIELD = os.environ.get("BUGZILLA_CF_PACKAGE_FIELD", "cf_package").strip() CF_PACKAGE_FIELD = os.environ.get("BUGZILLA_CF_PACKAGE_FIELD", "cf_package").strip()
CF_PACKAGE_FALLBACK = os.environ.get("BUGZILLA_CF_PACKAGE_FALLBACK", "---").strip() CF_PACKAGE_FALLBACK = os.environ.get("BUGZILLA_CF_PACKAGE_FALLBACK", "---").strip()
# CC behavior
BUGZILLA_CC_ENABLE = os.environ.get("BUGZILLA_CC_ENABLE", "true").strip().lower() == "true"
BUGZILLA_CC_ALLOW_NOREPLY = os.environ.get("BUGZILLA_CC_ALLOW_NOREPLY", "false").strip().lower() == "true"
BUGZILLA_CC_FALLBACK_COMMENT = os.environ.get("BUGZILLA_CC_FALLBACK_COMMENT", "true").strip().lower() == "true"
AUTHOR_EMAIL_MARKER = os.environ.get("BUGZILLA_AUTHOR_EMAIL_MARKER", "[mirror-bot:author-email]").strip()
# Behavior toggles # Behavior toggles
BUGZILLA_ATTACH_DIFF = os.environ.get("BUGZILLA_ATTACH_DIFF", "true").strip().lower() == "true" BUGZILLA_ATTACH_DIFF = os.environ.get("BUGZILLA_ATTACH_DIFF", "true").strip().lower() == "true"
BUGZILLA_FAILURE_LABEL = os.environ.get("BUGZILLA_FAILURE_LABEL", "bugzilla-needed").strip() BUGZILLA_FAILURE_LABEL = os.environ.get("BUGZILLA_FAILURE_LABEL", "bugzilla-needed").strip()
@@ -125,7 +162,6 @@ class BugzillaHybrid:
self.rpc_url = f"{self.base}/jsonrpc.cgi" self.rpc_url = f"{self.base}/jsonrpc.cgi"
self._token: Optional[str] = None # for basic via JSON-RPC self._token: Optional[str] = None # for basic via JSON-RPC
self._detect_mode() self._detect_mode()
# Log auth status
if BUGZILLA_AUTH_MODE in ("api_key", "") and BUGZILLA_API_KEY: if BUGZILLA_AUTH_MODE in ("api_key", "") and BUGZILLA_API_KEY:
logger.info("Bugzilla auth: API key mode") logger.info("Bugzilla auth: API key mode")
elif BUGZILLA_AUTH_MODE == "basic" or (BUGZILLA_USER and BUGZILLA_PASSWORD): elif BUGZILLA_AUTH_MODE == "basic" or (BUGZILLA_USER and BUGZILLA_PASSWORD):
@@ -134,7 +170,6 @@ class BugzillaHybrid:
logger.warning("Bugzilla auth NOT configured yet (set BUGZILLA_API_KEY or BUGZILLA_USER/BUGZILLA_PASSWORD).") logger.warning("Bugzilla auth NOT configured yet (set BUGZILLA_API_KEY or BUGZILLA_USER/BUGZILLA_PASSWORD).")
def _detect_mode(self): def _detect_mode(self):
# Try REST at /rest then /rest.cgi
try: try:
r = httpx.get(f"{self.base}/rest/version", timeout=10) r = httpx.get(f"{self.base}/rest/version", timeout=10)
if r.status_code == 200: if r.status_code == 200:
@@ -153,7 +188,6 @@ class BugzillaHybrid:
return return
except Exception: except Exception:
pass pass
# Fallback to JSON-RPC
self.mode = "jsonrpc" self.mode = "jsonrpc"
logger.warning("Bugzilla REST not available; falling back to JSON-RPC") logger.warning("Bugzilla REST not available; falling back to JSON-RPC")
@@ -161,7 +195,6 @@ class BugzillaHybrid:
def _rest_headers(self) -> Dict[str, str]: def _rest_headers(self) -> Dict[str, str]:
h = {"Accept": "application/json", "Content-Type": "application/json"} h = {"Accept": "application/json", "Content-Type": "application/json"}
# Prefer API key if available
if BUGZILLA_API_KEY: if BUGZILLA_API_KEY:
h["X-BUGZILLA-API-KEY"] = BUGZILLA_API_KEY h["X-BUGZILLA-API-KEY"] = BUGZILLA_API_KEY
return h return h
@@ -184,7 +217,6 @@ class BugzillaHybrid:
return data.get("result", {}) return data.get("result", {})
def _rpc(self, method: str, params_obj: Dict[str, Any], req_id: int = 1) -> Dict[str, Any]: def _rpc(self, method: str, params_obj: Dict[str, Any], req_id: int = 1) -> Dict[str, Any]:
# Decide auth automatically
mode = BUGZILLA_AUTH_MODE mode = BUGZILLA_AUTH_MODE
if (mode in ("", "api_key") and BUGZILLA_API_KEY): if (mode in ("", "api_key") and BUGZILLA_API_KEY):
params_obj = dict(params_obj, Bugzilla_api_key=BUGZILLA_API_KEY) params_obj = dict(params_obj, Bugzilla_api_key=BUGZILLA_API_KEY)
@@ -231,7 +263,6 @@ class BugzillaHybrid:
except Exception as e: except Exception as e:
logger.warning(f"Bugzilla REST create failed, switching to JSON-RPC: {e}") logger.warning(f"Bugzilla REST create failed, switching to JSON-RPC: {e}")
self.mode = "jsonrpc" self.mode = "jsonrpc"
# JSON-RPC
params = { params = {
"product": BUGZILLA_PRODUCT, "product": BUGZILLA_PRODUCT,
"component": component or BUGZILLA_COMPONENT_DEFAULT, "component": component or BUGZILLA_COMPONENT_DEFAULT,
@@ -291,7 +322,6 @@ class BugzillaHybrid:
self._rpc("Bug.add_comment", {"id": bug_id, "comment": comment}, req_id=2) self._rpc("Bug.add_comment", {"id": bug_id, "comment": comment}, req_id=2)
def update_bug_fields(self, bug_id: int, fields: Dict[str, Any]): def update_bug_fields(self, bug_id: int, fields: Dict[str, Any]):
# REST first
if self.mode == "rest": if self.mode == "rest":
try: try:
url = f"{self.rest_base}/bug/{bug_id}" url = f"{self.rest_base}/bug/{bug_id}"
@@ -303,11 +333,73 @@ class BugzillaHybrid:
except Exception as e: except Exception as e:
logger.warning(f"Bugzilla REST update failed, switching to JSON-RPC: {e}") logger.warning(f"Bugzilla REST update failed, switching to JSON-RPC: {e}")
self.mode = "jsonrpc" self.mode = "jsonrpc"
# JSON-RPC fallback
params = {"ids": [bug_id]} params = {"ids": [bug_id]}
params.update(fields) params.update(fields)
self._rpc("Bug.update", params, req_id=5) self._rpc("Bug.update", params, req_id=5)
def add_cc(self, bug_id: int, emails: List[str]):
if not emails:
return
if self.mode == "rest":
try:
url = f"{self.rest_base}/bug/{bug_id}"
payload = {"cc": {"add": emails}}
r = httpx.put(url, headers=self._rest_headers(), auth=self._rest_auth(), json=payload, timeout=30)
if r.status_code in (404, 405):
raise httpx.HTTPStatusError("REST update not available", request=r.request, response=r)
r.raise_for_status()
return
except Exception as e:
logger.warning(f"Bugzilla REST add_cc failed, switching to JSON-RPC: {e}")
self.mode = "jsonrpc"
params = {"ids": [bug_id], "cc": {"add": emails}}
self._rpc("Bug.update", params, req_id=6)
def list_comments(self, bug_id: int) -> List[str]:
texts: List[str] = []
if self.mode == "rest":
try:
url = f"{self.rest_base}/bug/{bug_id}/comment"
r = httpx.get(url, headers=self._rest_headers(), auth=self._rest_auth(), timeout=30)
r.raise_for_status()
data = r.json()
bugs = data.get("bugs") or {}
key = str(bug_id)
if key in bugs:
for c in (bugs[key].get("comments") or []):
txt = c.get("text")
if isinstance(txt, str):
texts.append(txt)
return texts
except Exception as e:
logger.warning(f"Bugzilla REST list_comments failed, switching to JSON-RPC: {e}")
self.mode = "jsonrpc"
# JSON-RPC
res = self._rpc("Bug.comments", {"ids": [bug_id]}, req_id=7)
bugs = res.get("bugs") or {}
key = str(bug_id)
if key in bugs:
for c in (bugs[key].get("comments") or []):
txt = c.get("text")
if isinstance(txt, str):
texts.append(txt)
return texts
def add_or_update_author_email_note(self, bug_id: int, email: str):
"""Post a marker comment with the author email if not already present."""
try:
existing = self.list_comments(bug_id)
except Exception as e:
logger.warning(f"Could not list comments for bug {bug_id}: {e}")
existing = []
marker_line = f"{AUTHOR_EMAIL_MARKER} PR author email could not be added to CC (not registered): {email}"
# Idempotent: if an existing marker has this exact email, do nothing
for text in existing:
if AUTHOR_EMAIL_MARKER in text and email in text:
return
# Otherwise add a new note
self.add_comment(bug_id, marker_line)
# ---------------- GitHub client (PyGithub) ---------------- # ---------------- GitHub client (PyGithub) ----------------
gh = Github(auth=Auth.Token(GITHUB_TOKEN)) if GITHUB_TOKEN else Github() gh = Github(auth=Auth.Token(GITHUB_TOKEN)) if GITHUB_TOKEN else Github()
@@ -336,7 +428,6 @@ def pr_comment_success(gitea_repo_url: str, bug_id: int) -> str:
) )
def pr_comment_failure(gitea_repo_url: str, repo: str, pr_url: str, target_branch: str) -> str: def pr_comment_failure(gitea_repo_url: str, repo: str, pr_url: str, target_branch: str) -> str:
# Uses default component name and required product/version in guidance
return ( return (
"Thanks for the contribution!\n\n" "Thanks for the contribution!\n\n"
"This repository is a read-only mirror of the canonical repo on Gitea:\n" "This repository is a read-only mirror of the canonical repo on Gitea:\n"
@@ -358,11 +449,112 @@ def fetch_pr_patch(owner: str, repo: str, pr_num: int) -> bytes:
r.raise_for_status() r.raise_for_status()
return r.content return r.content
def derive_pr_author_email(owner: str, repo: str, pr_num: int, head_sha: str) -> Optional[str]:
try:
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{head_sha}"
headers = {"Accept": "application/vnd.github+json"}
if GITHUB_TOKEN:
headers["Authorization"] = f"token {GITHUB_TOKEN}"
r = httpx.get(url, headers=headers, timeout=20)
if r.status_code == 200:
data = r.json()
email = (data.get("commit") or {}).get("author", {}).get("email")
if email:
return email
except Exception:
pass
try:
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_num}/commits"
headers = {"Accept": "application/vnd.github+json"}
if GITHUB_TOKEN:
headers["Authorization"] = f"token {GITHUB_TOKEN}"
r = httpx.get(url, headers=headers, timeout=20)
if r.status_code == 200:
commits = r.json()
if commits:
email = (commits[0].get("commit") or {}).get("author", {}).get("email")
if email:
return email
except Exception:
pass
try:
patch = fetch_pr_patch(owner, repo, pr_num).decode("utf-8", "ignore")
for line in patch.splitlines():
if line.startswith("From: ") and "<" in line and ">" in line:
cand = line[line.find("<")+1:line.find(">", line.find("<")+1)].strip()
if "@" in cand:
return cand
break
except Exception:
pass
return None
# ---------------- Routes ---------------- # ---------------- Routes ----------------
@app.get("/healthz") @app.get("/healthz")
async def healthz(): async def healthz():
return PlainTextResponse("ok") gh_status = {"ok": False}
bz_status = {"ok": False}
# GitHub quick check: rate limit endpoint
try:
gh_headers = {"Accept": "application/vnd.github+json"}
if GITHUB_TOKEN:
gh_headers["Authorization"] = f"token {GITHUB_TOKEN}"
r = httpx.get("https://api.github.com/rate_limit", headers=gh_headers, timeout=5)
gh_status["http_status"] = r.status_code
if r.status_code == 200:
data = r.json()
core = (data.get("resources") or {}).get("core") or {}
gh_status.update({
"remaining": core.get("remaining"),
"reset": core.get("reset"),
})
gh_status["ok"] = True
else:
gh_status["error"] = r.text[:200]
except Exception as e:
gh_status["error"] = str(e)[:200]
# Bugzilla quick check: REST /version if available, else JSON-RPC Bugzilla.version
try:
if getattr(bz, "mode", None) == "rest" and getattr(bz, "rest_base", None):
r2 = httpx.get(f"{bz.rest_base}/version",
headers=bz._rest_headers(),
auth=bz._rest_auth(),
timeout=5)
bz_status["http_status"] = r2.status_code
if r2.status_code == 200:
bz_status["version"] = (r2.json() or {}).get("version")
bz_status["ok"] = True
else:
bz_status["error"] = r2.text[:200]
else:
payload = {"method": "Bugzilla.version", "params": [{}], "id": 99}
# Include API key if configured (not strictly required for version)
if BUGZILLA_API_KEY:
payload["params"][0]["Bugzilla_api_key"] = BUGZILLA_API_KEY
r2 = httpx.post(bz.rpc_url,
headers={"Content-Type": "application/json"},
json=payload,
timeout=5)
bz_status["http_status"] = r2.status_code
if r2.status_code == 200:
data = r2.json()
if not data.get("error"):
bz_status["version"] = (data.get("result") or {}).get("version")
bz_status["ok"] = True
else:
bz_status["error"] = str(data["error"])[:200]
else:
bz_status["error"] = r2.text[:200]
except Exception as e:
bz_status["error"] = str(e)[:200]
overall_ok = bool(gh_status.get("ok") and bz_status.get("ok"))
status_code = 200 if overall_ok else 503
return JSONResponse({"ok": overall_ok, "github": gh_status, "bugzilla": bz_status},
status_code=status_code)
@app.post("/webhook/github") @app.post("/webhook/github")
async def github_webhook( async def github_webhook(
@@ -370,7 +562,6 @@ async def github_webhook(
x_hub_signature_256: str = Header(None), x_hub_signature_256: str = Header(None),
x_github_event: str = Header(None), x_github_event: str = Header(None),
): ):
# Validate Bugzilla required env
if not BUGZILLA_PRODUCT or not BUGZILLA_VERSION: if not BUGZILLA_PRODUCT or not BUGZILLA_VERSION:
raise HTTPException(status_code=500, detail="BUGZILLA_PRODUCT and BUGZILLA_VERSION must be set") raise HTTPException(status_code=500, detail="BUGZILLA_PRODUCT and BUGZILLA_VERSION must be set")
@@ -389,7 +580,7 @@ async def github_webhook(
if action not in ("opened", "reopened", "synchronize", "ready_for_review"): if action not in ("opened", "reopened", "synchronize", "ready_for_review"):
return JSONResponse({"status": "ignored", "action": action}) return JSONResponse({"status": "ignored", "action": action})
repo_full = payload["repository"]["full_name"] # owner/repo repo_full = payload["repository"]["full_name"]
owner = payload["repository"]["owner"]["login"] owner = payload["repository"]["owner"]["login"]
repo = payload["repository"]["name"] repo = payload["repository"]["name"]
pr = payload["pull_request"] pr = payload["pull_request"]
@@ -406,7 +597,6 @@ async def github_webhook(
pr_key = f"{repo_full}#{pr_num}" pr_key = f"{repo_full}#{pr_num}"
bug_id = state.get_bug(pr_key) bug_id = state.get_bug(pr_key)
# Acquire repo object only if we will write to GitHub
gh_repo = None gh_repo = None
if not GITHUB_DRY_RUN: if not GITHUB_DRY_RUN:
try: try:
@@ -415,7 +605,6 @@ async def github_webhook(
logger.warning(f"Could not access GitHub repo {repo_full}: {ge}") logger.warning(f"Could not access GitHub repo {repo_full}: {ge}")
if bug_id is None and action in ("opened", "reopened", "ready_for_review", "synchronize"): if bug_id is None and action in ("opened", "reopened", "ready_for_review", "synchronize"):
# Create Bugzilla bug
summary = f"[GH PR #{pr_num}] {GITEA_ORG}/{repo}: {pr_title}" summary = f"[GH PR #{pr_num}] {GITEA_ORG}/{repo}: {pr_title}"
description = ( description = (
"Source\n" "Source\n"
@@ -427,11 +616,10 @@ async def github_webhook(
f"{pr_body}\n" f"{pr_body}\n"
) )
try: try:
# Use the requested default component
bug_id = bz.create_bug(summary, description, component=BUGZILLA_COMPONENT_DEFAULT) bug_id = bz.create_bug(summary, description, component=BUGZILLA_COMPONENT_DEFAULT)
state.set_bug(pr_key, bug_id) state.set_bug(pr_key, bug_id)
logger.info(f"Created Bugzilla bug {bug_id} for {pr_key}") logger.info(f"Created Bugzilla bug {bug_id} for {pr_key}")
# Set cf_package to repo name, fallback to "---" if that fails # Set cf_package to repo, fallback to '---'
try: try:
bz.update_bug_fields(bug_id, {CF_PACKAGE_FIELD: repo}) bz.update_bug_fields(bug_id, {CF_PACKAGE_FIELD: repo})
logger.info(f"Set {CF_PACKAGE_FIELD}={repo} on bug {bug_id}") logger.info(f"Set {CF_PACKAGE_FIELD}={repo} on bug {bug_id}")
@@ -440,9 +628,26 @@ async def github_webhook(
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
bz.update_bug_fields(bug_id, {CF_PACKAGE_FIELD: CF_PACKAGE_FALLBACK}) bz.update_bug_fields(bug_id, {CF_PACKAGE_FIELD: CF_PACKAGE_FALLBACK})
logger.info(f"Set {CF_PACKAGE_FIELD}={CF_PACKAGE_FALLBACK} on bug {bug_id}") logger.info(f"Set {CF_PACKAGE_FIELD}={CF_PACKAGE_FALLBACK} on bug {bug_id}")
# Add PR author to CC (or comment fallback)
if BUGZILLA_CC_ENABLE:
email = derive_pr_author_email(owner, repo, pr_num, head_sha)
if email:
is_noreply = email.endswith("@users.noreply.github.com")
if not is_noreply or BUGZILLA_CC_ALLOW_NOREPLY:
try:
bz.add_cc(bug_id, [email])
logger.info(f"Added CC {email} to bug {bug_id}")
except Exception as e_cc:
logger.warning(f"Failed to add CC {email} to bug {bug_id}: {e_cc}")
if BUGZILLA_CC_FALLBACK_COMMENT:
with contextlib.suppress(Exception):
bz.add_or_update_author_email_note(bug_id, email)
else:
logger.info(f"Skipping noreply CC {email} (set BUGZILLA_CC_ALLOW_NOREPLY=true to allow)")
else:
logger.info("Could not derive PR author email; CC not added")
except Exception as e: except Exception as e:
logger.error(f"Bugzilla create failed for {pr_key}: {e}") logger.error(f"Bugzilla create failed for {pr_key}: {e}")
# Post failure comment and keep PR open (best effort)
if not GITHUB_DRY_RUN and gh_repo is not None: if not GITHUB_DRY_RUN and gh_repo is not None:
try: try:
gh_repo.get_pull(pr_num).create_issue_comment(pr_comment_failure(gitea_repo_url, repo, pr_url, target_branch)) gh_repo.get_pull(pr_num).create_issue_comment(pr_comment_failure(gitea_repo_url, repo, pr_url, target_branch))
@@ -457,7 +662,6 @@ async def github_webhook(
logger.info(f"[GITHUB_DRY_RUN] Would comment failure on PR #{pr_num}, label={BUGZILLA_FAILURE_LABEL}") logger.info(f"[GITHUB_DRY_RUN] Would comment failure on PR #{pr_num}, label={BUGZILLA_FAILURE_LABEL}")
return JSONResponse({"status": "bugzilla_failed", "action": action}) return JSONResponse({"status": "bugzilla_failed", "action": action})
# Attach patch if enabled
if BUGZILLA_ATTACH_DIFF: if BUGZILLA_ATTACH_DIFF:
try: try:
patch = fetch_pr_patch(owner, repo, pr_num) patch = fetch_pr_patch(owner, repo, pr_num)
@@ -470,7 +674,6 @@ async def github_webhook(
except Exception as e: except Exception as e:
logger.warning(f"Attach patch failed for bug {bug_id}: {e}") logger.warning(f"Attach patch failed for bug {bug_id}: {e}")
# Comment and close PR
comment = pr_comment_success(gitea_repo_url, bug_id) comment = pr_comment_success(gitea_repo_url, bug_id)
if not GITHUB_DRY_RUN and gh_repo is not None: if not GITHUB_DRY_RUN and gh_repo is not None:
try: try:
@@ -485,7 +688,6 @@ async def github_webhook(
return JSONResponse({"status": "ok", "bug_id": bug_id}) return JSONResponse({"status": "ok", "bug_id": bug_id})
elif bug_id is not None and action == "synchronize": elif bug_id is not None and action == "synchronize":
# Attach updated patch and ensure PR closed
try: try:
if BUGZILLA_ATTACH_DIFF: if BUGZILLA_ATTACH_DIFF:
patch = fetch_pr_patch(owner, repo, pr_num) patch = fetch_pr_patch(owner, repo, pr_num)