ADR-019: Cascade Monitor Pattern
Status
Accepted - 2025-10-01
Context
The cascade workflow needs to run after upstream changes are merged into fork_upstream. Automatic triggering created reliability and usability problems:
- Event trigger limitations:
pull_request_targetevents require workflows to exist on the target branch (fork_upstream), but that branch is a generated upstream tree without workflow files (ADR-038) - Human control: teams want to decide when integration happens, and may want to batch changes
- Visibility: the cascade lifecycle needs an audit trail and progress tracking
- Error recovery: failed or missed triggers need reliable detection and recovery
Decision
Make manual triggering the primary path and add a scheduled monitor as the safety net:
- Primary path: humans trigger
Cascade Integration(cascade.yml,workflow_dispatchwith anissue_numberinput) after reviewing and merging the sync PR - Safety net:
cascade-monitor.ymlruns every 6 hours (cron: '0 */6 * * *') and onworkflow_dispatch, detects missed triggers, and starts the cascade itself - Issue lifecycle tracking: cascade state is carried by labels on the upstream-sync tracking issue and by comments the workflows post (ADR-022)
- Automated failure recovery: the monitor retries cascades whose failure a human has cleared
Sync workflow instructions
The tracking issue created by sync.yml (labeled upstream-sync,human-required) tells the reviewer to merge the sync PR, then run the Cascade Integration workflow with the issue number, and notes that the monitor will start the cascade within 6 hours if nobody does. See the "Upstream Sync Ready for Review" body in sync.yml.
Monitor jobs
cascade-monitor.yml has four jobs:
- detect-missed-cascade: if
fork_upstreamhas commits thatfork_integrationlacks and an openupstream-syncissue exists, comment on the issue and rungh workflow run "Cascade Integration" -f issue_number=<n>. If the trigger call fails, the job comments "Auto-trigger Failed" on the issue and exits non-zero; no label is applied. - check-stale-conflicts: find open PRs labeled
conflict,cascade-blockedolder than 48 hours, open an escalation issue labeledescalation,high-priority,cascade-escalated,human-required, comment on the PR, and addcascade-escalatedto it (skipped when already escalated). - check-cascade-health: count
cascade-active,cascade-blocked, andcascade-escalateditems and write a status summary. - detect-recovery-ready: find open issues labeled
cascade-failedbut nothuman-required, relabel themcascade-active, comment, and re-run the cascade. If the retry trigger fails the issue is relabeledcascade-failed,human-requiredagain.
Issue lifecycle
# Normal progression (labels on the tracking issue)
upstream-sync + human-required → cascade-active → validated
# Blocked
cascade-active → cascade-blocked (conflicts or validation failure)
# Failed
cascade-active → cascade-failed + human-required
The cascade removes human-required and adds cascade-active when it starts, swaps cascade-active for cascade-blocked on conflicts or validation failures, and ends by removing the active, blocked, and failed labels and adding validated. Label changes are made in cascade.yml.
Human recovery workflow
- The cascade fails; the tracking issue gets
cascade-failed + human-required, and a separate issue labeledhigh-priority,human-requiredcarries the technical details - A developer investigates and fixes the cause
- The developer removes
human-requiredfrom the tracking issue - On its next run the monitor detects the cleared failure and retries the cascade
- The retry either completes or creates a new failure issue
Alternatives Considered
1. Direct push triggers
Rejected: fires on every push, with no way to distinguish sync merges from other pushes.2. Combined PR and push triggers
on:
push:
branches: [fork_upstream, fork_integration]
pull_request:
types: [closed]
branches: [fork_upstream, fork_integration]
fork_upstream. 3. External webhook system
Rejected: extra infrastructure for minimal benefit.
4. Frequent scheduled polling (every 5 minutes)
Rejected as the primary approach; a 6-hour schedule is kept as the backup in the monitor.
Consequences
Humans must remember to trigger cascades; a forgotten trigger is delayed by up to 6 hours until the monitor runs. Label-based state tracking adds steps to the cascade workflow, and the safety net depends on the monitor itself running. In exchange, integration timing is under team control and the process does not depend on GitHub event delivery to a branch without workflows.
Related Decisions
- ADR-001: Three-Branch Fork Management Strategy - Defines the cascade target branches
- ADR-005: Automated Conflict Management Strategy - Conflict handling within cascades
- ADR-008: Centralized Label Management Strategy - Label-based state management
- ADR-009: Asymmetric Cascade Review Strategy - Review requirements for cascades
- ADR-020: Human-Required Label Strategy - Label instead of assignee
- ADR-022: Issue Lifecycle Tracking Pattern - Lifecycle labels