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
82 lines
3.9 KiB
Python
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"
|