Improve status updates (#561)

- link to specific proc (only general build before)
- set status for all procs (before: only for the whole build on some SCMs)
- set status after restart
- set status to pending after waiting for approval
- make status of gitlab, gitea & github equal
- dedupe status update code
- dedupe `PostBuild` code

close #410, close #297, close #459, close #521
This commit is contained in:
Anbraten 2021-12-28 17:02:49 +01:00 committed by GitHub
parent c2b0c1d73e
commit 8e8f8967c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 318 additions and 407 deletions

View file

@ -313,98 +313,28 @@ func PostApproval(c *gin.Context) {
yamls = append(yamls, &remote.FileMeta{Data: y.Data, Name: y.Name}) 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 { if err != nil {
c.String(http.StatusInternalServerError, fmt.Sprintf("startBuild: %v", err)) msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName)
}
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"
log.Error().Err(err).Msg(msg) 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 build, err = startBuild(c, _store, build, user, repo, buildItems)
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 { 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) c.JSON(200, build)
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
} }
func PostDecline(c *gin.Context) { func PostDecline(c *gin.Context) {
var ( var (
_remote = server.Config.Services.Remote _store = store.FromContext(c)
_store = store.FromContext(c)
repo = session.Repo(c) repo = session.Repo(c)
user = session.User(c) user = session.User(c)
num, _ = strconv.ParseInt(c.Params.ByName("number"), 10, 64) num, _ = strconv.ParseInt(c.Params.ByName("number"), 10, 64)
@ -425,10 +355,15 @@ func PostDecline(c *gin.Context) {
return return
} }
uri := fmt.Sprintf("%s/%s/%d", server.Config.Server.Host, repo.FullName, build.Number) if build.Procs, err = _store.ProcList(build); err != nil {
err = _remote.Status(c, user, repo, build, uri, nil) log.Error().Err(err).Msg("can not get proc list from store")
if err != nil { }
log.Error().Msgf("error setting commit status for %s/%d: %v", repo.FullName, build.Number, err) 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) c.JSON(200, build)
@ -497,12 +432,9 @@ func PostBuild(c *gin.Context) {
_ = c.AbortWithError(404, err) _ = c.AbortWithError(404, err)
return return
} }
var yamls []*remote.FileMeta
netrc, err := _remote.Netrc(user, repo) for _, y := range configs {
if err != nil { yamls = append(yamls, &remote.FileMeta{Data: y.Data, Name: y.Name})
log.Error().Msgf("failure to generate netrc for %s. %s", repo.FullName, err)
_ = c.AbortWithError(500, err)
return
} }
build.ID = 0 build.ID = 0
@ -516,99 +448,63 @@ func PostBuild(c *gin.Context) {
build.Deploy = c.DefaultQuery("deploy_to", build.Deploy) build.Deploy = c.DefaultQuery("deploy_to", build.Deploy)
if event, ok := c.GetQuery("event"); ok { if event, ok := c.GetQuery("event"); ok {
if event := model.WebhookEvent(event); model.ValidateWebhookEvent(event) { build.Event = model.WebhookEvent(event)
build.Event = 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) err = _store.CreateBuild(build)
if err != nil { 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 return
} }
err = persistBuildConfigs(configs, build.ID) err = persistBuildConfigs(configs, build.ID)
if err != nil { if err != nil {
log.Error().Msgf("failure to persist build config for %s. %s", repo.FullName, err) msg := fmt.Sprintf("failure to persist build config for %s.", repo.FullName)
_ = c.AbortWithError(500, err) log.Error().Err(err).Msg(msg)
c.String(http.StatusInternalServerError, msg)
return return
} }
// Read query string parameters into buildParams, exclude reserved params // 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() { for key, val := range c.Request.URL.Query() {
switch key { switch key {
// Skip some options of the endpoint
case "fork", "event", "deploy_to": case "fork", "event", "deploy_to":
continue
default: default:
// We only accept string literals, because build parameters will be // We only accept string literals, because build parameters will be
// injected as environment variables // 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 build, buildItems, err := createBuildItems(c, _store, build, user, repo, yamls, envs)
// on status change notifications
last, _ := _store.GetBuildLastBefore(repo, build.Branch, build.ID)
secs, err := server.Config.Services.Secrets.SecretListBuild(repo, build)
if err != nil { if err != nil {
log.Debug().Msgf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName)
} log.Error().Err(err).Msg(msg)
regs, err := server.Config.Services.Registries.RegistryList(repo) c.String(http.StatusInternalServerError, msg)
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)
return return
} }
build = shared.SetBuildStepsOnBuild(b.Curr, buildItems)
err = _store.ProcCreate(build.Procs) build, err = startBuild(c, _store, build, user, repo, buildItems)
if err != nil { if err != nil {
log.Error().Msgf("cannot restart %s#%d: %s", repo.FullName, build.Number, err) msg := fmt.Sprintf("failure to start build for %s", repo.FullName)
build.Status = model.StatusError log.Error().Err(err).Msg(msg)
build.Started = time.Now().Unix() c.String(http.StatusInternalServerError, msg)
build.Finished = build.Started
build.Error = err.Error()
c.JSON(500, build)
return return
} }
c.JSON(202, build)
if err := publishToTopic(c, build, repo, model.Enqueued); err != nil { c.JSON(200, build)
log.Error().Err(err).Msg("publishToTopic")
}
if err := queueBuild(build, repo, buildItems); err != nil {
log.Error().Err(err).Msg("queueBuild")
}
} }
func DeleteBuildLogs(c *gin.Context) { func DeleteBuildLogs(c *gin.Context) {
@ -652,6 +548,101 @@ func DeleteBuildLogs(c *gin.Context) {
c.String(204, "") 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 { func persistBuildConfigs(configs []*model.Config, buildID int64) error {
for _, conf := range configs { for _, conf := range configs {
buildConfig := &model.BuildConfig{ buildConfig := &model.BuildConfig{

View file

@ -219,7 +219,7 @@ func PostHook(c *gin.Context) {
err = _store.CreateBuild(build, build.Procs...) err = _store.CreateBuild(build, build.Procs...)
if err != nil { 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) log.Error().Err(err).Msg(msg)
c.String(http.StatusInternalServerError, msg) c.String(http.StatusInternalServerError, msg)
return 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 build.Status == model.StatusBlocked {
if err := publishToTopic(c, build, repo, model.Enqueued); err != nil { if err := publishToTopic(c, build, repo, model.Enqueued); err != nil {
log.Error().Err(err).Msg("publishToTopic") 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) c.JSON(http.StatusOK, build)
return return
} }
build, err = startBuild(c, _store, build, repoUser, repo, remoteYamlConfigs) build, err = startBuild(c, _store, build, repoUser, repo, buildItems)
if err != nil { if err != nil {
msg := fmt.Sprintf("failure to start build for %s", repo.FullName) msg := fmt.Sprintf("failure to start build for %s", repo.FullName)
log.Error().Err(err).Msg(msg) log.Error().Err(err).Msg(msg)

View file

@ -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 { 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) 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 { if err := s.logger.Close(c, id); err != nil {
log.Error().Err(err).Msgf("done: cannot close build_id %d logger", proc.ID) 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) { func (s *RPC) updateRemoteStatus(ctx context.Context, repo *model.Repo, build *model.Build, proc *model.Proc) {
user, err := s.store.GetUser(repo.UserID) user, err := s.store.GetUser(repo.UserID)
if err == nil { if err != nil {
if refresher, ok := s.remote.(remote.Refresher); ok { log.Error().Err(err).Msgf("can not get user with id '%d'", repo.UserID)
ok, err := refresher.Refresh(ctx, user) return
if err != nil { }
log.Error().Err(err).Msgf("grpc: refresh oauth token of user '%s' failed", user.Login)
} else if ok { if refresher, ok := s.remote.(remote.Refresher); ok {
if err := s.store.UpdateUser(user); err != nil { ok, err := refresher.Refresh(ctx, user)
log.Error().Err(err).Msg("fail to save user to store after refresh oauth token") 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 { 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)
} }
} }
} }

View file

@ -63,6 +63,11 @@ func (p *Proc) Failing() bool {
return p.State == StatusError || p.State == StatusKilled || p.State == StatusFailure 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. // Tree creates a process tree from a flat process list.
func Tree(procs []*Proc) ([]*Proc, error) { func Tree(procs []*Proc) ([]*Proc, error) {
var nodes []*Proc var nodes []*Proc

View file

@ -26,6 +26,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/bitbucket/internal" "github.com/woodpecker-ci/woodpecker/server/remote/bitbucket/internal"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
) )
// Bitbucket cloud endpoints. // 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. // 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{ status := internal.BuildStatus{
State: convertStatus(b.Status), State: convertStatus(build.Status),
Desc: convertDesc(b.Status), Desc: common.GetBuildStatusDescription(build.Status),
Key: "Woodpecker", 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 // Activate activates the repository by registering repository push hooks with

View file

@ -254,7 +254,7 @@ func Test_bitbucket(t *testing.T) {
}) })
g.It("Should update the status", func() { 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() g.Assert(err).IsNil()
}) })
@ -352,4 +352,9 @@ var (
fakeBuild = &model.Build{ fakeBuild = &model.Build{
Commit: "9ecad50", Commit: "9ecad50",
} }
fakeProc = &model.Proc{
Name: "test",
State: model.StatusSuccess,
}
) )

View file

@ -32,15 +32,6 @@ const (
statusFailure = "FAILED" 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 // convertStatus is a helper function used to convert a Woodpecker status to a
// Bitbucket commit status. // Bitbucket commit status.
func convertStatus(status model.StatusValue) string { 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 // convertRepo is a helper function used to convert a Bitbucket repository
// structure to the common Woodpecker repository structure. // structure to the common Woodpecker repository structure.
func convertRepo(from *internal.Repo) *model.Repo { func convertRepo(from *internal.Repo) *model.Repo {

View file

@ -43,24 +43,6 @@ func Test_helper(t *testing.T) {
g.Assert(convertStatus(model.StatusError)).Equal(statusFailure) 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() { g.It("should convert repository", func() {
from := &internal.Repo{ from := &internal.Repo{
FullName: "octocat/hello-world", FullName: "octocat/hello-world",

View file

@ -34,6 +34,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/bitbucketserver/internal" "github.com/woodpecker-ci/woodpecker/server/remote/bitbucketserver/internal"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
) )
const ( 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. // 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{ status := internal.BuildStatus{
State: convertStatus(b.Status), State: convertStatus(build.Status),
Desc: convertDesc(b.Status), Desc: common.GetBuildStatusDescription(build.Status),
Name: fmt.Sprintf("Woodpecker #%d - %s", b.Number, b.Branch), Name: fmt.Sprintf("Woodpecker #%d - %s", build.Number, build.Branch),
Key: "Woodpecker", 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) { func (c *Config) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) {

View file

@ -34,13 +34,6 @@ const (
statusFailure = "FAILED" 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 // convertStatus is a helper function used to convert a Woodpecker status to a
// Bitbucket commit status. // Bitbucket commit status.
func convertStatus(status model.StatusValue) string { 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 // convertRepo is a helper function used to convert a Bitbucket server repository
// structure to the common Woodpecker repository structure. // structure to the common Woodpecker repository structure.
func convertRepo(from *internal.Repo) *model.Repo { func convertRepo(from *internal.Repo) *model.Repo {

View file

@ -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. // 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 // EMPTY: not implemented in Coding OAuth API
return nil return nil
} }

View file

@ -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)
}

View file

@ -33,6 +33,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
) )
const ( 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. // 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 { 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, u.Token) client, err := c.newClientToken(ctx, user.Token)
if err != nil { if err != nil {
return err return err
} }
status := getStatus(b.Status)
desc := getDesc(b.Status)
_, _, err = client.CreateStatus( _, _, err = client.CreateStatus(
r.Owner, repo.Owner,
r.Name, repo.Name,
b.Commit, build.Commit,
gitea.CreateStatusOption{ gitea.CreateStatusOption{
State: status, State: getStatus(proc.State),
TargetURL: link, TargetURL: common.GetBuildStatusLink(repo, build, proc),
Description: desc, Description: common.GetBuildStatusDescription(proc.State),
Context: c.Context, Context: common.GetBuildStatusContext(repo, build, proc),
}, },
) )
return err 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)) 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 // getStatus is a helper function that converts a Woodpecker
// status to a Gitea status. // status to a Gitea status.
func getStatus(status model.StatusValue) gitea.StatusState { func getStatus(status model.StatusValue) gitea.StatusState {
@ -490,26 +477,3 @@ func getStatus(status model.StatusValue) gitea.StatusState {
return gitea.StatusFailure 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
}
}

View file

@ -151,7 +151,7 @@ func Test_gitea(t *testing.T) {
}) })
g.It("Should return nil from send build status", func() { 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() g.Assert(err).IsNil()
}) })
@ -196,4 +196,9 @@ var (
fakeBuild = &model.Build{ fakeBuild = &model.Build{
Commit: "9ecad50", Commit: "9ecad50",
} }
fakeProc = &model.Proc{
Name: "test",
State: model.StatusSuccess,
}
) )

View file

@ -64,7 +64,7 @@ func parsePushHook(payload io.Reader) (repo *model.Repo, build *model.Build, err
return nil, nil, nil return nil, nil, nil
} }
// is this even needed? // TODO is this even needed?
if push.RefType == refBranch { if push.RefType == refBranch {
return nil, nil, nil return nil, nil, nil
} }

View file

@ -31,6 +31,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
) )
const ( const (
@ -418,66 +419,34 @@ func matchingHooks(hooks []*github.Hook, rawurl string) *github.Hook {
return nil return nil
} }
// var reDeploy = regexp.MustCompile(`.+/deployments/(\d+)`)
// TODO(bradrydzewski) refactor below functions
//
// Status sends the commit status to the remote system. // Status sends the commit status to the remote system.
// An example would be the GitHub pull request status. // 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 { func (c *client) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error {
client := c.newClientToken(ctx, u.Token) client := c.newClientToken(ctx, user.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 repoStatus(c context.Context, client *github.Client, r *model.Repo, b *model.Build, link, ctx string, proc *model.Proc) error { if build.Event == model.EventDeploy {
switch b.Event { matches := reDeploy.FindStringSubmatch(build.Link)
case model.EventPull: if len(matches) != 2 {
ctx += "/pr" return nil
default:
if len(b.Event) > 0 {
ctx += "/" + string(b.Event)
} }
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)) _, _, err := client.Repositories.CreateStatus(ctx, repo.Owner, repo.Name, build.Commit, &github.RepoStatus{
desc := github.String(convertDesc(b.Status)) Context: github.String(common.GetBuildStatusContext(repo, build, proc)),
State: github.String(convertStatus(proc.State)),
if proc != nil { Description: github.String(common.GetBuildStatusDescription(proc.State)),
ctx += "/" + proc.Name TargetURL: github.String(common.GetBuildStatusLink(repo, build, proc)),
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)
return err return err
} }

View file

@ -29,13 +29,13 @@ import (
"github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
"github.com/woodpecker-ci/woodpecker/shared/oauth2" "github.com/woodpecker-ci/woodpecker/shared/oauth2"
) )
const ( const (
defaultScope = "api" defaultScope = "api"
perPage = 100 perPage = 100
statusContext = "ci/drone"
) )
// Opts defines configuration options. // 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. // 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) client, err := newClient(g.URL, user.Token, g.SkipVerify)
if err != nil { if err != nil {
return err 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{ _, _, err = client.Commits.SetCommitStatus(_repo.ID, build.Commit, &gitlab.SetCommitStatusOptions{
Ref: gitlab.String(strings.ReplaceAll(build.Ref, "refs/heads/", "")), State: getStatus(proc.State),
State: getStatus(build.Status), Description: gitlab.String(common.GetBuildStatusDescription(proc.State)),
Description: gitlab.String(getDesc(build.Status)), TargetURL: gitlab.String(common.GetBuildStatusLink(repo, build, proc)),
TargetURL: &link, Context: gitlab.String(common.GetBuildStatusContext(repo, build, proc)),
Name: nil, PipelineID: gitlab.Int(int(build.Number)),
Context: gitlab.String(statusContext),
}, gitlab.WithContext(ctx)) }, gitlab.WithContext(ctx))
return err return err

View file

@ -20,16 +20,6 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model" "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. // getStatus is a helper that converts a Woodpecker status to a Gitlab status.
func getStatus(status model.StatusValue) gitlab.BuildStateValue { func getStatus(status model.StatusValue) gitlab.BuildStateValue {
switch status { switch status {
@ -47,26 +37,3 @@ func getStatus(status model.StatusValue) gitlab.BuildStateValue {
return gitlab.Failed 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
}
}

View file

@ -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. // 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 return nil
} }

View file

@ -164,7 +164,7 @@ func Test_gogs(t *testing.T) {
g.It("Should return no-op for usupporeted features", func() { g.It("Should return no-op for usupporeted features", func() {
_, err1 := c.Auth(ctx, "octocat", "4vyW6b49Z") _, 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, "") err3 := c.Deactivate(ctx, nil, nil, "")
g.Assert(err1).IsNotNil() g.Assert(err1).IsNotNil()
g.Assert(err2).IsNil() g.Assert(err2).IsNil()

View file

@ -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 // 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 { 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, link, proc) ret := _m.Called(ctx, u, r, b, proc)
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *model.User, *model.Repo, *model.Build, string, *model.Proc) error); ok { 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, link, proc) r0 = rf(ctx, u, r, b, proc)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)
} }

View file

@ -57,7 +57,7 @@ type Remote interface {
// Status sends the commit status to the remote system. // Status sends the commit status to the remote system.
// An example would be the GitHub pull request status. // 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 // Netrc returns a .netrc file that can be used to clone
// private repositories from a remote system. // private repositories from a remote system.