Skip to content

feat(api): add the reapply endpoint for failed schema changes#698

Draft
aparajon wants to merge 2 commits into
mainfrom
aparajon/reapply-api-strict
Draft

feat(api): add the reapply endpoint for failed schema changes#698
aparajon wants to merge 2 commits into
mainfrom
aparajon/reapply-api-strict

Conversation

@aparajon

@aparajon aparajon commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Why this matters

When an apply fails, operators today have to hand-craft a brand-new plan/apply cycle even when the desired schema hasn't changed. A first-class reapply endpoint makes the safe case — requeue exactly what failed — a single call, while refusing anything the endpoint cannot requeue safely.

What it does

  • Adds POST /api/reapply: re-plans the failed apply's stored desired schema and requeues the apply only when the fresh plan matches the stored plan exactly; any divergence is rejected so a drifted target is never silently re-driven.
  • Operator-only surface: this is a direct-API / Admin CLI endpoint gated at the write tier (configured admin group), like the other control verbs. There is deliberately no PR-comment command for it — reapply acts on a failed apply out-of-band of the PR workflow, and exposing it to PR commenters is a separate product decision.
  • Only a fresh terminal failure with a reappliable work shape qualifies: every task/operation failed (nothing completed) and all failed work in the plan's primary deployment. Anything else is rejected with guidance to create a new apply instead.
  • Storage inconsistencies (a failed task referencing a missing operation row) fail closed as errors for operator investigation rather than being dropped.
  • Counts reapply outcomes (success / rejected / error) on the control-operation counter, like the other control endpoints.

First of two stacked PRs — #592 follows and relaxes the shape guard so a partially completed apply can reapply just its failed remainder.

🤖 Generated with Claude Code

POST /api/reapply requeues a failed apply after re-planning its stored
desired schema. The fresh plan must match the stored plan exactly, and
the apply must be a fresh terminal failure whose work is fully failed
and confined to the plan's primary deployment — anything else is
rejected with guidance to create a new apply instead. Storage
inconsistencies (a failed task referencing a missing operation row)
fail closed as errors for operator investigation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new operator control endpoint to safely requeue a fully failed schema change apply by re-planning the stored desired schema and only proceeding when the fresh plan is equivalent to the stored plan, reducing operator toil while failing closed on drift or storage inconsistencies.

Changes:

  • Adds POST /api/reapply route + handler that re-plans and conditionally requeues a failed apply.
  • Introduces apitypes.ReapplyResponse and ReapplyStatusReapplied for the new endpoint’s response shape.
  • Adds unit tests covering acceptance/rejection cases (plan drift, partial completion, stale failure, env mismatch, missing operation row, etc.).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pkg/auth/tiers_test.go Ensures /api/reapply is classified as a write-tier endpoint.
pkg/apitypes/apitypes.go Adds response type/constants for POST /api/reapply and updates control-request docs.
pkg/api/service.go Registers the new route and exposes HandleReapply.
pkg/api/reapply_handlers.go Implements the reapply handler, planning equivalence check, and work-shape validation.
pkg/api/reapply_handlers_test.go Adds focused tests for strict reapply behavior and edge cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +219 to +226
if state.IsState(task.State, state.Task.Completed) {
return fmt.Errorf("apply %s completed part of its work, so a reapply would re-run finished tables; create a new apply instead: %w", apply.ApplyIdentifier, storage.ErrApplyNotReappliable)
}
if !state.IsState(task.State, state.Task.Failed, state.Task.Cancelled) {
// Neither completed nor failed work: nothing for this task to prove
// or violate about the reapply shape.
continue
}
Comment on lines +87 to +93
if s.storage == nil {
return nil, fmt.Errorf("storage is not available")
}
caller := resolveCaller(ctx, req.Caller)

apply, err := s.storage.Applies().GetByApplyIdentifier(ctx, req.ApplyID)
if err != nil {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants