From 403155501984fbf5dff4596a47912dc96f57474f Mon Sep 17 00:00:00 2001 From: rohanpatel2002 Date: Thu, 23 Jul 2026 14:40:34 +0530 Subject: [PATCH] fix(start): suggest recovery for exec format and backup migration failures Surface actionable CmdSuggestion when unhealthy containers log exec format error, storage migrations_name_key, or Studio ERR_INVALID_PACKAGE_CONFIG, and pin image pulls to linux/ to reduce wrong-arch Mailpit starts. --- apps/cli-go/internal/db/start/start.go | 15 ++++- .../internal/db/start/suggest_unhealthy.go | 57 +++++++++++++++++++ .../db/start/suggest_unhealthy_test.go | 51 +++++++++++++++++ apps/cli-go/internal/utils/docker.go | 4 ++ 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 apps/cli-go/internal/db/start/suggest_unhealthy.go create mode 100644 apps/cli-go/internal/db/start/suggest_unhealthy_test.go diff --git a/apps/cli-go/internal/db/start/start.go b/apps/cli-go/internal/db/start/start.go index 6cd411791c..3b9384bba9 100644 --- a/apps/cli-go/internal/db/start/start.go +++ b/apps/cli-go/internal/db/start/start.go @@ -213,11 +213,20 @@ func WaitForHealthyService(ctx context.Context, timeout time.Duration, started . policy := NewBackoffPolicy(ctx, timeout) err := backoff.Retry(probe, policy) if err != nil && !errors.Is(err, context.Canceled) { - // Print container logs for easier debugging + // Print container logs for easier debugging and suggest recovery steps. + var logBuf strings.Builder for _, containerId := range started { fmt.Fprintln(os.Stderr, containerId, "container logs:") - if err := utils.DockerStreamLogsOnce(context.Background(), containerId, os.Stderr, os.Stderr); err != nil { - fmt.Fprintln(os.Stderr, err) + w := io.MultiWriter(os.Stderr, &logBuf) + if logErr := utils.DockerStreamLogsOnce(context.Background(), containerId, w, w); logErr != nil { + fmt.Fprintln(os.Stderr, logErr) + } + } + if suggestion := SuggestFromUnhealthyLogs(logBuf.String()); len(suggestion) > 0 { + if len(utils.CmdSuggestion) > 0 { + utils.CmdSuggestion += "\n\n" + suggestion + } else { + utils.CmdSuggestion = suggestion } } } diff --git a/apps/cli-go/internal/db/start/suggest_unhealthy.go b/apps/cli-go/internal/db/start/suggest_unhealthy.go new file mode 100644 index 0000000000..6d206e7ebd --- /dev/null +++ b/apps/cli-go/internal/db/start/suggest_unhealthy.go @@ -0,0 +1,57 @@ +package start + +import ( + "fmt" + "runtime" + "strings" + + "github.com/supabase/cli/internal/utils" +) + +// SuggestFromUnhealthyLogs returns a CmdSuggestion for common local-stack +// startup failures seen after image upgrades or wrong-arch pulls +// (see https://github.com/supabase/supabase/issues/48224). +func SuggestFromUnhealthyLogs(logs string) string { + lower := strings.ToLower(logs) + var parts []string + + if strings.Contains(lower, "exec format error") { + arch := runtime.GOARCH + parts = append(parts, fmt.Sprintf( + "A container failed with \"exec format error\" (wrong CPU architecture image).\n"+ + "Try removing the mismatched images and restarting for linux/%s:\n"+ + " %s\n"+ + " docker image prune -f\n"+ + " %s", + arch, + utils.Aqua("supabase stop --no-backup"), + utils.Aqua("supabase start"), + )) + } + + if strings.Contains(lower, "migrations_name_key") || + (strings.Contains(lower, "migration failed") && strings.Contains(lower, "duplicate key")) { + parts = append(parts, fmt.Sprintf( + "Storage migrations failed against an existing database volume (often after upgrading images while restoring a backup).\n"+ + "Reset local data and start fresh:\n"+ + " %s\n"+ + " %s", + utils.Aqua("supabase stop --no-backup"), + utils.Aqua("supabase start"), + )) + } + + if strings.Contains(lower, "err_invalid_package_config") || + strings.Contains(lower, "invalid package config") { + parts = append(parts, fmt.Sprintf( + "Studio image looks corrupted or incomplete. Remove it and re-pull:\n"+ + " %s\n"+ + " docker image rm -f $(docker images -q supabase/studio) 2>/dev/null\n"+ + " %s", + utils.Aqua("supabase stop --no-backup"), + utils.Aqua("supabase start"), + )) + } + + return strings.Join(parts, "\n\n") +} diff --git a/apps/cli-go/internal/db/start/suggest_unhealthy_test.go b/apps/cli-go/internal/db/start/suggest_unhealthy_test.go new file mode 100644 index 0000000000..55113c9933 --- /dev/null +++ b/apps/cli-go/internal/db/start/suggest_unhealthy_test.go @@ -0,0 +1,51 @@ +package start + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSuggestFromUnhealthyLogs(t *testing.T) { + t.Run("suggests recovery for exec format error", func(t *testing.T) { + suggestion := SuggestFromUnhealthyLogs("exec /mailpit: exec format error\n") + require.NotEmpty(t, suggestion) + assert.Contains(t, suggestion, "exec format error") + assert.Contains(t, suggestion, "supabase stop --no-backup") + assert.Contains(t, suggestion, "supabase start") + assert.Contains(t, suggestion, "linux/"+runtime.GOARCH) + }) + + t.Run("suggests recovery for storage migrations_name_key", func(t *testing.T) { + logs := `Migration failed. Reason: duplicate key value violates unique constraint "migrations_name_key"` + suggestion := SuggestFromUnhealthyLogs(logs) + require.NotEmpty(t, suggestion) + assert.Contains(t, suggestion, "Storage migrations failed") + assert.Contains(t, suggestion, "supabase stop --no-backup") + assert.Contains(t, suggestion, "supabase start") + }) + + t.Run("suggests recovery for studio invalid package config", func(t *testing.T) { + logs := "Error: Invalid package config /app/apps/studio/node_modules/next/package.json.\n code: 'ERR_INVALID_PACKAGE_CONFIG'" + suggestion := SuggestFromUnhealthyLogs(logs) + require.NotEmpty(t, suggestion) + assert.Contains(t, suggestion, "Studio") + assert.Contains(t, suggestion, "supabase stop --no-backup") + assert.Contains(t, suggestion, "supabase start") + }) + + t.Run("combines multiple failure suggestions", func(t *testing.T) { + logs := "exec /mailpit: exec format error\nmigrations_name_key\nERR_INVALID_PACKAGE_CONFIG" + suggestion := SuggestFromUnhealthyLogs(logs) + require.NotEmpty(t, suggestion) + assert.Contains(t, suggestion, "exec format error") + assert.Contains(t, suggestion, "Storage migrations failed") + assert.Contains(t, suggestion, "Studio") + }) + + t.Run("returns empty for unrelated logs", func(t *testing.T) { + assert.Empty(t, SuggestFromUnhealthyLogs("listening on :5432\nready\n")) + }) +} diff --git a/apps/cli-go/internal/utils/docker.go b/apps/cli-go/internal/utils/docker.go index 29d2edd397..6f20ae2b78 100644 --- a/apps/cli-go/internal/utils/docker.go +++ b/apps/cli-go/internal/utils/docker.go @@ -10,6 +10,7 @@ import ( "log" "os" "regexp" + "runtime" "strings" "sync" "time" @@ -289,6 +290,9 @@ func registryFromImage(imageTag string) string { func DockerImagePull(ctx context.Context, imageTag string, w io.Writer) error { out, err := Docker.ImagePull(ctx, imageTag, image.PullOptions{ RegistryAuth: GetRegistryAuthForImage(imageTag), + // Prefer the host architecture so services like mailpit do not start + // with a wrong-arch binary (exec format error). See supabase/supabase#48224. + Platform: "linux/" + runtime.GOARCH, }) if err != nil { return errors.Errorf("failed to pull docker image: %w", err)