diff --git a/server/api/build.go b/server/api/build.go index 84d2daeb4..082005a78 100644 --- a/server/api/build.go +++ b/server/api/build.go @@ -313,98 +313,28 @@ func PostApproval(c *gin.Context) { yamls = append(yamls, &remote.FileMeta{Data: y.Data, Name: y.Name}) } - build, err = startBuild(c, _store, build, user, repo, yamls) + build, buildItems, err := createBuildItems(c, _store, build, user, repo, yamls, nil) if err != nil { - c.String(http.StatusInternalServerError, fmt.Sprintf("startBuild: %v", err)) - } - c.JSON(200, build) -} - -func startBuild(ctx context.Context, store store.Store, build *model.Build, user *model.User, repo *model.Repo, yamls []*remote.FileMeta) (*model.Build, error) { - netrc, err := server.Config.Services.Remote.Netrc(user, repo) - if err != nil { - msg := "Failed to generate netrc file" + msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName) log.Error().Err(err).Msg(msg) - return nil, fmt.Errorf("%s: %v", msg, err) + c.String(http.StatusInternalServerError, msg) + return } - // get the previous build so that we can send status change notifications - last, err := store.GetBuildLastBefore(repo, build.Branch, build.ID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - log.Error().Err(err).Str("repo", repo.FullName).Msgf("Error getting last build before build number '%d'", build.Number) - } - - secs, err := server.Config.Services.Secrets.SecretListBuild(repo, build) + build, err = startBuild(c, _store, build, user, repo, buildItems) if err != nil { - log.Error().Err(err).Msgf("Error getting secrets for %s#%d", repo.FullName, build.Number) + msg := fmt.Sprintf("failure to start build for %s", repo.FullName) + log.Error().Err(err).Msg(msg) + c.String(http.StatusInternalServerError, msg) + return } - regs, err := server.Config.Services.Registries.RegistryList(repo) - if err != nil { - log.Error().Err(err).Msgf("Error getting registry credentials for %s#%d", repo.FullName, build.Number) - } - - envs := map[string]string{} - if server.Config.Services.Environ != nil { - globals, _ := server.Config.Services.Environ.EnvironList(repo) - for _, global := range globals { - envs[global.Name] = global.Value - } - } - - b := shared.ProcBuilder{ - Repo: repo, - Curr: build, - Last: last, - Netrc: netrc, - Secs: secs, - Regs: regs, - Envs: envs, - Link: server.Config.Server.Host, - Yamls: yamls, - } - buildItems, err := b.Build() - if err != nil { - if _, err := shared.UpdateToStatusError(store, *build, err); err != nil { - log.Error().Err(err).Msgf("Error setting error status of build for %s#%d", repo.FullName, build.Number) - } - return nil, err - } - build = shared.SetBuildStepsOnBuild(b.Curr, buildItems) - - if err := store.ProcCreate(build.Procs); err != nil { - log.Error().Err(err).Str("repo", repo.FullName).Msgf("error persisting procs for %s#%d", repo.FullName, build.Number) - } - - defer func() { - for _, item := range buildItems { - uri := fmt.Sprintf("%s/%s/build/%d", server.Config.Server.Host, repo.FullName, build.Number) - if len(buildItems) > 1 { - err = server.Config.Services.Remote.Status(ctx, user, repo, build, uri, item.Proc) - } else { - err = server.Config.Services.Remote.Status(ctx, user, repo, build, uri, nil) - } - if err != nil { - log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, build.Number) - } - } - }() - - if err := publishToTopic(ctx, build, repo, model.Enqueued); err != nil { - log.Error().Err(err).Msg("publishToTopic") - } - if err := queueBuild(build, repo, buildItems); err != nil { - log.Error().Err(err).Msg("queueBuild") - } - - return build, nil + c.JSON(200, build) } func PostDecline(c *gin.Context) { var ( - _remote = server.Config.Services.Remote - _store = store.FromContext(c) - + _store = store.FromContext(c) repo = session.Repo(c) user = session.User(c) num, _ = strconv.ParseInt(c.Params.ByName("number"), 10, 64) @@ -425,10 +355,15 @@ func PostDecline(c *gin.Context) { return } - uri := fmt.Sprintf("%s/%s/%d", server.Config.Server.Host, repo.FullName, build.Number) - err = _remote.Status(c, user, repo, build, uri, nil) - if err != nil { - log.Error().Msgf("error setting commit status for %s/%d: %v", repo.FullName, build.Number, err) + if build.Procs, err = _store.ProcList(build); err != nil { + log.Error().Err(err).Msg("can not get proc list from store") + } + if build.Procs, err = model.Tree(build.Procs); err != nil { + log.Error().Err(err).Msg("can not build tree from proc list") + } + + if err := updateBuildStatus(c, build, repo, user); err != nil { + log.Error().Err(err).Msg("updateBuildStatus") } c.JSON(200, build) @@ -497,12 +432,9 @@ func PostBuild(c *gin.Context) { _ = c.AbortWithError(404, err) return } - - netrc, err := _remote.Netrc(user, repo) - if err != nil { - log.Error().Msgf("failure to generate netrc for %s. %s", repo.FullName, err) - _ = c.AbortWithError(500, err) - return + var yamls []*remote.FileMeta + for _, y := range configs { + yamls = append(yamls, &remote.FileMeta{Data: y.Data, Name: y.Name}) } build.ID = 0 @@ -516,99 +448,63 @@ func PostBuild(c *gin.Context) { build.Deploy = c.DefaultQuery("deploy_to", build.Deploy) if event, ok := c.GetQuery("event"); ok { - if event := model.WebhookEvent(event); model.ValidateWebhookEvent(event) { - build.Event = event + build.Event = model.WebhookEvent(event) + + if !model.ValidateWebhookEvent(build.Event) { + msg := fmt.Sprintf("build event '%s' is invalid", event) + c.String(http.StatusBadRequest, msg) + return } } err = _store.CreateBuild(build) if err != nil { - c.String(500, err.Error()) + msg := fmt.Sprintf("failure to save build for %s", repo.FullName) + log.Error().Err(err).Msg(msg) + c.String(http.StatusInternalServerError, msg) return } err = persistBuildConfigs(configs, build.ID) if err != nil { - log.Error().Msgf("failure to persist build config for %s. %s", repo.FullName, err) - _ = c.AbortWithError(500, err) + msg := fmt.Sprintf("failure to persist build config for %s.", repo.FullName) + log.Error().Err(err).Msg(msg) + c.String(http.StatusInternalServerError, msg) return } // Read query string parameters into buildParams, exclude reserved params - var buildParams = map[string]string{} + var envs = map[string]string{} for key, val := range c.Request.URL.Query() { switch key { + // Skip some options of the endpoint case "fork", "event", "deploy_to": + continue default: // We only accept string literals, because build parameters will be // injected as environment variables - buildParams[key] = val[0] + // TODO: sanitize the value + envs[key] = val[0] } } - // get the previous build so that we can send - // on status change notifications - last, _ := _store.GetBuildLastBefore(repo, build.Branch, build.ID) - secs, err := server.Config.Services.Secrets.SecretListBuild(repo, build) + build, buildItems, err := createBuildItems(c, _store, build, user, repo, yamls, envs) if err != nil { - log.Debug().Msgf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) - } - regs, err := server.Config.Services.Registries.RegistryList(repo) - if err != nil { - log.Debug().Msgf("Error getting registry credentials for %s#%d. %s", repo.FullName, build.Number, err) - } - if server.Config.Services.Environ != nil { - globals, _ := server.Config.Services.Environ.EnvironList(repo) - for _, global := range globals { - buildParams[global.Name] = global.Value - } - } - - var yamls []*remote.FileMeta - for _, y := range configs { - yamls = append(yamls, &remote.FileMeta{Data: y.Data, Name: y.Name}) - } - - b := shared.ProcBuilder{ - Repo: repo, - Curr: build, - Last: last, - Netrc: netrc, - Secs: secs, - Regs: regs, - Link: server.Config.Server.Host, - Yamls: yamls, - Envs: buildParams, - } - buildItems, err := b.Build() - if err != nil { - build.Status = model.StatusError - build.Started = time.Now().Unix() - build.Finished = build.Started - build.Error = err.Error() - c.JSON(500, build) + msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName) + log.Error().Err(err).Msg(msg) + c.String(http.StatusInternalServerError, msg) return } - build = shared.SetBuildStepsOnBuild(b.Curr, buildItems) - err = _store.ProcCreate(build.Procs) + build, err = startBuild(c, _store, build, user, repo, buildItems) if err != nil { - log.Error().Msgf("cannot restart %s#%d: %s", repo.FullName, build.Number, err) - build.Status = model.StatusError - build.Started = time.Now().Unix() - build.Finished = build.Started - build.Error = err.Error() - c.JSON(500, build) + msg := fmt.Sprintf("failure to start build for %s", repo.FullName) + log.Error().Err(err).Msg(msg) + c.String(http.StatusInternalServerError, msg) return } - c.JSON(202, build) - if err := publishToTopic(c, build, repo, model.Enqueued); err != nil { - log.Error().Err(err).Msg("publishToTopic") - } - if err := queueBuild(build, repo, buildItems); err != nil { - log.Error().Err(err).Msg("queueBuild") - } + c.JSON(200, build) } func DeleteBuildLogs(c *gin.Context) { @@ -652,6 +548,101 @@ func DeleteBuildLogs(c *gin.Context) { c.String(204, "") } +func createBuildItems(ctx context.Context, store store.Store, build *model.Build, user *model.User, repo *model.Repo, yamls []*remote.FileMeta, envs map[string]string) (*model.Build, []*shared.BuildItem, error) { + netrc, err := server.Config.Services.Remote.Netrc(user, repo) + if err != nil { + log.Error().Err(err).Msg("Failed to generate netrc file") + } + + // get the previous build so that we can send status change notifications + last, err := store.GetBuildLastBefore(repo, build.Branch, build.ID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + log.Error().Err(err).Str("repo", repo.FullName).Msgf("Error getting last build before build number '%d'", build.Number) + } + + secs, err := server.Config.Services.Secrets.SecretListBuild(repo, build) + if err != nil { + log.Error().Err(err).Msgf("Error getting secrets for %s#%d", repo.FullName, build.Number) + } + + regs, err := server.Config.Services.Registries.RegistryList(repo) + if err != nil { + log.Error().Err(err).Msgf("Error getting registry credentials for %s#%d", repo.FullName, build.Number) + } + + if envs == nil { + envs = map[string]string{} + } + if server.Config.Services.Environ != nil { + globals, _ := server.Config.Services.Environ.EnvironList(repo) + for _, global := range globals { + envs[global.Name] = global.Value + } + } + + b := shared.ProcBuilder{ + Repo: repo, + Curr: build, + Last: last, + Netrc: netrc, + Secs: secs, + Regs: regs, + Envs: envs, + Link: server.Config.Server.Host, + Yamls: yamls, + } + buildItems, err := b.Build() + if err != nil { + if _, err := shared.UpdateToStatusError(store, *build, err); err != nil { + log.Error().Err(err).Msgf("Error setting error status of build for %s#%d", repo.FullName, build.Number) + } + return nil, nil, err + } + + build = shared.SetBuildStepsOnBuild(b.Curr, buildItems) + + return build, buildItems, nil +} + +func startBuild(ctx context.Context, store store.Store, build *model.Build, user *model.User, repo *model.Repo, buildItems []*shared.BuildItem) (*model.Build, error) { + if err := store.ProcCreate(build.Procs); err != nil { + log.Error().Err(err).Str("repo", repo.FullName).Msgf("error persisting procs for %s#%d", repo.FullName, build.Number) + return nil, err + } + + if err := publishToTopic(ctx, build, repo, model.Enqueued); err != nil { + log.Error().Err(err).Msg("publishToTopic") + } + + if err := queueBuild(build, repo, buildItems); err != nil { + log.Error().Err(err).Msg("queueBuild") + return nil, err + } + + if err := updateBuildStatus(ctx, build, repo, user); err != nil { + log.Error().Err(err).Msg("updateBuildStatus") + } + + return build, nil +} + +func updateBuildStatus(ctx context.Context, build *model.Build, repo *model.Repo, user *model.User) error { + for _, proc := range build.Procs { + // skip child procs + if !proc.IsParent() { + continue + } + + err := server.Config.Services.Remote.Status(ctx, user, repo, build, proc) + if err != nil { + log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, build.Number) + return err + } + } + + return nil +} + func persistBuildConfigs(configs []*model.Config, buildID int64) error { for _, conf := range configs { buildConfig := &model.BuildConfig{ diff --git a/server/api/hook.go b/server/api/hook.go index ef1439fd7..e358b6549 100644 --- a/server/api/hook.go +++ b/server/api/hook.go @@ -219,7 +219,7 @@ func PostHook(c *gin.Context) { err = _store.CreateBuild(build, build.Procs...) if err != nil { - msg := fmt.Sprintf("failure to save commit for %s", repo.FullName) + msg := fmt.Sprintf("failure to save build for %s", repo.FullName) log.Error().Err(err).Msg(msg) c.String(http.StatusInternalServerError, msg) return @@ -236,16 +236,28 @@ func PostHook(c *gin.Context) { } } + build, buildItems, err := createBuildItems(c, _store, build, repoUser, repo, remoteYamlConfigs, nil) + if err != nil { + msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName) + log.Error().Err(err).Msg(msg) + c.String(http.StatusInternalServerError, msg) + return + } + if build.Status == model.StatusBlocked { if err := publishToTopic(c, build, repo, model.Enqueued); err != nil { log.Error().Err(err).Msg("publishToTopic") } + if err := updateBuildStatus(c, build, repo, repoUser); err != nil { + log.Error().Err(err).Msg("updateBuildStatus") + } + c.JSON(http.StatusOK, build) return } - build, err = startBuild(c, _store, build, repoUser, repo, remoteYamlConfigs) + build, err = startBuild(c, _store, build, repoUser, repo, buildItems) if err != nil { msg := fmt.Sprintf("failure to start build for %s", repo.FullName) log.Error().Err(err).Msg(msg) diff --git a/server/grpc/rpc.go b/server/grpc/rpc.go index 530b42f22..7d6ac6bdd 100644 --- a/server/grpc/rpc.go +++ b/server/grpc/rpc.go @@ -345,15 +345,9 @@ func (s *RPC) Done(c context.Context, id string, state rpc.State) error { if build, err = shared.UpdateStatusToDone(s.store, *build, buildStatus(procs), proc.Stopped); err != nil { log.Error().Err(err).Msgf("error: done: cannot update build_id %d final state", build.ID) } - - if !isMultiPipeline(procs) { - s.updateRemoteStatus(c, repo, build, nil) - } } - if isMultiPipeline(procs) { - s.updateRemoteStatus(c, repo, build, proc) - } + s.updateRemoteStatus(c, repo, build, proc) if err := s.logger.Close(c, id); err != nil { log.Error().Err(err).Msgf("done: cannot close build_id %d logger", proc.ID) @@ -431,21 +425,27 @@ func buildStatus(procs []*model.Proc) model.StatusValue { func (s *RPC) updateRemoteStatus(ctx context.Context, repo *model.Repo, build *model.Build, proc *model.Proc) { user, err := s.store.GetUser(repo.UserID) - if err == nil { - if refresher, ok := s.remote.(remote.Refresher); ok { - ok, err := refresher.Refresh(ctx, user) - if err != nil { - log.Error().Err(err).Msgf("grpc: refresh oauth token of user '%s' failed", user.Login) - } else if ok { - if err := s.store.UpdateUser(user); err != nil { - log.Error().Err(err).Msg("fail to save user to store after refresh oauth token") - } + if err != nil { + log.Error().Err(err).Msgf("can not get user with id '%d'", repo.UserID) + return + } + + if refresher, ok := s.remote.(remote.Refresher); ok { + ok, err := refresher.Refresh(ctx, user) + if err != nil { + log.Error().Err(err).Msgf("grpc: refresh oauth token of user '%s' failed", user.Login) + } else if ok { + if err := s.store.UpdateUser(user); err != nil { + log.Error().Err(err).Msg("fail to save user to store after refresh oauth token") } } - uri := fmt.Sprintf("%s/%s/%d", server.Config.Server.Host, repo.FullName, build.Number) - err = s.remote.Status(ctx, user, repo, build, uri, proc) + } + + // only do status updates for parent procs + if proc != nil && proc.IsParent() { + err = s.remote.Status(ctx, user, repo, build, proc) if err != nil { - log.Error().Msgf("error setting commit status for %s/%d: %v", repo.FullName, build.Number, err) + log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, build.Number) } } } diff --git a/server/model/proc.go b/server/model/proc.go index c3c7a2a71..af81b4561 100644 --- a/server/model/proc.go +++ b/server/model/proc.go @@ -63,6 +63,11 @@ func (p *Proc) Failing() bool { return p.State == StatusError || p.State == StatusKilled || p.State == StatusFailure } +// IsParent returns true if the process is a parent process. +func (p *Proc) IsParent() bool { + return p.PPID == 0 +} + // Tree creates a process tree from a flat process list. func Tree(procs []*Proc) ([]*Proc, error) { var nodes []*Proc diff --git a/server/remote/bitbucket/bitbucket.go b/server/remote/bitbucket/bitbucket.go index 8cd1b3693..443cf7682 100644 --- a/server/remote/bitbucket/bitbucket.go +++ b/server/remote/bitbucket/bitbucket.go @@ -26,6 +26,7 @@ import ( "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote/bitbucket/internal" + "github.com/woodpecker-ci/woodpecker/server/remote/common" ) // Bitbucket cloud endpoints. @@ -221,14 +222,14 @@ func (c *config) Dir(ctx context.Context, u *model.User, r *model.Repo, b *model } // Status creates a build status for the Bitbucket commit. -func (c *config) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { +func (c *config) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error { status := internal.BuildStatus{ - State: convertStatus(b.Status), - Desc: convertDesc(b.Status), + State: convertStatus(build.Status), + Desc: common.GetBuildStatusDescription(build.Status), Key: "Woodpecker", - URL: link, + URL: common.GetBuildStatusLink(repo, build, nil), } - return c.newClient(ctx, u).CreateStatus(r.Owner, r.Name, b.Commit, &status) + return c.newClient(ctx, user).CreateStatus(repo.Owner, repo.Name, build.Commit, &status) } // Activate activates the repository by registering repository push hooks with diff --git a/server/remote/bitbucket/bitbucket_test.go b/server/remote/bitbucket/bitbucket_test.go index 64a7c8c47..5e392bfa0 100644 --- a/server/remote/bitbucket/bitbucket_test.go +++ b/server/remote/bitbucket/bitbucket_test.go @@ -254,7 +254,7 @@ func Test_bitbucket(t *testing.T) { }) g.It("Should update the status", func() { - err := c.Status(ctx, fakeUser, fakeRepo, fakeBuild, "http://127.0.0.1", nil) + err := c.Status(ctx, fakeUser, fakeRepo, fakeBuild, fakeProc) g.Assert(err).IsNil() }) @@ -352,4 +352,9 @@ var ( fakeBuild = &model.Build{ Commit: "9ecad50", } + + fakeProc = &model.Proc{ + Name: "test", + State: model.StatusSuccess, + } ) diff --git a/server/remote/bitbucket/convert.go b/server/remote/bitbucket/convert.go index 643112e1e..cc835155e 100644 --- a/server/remote/bitbucket/convert.go +++ b/server/remote/bitbucket/convert.go @@ -32,15 +32,6 @@ const ( statusFailure = "FAILED" ) -const ( - descPending = "this build is pending" - descSuccess = "the build was successful" - descFailure = "the build failed" - descBlocked = "the build requires approval" - descDeclined = "the build was rejected" - descError = "oops, something went wrong" -) - // convertStatus is a helper function used to convert a Woodpecker status to a // Bitbucket commit status. func convertStatus(status model.StatusValue) string { @@ -54,25 +45,6 @@ func convertStatus(status model.StatusValue) string { } } -// convertDesc is a helper function used to convert a Woodpecker status to a -// Bitbucket status description. -func convertDesc(status model.StatusValue) string { - switch status { - case model.StatusPending, model.StatusRunning: - return descPending - case model.StatusSuccess: - return descSuccess - case model.StatusFailure: - return descFailure - case model.StatusBlocked: - return descBlocked - case model.StatusDeclined: - return descDeclined - default: - return descError - } -} - // convertRepo is a helper function used to convert a Bitbucket repository // structure to the common Woodpecker repository structure. func convertRepo(from *internal.Repo) *model.Repo { diff --git a/server/remote/bitbucket/convert_test.go b/server/remote/bitbucket/convert_test.go index 17eff675a..179896732 100644 --- a/server/remote/bitbucket/convert_test.go +++ b/server/remote/bitbucket/convert_test.go @@ -43,24 +43,6 @@ func Test_helper(t *testing.T) { g.Assert(convertStatus(model.StatusError)).Equal(statusFailure) }) - g.It("should convert passing desc", func() { - g.Assert(convertDesc(model.StatusSuccess)).Equal(descSuccess) - }) - - g.It("should convert pending desc", func() { - g.Assert(convertDesc(model.StatusPending)).Equal(descPending) - g.Assert(convertDesc(model.StatusRunning)).Equal(descPending) - }) - - g.It("should convert failing desc", func() { - g.Assert(convertDesc(model.StatusFailure)).Equal(descFailure) - }) - - g.It("should convert error desc", func() { - g.Assert(convertDesc(model.StatusKilled)).Equal(descError) - g.Assert(convertDesc(model.StatusError)).Equal(descError) - }) - g.It("should convert repository", func() { from := &internal.Repo{ FullName: "octocat/hello-world", diff --git a/server/remote/bitbucketserver/bitbucketserver.go b/server/remote/bitbucketserver/bitbucketserver.go index a22a199b9..d909b7b02 100644 --- a/server/remote/bitbucketserver/bitbucketserver.go +++ b/server/remote/bitbucketserver/bitbucketserver.go @@ -34,6 +34,7 @@ import ( "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote/bitbucketserver/internal" + "github.com/woodpecker-ci/woodpecker/server/remote/common" ) const ( @@ -185,18 +186,18 @@ func (c *Config) Dir(ctx context.Context, u *model.User, r *model.Repo, b *model } // Status is not supported by the bitbucketserver driver. -func (c *Config) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { +func (c *Config) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error { status := internal.BuildStatus{ - State: convertStatus(b.Status), - Desc: convertDesc(b.Status), - Name: fmt.Sprintf("Woodpecker #%d - %s", b.Number, b.Branch), + State: convertStatus(build.Status), + Desc: common.GetBuildStatusDescription(build.Status), + Name: fmt.Sprintf("Woodpecker #%d - %s", build.Number, build.Branch), Key: "Woodpecker", - URL: link, + URL: common.GetBuildStatusLink(repo, build, nil), } - client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token) + client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, user.Token) - return client.CreateStatus(b.Commit, &status) + return client.CreateStatus(build.Commit, &status) } func (c *Config) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { diff --git a/server/remote/bitbucketserver/convert.go b/server/remote/bitbucketserver/convert.go index 384a1f30a..392494ea1 100644 --- a/server/remote/bitbucketserver/convert.go +++ b/server/remote/bitbucketserver/convert.go @@ -34,13 +34,6 @@ const ( statusFailure = "FAILED" ) -const ( - descPending = "this build is pending" - descSuccess = "the build was successful" - descFailure = "the build failed" - descError = "oops, something went wrong" -) - // convertStatus is a helper function used to convert a Woodpecker status to a // Bitbucket commit status. func convertStatus(status model.StatusValue) string { @@ -54,21 +47,6 @@ func convertStatus(status model.StatusValue) string { } } -// convertDesc is a helper function used to convert a Woodpecker status to a -// Bitbucket status description. -func convertDesc(status model.StatusValue) string { - switch status { - case model.StatusPending, model.StatusRunning: - return descPending - case model.StatusSuccess: - return descSuccess - case model.StatusFailure: - return descFailure - default: - return descError - } -} - // convertRepo is a helper function used to convert a Bitbucket server repository // structure to the common Woodpecker repository structure. func convertRepo(from *internal.Repo) *model.Repo { diff --git a/server/remote/coding/coding.go b/server/remote/coding/coding.go index bc5eb5121..4d80950f6 100644 --- a/server/remote/coding/coding.go +++ b/server/remote/coding/coding.go @@ -243,7 +243,7 @@ func (c *Coding) Dir(ctx context.Context, u *model.User, r *model.Repo, b *model } // Status sends the commit status to the remote system. -func (c *Coding) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { +func (c *Coding) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, proc *model.Proc) error { // EMPTY: not implemented in Coding OAuth API return nil } diff --git a/server/remote/common/status.go b/server/remote/common/status.go new file mode 100644 index 000000000..42115cdba --- /dev/null +++ b/server/remote/common/status.go @@ -0,0 +1,60 @@ +package common + +import ( + "fmt" + + "github.com/woodpecker-ci/woodpecker/server" + "github.com/woodpecker-ci/woodpecker/server/model" +) + +const base = "ci/woodpecker" + +func GetBuildStatusContext(repo *model.Repo, build *model.Build, proc *model.Proc) string { + name := base + + switch build.Event { + case model.EventPull: + name += "/pr" + default: + if len(build.Event) > 0 { + name += "/" + string(build.Event) + } + } + + if proc != nil { + name += "/" + proc.Name + } + + return name +} + +// getBuildStatusDescription is a helper function that generates a description +// message for the current build status. +func GetBuildStatusDescription(status model.StatusValue) string { + switch status { + case model.StatusPending: + return "Pipeline is pending" + case model.StatusRunning: + return "Pipeline is running" + case model.StatusSuccess: + return "Pipeline was successful" + case model.StatusFailure, model.StatusError: + return "Pipeline failed" + case model.StatusKilled: + return "Pipeline was canceled" + case model.StatusBlocked: + return "Pipeline is pending approval" + case model.StatusDeclined: + return "Pipeline was rejected" + default: + return "unknown status" + } +} + +func GetBuildStatusLink(repo *model.Repo, build *model.Build, proc *model.Proc) string { + if proc == nil { + return fmt.Sprintf("%s/%s/build/%d", server.Config.Server.Host, repo.FullName, build.Number) + } + + return fmt.Sprintf("%s/%s/build/%d/%d", server.Config.Server.Host, repo.FullName, build.Number, proc.PID) +} diff --git a/server/remote/gitea/gitea.go b/server/remote/gitea/gitea.go index ea648d3c3..89e5ff5f6 100644 --- a/server/remote/gitea/gitea.go +++ b/server/remote/gitea/gitea.go @@ -33,6 +33,7 @@ import ( "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/remote" + "github.com/woodpecker-ci/woodpecker/server/remote/common" ) const ( @@ -337,27 +338,23 @@ func (c *Gitea) Dir(ctx context.Context, u *model.User, r *model.Repo, b *model. } // Status is supported by the Gitea driver. -func (c *Gitea) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { - client, err := c.newClientToken(ctx, u.Token) +func (c *Gitea) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error { + client, err := c.newClientToken(ctx, user.Token) if err != nil { return err } - status := getStatus(b.Status) - desc := getDesc(b.Status) - _, _, err = client.CreateStatus( - r.Owner, - r.Name, - b.Commit, + repo.Owner, + repo.Name, + build.Commit, gitea.CreateStatusOption{ - State: status, - TargetURL: link, - Description: desc, - Context: c.Context, + State: getStatus(proc.State), + TargetURL: common.GetBuildStatusLink(repo, build, proc), + Description: common.GetBuildStatusDescription(proc.State), + Context: common.GetBuildStatusContext(repo, build, proc), }, ) - return err } @@ -460,16 +457,6 @@ func (c *Gitea) newClientToken(ctx context.Context, token string) (*gitea.Client return gitea.NewClient(c.URL, gitea.SetToken(token), gitea.SetHTTPClient(httpClient), gitea.SetContext(ctx)) } -const ( - DescPending = "the build is pending" - DescRunning = "the build is running" - DescSuccess = "the build was successful" - DescFailure = "the build failed" - DescCanceled = "the build canceled" - DescBlocked = "the build is pending approval" - DescDeclined = "the build was rejected" -) - // getStatus is a helper function that converts a Woodpecker // status to a Gitea status. func getStatus(status model.StatusValue) gitea.StatusState { @@ -490,26 +477,3 @@ func getStatus(status model.StatusValue) gitea.StatusState { return gitea.StatusFailure } } - -// getDesc is a helper function that generates a description -// message for the build based on the status. -func getDesc(status model.StatusValue) string { - switch status { - case model.StatusPending: - return DescPending - case model.StatusRunning: - return DescRunning - case model.StatusSuccess: - return DescSuccess - case model.StatusFailure, model.StatusError: - return DescFailure - case model.StatusKilled: - return DescCanceled - case model.StatusBlocked: - return DescBlocked - case model.StatusDeclined: - return DescDeclined - default: - return DescFailure - } -} diff --git a/server/remote/gitea/gitea_test.go b/server/remote/gitea/gitea_test.go index 9df48dc6b..63f66a9e6 100644 --- a/server/remote/gitea/gitea_test.go +++ b/server/remote/gitea/gitea_test.go @@ -151,7 +151,7 @@ func Test_gitea(t *testing.T) { }) g.It("Should return nil from send build status", func() { - err := c.Status(ctx, fakeUser, fakeRepo, fakeBuild, "http://gitea.io", nil) + err := c.Status(ctx, fakeUser, fakeRepo, fakeBuild, fakeProc) g.Assert(err).IsNil() }) @@ -196,4 +196,9 @@ var ( fakeBuild = &model.Build{ Commit: "9ecad50", } + + fakeProc = &model.Proc{ + Name: "test", + State: model.StatusSuccess, + } ) diff --git a/server/remote/gitea/parse.go b/server/remote/gitea/parse.go index f00fabda8..3869cff0f 100644 --- a/server/remote/gitea/parse.go +++ b/server/remote/gitea/parse.go @@ -64,7 +64,7 @@ func parsePushHook(payload io.Reader) (repo *model.Repo, build *model.Build, err return nil, nil, nil } - // is this even needed? + // TODO is this even needed? if push.RefType == refBranch { return nil, nil, nil } diff --git a/server/remote/github/github.go b/server/remote/github/github.go index 0feea4acd..809e5ec17 100644 --- a/server/remote/github/github.go +++ b/server/remote/github/github.go @@ -31,6 +31,7 @@ import ( "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/remote" + "github.com/woodpecker-ci/woodpecker/server/remote/common" ) const ( @@ -418,66 +419,34 @@ func matchingHooks(hooks []*github.Hook, rawurl string) *github.Hook { return nil } -// -// TODO(bradrydzewski) refactor below functions -// +var reDeploy = regexp.MustCompile(`.+/deployments/(\d+)`) // Status sends the commit status to the remote system. // An example would be the GitHub pull request status. -func (c *client) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { - client := c.newClientToken(ctx, u.Token) - switch b.Event { - case "deployment": - return deploymentStatus(ctx, client, r, b, link) - default: - return repoStatus(ctx, client, r, b, link, c.Context, proc) - } -} +func (c *client) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error { + client := c.newClientToken(ctx, user.Token) -func repoStatus(c context.Context, client *github.Client, r *model.Repo, b *model.Build, link, ctx string, proc *model.Proc) error { - switch b.Event { - case model.EventPull: - ctx += "/pr" - default: - if len(b.Event) > 0 { - ctx += "/" + string(b.Event) + if build.Event == model.EventDeploy { + matches := reDeploy.FindStringSubmatch(build.Link) + if len(matches) != 2 { + return nil } + id, _ := strconv.Atoi(matches[1]) + + _, _, err := client.Repositories.CreateDeploymentStatus(ctx, repo.Owner, repo.Name, int64(id), &github.DeploymentStatusRequest{ + State: github.String(convertStatus(build.Status)), + Description: github.String(common.GetBuildStatusDescription(build.Status)), + LogURL: github.String(common.GetBuildStatusLink(repo, build, nil)), + }) + return err } - status := github.String(convertStatus(b.Status)) - desc := github.String(convertDesc(b.Status)) - - if proc != nil { - ctx += "/" + proc.Name - status = github.String(convertStatus(proc.State)) - desc = github.String(convertDesc(proc.State)) - } - - data := github.RepoStatus{ - Context: github.String(ctx), - State: status, - Description: desc, - TargetURL: github.String(link), - } - _, _, err := client.Repositories.CreateStatus(c, r.Owner, r.Name, b.Commit, &data) - return err -} - -var reDeploy = regexp.MustCompile(`.+/deployments/(\d+)`) - -func deploymentStatus(ctx context.Context, client *github.Client, r *model.Repo, b *model.Build, link string) error { - matches := reDeploy.FindStringSubmatch(b.Link) - if len(matches) != 2 { - return nil - } - id, _ := strconv.Atoi(matches[1]) - - data := github.DeploymentStatusRequest{ - State: github.String(convertStatus(b.Status)), - Description: github.String(convertDesc(b.Status)), - LogURL: github.String(link), - } - _, _, err := client.Repositories.CreateDeploymentStatus(ctx, r.Owner, r.Name, int64(id), &data) + _, _, err := client.Repositories.CreateStatus(ctx, repo.Owner, repo.Name, build.Commit, &github.RepoStatus{ + Context: github.String(common.GetBuildStatusContext(repo, build, proc)), + State: github.String(convertStatus(proc.State)), + Description: github.String(common.GetBuildStatusDescription(proc.State)), + TargetURL: github.String(common.GetBuildStatusLink(repo, build, proc)), + }) return err } diff --git a/server/remote/gitlab/gitlab.go b/server/remote/gitlab/gitlab.go index 2d41bc87f..c1385c2d4 100644 --- a/server/remote/gitlab/gitlab.go +++ b/server/remote/gitlab/gitlab.go @@ -29,13 +29,13 @@ import ( "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/remote" + "github.com/woodpecker-ci/woodpecker/server/remote/common" "github.com/woodpecker-ci/woodpecker/shared/oauth2" ) const ( - defaultScope = "api" - perPage = 100 - statusContext = "ci/drone" + defaultScope = "api" + perPage = 100 ) // Opts defines configuration options. @@ -347,7 +347,7 @@ func (g *Gitlab) Dir(ctx context.Context, user *model.User, repo *model.Repo, bu } // Status sends the commit status back to gitlab. -func (g *Gitlab) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, link string, proc *model.Proc) error { +func (g *Gitlab) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error { client, err := newClient(g.URL, user.Token, g.SkipVerify) if err != nil { return err @@ -359,12 +359,11 @@ func (g *Gitlab) Status(ctx context.Context, user *model.User, repo *model.Repo, } _, _, err = client.Commits.SetCommitStatus(_repo.ID, build.Commit, &gitlab.SetCommitStatusOptions{ - Ref: gitlab.String(strings.ReplaceAll(build.Ref, "refs/heads/", "")), - State: getStatus(build.Status), - Description: gitlab.String(getDesc(build.Status)), - TargetURL: &link, - Name: nil, - Context: gitlab.String(statusContext), + State: getStatus(proc.State), + Description: gitlab.String(common.GetBuildStatusDescription(proc.State)), + TargetURL: gitlab.String(common.GetBuildStatusLink(repo, build, proc)), + Context: gitlab.String(common.GetBuildStatusContext(repo, build, proc)), + PipelineID: gitlab.Int(int(build.Number)), }, gitlab.WithContext(ctx)) return err diff --git a/server/remote/gitlab/status.go b/server/remote/gitlab/status.go index 490cc36ae..d3ae11e22 100644 --- a/server/remote/gitlab/status.go +++ b/server/remote/gitlab/status.go @@ -20,16 +20,6 @@ import ( "github.com/woodpecker-ci/woodpecker/server/model" ) -const ( - DescPending = "the build is pending" - DescRunning = "the buils is running" - DescSuccess = "the build was successful" - DescFailure = "the build failed" - DescCanceled = "the build canceled" - DescBlocked = "the build is pending approval" - DescDeclined = "the build was rejected" -) - // getStatus is a helper that converts a Woodpecker status to a Gitlab status. func getStatus(status model.StatusValue) gitlab.BuildStateValue { switch status { @@ -47,26 +37,3 @@ func getStatus(status model.StatusValue) gitlab.BuildStateValue { return gitlab.Failed } } - -// getDesc is a helper function that generates a description -// message for the build based on the status. -func getDesc(status model.StatusValue) string { - switch status { - case model.StatusPending: - return DescPending - case model.StatusRunning: - return DescRunning - case model.StatusSuccess: - return DescSuccess - case model.StatusFailure, model.StatusError: - return DescFailure - case model.StatusKilled: - return DescCanceled - case model.StatusBlocked: - return DescBlocked - case model.StatusDeclined: - return DescDeclined - default: - return DescFailure - } -} diff --git a/server/remote/gogs/gogs.go b/server/remote/gogs/gogs.go index 2608ed708..43fbcc10c 100644 --- a/server/remote/gogs/gogs.go +++ b/server/remote/gogs/gogs.go @@ -209,7 +209,7 @@ func (c *client) Dir(ctx context.Context, u *model.User, r *model.Repo, b *model } // Status is not supported by the Gogs driver. -func (c *client) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { +func (c *client) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, proc *model.Proc) error { return nil } diff --git a/server/remote/gogs/gogs_test.go b/server/remote/gogs/gogs_test.go index 8e95774b5..831fdc281 100644 --- a/server/remote/gogs/gogs_test.go +++ b/server/remote/gogs/gogs_test.go @@ -164,7 +164,7 @@ func Test_gogs(t *testing.T) { g.It("Should return no-op for usupporeted features", func() { _, err1 := c.Auth(ctx, "octocat", "4vyW6b49Z") - err2 := c.Status(ctx, nil, nil, nil, "", nil) + err2 := c.Status(ctx, nil, nil, nil, nil) err3 := c.Deactivate(ctx, nil, nil, "") g.Assert(err1).IsNotNil() g.Assert(err2).IsNil() diff --git a/server/remote/mocks/remote.go b/server/remote/mocks/remote.go index 061f13321..3eec64db2 100644 --- a/server/remote/mocks/remote.go +++ b/server/remote/mocks/remote.go @@ -283,12 +283,12 @@ func (_m *Remote) Repos(ctx context.Context, u *model.User) ([]*model.Repo, erro } // Status provides a mock function with given fields: ctx, u, r, b, link, proc -func (_m *Remote) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { - ret := _m.Called(ctx, u, r, b, link, proc) +func (_m *Remote) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, proc *model.Proc) error { + ret := _m.Called(ctx, u, r, b, proc) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *model.User, *model.Repo, *model.Build, string, *model.Proc) error); ok { - r0 = rf(ctx, u, r, b, link, proc) + if rf, ok := ret.Get(0).(func(context.Context, *model.User, *model.Repo, *model.Build, *model.Proc) error); ok { + r0 = rf(ctx, u, r, b, proc) } else { r0 = ret.Error(0) } diff --git a/server/remote/remote.go b/server/remote/remote.go index eda3f1ff8..68a047db1 100644 --- a/server/remote/remote.go +++ b/server/remote/remote.go @@ -57,7 +57,7 @@ type Remote interface { // Status sends the commit status to the remote system. // An example would be the GitHub pull request status. - Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error + Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, p *model.Proc) error // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system.