72c272504c
Migration safety mechanism: the controller only manages PRs and
issues carrying a configurable opt-in label (default
``controller-managed``). Operators opt PRs in for parallel-run
trials, can pause management mid-flight by removing the label, and
gradually roll out without exposing the controller to PRs that
human reviewers are actively driving.
Components:
- tools/controller/master/label_gate.py — single source of truth for
the configured label name + pure predicates/filters over Forgejo
PR/issue dicts.
- ``opt_in_label_name()`` reads ``CONTROLLER_OPT_IN_LABEL`` env
(default 'controller-managed'); empty/whitespace falls back.
- ``has_opt_in_label(entity, name)`` defensively handles every
degenerate shape (non-dict entity, non-list labels, non-dict
label entries, missing name field).
- ``filter_by_opt_in_label`` / ``count_filtered`` for callers.
Wired through:
- discovery.run_discovery + backfill.run_startup_backfill +
reconciliation.run_reconciliation_tick each accept
``opt_in_label`` and ``require_opt_in_label`` kwargs.
- Function defaults are ``require_opt_in_label=False`` for API
back-compat (existing 30+ discovery/backfill/recon tests work
without changes).
- __main__.py defaults to ``--no-opt-in-label`` OFF (gate ENABLED in
production); add ``--no-opt-in-label`` to bypass.
- DiscoveryReport gains a ``label_filtered_out`` counter.
Reconciliation behavior:
- When opt_in_label is configured AND the Forgejo response carries a
``labels`` field AND the opt-in label is NOT present, the workflow
transitions to ABANDONED with reason ``opt-in-label-removed`` +
emits a controller_events 'reconciliation' row.
- Partial Forgejo responses (no ``labels`` field) skip the label
check — never ABANDON on incomplete data.
Master loop extension:
- ``reconciliation_args`` now accepts an optional 5th element — a
kwargs dict threaded through to ``run_reconciliation_tick``.
__main__.py uses this to pass ``require_opt_in_label`` per the CLI
flag. 4-tuple back-compat preserved.
Tests (+29 in test_label_gate.py, 0 regressions across 569 tests):
- Predicate edge cases (every degenerate shape returns False)
- Env-var resolution (default, override, empty, whitespace)
- filter/count helpers
- Discovery + backfill: kept/filtered counts, gate disabled,
explicit label overrides env
- Reconciliation: label removed → ABANDONED, label present →
no-op, partial response → no-op, gate disabled → bypass, event
row records reason
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
210 lines
7.1 KiB
Python
210 lines
7.1 KiB
Python
"""Master startup backfill — discover all open PRs/issues at deploy time.
|
|
|
|
Per plan v9: when the master starts (first deploy OR after a long
|
|
outage), it needs to learn about existing open PRs/issues that
|
|
weren't created via discovery-tick-during-uptime. Without this,
|
|
existing PRs would have to wait for the next discovery tick AND
|
|
the operator would have no visibility into "what's the controller
|
|
about to take ownership of."
|
|
|
|
This module is a thin wrapper around ``run_discovery``: it runs the
|
|
same sweep, then records a one-time ``controller_events`` marker
|
|
("backfill complete") so subsequent restarts can skip the operator-
|
|
visible logging.
|
|
|
|
For v1 simplicity, backfill = "run discovery once, log loudly." The
|
|
idempotent skip in run_discovery prevents duplicate workflow rows.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.engine import Engine
|
|
|
|
from ..db.session import session_scope
|
|
from .discovery import (
|
|
DiscoveryReport,
|
|
ListIssuesCallback,
|
|
ListPRsCallback,
|
|
run_discovery,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Magic event_type the marker uses. Looking for "is there a row with
|
|
# this event_type for this (owner, repo)?" tells us whether backfill
|
|
# has been completed.
|
|
BACKFILL_MARKER_EVENT_TYPE = "controller-backfill-complete"
|
|
|
|
|
|
@dataclass
|
|
class BackfillReport:
|
|
"""Per-backfill summary. Exposes the inner DiscoveryReport plus
|
|
whether this was the first-ever backfill for this (owner, repo)."""
|
|
|
|
first_time: bool
|
|
discovery: DiscoveryReport
|
|
|
|
|
|
def has_backfill_run(engine: Engine, *, owner: str, repo: str) -> bool:
|
|
"""Check the controller_events table for the backfill marker.
|
|
|
|
The marker is a single ``event_type='controller-backfill-complete'``
|
|
row whose payload contains the (owner, repo). Idempotent: every
|
|
backfill emits one such marker; the existence check determines
|
|
whether the backfill is the first time vs. a re-run.
|
|
"""
|
|
with session_scope(engine) as session:
|
|
rows = session.execute(
|
|
text(
|
|
"SELECT e.payload "
|
|
"FROM controller_events e "
|
|
"WHERE e.event_type = :ev_type"
|
|
),
|
|
{"ev_type": BACKFILL_MARKER_EVENT_TYPE},
|
|
).all()
|
|
for r in rows:
|
|
if not r.payload:
|
|
continue
|
|
try:
|
|
payload = json.loads(r.payload)
|
|
except (TypeError, ValueError, json.JSONDecodeError):
|
|
continue
|
|
if (
|
|
isinstance(payload, dict)
|
|
and payload.get("owner") == owner
|
|
and payload.get("repo") == repo
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def run_startup_backfill(
|
|
engine: Engine,
|
|
*,
|
|
owner: str,
|
|
repo: str,
|
|
list_prs: ListPRsCallback,
|
|
list_issues: ListIssuesCallback | None = None,
|
|
opt_in_label: str | None = None,
|
|
require_opt_in_label: bool = False,
|
|
) -> BackfillReport:
|
|
"""One backfill sweep at master startup.
|
|
|
|
Calls ``run_discovery`` to populate DISCOVERED workflows for any
|
|
open PRs/issues the controller hasn't seen yet. Records a
|
|
backfill-complete marker in ``controller_events`` so subsequent
|
|
restarts know "this isn't the first time."
|
|
|
|
Always returns; the marker row is created regardless of how many
|
|
new workflows landed (so a second startup correctly reports
|
|
``first_time=False`` even if discovery found zero new entities).
|
|
|
|
Phase 1k: backfill honors the same opt-in label gate as discovery.
|
|
Defaults to ``CONTROLLER_OPT_IN_LABEL`` env (or ``controller-managed``).
|
|
"""
|
|
first = not has_backfill_run(engine, owner=owner, repo=repo)
|
|
if first:
|
|
logger.info(
|
|
"controller backfill: first-time for %s/%s — sweeping "
|
|
"all open PRs + issues",
|
|
owner, repo,
|
|
)
|
|
else:
|
|
logger.info(
|
|
"controller backfill: re-run for %s/%s — skipping if "
|
|
"no new entities",
|
|
owner, repo,
|
|
)
|
|
|
|
discovery_report = run_discovery(
|
|
engine, owner=owner, repo=repo,
|
|
list_prs=list_prs, list_issues=list_issues,
|
|
opt_in_label=opt_in_label,
|
|
require_opt_in_label=require_opt_in_label,
|
|
)
|
|
|
|
_emit_backfill_marker(
|
|
engine, owner=owner, repo=repo,
|
|
first_time=first, discovery_report=discovery_report,
|
|
)
|
|
|
|
if first or discovery_report.new_workflows:
|
|
logger.info(
|
|
"controller backfill done: PRs=%d issues=%d new_workflows=%d "
|
|
"skipped=%d first_time=%s",
|
|
discovery_report.prs_seen, discovery_report.issues_seen,
|
|
discovery_report.new_workflows,
|
|
discovery_report.existing_skipped, first,
|
|
)
|
|
return BackfillReport(first_time=first, discovery=discovery_report)
|
|
|
|
|
|
def _emit_backfill_marker(
|
|
engine: Engine, *, owner: str, repo: str,
|
|
first_time: bool, discovery_report: DiscoveryReport,
|
|
) -> None:
|
|
"""Insert the backfill-complete marker row.
|
|
|
|
The marker is associated with the FIRST new workflow created
|
|
(if any) so it has a valid ``workflow_id`` FK. If no new workflows
|
|
landed but a workflow exists for this (owner, repo), use that.
|
|
If no workflows exist at all (truly empty Forgejo), the marker is
|
|
skipped — the next backfill will revisit.
|
|
"""
|
|
now = datetime.now(timezone.utc)
|
|
with session_scope(engine) as session:
|
|
# Pick a workflow_id to associate the marker with.
|
|
if discovery_report.new_entities:
|
|
wf_id = discovery_report.new_entities[0].workflow_id
|
|
else:
|
|
existing = session.execute(
|
|
text(
|
|
"SELECT workflow_id FROM workflows "
|
|
"WHERE owner = :owner AND repo = :repo LIMIT 1"
|
|
),
|
|
{"owner": owner, "repo": repo},
|
|
).first()
|
|
if existing is None:
|
|
logger.info(
|
|
"backfill marker NOT recorded for %s/%s: no workflows "
|
|
"exist to attach the marker FK to (empty Forgejo). "
|
|
"Next backfill will retry.",
|
|
owner, repo,
|
|
)
|
|
return
|
|
wf_id = existing.workflow_id
|
|
|
|
session.execute(
|
|
text(
|
|
"INSERT INTO controller_events "
|
|
"(workflow_id, ts, event_type, payload, "
|
|
" forgejo_write_pending, replay_attempts) "
|
|
"VALUES (:wf_id, :ts, :ev_type, :payload, 0, 0)"
|
|
),
|
|
{
|
|
"wf_id": wf_id, "ts": now,
|
|
"ev_type": BACKFILL_MARKER_EVENT_TYPE,
|
|
"payload": json.dumps({
|
|
"owner": owner, "repo": repo,
|
|
"first_time": first_time,
|
|
"prs_seen": discovery_report.prs_seen,
|
|
"issues_seen": discovery_report.issues_seen,
|
|
"new_workflows": discovery_report.new_workflows,
|
|
}),
|
|
},
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"BACKFILL_MARKER_EVENT_TYPE",
|
|
"BackfillReport",
|
|
"has_backfill_run",
|
|
"run_startup_backfill",
|
|
]
|