From d756f521d4cad48e089db7fa64e0711d9d577f03 Mon Sep 17 00:00:00 2001 From: Jonathan Irwin Date: Thu, 7 May 2026 15:18:38 -0400 Subject: [PATCH 1/3] feat: surface initError on the build when deploy fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a deploy hits a terminal failure (e.g. capacity unavailable, init_failure), the backend now writes a user-facing message to the build's initError field synchronously. The CLI was polling the build status but discarding initError, so the user only saw "✗ Build failed with status: init_failure" with no actionable detail. This wires initError through buildStatusUpdateMsg → buildCompleteMsg → logDrainCompleteMsg and prints it after the failure line in both SimpleOutput and interactive modes. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/api/types.go | 9 +++++---- internal/ui/commands/deploy.go | 34 +++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/internal/api/types.go b/internal/api/types.go index a53b959..856389b 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -379,10 +379,11 @@ type Container struct { // AppBuild represents a build for a Cerebrium application type AppBuild struct { - Id string `json:"id"` - Status string `json:"status"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + Id string `json:"id"` + Status string `json:"status"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + InitError *string `json:"initError,omitempty"` } // BaseImagePayload represents the payload for creating a base image diff --git a/internal/ui/commands/deploy.go b/internal/ui/commands/deploy.go index d506a3a..e7da15f 100644 --- a/internal/ui/commands/deploy.go +++ b/internal/ui/commands/deploy.go @@ -485,7 +485,7 @@ func (m *DeployView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if ui.IsTerminalStatus(msg.status) { // Terminal status detected, trigger completion return m, func() tea.Msg { - return buildCompleteMsg{status: msg.status} + return buildCompleteMsg{status: msg.status, initError: msg.initError} } } @@ -506,7 +506,7 @@ func (m *DeployView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Wait 2 seconds to allow remaining logs to arrive return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { - return logDrainCompleteMsg(msg) + return logDrainCompleteMsg{status: msg.status, initError: msg.initError} }) case logDrainCompleteMsg: @@ -563,15 +563,23 @@ func (m *DeployView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.conf.SimpleOutput() { fmt.Printf("✗ Build failed with status: %s\n", msg.status) + if msg.initError != nil && *msg.initError != "" { + fmt.Println() + fmt.Println(*msg.initError) + } return m, tea.Quit } // Print error message to scrollback in interactive mode - return m, tea.Sequence( + cmds := []tea.Cmd{ tea.Println(""), tea.Println(ui.ErrorStyle.Render(fmt.Sprintf("✗ Build failed with status: %s", msg.status))), - tea.Quit, - ) + } + if msg.initError != nil && *msg.initError != "" { + cmds = append(cmds, tea.Println(""), tea.Println(*msg.initError)) + } + cmds = append(cmds, tea.Quit) + return m, tea.Sequence(cmds...) } case confirmationResponseMsg: @@ -880,8 +888,9 @@ type appCreatedMsg struct { type zipUploadedMsg struct{} type buildStatusUpdateMsg struct { - buildID string - status string + buildID string + status string + initError *string } type buildStatusPollErrorMsg struct { @@ -889,11 +898,13 @@ type buildStatusPollErrorMsg struct { } type buildCompleteMsg struct { - status string + status string + initError *string } type logDrainCompleteMsg struct { - status string // Final build status to use for completion + status string // Final build status to use for completion + initError *string // User-facing error message persisted on the build } type buildCancelledMsg struct { @@ -1213,8 +1224,9 @@ func (m *DeployView) pollBuildStatus() tea.Msg { // Return status update message return buildStatusUpdateMsg{ - buildID: build.Id, - status: build.Status, + buildID: build.Id, + status: build.Status, + initError: build.InitError, } } From a7d78099add54efb2a8194f67ab471d464c32ecc Mon Sep 17 00:00:00 2001 From: Jonathan Irwin Date: Fri, 8 May 2026 14:43:22 -0400 Subject: [PATCH 2/3] review: use string instead of *string for InitError No functional difference between nil and empty in this case, so the non-pointer type is simpler to work with. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/api/types.go | 10 +++++----- internal/ui/commands/deploy.go | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/api/types.go b/internal/api/types.go index 856389b..5d25733 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -379,11 +379,11 @@ type Container struct { // AppBuild represents a build for a Cerebrium application type AppBuild struct { - Id string `json:"id"` - Status string `json:"status"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - InitError *string `json:"initError,omitempty"` + Id string `json:"id"` + Status string `json:"status"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + InitError string `json:"initError,omitempty"` } // BaseImagePayload represents the payload for creating a base image diff --git a/internal/ui/commands/deploy.go b/internal/ui/commands/deploy.go index e7da15f..228f873 100644 --- a/internal/ui/commands/deploy.go +++ b/internal/ui/commands/deploy.go @@ -563,9 +563,9 @@ func (m *DeployView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.conf.SimpleOutput() { fmt.Printf("✗ Build failed with status: %s\n", msg.status) - if msg.initError != nil && *msg.initError != "" { + if msg.initError != "" { fmt.Println() - fmt.Println(*msg.initError) + fmt.Println(msg.initError) } return m, tea.Quit } @@ -575,8 +575,8 @@ func (m *DeployView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { tea.Println(""), tea.Println(ui.ErrorStyle.Render(fmt.Sprintf("✗ Build failed with status: %s", msg.status))), } - if msg.initError != nil && *msg.initError != "" { - cmds = append(cmds, tea.Println(""), tea.Println(*msg.initError)) + if msg.initError != "" { + cmds = append(cmds, tea.Println(""), tea.Println(msg.initError)) } cmds = append(cmds, tea.Quit) return m, tea.Sequence(cmds...) @@ -890,7 +890,7 @@ type zipUploadedMsg struct{} type buildStatusUpdateMsg struct { buildID string status string - initError *string + initError string } type buildStatusPollErrorMsg struct { @@ -899,12 +899,12 @@ type buildStatusPollErrorMsg struct { type buildCompleteMsg struct { status string - initError *string + initError string } type logDrainCompleteMsg struct { - status string // Final build status to use for completion - initError *string // User-facing error message persisted on the build + status string // Final build status to use for completion + initError string // User-facing error message persisted on the build } type buildCancelledMsg struct { From 4f7133afb2e8591625ea7339cd72c4827aabf1cd Mon Sep 17 00:00:00 2001 From: Jonathan Irwin Date: Fri, 8 May 2026 14:46:43 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20convert=20buildCompleteMsg=20?= =?UTF-8?q?=E2=86=92=20logDrainCompleteMsg=20via=20type=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both struct types share the same fields, so use Go's type conversion as the original code did rather than a struct literal (staticcheck S1016). Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/ui/commands/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ui/commands/deploy.go b/internal/ui/commands/deploy.go index 228f873..7e2285e 100644 --- a/internal/ui/commands/deploy.go +++ b/internal/ui/commands/deploy.go @@ -506,7 +506,7 @@ func (m *DeployView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Wait 2 seconds to allow remaining logs to arrive return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { - return logDrainCompleteMsg{status: msg.status, initError: msg.initError} + return logDrainCompleteMsg(msg) }) case logDrainCompleteMsg: