Files
rune/scripts/bulk_issue_guard.py
T
Nick f170a01b1e
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
Auto-version and Release / release (push) Canceled after 0s
deploy / deploy (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
chore: audit doc refresh + store docs (batch 174)
2026-10-10 01:45:56 +01:00

82 lines
3.9 KiB
Python

"""Bulk-issue guard: ceiling + discipline wrapper for mass Gitea issue posts.
ANTI-BALLOONING guard (Gitea end), built 2026-10-01 after the rebase
tranche ballooned to 1,852 per-unit issues via scripts/rebase_issue_factory.py.
Any bulk post above the procedural tripwire MUST go through this wrapper:
- smoke-first: post ONE unit, verify via API, then unleash the batch
- resume-safe: progress file, re-runs skip done keys
- retry/backoff: api() retries=5, timeout 60s, 2*(attempt+1) backoff
- epic-fold shape: per-FILE epics (never per-unit bulk), members absorbed
- dynamic fold-sized bound: posted issues <= epic/file count (one epic
per file; units live as member link-lists inside epics, never as issues).
The limiting factor is how the batch folds into epics — never a fixed
number. BULK_ISSUE_CEILING (50) is a procedural tripwire only: above
that many POSTED issues the smoke-first + resume-safe + retry/backoff
discipline is mandatory. It caps nothing on its own.
PRESERVE-FIRST: auto-label, pipeline-gate, merge-robot, human-only closure
untouched. Companion: TRANCHE-TEMPLATE.md §7 (template end).
"""
import json
import os
import time
import urllib.request
BULK_ISSUE_CEILING = 50 # procedural tripwire (matches TRANCHE-TEMPLATE.md §7):
# above this many POSTED issues, smoke-first + resume-safe + retry/backoff
# discipline is mandatory. This is NOT a cap on units — the substantive
# bound is dynamic (see check_ceiling): posted issues <= epic/file count,
# units live as member link-lists inside epics, never as issues.
API_RETRIES = 5
API_TIMEOUT = 60
def api(method, path, token, base="http://127.0.0.1:3000", payload=None):
"""Gitea API call with retry + backoff (fold-worker timeout law)."""
data = json.dumps(payload).encode() if payload is not None else None
last = None
for attempt in range(API_RETRIES):
try:
req = urllib.request.Request(
base + path, data=data, method=method,
headers={"Authorization": "token " + token,
"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=API_TIMEOUT) as r:
return json.load(r)
except Exception as e: # noqa: BLE001 — retry loop must catch every transient failure class; the final raise carries the last error
last = e
time.sleep(2 * (attempt + 1))
raise RuntimeError("api failed after retries: %r" % (last,))
def check_ceiling(unit_count, epic_map=None, approved=False):
"""Dynamic fold-sized bound: posted issues <= epic/file count.
The limiting factor is how the batch folds into epics (one epic per
file; units absorbed as member link-lists), never a fixed number.
BULK_ISSUE_CEILING (50) is a procedural tripwire only: above that many
POSTED issues, smoke-first + resume-safe + retry/backoff discipline is
mandatory. The substantive check: with an epic map, the allowed posted
count is len(epic_map) — one issue per epic. Without a map above the
tripwire, fold first; without approval, stop. Returns the required
epic granularity ('per-file' above the tripwire, 'per-unit-ok' below).
This is the check rebase_issue_factory.py never had (1,852 balloon).
"""
if unit_count <= BULK_ISSUE_CEILING:
return "per-unit-ok"
if epic_map is None:
raise RuntimeError(
"tripwire crossed: %d units > %d with no epic map — "
"fold into per-FILE epics first (bound = epic count, "
"one posted issue per file)" % (unit_count, BULK_ISSUE_CEILING))
bound = len(epic_map)
if not approved:
raise RuntimeError(
"tripwire crossed: %d units > %d — epic map present "
"(fold bound: %d posted issues, one per file), "
"explicit approval still required" % (
unit_count, BULK_ISSUE_CEILING, bound))
return "per-file"