diff --git a/internal/store/store.go b/internal/store/store.go index 4737147a..045784b4 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -1224,37 +1224,71 @@ func (s *Store) RepairCloudUpgrade(project string, apply bool) (CloudUpgradeRepa if err != nil { return CloudUpgradeRepairReport{}, fmt.Errorf("diagnose legacy cloud upgrade mutations: %w", err) } + + // Apply the repairable subset first. Holding the entire repair pass hostage + // to a single unrecoverable mutation forces operators into manual SQLite + // surgery for the rest of the queue; instead we let recoverable findings + // make forward progress and surface the residual blockers afterwards. + appliedRepairs := false + if apply && legacyReport.RepairableCount > 0 { + if err := s.applyCloudUpgradeLegacyMutationRepairs(project); err != nil { + return CloudUpgradeRepairReport{}, fmt.Errorf("apply cloud upgrade legacy mutation repairs: %w", err) + } + appliedRepairs = true + } + if legacyReport.BlockedCount > 0 { - first := legacyReport.Findings[0] + // Pick the first non-repairable finding so the user sees the actual + // blocker. Findings[0] is ordered by sync_mutations.seq and may be a + // repairable one, which previously produced misleading error messages. + var blocked CloudUpgradeLegacyMutationFinding + for _, f := range legacyReport.Findings { + if !f.Repairable { + blocked = f + break + } + } + var msg string + switch { + case appliedRepairs: + msg = fmt.Sprintf("applied %d repairable payload(s); %d remain blocked: manual-action-required: %s (seq=%d entity=%s entity_key=%q op=%s)", + legacyReport.RepairableCount, legacyReport.BlockedCount, blocked.Message, blocked.Seq, blocked.Entity, blocked.EntityKey, blocked.Op) + case legacyReport.RepairableCount > 0: + msg = fmt.Sprintf("%d repairable payload(s) would apply; %d would remain blocked: manual-action-required: %s (seq=%d entity=%s entity_key=%q op=%s)", + legacyReport.RepairableCount, legacyReport.BlockedCount, blocked.Message, blocked.Seq, blocked.Entity, blocked.EntityKey, blocked.Op) + default: + msg = fmt.Sprintf("manual-action-required: %s (seq=%d entity=%s entity_key=%q op=%s)", + blocked.Message, blocked.Seq, blocked.Entity, blocked.EntityKey, blocked.Op) + } return CloudUpgradeRepairReport{ Class: UpgradeRepairClassBlocked, ReasonCode: UpgradeReasonBlockedLegacyMutationManual, - Message: fmt.Sprintf("manual-action-required: %s (seq=%d entity=%s op=%s)", first.Message, first.Seq, first.Entity, first.Op), - Applied: false, + Message: msg, + Applied: appliedRepairs, }, nil } if legacyReport.RepairableCount > 0 { - report := CloudUpgradeRepairReport{ - Class: UpgradeRepairClassRepairable, - ReasonCode: UpgradeReasonRepairableLegacyMutationPayload, - Message: fmt.Sprintf("project %q has %d repairable legacy mutation payload issue(s)", project, legacyReport.RepairableCount), - PlannedAction: "repair_legacy_mutation_payloads", - Applied: false, - } - if !apply { - return report, nil - } - if err := s.applyCloudUpgradeLegacyMutationRepairs(project); err != nil { - return CloudUpgradeRepairReport{}, fmt.Errorf("apply cloud upgrade legacy mutation repairs: %w", err) + if !appliedRepairs { + return CloudUpgradeRepairReport{ + Class: UpgradeRepairClassRepairable, + ReasonCode: UpgradeReasonRepairableLegacyMutationPayload, + Message: fmt.Sprintf("project %q has %d repairable legacy mutation payload issue(s)", project, legacyReport.RepairableCount), + PlannedAction: "repair_legacy_mutation_payloads", + Applied: false, + }, nil } - report.Applied = true - report.Message = fmt.Sprintf("applied deterministic legacy mutation payload repairs for project %q", project) _ = s.SaveCloudUpgradeState(CloudUpgradeState{ Project: project, Stage: UpgradeStageRepairApplied, RepairClass: UpgradeRepairClassRepairable, }) - return report, nil + return CloudUpgradeRepairReport{ + Class: UpgradeRepairClassRepairable, + ReasonCode: UpgradeReasonRepairableLegacyMutationPayload, + Message: fmt.Sprintf("applied deterministic legacy mutation payload repairs for project %q", project), + PlannedAction: "repair_legacy_mutation_payloads", + Applied: true, + }, nil } requiresBackfill, err := s.projectSyncBackfillRequired(project)