Skip to content

Surface migration terminal failure cause - #19

Merged
AleBaccin merged 2 commits into
mainfrom
alebaccin/surface-terminal-failure
Oct 2, 2026
Merged

AleBaccin merged 2 commits into
mainfrom
alebaccin/surface-terminal-failure

Conversation

@AleBaccin

@AleBaccin AleBaccin commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What

When a migration reaches a terminal state, ELM now records why. Nothing a customer touches rendered it — an operator saw Migration failed, or Failed: 42 resources failed, which is a symptom and is often empty entirely (a target repository policy blocks repo creation before any resource is imported).

This renders the cause.

Failure
  ✗ Creating the target repository was blocked by a policy on the target
    organization or enterprise.
    Code                repository_policy
    Occurred            2m ago (2026-09-04T12:58:37Z)

Notes for review

  • --json needed no change. Every command emits via render.WriteRawJSON(out, resp.Raw) — the raw API document, not re-serialized structs. terminal_failure appears there the moment gh/gh serializes it. Pinned with a regression test rather than left as a happy accident.

  • One shared formatter (internal/render/terminal_failure.go) consumed by both the status renderer and the watch TUI, so the two surfaces can't drift. The mvnd half of this project shipped a bug where a struct was built at three sites and only two learned about a new field.

  • internal/tui needed no change — sourceDetailView delegates to render.MigrationStatus and inherits the section.

  • Precedence: combined over target. combined_state.terminal_failure is populated only when combined status is FAILED, deliberately not TERMINATED (a user abort). Target is a faithful passthrough that can be set while combined describes something else, so preferring it would surface a cause on migrations the server doesn't consider failed.

  • The dedup is the fragile part. elm-exporter#872 makes combined_state.display_message prefer the authored summary for FAILED, and renderCombinedState already prints DisplayMessage — the same sentence would render twice. The summary is seeded into the existing renderedValues slice so the existing normalization suppresses the echo. Covered by tests, and mutation-verified: removing the seeding fails exactly those two subtests.

  • The code is rendered raw, not through friendlyValue(). It's a contract token; keeping it verbatim makes it greppable and quotable in a support escalation. Unrecognized codes render fine — the server's summary is printed verbatim and nothing switches on the code to produce text, so this won't go stale as mvnd adds codes.

  • Absent occurred_at omits the line rather than printing —, which would wrongly imply a failure at an unknown time. The server returns null deliberately to distinguish "no time recorded" from "failed in 1970".

Testing

make fmt vet lint test clean. make kitchen-sink has a new failed-migration fixture whose display_message deliberately equals the summary, so the preview demonstrates the dedup.

kitchen-sink output 🖌️

➜  gh-elm git:(alebaccin/surface-terminal-failure) ✗ go run cmd/kitchen-sink/main.go
=== gh elm migration create ===

✓ Migration successfully created
  Migration ID        7e3f16ca-44da-4f9a-a806-2c798a6afda7
  Expires             2026-08-13T10:00:00Z


=== gh elm migration status ===

source-org/monolith → target-org/monolith
  ● In progress
  Migration ID        7e3f16ca-44da-4f9a-a806-2c798a6afda7
  Target migration ID 4242
  Visibility          internal
  Created             2026-08-06T08:00:00Z
  Started             2026-08-06T08:05:00Z
  Completed           —
  Expires             2026-08-13T10:00:00Z

Target
  ● In progress
  ✓ Target available

Progress · target-org/monolith
  Backfill            ████████████░░  1100 / 1250 processed  ✗ 3 failed
  Live updates        █████████████░  79 / 84 processed  ✗ 1 failed
  ○ Resources still being sent
  ✓ Initial Git push complete
  ○ Source repository unlocked

Cutover
  ● Processing
  ○ Not ready for cutover
  Backfill is still processing resources.
  ! Backfill has not completed
  ! Four resources require attention

Repository states
  • target-org/monolith · Backfill · 1,100 of 1,250 resources processed

Messages
  ✓ [preflight: repository access] Passed · 2026-08-06T08:04:00Z
  ! Live updates are falling behind. · 2026-08-06T09:30:00Z
  ✗ Three backfill resources failed and should be reviewed. · 2026-08-06T09:42:00Z


=== gh elm migration status (failed) ===

source-org/billing → target-org/billing
  ✗ Failed
  Migration ID        b41d0c6e-9d2a-4f70-8c1b-5a6f2f0f4e18
  Target migration ID 4307
  Visibility          internal
  Created             2026-09-04T12:55:00Z
  Started             2026-09-04T12:56:10Z
  Completed           —
  Expires             2026-09-11T12:55:00Z

Failure
  ✗ Creating the target repository was blocked by a policy on the target organization or enterprise.
  Code                repository_policy
  Occurred            24d ago (2026-09-04T12:58:37Z)

Target
  ✗ Failed
  ✓ Target available

Progress · target-org/billing
  Backfill            ████████████░░  22 / 24 processed  ✗ 2 failed
  Live updates        ░░░░░░░░░░░░░░  0 / 0 processed  ✓ no failures
  ○ Resources still being sent
  ○ Initial Git push pending
  ○ Source repository unlocked

Cutover
  ✗ Failed
  ○ Not ready for cutover

Repository states
  • target-org/billing · Backfill · Failed: 2 resources failed

Messages
  ✗ Two backfill resources failed and should be reviewed. · 2026-09-04T12:58:40Z


=== gh elm migration status (terminated, target reported a fault) ===

