diff --git a/.gitea/scripts/sop-checklist-gate.py b/.gitea/scripts/sop-checklist-gate.py index 1fb24693..995fbc7b 100755 --- a/.gitea/scripts/sop-checklist-gate.py +++ b/.gitea/scripts/sop-checklist-gate.py @@ -620,8 +620,8 @@ def render_status( state is "success" if every item has at least one valid ack (body section presence is informational only — peer-ack is the - real gate). "pending" is reserved for the soft-fail path - (tier:low) and is set by the caller. + real gate). tier:low PRs receive state="success" (soft-fail — no + acks required); the description carries "[info tier:low]" prefix. """ n = len(items) fully_acked = [ @@ -640,8 +640,11 @@ def render_status( shown += f", +{len(missing) - 3}" desc_parts.append(f"missing: {shown}") if missing_body: - desc_parts.append(f"body-unfilled: {len(missing_body)}") - state = "success" if not missing else "failure" + shown = ", ".join(missing_body[:3]) + if len(missing_body) > 3: + shown += f", +{len(missing_body) - 3}" + desc_parts.append(f"body-unfilled: {shown}") + state = "success" if not missing and not missing_body else "failure" return state, " — ".join(desc_parts) @@ -773,9 +776,12 @@ def main(argv: list[str] | None = None) -> int: state, description = render_status(items, ack_state, body_state) mode = get_tier_mode(pr, cfg) - if state == "failure" and mode == "soft": - state = "pending" - description = f"[soft-fail tier:low] {description}" + if mode == "soft": + # tier:low: acks are informational only — post success so BP gate passes. + # Description carries "[info tier:low]" prefix so reviewers know acks + # were not required (vs a tier:medium+ PR that truly passed all acks). + state = "success" + description = f"[info tier:low] {description}" # Diagnostics to job log. print(f"::notice::PR #{args.pr} author={author} head={head_sha[:7]} mode={mode}") diff --git a/.gitea/scripts/tests/test_sop_checklist_gate.py b/.gitea/scripts/tests/test_sop_checklist_gate.py index d951f974..7622c79a 100644 --- a/.gitea/scripts/tests/test_sop_checklist_gate.py +++ b/.gitea/scripts/tests/test_sop_checklist_gate.py @@ -410,6 +410,7 @@ class TestRenderStatus(unittest.TestCase): self._state_with(all_slugs), {it["slug"]: False for it in self.items}, ) + self.assertEqual(state, "failure") self.assertIn("body-unfilled", desc) @@ -519,6 +520,31 @@ class TestEndToEndAckFlow(unittest.TestCase): self.assertEqual(result_state, "success") self.assertIn("7/7", desc) + def test_all_acks_still_fail_when_body_section_unfilled(self): + items = _items_by_slug() + aliases = _numeric_aliases() + comments = [ + _comment("qa-bot", "/sop-ack comprehensive-testing"), + _comment("eng-bot", "/sop-ack local-postgres-e2e"), + _comment("eng-bot", "/sop-ack staging-smoke"), + _comment("mgr-bot", "/sop-ack root-cause"), + _comment("eng-bot", "/sop-ack five-axis-review"), + _comment("mgr-bot", "/sop-ack no-backwards-compat"), + _comment("eng-bot", "/sop-ack memory-consulted"), + ] + + def probe(slug, users): + return list(users) + + state = sop.compute_ack_state(comments, "alice-author", items, aliases, probe) + body = {it["slug"]: True for it in items.values()} + body["root-cause"] = False + items_list = list(items.values()) + result_state, desc = sop.render_status(items_list, state, body) + self.assertEqual(result_state, "failure") + self.assertIn("7/7", desc) + self.assertIn("body-unfilled: root-cause", desc) + if __name__ == "__main__": unittest.main(verbosity=2)