From 76587bddc2c74d1529b40a1e178ad92741528ecc Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Mon, 21 Jul 2025 22:36:40 -0400 Subject: [PATCH 1/2] fix(build): Show logs when build fails in prebuild phases Fixes #95. When builds fail during prebuild phases (INSTALL, PRE_BUILD, etc.), users now see the relevant error logs instead of just a spinner. This helps users understand failures like malformed app.json or apppack.toml files. The implementation checks CodeBuild status (FAILED, STOPPED, TIMED_OUT) during prebuild phases and streams logs when failures are detected. This preserves existing UX for successful builds while providing error visibility for failures. --- cmd/build.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/build.go b/cmd/build.go index 5ebca7d..619deda 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -409,6 +409,14 @@ func watchBuildPhase(a *app.App, buildStatus *app.BuildStatus) error { go StreamEvents(a.Session, buildStatus.Build.Logs, aws.String("build"), stopTailing) } } else if *build.CurrentPhase == "SUBMITTED" || *build.CurrentPhase == "QUEUED" || *build.CurrentPhase == "PROVISIONING" || *build.CurrentPhase == "DOWNLOAD_SOURCE" || *build.CurrentPhase == "INSTALL" || *build.CurrentPhase == "PRE_BUILD" { + if build.BuildStatus != nil && (*build.BuildStatus == "FAILED" || *build.BuildStatus == "STOPPED" || *build.BuildStatus == "TIMED_OUT") { + ui.Spinner.Stop() + fmt.Printf("Build failed during %s phase, showing logs:\n", strings.ToLower(strings.ReplaceAll(*build.CurrentPhase, "_", " "))) + if strings.HasPrefix(buildStatus.Build.Logs, "s3://") { + return S3Log(a.Session, buildStatus.Build.Logs) + } + return StreamEvents(a.Session, buildStatus.Build.Logs, nil, nil) + } ui.StartSpinner() caser := cases.Title(language.English) ui.Spinner.Suffix = fmt.Sprintf(" CodeBuild phase: %s", caser.String(strings.ToLower(strings.ReplaceAll(*build.CurrentPhase, "_", " ")))) From e29aa213b1f47f73f189f521e68a71a0848534a5 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Tue, 22 Jul 2025 10:43:44 -0400 Subject: [PATCH 2/2] refactor(build): Reduce cyclomatic complexity of watchBuildPhase Extract helper functions to reduce cyclomatic complexity from 21 to improve maintainability: - isPreBuildPhase(): Check if phase is a prebuild phase - isBuildFailed(): Check if CodeBuild status indicates failure - handlePreBuildPhaseFailure(): Handle prebuild phase failures and show logs - startBuildLogTailing(): Start log tailing for BUILD phase - updatePreBuildSpinner(): Update spinner for prebuild phases Replaced complex if-else chain with cleaner switch statement for better readability. --- cmd/build.go | 73 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/cmd/build.go b/cmd/build.go index 619deda..efacd10 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -378,6 +378,44 @@ func StreamEvents(sess *session.Session, logURL string, marker *string, stopTail } } +func isPreBuildPhase(phase string) bool { + return phase == "SUBMITTED" || phase == "QUEUED" || phase == "PROVISIONING" || + phase == "DOWNLOAD_SOURCE" || phase == "INSTALL" || phase == "PRE_BUILD" +} + +func isBuildFailed(build *codebuild.Build) bool { + return build.BuildStatus != nil && + (*build.BuildStatus == "FAILED" || *build.BuildStatus == "STOPPED" || *build.BuildStatus == "TIMED_OUT") +} + +func handlePreBuildPhaseFailure(buildStatus *app.BuildStatus, phase string, sess *session.Session) error { + ui.Spinner.Stop() + fmt.Printf("Build failed during %s phase, showing logs:\n", + strings.ToLower(strings.ReplaceAll(phase, "_", " "))) + if strings.HasPrefix(buildStatus.Build.Logs, "s3://") { + return S3Log(sess, buildStatus.Build.Logs) + } + return StreamEvents(sess, buildStatus.Build.Logs, nil, nil) +} + +func startBuildLogTailing(buildStatus *app.BuildStatus, sess *session.Session, buildLogTailing *bool, stopTailing chan bool) error { + if strings.HasPrefix(buildStatus.Build.Logs, "s3://") { + return S3Log(sess, buildStatus.Build.Logs) + } + if !*buildLogTailing { + *buildLogTailing = true + go StreamEvents(sess, buildStatus.Build.Logs, aws.String("build"), stopTailing) + } + return nil +} + +func updatePreBuildSpinner(phase string) { + ui.StartSpinner() + caser := cases.Title(language.English) + ui.Spinner.Suffix = fmt.Sprintf(" CodeBuild phase: %s", + caser.String(strings.ToLower(strings.ReplaceAll(phase, "_", " ")))) +} + func watchBuildPhase(a *app.App, buildStatus *app.BuildStatus) error { ui.StartSpinner() buildStatus, err := a.GetBuildStatus(buildStatus.BuildNumber) @@ -387,6 +425,7 @@ func watchBuildPhase(a *app.App, buildStatus *app.BuildStatus) error { if strings.HasPrefix(buildStatus.Build.Logs, "s3://") { return S3Log(a.Session, buildStatus.Build.Logs) } + codebuildSvc := codebuild.New(a.Session) buildLogTailing := false stopTailing := make(chan bool) @@ -399,34 +438,28 @@ func watchBuildPhase(a *app.App, buildStatus *app.BuildStatus) error { if err != nil { return err } + build := builds.Builds[0] - if *build.CurrentPhase == "BUILD" { - if strings.HasPrefix(buildStatus.Build.Logs, "s3://") { - return S3Log(a.Session, buildStatus.Build.Logs) + currentPhase := *build.CurrentPhase + + switch { + case currentPhase == "BUILD": + if err := startBuildLogTailing(buildStatus, a.Session, &buildLogTailing, stopTailing); err != nil { + return err } - if !buildLogTailing { - buildLogTailing = true - go StreamEvents(a.Session, buildStatus.Build.Logs, aws.String("build"), stopTailing) + case isPreBuildPhase(currentPhase): + if isBuildFailed(build) { + return handlePreBuildPhaseFailure(buildStatus, currentPhase, a.Session) } - } else if *build.CurrentPhase == "SUBMITTED" || *build.CurrentPhase == "QUEUED" || *build.CurrentPhase == "PROVISIONING" || *build.CurrentPhase == "DOWNLOAD_SOURCE" || *build.CurrentPhase == "INSTALL" || *build.CurrentPhase == "PRE_BUILD" { - if build.BuildStatus != nil && (*build.BuildStatus == "FAILED" || *build.BuildStatus == "STOPPED" || *build.BuildStatus == "TIMED_OUT") { - ui.Spinner.Stop() - fmt.Printf("Build failed during %s phase, showing logs:\n", strings.ToLower(strings.ReplaceAll(*build.CurrentPhase, "_", " "))) - if strings.HasPrefix(buildStatus.Build.Logs, "s3://") { - return S3Log(a.Session, buildStatus.Build.Logs) - } - return StreamEvents(a.Session, buildStatus.Build.Logs, nil, nil) - } - ui.StartSpinner() - caser := cases.Title(language.English) - ui.Spinner.Suffix = fmt.Sprintf(" CodeBuild phase: %s", caser.String(strings.ToLower(strings.ReplaceAll(*build.CurrentPhase, "_", " ")))) - } else { - logrus.WithFields(logrus.Fields{"phase": *build.CurrentPhase}).Debug("watch build stopped") + updatePreBuildSpinner(currentPhase) + default: + logrus.WithFields(logrus.Fields{"phase": currentPhase}).Debug("watch build stopped") if buildLogTailing { stopTailing <- true } return nil } + time.Sleep(5 * time.Second) buildStatus, err = a.GetBuildStatus(buildStatus.BuildNumber) if err != nil {