Files
cleveragents-core/tools/controller/master/backfill.py
T
drew 72c272504c feat(controller): Phase 1k — controller-managed opt-in label gate
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>
2026-05-18 14:45:22 -04:00

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",
]