feat(3 U3): YunoHost-style PR comment (🌻 + level badge + summary card images, linked) updated in place per PR; text fallback; bridge tests + dashboard do_HEAD
This commit is contained in:
@ -41,8 +41,16 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
GITEA_API = os.environ.get("GITEA_API", "https://git.autonomic.zone/api/v1")
|
||||
DRONE_URL = os.environ.get("DRONE_URL", "https://drone.ci.commoninternet.net")
|
||||
# Dashboard base URL — where per-run artifacts (summary card PNG, level badge SVG) are served
|
||||
# (Phase 3 U2.3: /runs/<run_id>/...). The PR comment (U3) embeds the card + badge from here. The
|
||||
# run_id is the Drone build number (== `num`), so the URLs are /runs/<num>/{summary.png,badge.svg}.
|
||||
DASH_URL = os.environ.get("DASH_URL", "https://ci.commoninternet.net")
|
||||
CI_REPO = os.environ.get("CI_REPO", "recipe-maintainers/cc-ci")
|
||||
TRIGGER = "!testme"
|
||||
# Hidden HTML-comment marker embedded in the bot's PR comment so a re-`!testme` UPDATES the same
|
||||
# comment in place (R2/U3 "one comment per PR, updated in place") instead of stacking new ones.
|
||||
# Invisible in rendered Gitea markdown.
|
||||
COMMENT_MARKER = "<!-- cc-ci:testme -->"
|
||||
|
||||
|
||||
def parse_trigger(body):
|
||||
@ -160,9 +168,49 @@ def build_status(num):
|
||||
_TERMINAL = {"success", "failure", "error", "killed"}
|
||||
|
||||
|
||||
def artifact_available(url):
|
||||
"""True iff the dashboard serves `url` (HTTP 200). Used to decide image-vs-text fallback for the
|
||||
PR comment (R7: a render failure → text, never a broken image). Best-effort; any error → False."""
|
||||
try:
|
||||
req = urllib.request.Request(url, method="HEAD")
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return getattr(r, "status", r.getcode()) == 200
|
||||
except Exception: # noqa: BLE001 — unreachable/404/timeout all mean "fall back to text"
|
||||
return False
|
||||
|
||||
|
||||
def start_comment_body(recipe, sha, run_url, mode=""):
|
||||
"""U3.1 — the YunoHost-shaped placeholder posted when a run starts: 🌻 marker + ⏳ + live-logs
|
||||
link. Edited in place to the image-forward result by watch_and_reflect on completion."""
|
||||
return (
|
||||
f"{COMMENT_MARKER}\n"
|
||||
f"🌻 **cc-ci** — testing `{recipe}` @ `{sha[:8]}`{mode}\n\n"
|
||||
f"⏳ Run in progress — level pending. [Live logs]({run_url})."
|
||||
)
|
||||
|
||||
|
||||
def result_comment_body(recipe, sha, num, run_url, status):
|
||||
"""U3.2 — the YunoHost-shaped result comment: 🌻 marker + a level/status **badge** + the
|
||||
**summary card** image, both linking to the run; falls back to a compact text verdict if the
|
||||
rendered card/badge isn't available (render failed, or the build didn't complete) — R7."""
|
||||
badge_url = f"{DASH_URL}/runs/{num}/badge.svg"
|
||||
card_url = f"{DASH_URL}/runs/{num}/summary.png"
|
||||
icon = "✅" if status == "success" else "❌"
|
||||
verdict = "passed" if status == "success" else (status or "did not complete")
|
||||
header = f"{COMMENT_MARKER}\n🌻 **cc-ci** — `{recipe}` @ `{sha[:8]}` {icon} **{verdict}**"
|
||||
links = f"[full logs]({run_url}) · [dashboard]({DASH_URL}/)"
|
||||
# Image-forward (YunoHost style) only when the card actually rendered + is served; else text.
|
||||
if artifact_available(card_url):
|
||||
body = f"{header}\n\n[]({run_url})"
|
||||
if artifact_available(badge_url):
|
||||
body += f"\n\n[]({run_url})"
|
||||
return f"{body}\n\n{links}"
|
||||
return f"{header} → {run_url}\n\n_(summary card unavailable — see the run for details.)_ {links}"
|
||||
|
||||
|
||||
def watch_and_reflect(owner, name, number, num, recipe, sha, comment_id, run_url):
|
||||
"""Poll the Drone build to completion, then edit the PR comment to reflect the outcome (D7).
|
||||
Bounded by the build timeout (60m) + margin."""
|
||||
"""Poll the Drone build to completion, then edit the PR comment to the YunoHost-style image-forward
|
||||
result (🌻 + badge + summary card, linked; text fallback) — D7/R2/U3. Bounded by build timeout."""
|
||||
import time as _t
|
||||
|
||||
deadline = _t.time() + 75 * 60
|
||||
@ -172,15 +220,8 @@ def watch_and_reflect(owner, name, number, num, recipe, sha, comment_id, run_url
|
||||
if last in _TERMINAL:
|
||||
break
|
||||
_t.sleep(15)
|
||||
icon = {"success": "✅"}.get(last, "❌")
|
||||
verdict = "passed" if last == "success" else (last or "did not complete")
|
||||
if comment_id:
|
||||
edit_comment(
|
||||
owner,
|
||||
name,
|
||||
comment_id,
|
||||
f"cc-ci: run for `{recipe}` @ `{sha[:8]}` {icon} **{verdict}** → {run_url}",
|
||||
)
|
||||
edit_comment(owner, name, comment_id, result_comment_body(recipe, sha, num, run_url, last))
|
||||
log(f"reflected outcome build {num} ({recipe} PR #{number}): {last}")
|
||||
|
||||
|
||||
@ -194,6 +235,15 @@ def list_comments(full_name, number):
|
||||
return cs if status == 200 and cs else []
|
||||
|
||||
|
||||
def find_existing_comment(full_name, number):
|
||||
"""Return the id of the bot's existing cc-ci PR comment (carrying COMMENT_MARKER), or None — so a
|
||||
re-`!testme` UPDATES that comment in place (R2/U3) rather than stacking a new one each run."""
|
||||
for c in list_comments(full_name, number):
|
||||
if COMMENT_MARKER in (c.get("body") or ""):
|
||||
return c.get("id")
|
||||
return None
|
||||
|
||||
|
||||
def _claim(comment_id) -> bool:
|
||||
"""Atomically claim a comment id for processing. Returns False if already claimed (dedup)."""
|
||||
if comment_id is None:
|
||||
@ -222,10 +272,15 @@ def process_testme(full_name, owner, name, number, user, comment_id, source, qui
|
||||
return None, "trigger failed"
|
||||
run_url = f"{DRONE_URL}/{CI_REPO}/{num}"
|
||||
mode = " **(--quick: lower-confidence fast lane; does not gate merge)**" if quick else ""
|
||||
cid = post_comment(
|
||||
owner, name, number,
|
||||
f"cc-ci: started CI run for `{name}` @ `{head['sha'][:8]}`{mode} → {run_url}",
|
||||
)
|
||||
# R2/U3: one comment per PR, updated in place. Reuse the existing marked comment if present
|
||||
# (re-`!testme` refreshes it back to the ⏳ placeholder), else post a new one.
|
||||
start_body = start_comment_body(name, head["sha"], run_url, mode)
|
||||
existing = find_existing_comment(full_name, number)
|
||||
if existing:
|
||||
edit_comment(owner, name, existing, start_body)
|
||||
cid = existing
|
||||
else:
|
||||
cid = post_comment(owner, name, number, start_body)
|
||||
log(
|
||||
f"[{source}] triggered build {num} for {name}@{head['sha'][:8]} "
|
||||
f"(PR #{number}, comment {comment_id}) by {user}"
|
||||
|
||||
Reference in New Issue
Block a user