source-org/payments → target-org/payments
  ✗ Terminated
  Migration ID        c52e1d7f-ae3b-4081-9d2c-6b7a3a1b5f29
  Target migration ID 4311
  Visibility          internal
  Created             2026-09-04T13:10:00Z
  Started             2026-09-04T13:11:05Z
  Completed           —
  Expires             2026-09-11T13:10:00Z

Target
  ✗ Failed
  ✓ Target available

Cutover
  ✗ Terminated
  ○ Not ready for cutover
  Migration terminated by request.


=== gh elm migration list ===

Migrations (3)

  ● In progress  source-org/monolith → target-org/monolith
  Migration ID        7e3f16ca-44da-4f9a-a806-2c798a6afda7
  Target migration ID 4242
  Visibility          internal
  Created             2026-08-06T08:00:00Z
  Started             2026-08-06T08:05:00Z
  Completed           —
  Expires             2026-08-13T10:00:00Z

  ✓ Completed  source-org/api → target-org/api
  Migration ID        c7fd8baa-8680-43c6-8286-c2409b2b3f51
  Target migration ID 4188
  Visibility          private
  Created             2026-08-05T13:00:00Z
  Started             2026-08-05T13:04:00Z
  Completed           2026-08-05T15:31:00Z
  Expires             2026-08-12T13:00:00Z

  ✗ Failed  source-org/web → target-org/web
  Migration ID        11ef6308-489c-466f-a755-5196f809bcc9
  Target migration ID 4101
  Visibility          internal
  Created             2026-08-04T09:00:00Z
  Started             2026-08-04T09:02:00Z
  Completed           —
  Expires             2026-08-11T09:00:00Z

Showing 3 of 7 · next cursor: eyJtaWdyYXRpb25faWQiOiIxMWVmNjMwOCJ9


=== gh elm migration cutover revert ===

  ✓ Cutover reverted

Changes
  ✓ Source repository unarchived
  ✓ In-progress cutover terminated
  ○ No in-progress migration to terminate

Operator saw "Migration failed" with no reason. Backend now reports a
terminal_failure (code, summary, occurred_at) on both target_state and
combined_state; render it.

One shared formatter in internal/render so status output and the watch TUI
cannot drift. Combined wins over target: combined is only set on FAILED, not
TERMINATED (user abort), so preferring target would show a cause on migrations
the server does not consider failed.

Summary is seeded into renderedValues because elm-exporter now prefers the
authored summary for combined display_message, which would otherwise print the
same sentence twice.

--json needed no change; it already passes the raw API document through.
Pinned with a test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e558d009-3ea1-4455-b1fd-c2a180332509
Copilot AI balanced review requested due to automatic review settings September 28, 2026 10:24
@AleBaccin
AleBaccin requested a review from a team as a code owner September 28, 2026 10:24

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.

Copilot review overview

🟡 Changes recommended

Target fallback can mislabel terminated migrations, and cutover status can suppress the failure explanation entirely.

Review effort: Balanced
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Adds rendering for server-recorded terminal migration failure causes across status and watch views.

Changes:

  • Models and decodes terminal failure metadata.
  • Adds shared failure formatting with timestamps and deduplication.
  • Extends tests and kitchen-sink fixtures.
File Description
internal/​render/​terminal_failure.go Resolves and formats terminal failures.
internal/​render/​terminal_failure_test.go Tests failure rendering and precedence.
internal/​render/​migration.go Integrates failures into migration output.
internal/​elmapi/​migrations.go Adds terminal failure API fields.
internal/​elmapi/​migrations_test.go Tests decoding and raw JSON preservation.
internal/​cmd/​migration/​watch/​view.go Displays failure causes in watch mode.
internal/​cmd/​migration/​watch/​watch_test.go Tests watch failure details.
cmd/​kitchen-sink/​main.go Adds a failed-migration preview fixture.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/render/migration.go Outdated
Comment thread internal/render/terminal_failure.go
Comment thread internal/render/terminal_failure_test.go
@AleBaccin
AleBaccin marked this pull request as draft September 28, 2026 12:19
Addresses review on #19. Both bugs were places where the code contradicted
a doc comment added in the same PR.

Gate the target fallback. TerminalFailureFor fell back to the target cause
unconditionally, so a terminated migration -- a deliberate abort -- rendered
a Failure section whenever the target had separately reported a fault,
misattributing the user's own cancellation as a failure. The target cause is
now used only when combined state is absent, undecided, or itself failed.
Needs its own predicate: statusGlyph/statusText deliberately lump failed in
with terminated for glyph purposes, which is the exact distinction here.

Render the failure in CutoverStatus. It passed the cause to
renderCombinedState, which suppresses a display_message matching the summary,
but never rendered the section -- so cutover status showed a bare Failed and
deleted text that was visible before this PR. The dedup is now correct rather
than destructive, and cutover gains Code and Occurred.

Drop t.Helper() from pinNow: setup helper, no assertions.

The existing cutover test asserted NotContains(summary), encoding the bug; it
now asserts the cause renders exactly once, guarding the drop and the
double-print together. Mutation-verified: reverting either fix fails exactly
the new cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e558d009-3ea1-4455-b1fd-c2a180332509
@AleBaccin
AleBaccin marked this pull request as ready for review September 28, 2026 13:55

@juruen juruen 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.

I'm new to this code base, but this LGTM.

@AleBaccin
AleBaccin added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit c74731f Oct 2, 2026
13 checks passed
@AleBaccin
AleBaccin deleted the alebaccin/surface-terminal-failure branch October 2, 2026 09:16
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.

3 participants