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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -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 {

View file

@ -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",

View file

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

View file

@ -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 {

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.
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
}

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/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
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

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.
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
}

View file

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

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

View file

@ -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.