From bed8743c9436186cc9816e795326572a56ef313c Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Sun, 18 Oct 2026 05:08:14 +0000 Subject: [PATCH] checks(): a status from one of Time's own workflows is not a check (#124) checks(): a status from one of Time's own workflows is not a check (#124) A seat's review wakes heartbeat.yml during a heartbeat; the concurrency group cancels the run and Gitea records it as a failure on the PR head. Those statuses neither block nor earn a merge; CI decides. Owner-approved merge. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD --- .github/time/test_time.py | 9 +++++++++ .github/time/time.py | 8 +++++++- CHANGELOG.md | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/time/test_time.py b/.github/time/test_time.py index 36ddfdb..91bd1b7 100644 --- a/.github/time/test_time.py +++ b/.github/time/test_time.py @@ -395,6 +395,15 @@ w3 = run(World([card(1, D)], {1: [beat("would-dispatch")]}), {"TIME_MODE": "live assert "VENICE_API_KEY" in w3.posted[-1][1] print("ALL OK 4") +# 15a a status from one of Time's own workflows is not a check: a heartbeat run cancelled by the concurrency group (a seat's +# review wakes heartbeat.yml during a heartbeat) lands on the head as "failure" and must neither block nor earn a merge +w = pr_world(checks=[]); w.gh_pr[7]["statuses"] = [{"state": "failure", "context": "heartbeat / tick (pull_request_review)"}, {"state": "success", "context": "ci / test (pull_request)"}] +w = run(w, {**K, "TIME_AUTOMERGE": "on"}); assert merged(w), "a cancelled heartbeat run is not a failing check" +w = pr_world(checks=[]); w.gh_pr[7]["statuses"] = [{"state": "success", "context": "heartbeat / tick (pull_request_review)"}] +w = run(w, {**K, "TIME_AUTOMERGE": "on"}); assert not merged(w) and "running CI" in w.posted[0][1], "nor does a green one count as CI" +w = pr_world(checks=[]); w.gh_pr[7]["statuses"] = [{"state": "pending", "context": "mention / reply (issue_comment)"}, {"state": "success", "context": "ci / test (pull_request)"}] +w = run(w, {**K, "TIME_AUTOMERGE": "on"}); assert merged(w), "a pending mention run does not hold the merge" + # 15 the worker's own status can block a merge but never earns one: Time dispatches CI instead, then merges on it w = pr_world(checks=[]); w.gh_pr[7]["statuses"] = [{"state": "success", "context": "time/worker-run"}] w = run(w, {**K, "TIME_AUTOMERGE": "on"}); assert not merged(w), "worker status alone is not green" diff --git a/.github/time/time.py b/.github/time/time.py index 8e9e233..08f1f11 100644 --- a/.github/time/time.py +++ b/.github/time/time.py @@ -713,6 +713,9 @@ def touches_protected(pr_number: int) -> list[str]: WORKER_CONTEXT = "time/worker-run" +# Time's own workflows report on a PR head too: a seat's review wakes heartbeat.yml, and a run the concurrency group +# cancels lands on the head as "failure". None of that says anything about the code, so checks() never reads it. +OWN_CONTEXTS = ("heartbeat /", "mention /", "venture /", "astra /", "doctor /", "update /", "steward /") def checks(sha: str) -> str: @@ -723,7 +726,10 @@ def checks(sha: str) -> str: states = gh("GET", f"/repos/{REPO}/commits/{sha}/status").get("statuses") or [] latest: dict[str, str] = {} for st in states: # one line per context, newest wins - latest.setdefault(st.get("context") or "", (st.get("status") or st.get("state") or "").lower()) + ctx = st.get("context") or "" + if ctx.startswith(OWN_CONTEXTS): + continue + latest.setdefault(ctx, (st.get("status") or st.get("state") or "").lower()) if any(v == "pending" for v in latest.values()): return "pending" if not all(v in ("success", "skipped") for v in latest.values()): diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de49a1..605c48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Security +- A commit status from one of Time's own workflows (`heartbeat /`, `mention /`, `venture /`, `astra /`, `doctor /`, `update /`, `steward /`) is not a check: it neither blocks nor earns a merge. A seat's review is a `pull_request_review` event that wakes `heartbeat.yml` during a heartbeat; the concurrency group cancels the run and Gitea records it as a failure on the PR head, which had stopped approved, CI-green PRs from landing. - The worker's own commit status (`time/worker-run`) can fail or delay a merge but never earns one: with no other check on the PR head, Time dispatches the project's CI workflow on the PR branch (`TIME_CI_WORKFLOW`, default `ci.yml`, which must accept `workflow_dispatch`; GitHub skips `pull_request` runs for pushes made with the Actions token) and merges on that. `off` keeps the old "no checks" stop. - `.github/` is protected from auto-merge whatever `TIME_PROTECTED` says. - `labs-aicharts.yml` passes the deploy URL through `env` instead of interpolating it into the shell.