From c04a1b4023a303d3c0a9d933d89ebbef4c912a9c Mon Sep 17 00:00:00 2001 From: Elad Aharon Date: Sun, 14 Sep 2025 09:20:02 +0300 Subject: [PATCH] Support --fail-on-error for trigger cmd Updated `trigger` method to accept failOnError parameter, ensuring given status from waitForBuild isn't treated as error like "FAILURE". --- README.md | 1 + cmd/multitrigger/triggerBuilds.go | 3 +- cmd/multitrigger/triggerBuilds_test.go | 3 +- cmd/trigger.go | 22 ++++++-- cmd/trigger_test.go | 2 +- pkg/utils/testutils/teamcity.go | 3 +- teamcity/build_test.go | 71 ++++++++++++++++++++++++++ teamcity/teamcity.go | 3 +- version/version.txt | 2 +- 9 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 teamcity/build_test.go diff --git a/README.md b/README.md index 22a0482..425bb56 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ The trigger command is used to trigger a single TeamCity build. It allows you to | `--require-artifacts` | If downloadArtifacts is true, and no artifacts found, return an error | | `-w, --wait-for-build` | Wait for build to finish and get status | | `-t, --wait-timeout duration` | Timeout for waiting for build to finish (default 15m0s) | +| `--fail-on-error` | Fail command if build status is not SUCCESS (only valid with --wait-for-build) | #### Example diff --git a/cmd/multitrigger/triggerBuilds.go b/cmd/multitrigger/triggerBuilds.go index 70e1e11..5ff7702 100644 --- a/cmd/multitrigger/triggerBuilds.go +++ b/cmd/multitrigger/triggerBuilds.go @@ -5,9 +5,10 @@ import ( "bbox/teamcity" "errors" "fmt" - log "github.com/sirupsen/logrus" "sync" "time" + + log "github.com/sirupsen/logrus" ) // triggerBuilds triggers the builds for each set of build parameters, wait and download artifacts if needed using work group. diff --git a/cmd/multitrigger/triggerBuilds_test.go b/cmd/multitrigger/triggerBuilds_test.go index f31e116..deb7792 100644 --- a/cmd/multitrigger/triggerBuilds_test.go +++ b/cmd/multitrigger/triggerBuilds_test.go @@ -5,9 +5,10 @@ import ( "bbox/pkg/utils/testutils" "bbox/teamcity" "errors" - "github.com/stretchr/testify/assert" "testing" "time" + + "github.com/stretchr/testify/assert" ) type buildTestCase struct { diff --git a/cmd/trigger.go b/cmd/trigger.go index 2b1108c..04aef0d 100644 --- a/cmd/trigger.go +++ b/cmd/trigger.go @@ -3,6 +3,7 @@ package cmd import ( "net/url" "os" + "strings" "time" "bbox/teamcity" @@ -23,6 +24,7 @@ var ( waitForBuild bool waitForBuildTimeout = 15 * time.Minute requireArtifacts bool + failOnError bool ) var triggerCmd = &cobra.Command{ @@ -30,6 +32,12 @@ var triggerCmd = &cobra.Command{ Short: "Trigger a single TeamCity Build", Long: `Trigger a single TeamCity Build`, Run: func(cmd *cobra.Command, args []string) { + // Validate that failOnError is only used with waitForBuild + if failOnError && !waitForBuild { + log.Errorf("--fail-on-error can only be used with --wait-for-build") + os.Exit(2) + } + url, err := url.Parse(TeamcityURL) if err != nil { log.Errorf("error parsing TeamCity URL: %s", err) @@ -42,7 +50,7 @@ var triggerCmd = &cobra.Command{ log.Errorf("error initializing TeamCity Client: %s", err) os.Exit(2) } - trigger(client, buildTypeID, branchName, artifactsPath, propertiesFlag, requireArtifacts, waitForBuild, downloadArtifacts, waitForBuildTimeout) + trigger(client, buildTypeID, branchName, artifactsPath, propertiesFlag, requireArtifacts, waitForBuild, downloadArtifacts, waitForBuildTimeout, failOnError) }, } @@ -57,9 +65,10 @@ func init() { triggerCmd.PersistentFlags().StringVarP(&branchName, "branch-name", "b", branchName, "The Branch Name") triggerCmd.PersistentFlags().StringToStringVarP(&propertiesFlag, "properties", "p", nil, "The properties in key=value format") triggerCmd.PersistentFlags().BoolVar(&requireArtifacts, "require-artifacts", false, "If downloadArtifacts is true, and no artifacts found, return an error") + triggerCmd.PersistentFlags().BoolVar(&failOnError, "fail-on-error", false, "Fail command if build status is not SUCCESS (only valid with --wait-for-build)") } -func trigger(client *teamcity.Client, buildTypeID, branchName, artifactsPath string, propertiesFlag map[string]string, requireArtifacts, waitForBuild, downloadArtifacts bool, waitForBuildTimeout time.Duration) { +func trigger(client *teamcity.Client, buildTypeID, branchName, artifactsPath string, propertiesFlag map[string]string, requireArtifacts, waitForBuild, downloadArtifacts bool, waitForBuildTimeout time.Duration, failOnError bool) { log.WithFields(log.Fields{ "TeamcityURL": TeamcityURL, "branchName": branchName, @@ -99,7 +108,14 @@ func trigger(client *teamcity.Client, buildTypeID, branchName, artifactsPath str "buildState": build.State, }).Infof("Build %s Finished", triggerResponse.BuildType.Name) - if downloadArtifacts && status == "SUCCESS" { + if failOnError { + if !strings.EqualFold(status, "SUCCESS") { + log.Errorf("build %s finished with status '%s', but expected 'SUCCESS'", triggerResponse.BuildType.Name, status) + os.Exit(2) + } + } + + if downloadArtifacts && strings.EqualFold(status, "SUCCESS") { artifactsExist := client.Artifacts.BuildHasArtifact(build.ID) if requireArtifacts && !artifactsExist { diff --git a/cmd/trigger_test.go b/cmd/trigger_test.go index 87b4447..5b758d3 100644 --- a/cmd/trigger_test.go +++ b/cmd/trigger_test.go @@ -119,7 +119,7 @@ func TestTrigger(t *testing.T) { mockArtifacts.On("GetArtifactChildren", tt.triggerBuildResponse.ID).Return(tt.getArtifactChildrenResponse, tt.getArtifactChildrenError) } - trigger(client, tt.buildTypeID, tt.branchName, tt.artifactsPath, tt.properties, tt.requireArtifacts, tt.waitForBuild, tt.downloadArtifacts, tt.waitForBuildTimeout) + trigger(client, tt.buildTypeID, tt.branchName, tt.artifactsPath, tt.properties, tt.requireArtifacts, tt.waitForBuild, tt.downloadArtifacts, tt.waitForBuildTimeout, false) mockBuild.AssertExpectations(t) mockArtifacts.AssertExpectations(t) diff --git a/pkg/utils/testutils/teamcity.go b/pkg/utils/testutils/teamcity.go index bbcdcba..4b4627a 100644 --- a/pkg/utils/testutils/teamcity.go +++ b/pkg/utils/testutils/teamcity.go @@ -3,9 +3,10 @@ package testutils import ( "bbox/pkg/types" "bbox/teamcity" - "github.com/stretchr/testify/mock" "net/http" "time" + + "github.com/stretchr/testify/mock" ) type MockTeamCityClient struct { diff --git a/teamcity/build_test.go b/teamcity/build_test.go new file mode 100644 index 0000000..c537b2c --- /dev/null +++ b/teamcity/build_test.go @@ -0,0 +1,71 @@ +package teamcity + +import ( + "bbox/pkg/types" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildService_WaitForBuild(t *testing.T) { + tests := []struct { + name string + mockResponse types.BuildStatusResponse // Single response that represents finished state + expectedResult types.BuildStatusResponse + expectedError string + }{ + { + name: "Successful build", + mockResponse: types.BuildStatusResponse{ID: 123, Status: "SUCCESS", State: "finished"}, + expectedResult: types.BuildStatusResponse{ID: 123, Status: "SUCCESS", State: "finished"}, + expectedError: "", + }, + { + name: "Failed build", + mockResponse: types.BuildStatusResponse{ID: 123, Status: "FAILURE", State: "finished"}, + expectedResult: types.BuildStatusResponse{ID: 123, Status: "FAILURE", State: "finished"}, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a test server that returns finished state immediately + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + })) + defer server.Close() + + // Parse server URL + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + // Create client + client, err := NewTeamCityClient(serverURL, "testuser", "testpass") + require.NoError(t, err) + + // Call WaitForBuild + result, err := client.Build.WaitForBuild("TestBuild", 123, 30*time.Second) + + // Assert results + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.expectedResult.ID, result.ID) + assert.Equal(t, tt.expectedResult.Status, result.Status) + assert.Equal(t, tt.expectedResult.State, result.State) + }) + } +} diff --git a/teamcity/teamcity.go b/teamcity/teamcity.go index 43ba521..19120b7 100644 --- a/teamcity/teamcity.go +++ b/teamcity/teamcity.go @@ -6,12 +6,13 @@ import ( "encoding/json" "errors" "fmt" - log "github.com/sirupsen/logrus" "io" "net/http" "net/url" "strings" "time" + + log "github.com/sirupsen/logrus" ) var ( diff --git a/version/version.txt b/version/version.txt index 867bf6b..9084fa2 100644 --- a/version/version.txt +++ b/version/version.txt @@ -1 +1 @@ -1.0.0-rc1 +1.1.0