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>
190 lines
4.7 KiB
Python
190 lines
4.7 KiB
Python
"""Master controller — singleton orchestrator.
|
|
|
|
Per plan v9: the master process is a singleton per (owner, repo). It:
|
|
1. Runs discovery (Forgejo poll) — Phase 1d-3
|
|
2. Drives the state machine: for each non-terminal workflow, advance
|
|
one step based on the outcome of its latest completed attempt
|
|
3. Runs the reaper + pickup guard each tick
|
|
4. Performs Forgejo writes (status comments, labels, merges) —
|
|
Phase 1d-3+
|
|
5. Runs periodic reconciliation (DB↔Forgejo) — Phase 1d-3+
|
|
|
|
This package ships incrementally. Phase 1d-1 (already shipped) had
|
|
the state machine + reaper + pickup guard. This Phase 1d-2 adds:
|
|
- ``outcomes.py``: maps worker output_payload outcomes → state machine
|
|
events (e.g., implementer outcome='resolved' → 'implementer_pushed';
|
|
outcome='rebase-failed' → 'implementer_rebase_failed').
|
|
- ``tick.py``: one tick handler — scans recently-completed attempts,
|
|
applies events to their workflows, emits controller_events.
|
|
"""
|
|
from .outcomes import (
|
|
EventMapResult,
|
|
map_outcome_to_event,
|
|
)
|
|
from .tick import (
|
|
TickReport,
|
|
run_tick,
|
|
)
|
|
from .scheduler import (
|
|
MAX_TIER,
|
|
PrefetchCallback,
|
|
ScheduledAttempt,
|
|
SchedulerReport,
|
|
schedule_next_attempts,
|
|
)
|
|
from .discovery import (
|
|
DiscoveredEntity,
|
|
DiscoveryReport,
|
|
ListIssuesCallback,
|
|
ListPRsCallback,
|
|
run_discovery,
|
|
)
|
|
from .forgejo_writes import (
|
|
AddLabelCallback,
|
|
GetLabelsCallback,
|
|
LabelAdjustResult,
|
|
ListCommentsCallback,
|
|
PostCommentCallback,
|
|
RemoveLabelCallback,
|
|
StatusCommentResult,
|
|
adjust_labels,
|
|
build_marker,
|
|
comment_has_fingerprint,
|
|
compute_fingerprint,
|
|
post_status_comment,
|
|
)
|
|
from .merging import (
|
|
MAX_BACKOFF_S,
|
|
MAX_MERGE_RETRIES,
|
|
MergeCallback,
|
|
MergeResponse,
|
|
MergingHandlerReport,
|
|
run_merging_tick,
|
|
)
|
|
from .forgejo_http import (
|
|
ForgejoCallbacks,
|
|
build_callbacks,
|
|
)
|
|
from .backfill import (
|
|
BACKFILL_MARKER_EVENT_TYPE,
|
|
BackfillReport,
|
|
has_backfill_run,
|
|
run_startup_backfill,
|
|
)
|
|
from .ci_summarize import (
|
|
LogFetcher,
|
|
summarize_ci_status,
|
|
)
|
|
from .label_gate import (
|
|
DEFAULT_OPT_IN_LABEL,
|
|
count_filtered,
|
|
filter_by_opt_in_label,
|
|
has_opt_in_label,
|
|
opt_in_label_name,
|
|
)
|
|
from .prefetch import (
|
|
GetPRDetailsCallback,
|
|
GetPRDiffCallback,
|
|
ListPRCommentsCallback,
|
|
ListPRReviewsCallback,
|
|
PrefetchDataCallbacks,
|
|
build_conflict_resolver_input,
|
|
build_estimator_input,
|
|
build_implementer_input,
|
|
build_reviewer_input,
|
|
make_prefetch_callback,
|
|
)
|
|
from .reconciliation import (
|
|
GetIssueStateCallback,
|
|
GetPRStateCallback,
|
|
ReconciliationAction,
|
|
ReconciliationReport,
|
|
run_reconciliation_tick,
|
|
)
|
|
from .loop import (
|
|
MasterConfig,
|
|
MasterTickReport,
|
|
master_main_loop,
|
|
run_master_iteration,
|
|
)
|
|
|
|
__all__ = [
|
|
# Discovery
|
|
"DiscoveredEntity",
|
|
"DiscoveryReport",
|
|
"ListIssuesCallback",
|
|
"ListPRsCallback",
|
|
"run_discovery",
|
|
# Outcome mapping
|
|
"EventMapResult",
|
|
"map_outcome_to_event",
|
|
# Scheduler
|
|
"MAX_TIER",
|
|
"PrefetchCallback",
|
|
"ScheduledAttempt",
|
|
"SchedulerReport",
|
|
"schedule_next_attempts",
|
|
# Tick
|
|
"TickReport",
|
|
"run_tick",
|
|
# Main loop
|
|
"MasterConfig",
|
|
"MasterTickReport",
|
|
"master_main_loop",
|
|
"run_master_iteration",
|
|
# Forgejo writes
|
|
"AddLabelCallback",
|
|
"GetLabelsCallback",
|
|
"LabelAdjustResult",
|
|
"ListCommentsCallback",
|
|
"PostCommentCallback",
|
|
"RemoveLabelCallback",
|
|
"StatusCommentResult",
|
|
"adjust_labels",
|
|
"build_marker",
|
|
"comment_has_fingerprint",
|
|
"compute_fingerprint",
|
|
"post_status_comment",
|
|
# MERGING handler
|
|
"MAX_BACKOFF_S",
|
|
"MAX_MERGE_RETRIES",
|
|
"MergeCallback",
|
|
"MergeResponse",
|
|
"MergingHandlerReport",
|
|
"run_merging_tick",
|
|
# HTTP adapter (Forgejo wiring)
|
|
"ForgejoCallbacks",
|
|
"build_callbacks",
|
|
# Backfill (master startup)
|
|
"BACKFILL_MARKER_EVENT_TYPE",
|
|
"BackfillReport",
|
|
"has_backfill_run",
|
|
"run_startup_backfill",
|
|
# Reconciliation (periodic DB ↔ Forgejo sync)
|
|
"GetIssueStateCallback",
|
|
"GetPRStateCallback",
|
|
"ReconciliationAction",
|
|
"ReconciliationReport",
|
|
"run_reconciliation_tick",
|
|
# Prefetch (Phase 1h)
|
|
"GetPRDetailsCallback",
|
|
"GetPRDiffCallback",
|
|
"ListPRCommentsCallback",
|
|
"ListPRReviewsCallback",
|
|
"PrefetchDataCallbacks",
|
|
"build_conflict_resolver_input",
|
|
"build_estimator_input",
|
|
"build_implementer_input",
|
|
"build_reviewer_input",
|
|
"make_prefetch_callback",
|
|
# CI summarizer (Phase 1j)
|
|
"LogFetcher",
|
|
"summarize_ci_status",
|
|
# Opt-in label gate (Phase 1k)
|
|
"DEFAULT_OPT_IN_LABEL",
|
|
"count_filtered",
|
|
"filter_by_opt_in_label",
|
|
"has_opt_in_label",
|
|
"opt_in_label_name",
|
|
]
|