docs/plans/2025-12-25-event-driven-rollout-creation-design.md
Date: 2025-12-25 Status: Approved
This design replaces the polling-based auto-rollout scheduler with an event-driven architecture. Rollouts will be created automatically and instantly when approval and plan check conditions are met, eliminating both the 5-second polling delay and the need for users to manually click "Create Rollout".
Current Problems:
Goals:
Scope:
Auto-Rollout Scheduler:
backend/runner/auto_rollout/scheduler.goproject.RequireIssueApproval == true)project.RequirePlanCheckNoError == true)CreateRollout() when conditions are metManual Button:
frontend/src/components/Plan/components/HeaderSection/Actions/registry/actions/rollout.ts!plan.hasRollout and preconditions metCreateRollout() RPCCreateRollout():
backend/api/v1/rollout_service.goplan.Config.HasRollout = true to prevent further spec modificationsEvent-Driven Flow:
┌─────────────────┐ ┌──────────────────┐
│ ApproveIssue() │────────▶│ TryCreateRollout │
│ (all approved) │ │ (Issue ID) │
└─────────────────┘ └──────────────────┘
│
▼
┌─────────────────┐ ┌──────────────────┐
│ PlanCheckRunner │────────▶│ Check all │
│ (status = DONE) │ │ conditions │
└─────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ CreateRollout() │
│ (if ready) │
└──────────────────┘
Components to Remove:
backend/runner/auto_rollout/scheduler.go - Entire schedulerComponents to Add:
TryCreateRollout(ctx, issueID) - Centralized condition checkingComponents to Modify:
CreateRollout() - Add idempotency checkApproveIssue() - Call TryCreateRollout when fully approvedLocation: backend/api/v1/rollout_service.go
Signature:
func (s *RolloutService) TryCreateRollout(ctx context.Context, issueID int) error
Logic Flow:
plan.Config.HasRollout == trueproject.RequireIssueApproval == true:
issue.approvalStatus == APPROVEDproject.RequirePlanCheckNoError == true:
CreateRollout() with planCharacteristics:
Add at the beginning of CreateRollout():
// Idempotency check
if plan.Config.HasRollout {
return nil, status.Errorf(codes.AlreadyExists, "rollout already exists for plan")
}
This ensures:
Location: backend/api/v1/issue_service.go in ApproveIssue()
When to trigger:
issue.approvers in databaseissue.approvalStatus becomes APPROVED (all approvers stamped)Implementation:
// After database update
if issue.approvalStatus == Issue_ApprovalStatus_APPROVED {
go func() {
if err := s.rolloutService.TryCreateRollout(ctx, issueID); err != nil {
slog.Error("failed to auto-create rollout after approval",
log.BBError(err),
slog.Int("issue_id", issueID))
}
}()
}
Location: backend/runner/plancheck/executor.go (or wherever plan_check_run status is updated to DONE)
When to trigger:
plan_check_run.status = DONE in databaseDONE (success)FAILED or CANCELEDImplementation:
// After updating plan check run to DONE
if planCheckRun.Status == PlanCheckRun_Status_DONE {
// Get issue from plan
issueID := getIssueIDFromPlan(plan)
if issueID > 0 {
go func() {
if err := rolloutService.TryCreateRollout(ctx, issueID); err != nil {
slog.Error("failed to auto-create rollout after plan check",
log.BBError(err),
slog.Int("issue_id", issueID))
}
}()
}
}
Race condition between events:
TryCreateRollout()HasRollout + database transaction in CreateRollout()No plan checks exist:
Approval template discovery incomplete:
Manual button after auto-creation:
Plan specs change after approval:
plan.Config.HasRollout == true, specs can't be modified (existing validation)TryCreateRollout() tests:
CreateRollout() idempotency:
Approval flow:
Plan check flow:
Race condition:
Code changes:
Testing:
Deployment:
Monitoring:
If issues are discovered: