Add own workflow model (#1784)

Closes #1287

---------

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
qwerty287 2023-06-27 18:01:18 +02:00 committed by GitHub
parent b1787f82dc
commit 3033abc3b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 935 additions and 480 deletions

View file

@ -74,7 +74,7 @@ func pipelinePs(c *cli.Context) error {
return err return err
} }
for _, step := range pipeline.Steps { for _, step := range pipeline.Workflows {
for _, child := range step.Children { for _, child := range step.Children {
if err := tmpl.Execute(os.Stdout, child); err != nil { if err := tmpl.Execute(os.Stdout, child); err != nil {
return err return err

View file

@ -3717,12 +3717,6 @@ const docTemplate = `{
"status": { "status": {
"$ref": "#/definitions/StatusValue" "$ref": "#/definitions/StatusValue"
}, },
"steps": {
"type": "array",
"items": {
"$ref": "#/definitions/Step"
}
},
"timestamp": { "timestamp": {
"type": "integer" "type": "integer"
}, },
@ -3737,6 +3731,12 @@ const docTemplate = `{
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
},
"workflows": {
"type": "array",
"items": {
"$ref": "#/definitions/model.Workflow"
}
} }
} }
}, },
@ -3974,24 +3974,9 @@ const docTemplate = `{
"Step": { "Step": {
"type": "object", "type": "object",
"properties": { "properties": {
"agent_id": {
"type": "integer"
},
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/Step"
}
},
"end_time": { "end_time": {
"type": "integer" "type": "integer"
}, },
"environ": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"error": { "error": {
"type": "string" "type": "string"
}, },
@ -4010,9 +3995,6 @@ const docTemplate = `{
"pipeline_id": { "pipeline_id": {
"type": "integer" "type": "integer"
}, },
"platform": {
"type": "string"
},
"ppid": { "ppid": {
"type": "integer" "type": "integer"
}, },
@ -4128,6 +4110,53 @@ const docTemplate = `{
"LogEntryMetadata", "LogEntryMetadata",
"LogEntryProgress" "LogEntryProgress"
] ]
},
"model.Workflow": {
"type": "object",
"properties": {
"agent_id": {
"type": "integer"
},
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/Step"
}
},
"end_time": {
"type": "integer"
},
"environ": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"error": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"pid": {
"type": "integer"
},
"pipeline_id": {
"type": "integer"
},
"platform": {
"type": "string"
},
"start_time": {
"type": "integer"
},
"state": {
"$ref": "#/definitions/StatusValue"
}
}
} }
} }
}` }`

View file

@ -364,9 +364,10 @@ Context prefix Woodpecker will use to publish status messages to SCM. You probab
Template for the status messages published to forges, uses [Go templates](https://pkg.go.dev/text/template) as template language. Template for the status messages published to forges, uses [Go templates](https://pkg.go.dev/text/template) as template language.
Supported variables: Supported variables:
- `context`: Woodpecker's context (see `WOODPECKER_STATUS_CONTEXT`) - `context`: Woodpecker's context (see `WOODPECKER_STATUS_CONTEXT`)
- `event`: the event which started the pipeline - `event`: the event which started the pipeline
- `pipeline`: the pipeline's name - `workflow`: the workflow's name
- `owner`: the repo's owner - `owner`: the repo's owner
- `repo`: the repo's name - `repo`: the repo's name

View file

@ -37,7 +37,7 @@ func EnvVarSubst(yaml string, environ map[string]string) (string, error) {
} }
// MetadataFromStruct return the metadata from a pipeline will run with. // MetadataFromStruct return the metadata from a pipeline will run with.
func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline, last *model.Pipeline, workflow *model.Step, link string) metadata.Metadata { func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline, last *model.Pipeline, workflow *model.Workflow, link string) metadata.Metadata {
host := link host := link
uri, err := url.Parse(link) uri, err := url.Parse(link)
if err == nil { if err == nil {

View file

@ -61,7 +61,7 @@ func TestMetadataFromStruct(t *testing.T) {
forge metadata.ServerForge forge metadata.ServerForge
repo *model.Repo repo *model.Repo
pipeline, last *model.Pipeline pipeline, last *model.Pipeline
workflow *model.Step workflow *model.Workflow
link string link string
expectedMetadata metadata.Metadata expectedMetadata metadata.Metadata
expectedEnviron map[string]string expectedEnviron map[string]string
@ -92,7 +92,7 @@ func TestMetadataFromStruct(t *testing.T) {
repo: &model.Repo{FullName: "testUser/testRepo", Link: "https://gitea.com/testUser/testRepo", Clone: "https://gitea.com/testUser/testRepo.git", Branch: "main", IsSCMPrivate: true}, repo: &model.Repo{FullName: "testUser/testRepo", Link: "https://gitea.com/testUser/testRepo", Clone: "https://gitea.com/testUser/testRepo.git", Branch: "main", IsSCMPrivate: true},
pipeline: &model.Pipeline{Number: 3}, pipeline: &model.Pipeline{Number: 3},
last: &model.Pipeline{Number: 2}, last: &model.Pipeline{Number: 2},
workflow: &model.Step{Name: "hello"}, workflow: &model.Workflow{Name: "hello"},
link: "https://example.com", link: "https://example.com",
expectedMetadata: metadata.Metadata{ expectedMetadata: metadata.Metadata{
Forge: metadata.Forge{Type: "gitea", URL: "https://gitea.com"}, Forge: metadata.Forge{Type: "gitea", URL: "https://gitea.com"},

View file

@ -20,7 +20,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/google/uuid"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -53,7 +52,7 @@ type StepBuilder struct {
} }
type Item struct { type Item struct {
Workflow *model.Step Workflow *model.Workflow
Platform string Platform string
Labels map[string]string Labels map[string]string
DependsOn []string DependsOn []string
@ -79,8 +78,7 @@ func (b *StepBuilder) Build() ([]*Item, error) {
} }
for _, axis := range axes { for _, axis := range axes {
workflow := &model.Step{ workflow := &model.Workflow{
UUID: uuid.New().String(), // TODO(#1784): Remove once workflows are a separate entity in database
PipelineID: b.Curr.ID, PipelineID: b.Curr.ID,
PID: pidSequence, PID: pidSequence,
State: model.StatusPending, State: model.StatusPending,
@ -284,7 +282,6 @@ func (b *StepBuilder) toInternalRepresentation(parsed *yaml_types.Workflow, envi
func SetPipelineStepsOnPipeline(pipeline *model.Pipeline, pipelineItems []*Item) *model.Pipeline { func SetPipelineStepsOnPipeline(pipeline *model.Pipeline, pipelineItems []*Item) *model.Pipeline {
var pidSequence int var pidSequence int
for _, item := range pipelineItems { for _, item := range pipelineItems {
pipeline.Steps = append(pipeline.Steps, item.Workflow)
if pidSequence < item.Workflow.PID { if pidSequence < item.Workflow.PID {
pidSequence = item.Workflow.PID pidSequence = item.Workflow.PID
} }
@ -309,9 +306,10 @@ func SetPipelineStepsOnPipeline(pipeline *model.Pipeline, pipelineItems []*Item)
if item.Workflow.State == model.StatusSkipped { if item.Workflow.State == model.StatusSkipped {
step.State = model.StatusSkipped step.State = model.StatusSkipped
} }
pipeline.Steps = append(pipeline.Steps, step) item.Workflow.Children = append(item.Workflow.Children, step)
} }
} }
pipeline.Workflows = append(pipeline.Workflows, item.Workflow)
} }
return pipeline return pipeline

View file

@ -545,14 +545,11 @@ steps:
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(pipeline.Steps) != 3 { if len(pipeline.Workflows) != 1 {
t.Fatal("Should generate three in total") t.Fatal("Should generate three in total")
} }
if pipeline.Steps[1].PPID != 1 { if len(pipeline.Workflows[0].Children) != 2 {
t.Fatal("Clone step should be a children of the stage") t.Fatal("Workflow should have two children")
}
if pipeline.Steps[2].PPID != 1 {
t.Fatal("Pipeline step should be a children of the stage")
} }
} }

View file

@ -152,8 +152,7 @@ func GetPipeline(c *gin.Context) {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
steps, _ := _store.StepList(pl) if pl.Workflows, err = _store.WorkflowGetTree(pl); err != nil {
if pl.Steps, err = model.Tree(steps); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
@ -172,12 +171,7 @@ func GetPipelineLast(c *gin.Context) {
return return
} }
steps, err := _store.StepList(pl) if pl.Workflows, err = _store.WorkflowGetTree(pl); err != nil {
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
if pl.Steps, err = model.Tree(steps); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
} }

View file

@ -21,9 +21,10 @@ import (
"net/http" "net/http"
"net/url" "net/url"
shared_utils "github.com/woodpecker-ci/woodpecker/shared/utils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
shared_utils "github.com/woodpecker-ci/woodpecker/shared/utils"
"github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/forge" "github.com/woodpecker-ci/woodpecker/server/forge"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucket/internal" "github.com/woodpecker-ci/woodpecker/server/forge/bitbucket/internal"
@ -227,7 +228,7 @@ func (c *config) Dir(_ context.Context, _ *model.User, _ *model.Repo, _ *model.P
} }
// Status creates a pipeline status for the Bitbucket commit. // Status creates a pipeline status for the Bitbucket commit.
func (c *config) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, _ *model.Step) error { func (c *config) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, _ *model.Workflow) error {
status := internal.PipelineStatus{ status := internal.PipelineStatus{
State: convertStatus(pipeline.Status), State: convertStatus(pipeline.Status),
Desc: common.GetPipelineStatusDescription(pipeline.Status), Desc: common.GetPipelineStatusDescription(pipeline.Status),

View file

@ -227,7 +227,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, fakePipeline, fakeStep) err := c.Status(ctx, fakeUser, fakeRepo, fakePipeline, fakeWorkflow)
g.Assert(err).IsNil() g.Assert(err).IsNil()
}) })
@ -309,7 +309,7 @@ var (
Commit: "9ecad50", Commit: "9ecad50",
} }
fakeStep = &model.Step{ fakeWorkflow = &model.Workflow{
Name: "test", Name: "test",
State: model.StatusSuccess, State: model.StatusSuccess,
} }

View file

@ -200,7 +200,7 @@ func (c *Config) Dir(_ context.Context, _ *model.User, _ *model.Repo, _ *model.P
} }
// Status is not supported by the bitbucketserver driver. // Status is not supported by the bitbucketserver driver.
func (c *Config) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, _ *model.Step) error { func (c *Config) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, _ *model.Workflow) error {
status := internal.PipelineStatus{ status := internal.PipelineStatus{
State: convertStatus(pipeline.Status), State: convertStatus(pipeline.Status),
Desc: common.GetPipelineStatusDescription(pipeline.Status), Desc: common.GetPipelineStatusDescription(pipeline.Status),

View file

@ -23,7 +23,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
) )
func GetPipelineStatusContext(repo *model.Repo, pipeline *model.Pipeline, step *model.Step) string { func GetPipelineStatusContext(repo *model.Repo, pipeline *model.Pipeline, workflow *model.Workflow) string {
event := string(pipeline.Event) event := string(pipeline.Event)
switch pipeline.Event { switch pipeline.Event {
case model.EventPull: case model.EventPull:
@ -38,7 +38,7 @@ func GetPipelineStatusContext(repo *model.Repo, pipeline *model.Pipeline, step *
err = tmpl.Execute(&ctx, map[string]interface{}{ err = tmpl.Execute(&ctx, map[string]interface{}{
"context": server.Config.Server.StatusContext, "context": server.Config.Server.StatusContext,
"event": event, "event": event,
"pipeline": step.Name, "workflow": workflow.Name,
"owner": repo.Owner, "owner": repo.Owner,
"repo": repo.Name, "repo": repo.Name,
}) })
@ -72,10 +72,10 @@ func GetPipelineStatusDescription(status model.StatusValue) string {
} }
} }
func GetPipelineStatusLink(repo *model.Repo, pipeline *model.Pipeline, step *model.Step) string { func GetPipelineStatusLink(repo *model.Repo, pipeline *model.Pipeline, workflow *model.Workflow) string {
if step == nil { if workflow == nil {
return fmt.Sprintf("%s/repos/%d/pipeline/%d", server.Config.Server.Host, repo.ID, pipeline.Number) return fmt.Sprintf("%s/repos/%d/pipeline/%d", server.Config.Server.Host, repo.ID, pipeline.Number)
} }
return fmt.Sprintf("%s/repos/%d/pipeline/%d/%d", server.Config.Server.Host, repo.ID, pipeline.Number, step.PID) return fmt.Sprintf("%s/repos/%d/pipeline/%d/%d", server.Config.Server.Host, repo.ID, pipeline.Number, workflow.PID)
} }

View file

@ -33,17 +33,17 @@ func TestGetPipelineStatusContext(t *testing.T) {
repo := &model.Repo{Owner: "user1", Name: "repo1"} repo := &model.Repo{Owner: "user1", Name: "repo1"}
pipeline := &model.Pipeline{Event: model.EventPull} pipeline := &model.Pipeline{Event: model.EventPull}
step := &model.Step{Name: "lint"} workflow := &model.Workflow{Name: "lint"}
assert.EqualValues(t, "", GetPipelineStatusContext(repo, pipeline, step)) assert.EqualValues(t, "", GetPipelineStatusContext(repo, pipeline, workflow))
server.Config.Server.StatusContext = "ci/woodpecker" server.Config.Server.StatusContext = "ci/woodpecker"
server.Config.Server.StatusContextFormat = "{{ .context }}/{{ .event }}/{{ .pipeline }}" server.Config.Server.StatusContextFormat = "{{ .context }}/{{ .event }}/{{ .workflow }}"
assert.EqualValues(t, "ci/woodpecker/pr/lint", GetPipelineStatusContext(repo, pipeline, step)) assert.EqualValues(t, "ci/woodpecker/pr/lint", GetPipelineStatusContext(repo, pipeline, workflow))
pipeline.Event = model.EventPush pipeline.Event = model.EventPush
assert.EqualValues(t, "ci/woodpecker/push/lint", GetPipelineStatusContext(repo, pipeline, step)) assert.EqualValues(t, "ci/woodpecker/push/lint", GetPipelineStatusContext(repo, pipeline, workflow))
server.Config.Server.StatusContext = "ci" server.Config.Server.StatusContext = "ci"
server.Config.Server.StatusContextFormat = "{{ .context }}:{{ .owner }}/{{ .repo }}:{{ .event }}:{{ .pipeline }}" server.Config.Server.StatusContextFormat = "{{ .context }}:{{ .owner }}/{{ .repo }}:{{ .event }}:{{ .workflow }}"
assert.EqualValues(t, "ci:user1/repo1:push:lint", GetPipelineStatusContext(repo, pipeline, step)) assert.EqualValues(t, "ci:user1/repo1:push:lint", GetPipelineStatusContext(repo, pipeline, workflow))
} }

View file

@ -61,7 +61,7 @@ type Forge interface {
// Status sends the commit status to the forge. // Status sends the commit status to the forge.
// 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.Pipeline, p *model.Step) error Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Pipeline, p *model.Workflow) 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 forge. // private repositories from a forge.

View file

@ -321,7 +321,7 @@ 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, user *model.User, repo *model.Repo, pipeline *model.Pipeline, step *model.Step) error { func (c *Gitea) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, workflow *model.Workflow) error {
client, err := c.newClientToken(ctx, user.Token) client, err := c.newClientToken(ctx, user.Token)
if err != nil { if err != nil {
return err return err
@ -332,10 +332,10 @@ func (c *Gitea) Status(ctx context.Context, user *model.User, repo *model.Repo,
repo.Name, repo.Name,
pipeline.Commit, pipeline.Commit,
gitea.CreateStatusOption{ gitea.CreateStatusOption{
State: getStatus(step.State), State: getStatus(workflow.State),
TargetURL: common.GetPipelineStatusLink(repo, pipeline, step), TargetURL: common.GetPipelineStatusLink(repo, pipeline, workflow),
Description: common.GetPipelineStatusDescription(step.State), Description: common.GetPipelineStatusDescription(workflow.State),
Context: common.GetPipelineStatusContext(repo, pipeline, step), Context: common.GetPipelineStatusContext(repo, pipeline, workflow),
}, },
) )
return err return err

View file

@ -25,12 +25,12 @@ import (
"github.com/franela/goblin" "github.com/franela/goblin"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/woodpecker-ci/woodpecker/shared/utils"
"github.com/woodpecker-ci/woodpecker/server/forge/gitea/fixtures" "github.com/woodpecker-ci/woodpecker/server/forge/gitea/fixtures"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store" "github.com/woodpecker-ci/woodpecker/server/store"
mocks_store "github.com/woodpecker-ci/woodpecker/server/store/mocks" mocks_store "github.com/woodpecker-ci/woodpecker/server/store/mocks"
"github.com/woodpecker-ci/woodpecker/shared/utils"
) )
func Test_gitea(t *testing.T) { func Test_gitea(t *testing.T) {
@ -132,7 +132,7 @@ func Test_gitea(t *testing.T) {
}) })
g.It("Should return nil from send pipeline status", func() { g.It("Should return nil from send pipeline status", func() {
err := c.Status(ctx, fakeUser, fakeRepo, fakePipeline, fakeStep) err := c.Status(ctx, fakeUser, fakeRepo, fakePipeline, fakeWorkflow)
g.Assert(err).IsNil() g.Assert(err).IsNil()
}) })
@ -195,7 +195,7 @@ var (
Commit: "9ecad50", Commit: "9ecad50",
} }
fakeStep = &model.Step{ fakeWorkflow = &model.Workflow{
Name: "test", Name: "test",
State: model.StatusSuccess, State: model.StatusSuccess,
} }

View file

@ -456,7 +456,7 @@ var reDeploy = regexp.MustCompile(`.+/deployments/(\d+)`)
// Status sends the commit status to the forge. // Status sends the commit status to the forge.
// 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, user *model.User, repo *model.Repo, pipeline *model.Pipeline, step *model.Step) error { func (c *client) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, workflow *model.Workflow) error {
client := c.newClientToken(ctx, user.Token) client := c.newClientToken(ctx, user.Token)
if pipeline.Event == model.EventDeploy { if pipeline.Event == model.EventDeploy {
@ -475,10 +475,10 @@ func (c *client) Status(ctx context.Context, user *model.User, repo *model.Repo,
} }
_, _, err := client.Repositories.CreateStatus(ctx, repo.Owner, repo.Name, pipeline.Commit, &github.RepoStatus{ _, _, err := client.Repositories.CreateStatus(ctx, repo.Owner, repo.Name, pipeline.Commit, &github.RepoStatus{
Context: github.String(common.GetPipelineStatusContext(repo, pipeline, step)), Context: github.String(common.GetPipelineStatusContext(repo, pipeline, workflow)),
State: github.String(convertStatus(step.State)), State: github.String(convertStatus(workflow.State)),
Description: github.String(common.GetPipelineStatusDescription(step.State)), Description: github.String(common.GetPipelineStatusDescription(workflow.State)),
TargetURL: github.String(common.GetPipelineStatusLink(repo, pipeline, step)), TargetURL: github.String(common.GetPipelineStatusLink(repo, pipeline, workflow)),
}) })
return err return err
} }

View file

@ -395,7 +395,7 @@ func (g *GitLab) Dir(ctx context.Context, user *model.User, repo *model.Repo, pi
} }
// 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, pipeline *model.Pipeline, step *model.Step) error { func (g *GitLab) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, workflow *model.Workflow) 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
@ -407,10 +407,10 @@ func (g *GitLab) Status(ctx context.Context, user *model.User, repo *model.Repo,
} }
_, _, err = client.Commits.SetCommitStatus(_repo.ID, pipeline.Commit, &gitlab.SetCommitStatusOptions{ _, _, err = client.Commits.SetCommitStatus(_repo.ID, pipeline.Commit, &gitlab.SetCommitStatusOptions{
State: getStatus(step.State), State: getStatus(workflow.State),
Description: gitlab.String(common.GetPipelineStatusDescription(step.State)), Description: gitlab.String(common.GetPipelineStatusDescription(workflow.State)),
TargetURL: gitlab.String(common.GetPipelineStatusLink(repo, pipeline, step)), TargetURL: gitlab.String(common.GetPipelineStatusLink(repo, pipeline, workflow)),
Context: gitlab.String(common.GetPipelineStatusContext(repo, pipeline, step)), Context: gitlab.String(common.GetPipelineStatusContext(repo, pipeline, workflow)),
}, gitlab.WithContext(ctx)) }, gitlab.WithContext(ctx))
return err return err

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.28.2. DO NOT EDIT. // Code generated by mockery v2.29.0. DO NOT EDIT.
package mocks package mocks
@ -379,11 +379,11 @@ func (_m *Forge) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error
} }
// Status provides a mock function with given fields: ctx, u, r, b, p // Status provides a mock function with given fields: ctx, u, r, b, p
func (_m *Forge) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Pipeline, p *model.Step) error { func (_m *Forge) Status(ctx context.Context, u *model.User, r *model.Repo, b *model.Pipeline, p *model.Workflow) error {
ret := _m.Called(ctx, u, r, b, p) ret := _m.Called(ctx, u, r, b, p)
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *model.User, *model.Repo, *model.Pipeline, *model.Step) error); ok { if rf, ok := ret.Get(0).(func(context.Context, *model.User, *model.Repo, *model.Pipeline, *model.Workflow) error); ok {
r0 = rf(ctx, u, r, b, p) r0 = rf(ctx, u, r, b, p)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)

View file

@ -105,24 +105,24 @@ func (s *RPC) Extend(c context.Context, id string) error {
// Update implements the rpc.Update function // Update implements the rpc.Update function
func (s *RPC) Update(c context.Context, id string, state rpc.State) error { func (s *RPC) Update(c context.Context, id string, state rpc.State) error {
stepID, err := strconv.ParseInt(id, 10, 64) workflowID, err := strconv.ParseInt(id, 10, 64)
if err != nil { if err != nil {
return err return err
} }
pstep, err := s.store.StepLoad(stepID) workflow, err := s.store.WorkflowLoad(workflowID)
if err != nil { if err != nil {
log.Error().Msgf("error: rpc.update: cannot find step with id %d: %s", stepID, err) log.Error().Msgf("error: rpc.update: cannot find workflow with id %d: %s", workflowID, err)
return err return err
} }
currentPipeline, err := s.store.GetPipeline(pstep.PipelineID) currentPipeline, err := s.store.GetPipeline(workflow.PipelineID)
if err != nil { if err != nil {
log.Error().Msgf("error: cannot find pipeline with id %d: %s", pstep.PipelineID, err) log.Error().Msgf("error: cannot find pipeline with id %d: %s", workflow.PipelineID, err)
return err return err
} }
step, err := s.store.StepChild(currentPipeline, pstep.PID, state.Step) step, err := s.store.StepChild(currentPipeline, workflow.PID, state.Step)
if err != nil { if err != nil {
log.Error().Msgf("error: cannot find step with name %s: %s", state.Step, err) log.Error().Msgf("error: cannot find step with name %s: %s", state.Step, err)
return err return err
@ -134,17 +134,11 @@ func (s *RPC) Update(c context.Context, id string, state rpc.State) error {
return err return err
} }
if step, err = pipeline.UpdateStepStatus(s.store, *step, state, currentPipeline.Started); err != nil { if err := pipeline.UpdateStepStatus(s.store, step, state, currentPipeline.Started); err != nil {
log.Error().Err(err).Msg("rpc.update: cannot update step") log.Error().Err(err).Msg("rpc.update: cannot update step")
} }
s.updateForgeStatus(c, repo, currentPipeline, step) if currentPipeline.Workflows, err = s.store.WorkflowGetTree(currentPipeline); err != nil {
currentPipeline.Steps, err = s.store.StepList(currentPipeline)
if err != nil {
log.Error().Err(err).Msg("can not get step list from store")
}
if currentPipeline.Steps, err = model.Tree(currentPipeline.Steps); err != nil {
log.Error().Err(err).Msg("can not build tree from step list") log.Error().Err(err).Msg("can not build tree from step list")
return err return err
} }
@ -172,7 +166,7 @@ func (s *RPC) Init(c context.Context, id string, state rpc.State) error {
return err return err
} }
step, err := s.store.StepLoad(stepID) workflow, err := s.store.WorkflowLoad(stepID)
if err != nil { if err != nil {
log.Error().Msgf("error: cannot find step with id %d: %s", stepID, err) log.Error().Msgf("error: cannot find step with id %d: %s", stepID, err)
return err return err
@ -182,11 +176,11 @@ func (s *RPC) Init(c context.Context, id string, state rpc.State) error {
if err != nil { if err != nil {
return err return err
} }
step.AgentID = agent.ID workflow.AgentID = agent.ID
currentPipeline, err := s.store.GetPipeline(step.PipelineID) currentPipeline, err := s.store.GetPipeline(workflow.PipelineID)
if err != nil { if err != nil {
log.Error().Msgf("error: cannot find pipeline with id %d: %s", step.PipelineID, err) log.Error().Msgf("error: cannot find pipeline with id %d: %s", workflow.PipelineID, err)
return err return err
} }
@ -202,10 +196,10 @@ func (s *RPC) Init(c context.Context, id string, state rpc.State) error {
} }
} }
s.updateForgeStatus(c, repo, currentPipeline, step) s.updateForgeStatus(c, repo, currentPipeline, workflow)
defer func() { defer func() {
currentPipeline.Steps, _ = s.store.StepList(currentPipeline) currentPipeline.Workflows, _ = s.store.WorkflowGetTree(currentPipeline)
message := pubsub.Message{ message := pubsub.Message{
Labels: map[string]string{ Labels: map[string]string{
"repo": repo.FullName, "repo": repo.FullName,
@ -221,11 +215,11 @@ func (s *RPC) Init(c context.Context, id string, state rpc.State) error {
} }
}() }()
step, err = pipeline.UpdateStepToStatusStarted(s.store, *step, state) workflow, err = pipeline.UpdateWorkflowToStatusStarted(s.store, *workflow, state)
if err != nil { if err != nil {
return err return err
} }
s.updateForgeStatus(c, repo, currentPipeline, step) s.updateForgeStatus(c, repo, currentPipeline, workflow)
return nil return nil
} }
@ -236,12 +230,17 @@ func (s *RPC) Done(c context.Context, id string, state rpc.State) error {
return err return err
} }
workflow, err := s.store.StepLoad(workflowID) workflow, err := s.store.WorkflowLoad(workflowID)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("cannot find step with id %d", workflowID) log.Error().Err(err).Msgf("cannot find step with id %d", workflowID)
return err return err
} }
workflow.Children, err = s.store.StepListFromWorkflowFind(workflow)
if err != nil {
return err
}
currentPipeline, err := s.store.GetPipeline(workflow.PipelineID) currentPipeline, err := s.store.GetPipeline(workflow.PipelineID)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("cannot find pipeline with id %d", workflow.PipelineID) log.Error().Err(err).Msgf("cannot find pipeline with id %d", workflow.PipelineID)
@ -261,7 +260,7 @@ func (s *RPC) Done(c context.Context, id string, state rpc.State) error {
logger.Trace().Msgf("gRPC Done with state: %#v", state) logger.Trace().Msgf("gRPC Done with state: %#v", state)
if workflow, err = pipeline.UpdateStepStatusToDone(s.store, *workflow, state); err != nil { if workflow, err = pipeline.UpdateWorkflowStatusToDone(s.store, *workflow, state); err != nil {
logger.Error().Err(err).Msgf("pipeline.UpdateStepStatusToDone: cannot update workflow state: %s", err) logger.Error().Err(err).Msgf("pipeline.UpdateStepStatusToDone: cannot update workflow state: %s", err)
} }
@ -275,14 +274,14 @@ func (s *RPC) Done(c context.Context, id string, state rpc.State) error {
logger.Error().Err(queueErr).Msg("queue.Done: cannot ack workflow") logger.Error().Err(queueErr).Msg("queue.Done: cannot ack workflow")
} }
steps, err := s.store.StepList(currentPipeline) currentPipeline.Workflows, err = s.store.WorkflowGetTree(currentPipeline)
if err != nil { if err != nil {
return err return err
} }
s.completeChildrenIfParentCompleted(steps, workflow) s.completeChildrenIfParentCompleted(workflow)
if !model.IsThereRunningStage(steps) { if !model.IsThereRunningStage(currentPipeline.Workflows) {
if currentPipeline, err = pipeline.UpdateStatusToDone(s.store, *currentPipeline, model.PipelineStatus(steps), workflow.Stopped); err != nil { if currentPipeline, err = pipeline.UpdateStatusToDone(s.store, *currentPipeline, model.PipelineStatus(currentPipeline.Workflows), workflow.Stopped); err != nil {
logger.Error().Err(err).Msgf("pipeline.UpdateStatusToDone: cannot update workflow final state") logger.Error().Err(err).Msgf("pipeline.UpdateStatusToDone: cannot update workflow final state")
} }
} }
@ -291,14 +290,16 @@ func (s *RPC) Done(c context.Context, id string, state rpc.State) error {
// make sure writes to pubsub are non blocking (https://github.com/woodpecker-ci/woodpecker/blob/c919f32e0b6432a95e1a6d3d0ad662f591adf73f/server/logging/log.go#L9) // make sure writes to pubsub are non blocking (https://github.com/woodpecker-ci/woodpecker/blob/c919f32e0b6432a95e1a6d3d0ad662f591adf73f/server/logging/log.go#L9)
go func() { go func() {
for _, step := range steps { for _, wf := range currentPipeline.Workflows {
if err := s.logger.Close(c, step.ID); err != nil { for _, step := range wf.Children {
logger.Error().Err(err).Msgf("done: cannot close log stream for step %d", step.ID) if err := s.logger.Close(c, step.ID); err != nil {
logger.Error().Err(err).Msgf("done: cannot close log stream for step %d", step.ID)
}
} }
} }
}() }()
if err := s.notify(c, repo, currentPipeline, steps); err != nil { if err := s.notify(c, repo, currentPipeline); err != nil {
return err return err
} }
@ -306,7 +307,7 @@ func (s *RPC) Done(c context.Context, id string, state rpc.State) error {
s.pipelineCount.WithLabelValues(repo.FullName, currentPipeline.Branch, string(currentPipeline.Status), "total").Inc() s.pipelineCount.WithLabelValues(repo.FullName, currentPipeline.Branch, string(currentPipeline.Status), "total").Inc()
s.pipelineTime.WithLabelValues(repo.FullName, currentPipeline.Branch, string(currentPipeline.Status), "total").Set(float64(currentPipeline.Finished - currentPipeline.Started)) s.pipelineTime.WithLabelValues(repo.FullName, currentPipeline.Branch, string(currentPipeline.Status), "total").Set(float64(currentPipeline.Finished - currentPipeline.Started))
} }
if model.IsMultiPipeline(steps) { if currentPipeline.IsMultiPipeline() {
s.pipelineTime.WithLabelValues(repo.FullName, currentPipeline.Branch, string(workflow.State), workflow.Name).Set(float64(workflow.Stopped - workflow.Started)) s.pipelineTime.WithLabelValues(repo.FullName, currentPipeline.Branch, string(workflow.State), workflow.Name).Set(float64(workflow.Stopped - workflow.Started))
} }
@ -372,17 +373,17 @@ func (s *RPC) ReportHealth(ctx context.Context, status string) error {
return s.store.AgentUpdate(agent) return s.store.AgentUpdate(agent)
} }
func (s *RPC) completeChildrenIfParentCompleted(steps []*model.Step, completedWorkflow *model.Step) { func (s *RPC) completeChildrenIfParentCompleted(completedWorkflow *model.Workflow) {
for _, p := range steps { for _, c := range completedWorkflow.Children {
if p.Running() && p.PPID == completedWorkflow.PID { if c.Running() {
if _, err := pipeline.UpdateStepToStatusSkipped(s.store, *p, completedWorkflow.Stopped); err != nil { if _, err := pipeline.UpdateStepToStatusSkipped(s.store, *c, completedWorkflow.Stopped); err != nil {
log.Error().Msgf("error: done: cannot update step_id %d child state: %s", p.ID, err) log.Error().Msgf("error: done: cannot update step_id %d child state: %s", c.ID, err)
} }
} }
} }
} }
func (s *RPC) updateForgeStatus(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, step *model.Step) { func (s *RPC) updateForgeStatus(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, workflow *model.Workflow) {
user, err := s.store.GetUser(repo.UserID) user, err := s.store.GetUser(repo.UserID)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("can not get user with id '%d'", repo.UserID) log.Error().Err(err).Msgf("can not get user with id '%d'", repo.UserID)
@ -401,18 +402,15 @@ func (s *RPC) updateForgeStatus(ctx context.Context, repo *model.Repo, pipeline
} }
// only do status updates for parent steps // only do status updates for parent steps
if step != nil && step.IsParent() { if workflow != nil {
err = s.forge.Status(ctx, user, repo, pipeline, step) err = s.forge.Status(ctx, user, repo, pipeline, workflow)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number) log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number)
} }
} }
} }
func (s *RPC) notify(c context.Context, repo *model.Repo, pipeline *model.Pipeline, steps []*model.Step) (err error) { func (s *RPC) notify(c context.Context, repo *model.Repo, pipeline *model.Pipeline) (err error) {
if pipeline.Steps, err = model.Tree(steps); err != nil {
return err
}
message := pubsub.Message{ message := pubsub.Message{
Labels: map[string]string{ Labels: map[string]string{
"repo": repo.FullName, "repo": repo.FullName,

View file

@ -45,7 +45,7 @@ type Pipeline struct {
Link string `json:"link_url" xorm:"pipeline_link"` Link string `json:"link_url" xorm:"pipeline_link"`
Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"` Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"`
Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"` Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"`
Steps []*Step `json:"steps,omitempty" xorm:"-"` Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"`
ChangedFiles []string `json:"changed_files,omitempty" xorm:"json 'changed_files'"` ChangedFiles []string `json:"changed_files,omitempty" xorm:"json 'changed_files'"`
AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"`
PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"`
@ -56,6 +56,11 @@ func (Pipeline) TableName() string {
return "pipelines" return "pipelines"
} }
// IsMultiPipeline checks if step list contain more than one parent step
func (p Pipeline) IsMultiPipeline() bool {
return len(p.Workflows) > 1
}
type UpdatePipelineStore interface { type UpdatePipelineStore interface {
UpdatePipeline(*Pipeline) error UpdatePipeline(*Pipeline) error
} }

View file

@ -15,8 +15,6 @@
package model package model
import "fmt"
// StepStore persists process information to storage. // StepStore persists process information to storage.
type StepStore interface { type StepStore interface {
StepLoad(int64) (*Step, error) StepLoad(int64) (*Step, error)
@ -30,21 +28,17 @@ type StepStore interface {
// Step represents a process in the pipeline. // Step represents a process in the pipeline.
type Step struct { type Step struct {
ID int64 `json:"id" xorm:"pk autoincr 'step_id'"` ID int64 `json:"id" xorm:"pk autoincr 'step_id'"`
UUID string `json:"uuid" xorm:"UNIQUE INDEX 'step_uuid'"` UUID string `json:"uuid" xorm:"UNIQUE INDEX 'step_uuid'"`
PipelineID int64 `json:"pipeline_id" xorm:"UNIQUE(s) INDEX 'step_pipeline_id'"` PipelineID int64 `json:"pipeline_id" xorm:"UNIQUE(s) INDEX 'step_pipeline_id'"`
PID int `json:"pid" xorm:"UNIQUE(s) 'step_pid'"` PID int `json:"pid" xorm:"UNIQUE(s) 'step_pid'"`
PPID int `json:"ppid" xorm:"step_ppid"` PPID int `json:"ppid" xorm:"step_ppid"`
Name string `json:"name" xorm:"step_name"` Name string `json:"name" xorm:"step_name"`
State StatusValue `json:"state" xorm:"step_state"` State StatusValue `json:"state" xorm:"step_state"`
Error string `json:"error,omitempty" xorm:"VARCHAR(500) step_error"` Error string `json:"error,omitempty" xorm:"VARCHAR(500) step_error"`
ExitCode int `json:"exit_code" xorm:"step_exit_code"` ExitCode int `json:"exit_code" xorm:"step_exit_code"`
Started int64 `json:"start_time,omitempty" xorm:"step_started"` Started int64 `json:"start_time,omitempty" xorm:"step_started"`
Stopped int64 `json:"end_time,omitempty" xorm:"step_stopped"` Stopped int64 `json:"end_time,omitempty" xorm:"step_stopped"`
AgentID int64 `json:"agent_id,omitempty" xorm:"step_agent_id"`
Platform string `json:"platform,omitempty" xorm:"step_platform"`
Environ map[string]string `json:"environ,omitempty" xorm:"json 'step_environ'"`
Children []*Step `json:"children,omitempty" xorm:"-"`
} // @name Step } // @name Step
type UpdateStepStore interface { type UpdateStepStore interface {
@ -65,83 +59,3 @@ func (p *Step) Running() bool {
func (p *Step) Failing() bool { func (p *Step) 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 *Step) IsParent() bool {
return p.PPID == 0
}
// IsMultiPipeline checks if step list contain more than one parent step
func IsMultiPipeline(steps []*Step) bool {
c := 0
for _, step := range steps {
if step.IsParent() {
c++
}
if c > 1 {
return true
}
}
return false
}
// Tree creates a process tree from a flat process list.
func Tree(steps []*Step) ([]*Step, error) {
var nodes []*Step
// init parent nodes
for i := range steps {
if steps[i].IsParent() {
nodes = append(nodes, steps[i])
}
}
// assign children to parents
for i := range steps {
if !steps[i].IsParent() {
parent, err := findNode(nodes, steps[i].PPID)
if err != nil {
return nil, err
}
parent.Children = append(parent.Children, steps[i])
}
}
return nodes, nil
}
// PipelineStatus determine pipeline status based on corresponding step list
func PipelineStatus(steps []*Step) StatusValue {
status := StatusSuccess
for _, p := range steps {
if p.IsParent() && p.Failing() {
status = p.State
}
}
return status
}
// IsThereRunningStage determine if it contains steps running or pending to run
// TODO: return false based on depends_on (https://github.com/woodpecker-ci/woodpecker/pull/730#discussion_r795681697)
func IsThereRunningStage(steps []*Step) bool {
for _, p := range steps {
if p.IsParent() {
if p.Running() {
return true
}
}
}
return false
}
func findNode(nodes []*Step, pid int) (*Step, error) {
for _, node := range nodes {
if node.PID == pid {
return node, nil
}
}
return nil, fmt.Errorf("Corrupt step structure")
}

View file

@ -1,69 +0,0 @@
// Copyright 2021 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTree(t *testing.T) {
steps := []*Step{{
ID: 25,
UUID: "f80df0bb-77a7-4964-9412-2e1049872d57",
PID: 2,
PipelineID: 6,
PPID: 1,
Name: "clone",
State: StatusSuccess,
Error: "0",
}, {
ID: 24,
UUID: "c19b49c5-990d-4722-ba9c-1c4fe9db1f91",
PipelineID: 6,
PID: 1,
PPID: 0,
Name: "lint",
State: StatusFailure,
Error: "1",
}, {
ID: 26,
UUID: "4380146f-c0ff-4482-8107-c90937d1faba",
PipelineID: 6,
PID: 3,
PPID: 1,
Name: "lint",
State: StatusFailure,
Error: "1",
}}
steps, err := Tree(steps)
assert.NoError(t, err)
assert.Len(t, steps, 1)
assert.Len(t, steps[0].Children, 2)
steps = []*Step{{
ID: 25,
UUID: "f80df0bb-77a7-4964-9412-2e1049872d57",
PID: 2,
PipelineID: 6,
PPID: 1,
Name: "clone",
State: StatusSuccess,
Error: "0",
}}
_, err = Tree(steps)
assert.Error(t, err)
}

75
server/model/workflow.go Normal file
View file

@ -0,0 +1,75 @@
// Copyright 2021 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
// Workflow represents a workflow in the pipeline.
type Workflow struct {
ID int64 `json:"id" xorm:"pk autoincr 'workflow_id'"`
PipelineID int64 `json:"pipeline_id" xorm:"UNIQUE(s) INDEX 'workflow_pipeline_id'"`
PID int `json:"pid" xorm:"UNIQUE(s) 'workflow_pid'"`
Name string `json:"name" xorm:"workflow_name"`
State StatusValue `json:"state" xorm:"workflow_state"`
Error string `json:"error,omitempty" xorm:"VARCHAR(500) workflow_error"`
Started int64 `json:"start_time,omitempty" xorm:"workflow_started"`
Stopped int64 `json:"end_time,omitempty" xorm:"workflow_stopped"`
AgentID int64 `json:"agent_id,omitempty" xorm:"workflow_agent_id"`
Platform string `json:"platform,omitempty" xorm:"workflow_platform"`
Environ map[string]string `json:"environ,omitempty" xorm:"json 'workflow_environ'"`
Children []*Step `json:"children,omitempty" xorm:"-"`
}
type UpdateWorkflowStore interface {
WorkflowUpdate(*Workflow) error
}
// TableName return database table name for xorm
func (Workflow) TableName() string {
return "workflows"
}
// Running returns true if the process state is pending or running.
func (p *Workflow) Running() bool {
return p.State == StatusPending || p.State == StatusRunning
}
// Failing returns true if the process state is failed, killed or error.
func (p *Workflow) Failing() bool {
return p.State == StatusError || p.State == StatusKilled || p.State == StatusFailure
}
// IsThereRunningStage determine if it contains workflows running or pending to run
// TODO: return false based on depends_on (https://github.com/woodpecker-ci/woodpecker/pull/730#discussion_r795681697)
func IsThereRunningStage(workflows []*Workflow) bool {
for _, p := range workflows {
if p.Running() {
return true
}
}
return false
}
// PipelineStatus determine pipeline status based on corresponding step list
func PipelineStatus(workflows []*Workflow) StatusValue {
status := StatusSuccess
for _, p := range workflows {
if p.Failing() {
status = p.State
}
}
return status
}

View file

@ -32,7 +32,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode
return &ErrBadRequest{Msg: "Cannot cancel a non-running or non-pending or non-blocked pipeline"} return &ErrBadRequest{Msg: "Cannot cancel a non-running or non-pending or non-blocked pipeline"}
} }
steps, err := store.StepList(pipeline) workflows, err := store.WorkflowGetTree(pipeline)
if err != nil { if err != nil {
return &ErrNotFound{Msg: err.Error()} return &ErrNotFound{Msg: err.Error()}
} }
@ -42,15 +42,12 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode
stepsToCancel []string stepsToCancel []string
stepsToEvict []string stepsToEvict []string
) )
for _, step := range steps { for _, workflow := range workflows {
if step.PPID != 0 { if workflow.State == model.StatusRunning {
continue stepsToCancel = append(stepsToCancel, fmt.Sprint(workflow.ID))
} }
if step.State == model.StatusRunning { if workflow.State == model.StatusPending {
stepsToCancel = append(stepsToCancel, fmt.Sprint(step.ID)) stepsToEvict = append(stepsToEvict, fmt.Sprint(workflow.ID))
}
if step.State == model.StatusPending {
stepsToEvict = append(stepsToEvict, fmt.Sprint(step.ID))
} }
} }
@ -70,15 +67,16 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode
// Then update the DB status for pending pipelines // Then update the DB status for pending pipelines
// Running ones will be set when the agents stop on the cancel signal // Running ones will be set when the agents stop on the cancel signal
for _, step := range steps { for _, workflow := range workflows {
if step.State == model.StatusPending { if workflow.State == model.StatusPending {
if step.PPID != 0 { if _, err = UpdateWorkflowToStatusSkipped(store, *workflow); err != nil {
log.Error().Err(err).Msgf("cannot update workflow with id %d state", workflow.ID)
}
}
for _, step := range workflow.Children {
if step.State == model.StatusPending {
if _, err = UpdateStepToStatusSkipped(store, *step, 0); err != nil { if _, err = UpdateStepToStatusSkipped(store, *step, 0); err != nil {
log.Error().Msgf("error: done: cannot update step_id %d state: %s", step.ID, err) log.Error().Err(err).Msgf("cannot update workflow with id %d state", workflow.ID)
}
} else {
if _, err = UpdateStepToStatusKilled(store, *step); err != nil {
log.Error().Msgf("error: done: cannot update step_id %d state: %s", step.ID, err)
} }
} }
} }
@ -92,11 +90,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode
updatePipelineStatus(ctx, killedPipeline, repo, user) updatePipelineStatus(ctx, killedPipeline, repo, user)
steps, err = store.StepList(killedPipeline) if killedPipeline.Workflows, err = store.WorkflowGetTree(killedPipeline); err != nil {
if err != nil {
return &ErrNotFound{Msg: err.Error()}
}
if killedPipeline.Steps, err = model.Tree(steps); err != nil {
return err return err
} }
if err := publishToTopic(ctx, killedPipeline, repo); err != nil { if err := publishToTopic(ctx, killedPipeline, repo); err != nil {

View file

@ -100,7 +100,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
pipeline.Status = model.StatusBlocked pipeline.Status = model.StatusBlocked
} }
err = _store.CreatePipeline(pipeline, pipeline.Steps...) err = _store.CreatePipeline(pipeline)
if err != nil { if err != nil {
msg := fmt.Sprintf("failure to save pipeline for %s", repo.FullName) msg := fmt.Sprintf("failure to save pipeline for %s", repo.FullName)
log.Error().Err(err).Msg(msg) log.Error().Err(err).Msg(msg)

View file

@ -35,11 +35,7 @@ func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, u
return nil, fmt.Errorf("error updating pipeline. %w", err) return nil, fmt.Errorf("error updating pipeline. %w", err)
} }
pipeline.Steps, err = store.StepList(pipeline) if pipeline.Workflows, err = store.WorkflowGetTree(pipeline); err != nil {
if err != nil {
log.Error().Err(err).Msg("can not get step list from store")
}
if pipeline.Steps, err = model.Tree(pipeline.Steps); err != nil {
log.Error().Err(err).Msg("can not build tree from step list") log.Error().Err(err).Msg("can not build tree from step list")
} }

View file

@ -24,13 +24,8 @@ import (
) )
func updatePipelineStatus(ctx context.Context, pipeline *model.Pipeline, repo *model.Repo, user *model.User) { func updatePipelineStatus(ctx context.Context, pipeline *model.Pipeline, repo *model.Repo, user *model.User) {
for _, step := range pipeline.Steps { for _, workflow := range pipeline.Workflows {
// skip child steps err := server.Config.Services.Forge.Status(ctx, user, repo, pipeline, workflow)
if !step.IsParent() {
continue
}
err := server.Config.Services.Forge.Status(ctx, user, repo, pipeline, step)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number) log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number)
return return

View file

@ -33,7 +33,7 @@ func start(ctx context.Context, store store.Store, activePipeline *model.Pipelin
log.Error().Err(err).Msg("Failed to cancel previous pipelines") log.Error().Err(err).Msg("Failed to cancel previous pipelines")
} }
if err := store.StepCreate(activePipeline.Steps); err != nil { if err := store.WorkflowsCreate(activePipeline.Workflows); err != nil {
log.Error().Err(err).Str("repo", repo.FullName).Msgf("error persisting steps for %s#%d", repo.FullName, activePipeline.Number) log.Error().Err(err).Str("repo", repo.FullName).Msgf("error persisting steps for %s#%d", repo.FullName, activePipeline.Number)
return nil, err return nil, err
} }
@ -49,10 +49,11 @@ func start(ctx context.Context, store store.Store, activePipeline *model.Pipelin
// open logs streamer for each step // open logs streamer for each step
go func() { go func() {
steps := activePipeline.Steps for _, wf := range activePipeline.Workflows {
for _, step := range steps { for _, step := range wf.Children {
if err := server.Config.Services.Logs.Open(context.Background(), step.ID); err != nil { if err := server.Config.Services.Logs.Open(context.Background(), step.ID); err != nil {
log.Error().Err(err).Msgf("could not open log stream for step %d", step.ID) log.Error().Err(err).Msgf("could not open log stream for step %d", step.ID)
}
} }
} }
}() }()

View file

@ -22,7 +22,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
) )
func UpdateStepStatus(store model.UpdateStepStore, step model.Step, state rpc.State, started int64) (*model.Step, error) { func UpdateStepStatus(store model.UpdateStepStore, step *model.Step, state rpc.State, started int64) error {
if state.Exited { if state.Exited {
step.Stopped = state.Finished step.Stopped = state.Finished
step.ExitCode = state.ExitCode step.ExitCode = state.ExitCode
@ -42,7 +42,7 @@ func UpdateStepStatus(store model.UpdateStepStore, step model.Step, state rpc.St
if step.Started == 0 && step.Stopped != 0 { if step.Started == 0 && step.Stopped != 0 {
step.Started = started step.Started = started
} }
return &step, store.StepUpdate(&step) return store.StepUpdate(step)
} }
func UpdateStepToStatusStarted(store model.UpdateStepStore, step model.Step, state rpc.State) (*model.Step, error) { func UpdateStepToStatusStarted(store model.UpdateStepStore, step model.Step, state rpc.State) (*model.Step, error) {

View file

@ -19,6 +19,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/woodpecker-ci/woodpecker/pipeline/rpc" "github.com/woodpecker-ci/woodpecker/pipeline/rpc"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
) )
@ -40,7 +42,9 @@ func TestUpdateStepStatusNotExited(t *testing.T) {
ExitCode: 137, ExitCode: 137,
Error: "not an error", Error: "not an error",
} }
step, _ := UpdateStepStatus(&mockUpdateStepStore{}, model.Step{}, state, int64(1)) step := &model.Step{}
err := UpdateStepStatus(&mockUpdateStepStore{}, step, state, int64(1))
assert.NoError(t, err)
if step.State != model.StatusRunning { if step.State != model.StatusRunning {
t.Errorf("Step status not equals '%s' != '%s'", model.StatusRunning, step.State) t.Errorf("Step status not equals '%s' != '%s'", model.StatusRunning, step.State)
@ -67,7 +71,8 @@ func TestUpdateStepStatusNotExitedButStopped(t *testing.T) {
ExitCode: 137, ExitCode: 137,
Error: "not an error", Error: "not an error",
} }
step, _ = UpdateStepStatus(&mockUpdateStepStore{}, *step, state, int64(42)) err := UpdateStepStatus(&mockUpdateStepStore{}, step, state, int64(42))
assert.NoError(t, err)
if step.State != model.StatusRunning { if step.State != model.StatusRunning {
t.Errorf("Step status not equals '%s' != '%s'", model.StatusRunning, step.State) t.Errorf("Step status not equals '%s' != '%s'", model.StatusRunning, step.State)
@ -92,7 +97,10 @@ func TestUpdateStepStatusExited(t *testing.T) {
ExitCode: 137, ExitCode: 137,
Error: "an error", Error: "an error",
} }
step, _ := UpdateStepStatus(&mockUpdateStepStore{}, model.Step{}, state, int64(42))
step := &model.Step{}
err := UpdateStepStatus(&mockUpdateStepStore{}, step, state, int64(42))
assert.NoError(t, err)
if step.State != model.StatusKilled { if step.State != model.StatusKilled {
t.Errorf("Step status not equals '%s' != '%s'", model.StatusKilled, step.State) t.Errorf("Step status not equals '%s' != '%s'", model.StatusKilled, step.State)
@ -116,7 +124,9 @@ func TestUpdateStepStatusExitedButNot137(t *testing.T) {
Finished: int64(34), Finished: int64(34),
Error: "an error", Error: "an error",
} }
step, _ := UpdateStepStatus(&mockUpdateStepStore{}, model.Step{}, state, int64(42)) step := &model.Step{}
err := UpdateStepStatus(&mockUpdateStepStore{}, step, state, int64(42))
assert.NoError(t, err)
if step.State != model.StatusFailure { if step.State != model.StatusFailure {
t.Errorf("Step status not equals '%s' != '%s'", model.StatusFailure, step.State) t.Errorf("Step status not equals '%s' != '%s'", model.StatusFailure, step.State)
@ -141,7 +151,9 @@ func TestUpdateStepStatusExitedWithCode(t *testing.T) {
ExitCode: 1, ExitCode: 1,
Error: "an error", Error: "an error",
} }
step, _ := UpdateStepStatus(&mockUpdateStepStore{}, model.Step{}, state, int64(42)) step := &model.Step{}
err := UpdateStepStatus(&mockUpdateStepStore{}, step, state, int64(42))
assert.NoError(t, err)
if step.State != model.StatusFailure { if step.State != model.StatusFailure {
t.Errorf("Step status not equals '%s' != '%s'", model.StatusFailure, step.State) t.Errorf("Step status not equals '%s' != '%s'", model.StatusFailure, step.State)

View file

@ -33,9 +33,6 @@ func publishToTopic(c context.Context, pipeline *model.Pipeline, repo *model.Rep
}, },
} }
pipelineCopy := *pipeline pipelineCopy := *pipeline
if pipelineCopy.Steps, err = model.Tree(pipelineCopy.Steps); err != nil {
return err
}
message.Data, _ = json.Marshal(model.Event{ message.Data, _ = json.Marshal(model.Event{
Repo: *repo, Repo: *repo,

View file

@ -0,0 +1,45 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pipeline
import (
"github.com/woodpecker-ci/woodpecker/pipeline/rpc"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func UpdateWorkflowToStatusStarted(store model.UpdateWorkflowStore, workflow model.Workflow, state rpc.State) (*model.Workflow, error) {
workflow.Started = state.Started
workflow.State = model.StatusRunning
return &workflow, store.WorkflowUpdate(&workflow)
}
func UpdateWorkflowToStatusSkipped(store model.UpdateWorkflowStore, workflow model.Workflow) (*model.Workflow, error) {
workflow.State = model.StatusSkipped
return &workflow, store.WorkflowUpdate(&workflow)
}
func UpdateWorkflowStatusToDone(store model.UpdateWorkflowStore, workflow model.Workflow, state rpc.State) (*model.Workflow, error) {
workflow.Stopped = state.Finished
workflow.Error = state.Error
if state.Started == 0 {
workflow.State = model.StatusSkipped
} else {
workflow.State = model.StatusSuccess
}
if workflow.Error != "" {
workflow.State = model.StatusFailure
}
return &workflow, store.WorkflowUpdate(&workflow)
}

View file

@ -117,7 +117,7 @@ func (q *fifo) Error(_ context.Context, id string, err error) error {
return q.finished([]string{id}, model.StatusFailure, err) return q.finished([]string{id}, model.StatusFailure, err)
} }
// Error signals that the item is done executing with error. // ErrorAtOnce signals that the item is done executing with error.
func (q *fifo) ErrorAtOnce(_ context.Context, id []string, err error) error { func (q *fifo) ErrorAtOnce(_ context.Context, id []string, err error) error {
return q.finished(id, model.StatusFailure, err) return q.finished(id, model.StatusFailure, err)
} }

View file

@ -0,0 +1,87 @@
// Copyright 2022 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package migration
import (
"xorm.io/xorm"
"github.com/woodpecker-ci/woodpecker/server/model"
)
type oldStep018 struct {
ID int64 `xorm:"pk autoincr 'step_id'"`
PipelineID int64 `xorm:"UNIQUE(s) INDEX 'step_pipeline_id'"`
PID int `xorm:"UNIQUE(s) 'step_pid'"`
PPID int `xorm:"step_ppid"`
Name string `xorm:"step_name"`
State model.StatusValue `xorm:"step_state"`
Error string `xorm:"VARCHAR(500) step_error"`
Started int64 `xorm:"step_started"`
Stopped int64 `xorm:"step_stopped"`
AgentID int64 `xorm:"step_agent_id"`
Platform string `xorm:"step_platform"`
Environ map[string]string `xorm:"json 'step_environ'"`
}
func (oldStep018) TableName() string {
return "steps"
}
var parentStepsToWorkflows = task{
name: "parent-steps-to-workflows",
required: true,
fn: func(sess *xorm.Session) error {
if err := sess.Sync(new(model.Workflow)); err != nil {
return err
}
// make sure the columns exist before removing them
if err := sess.Sync(new(oldStep018)); err != nil {
return err
}
var parentSteps []*oldStep018
err := sess.Where("step_ppid = ?", 0).Find(&parentSteps)
if err != nil {
return err
}
for _, p := range parentSteps {
asWorkflow := &model.Workflow{
PipelineID: p.PipelineID,
PID: p.PID,
Name: p.Name,
State: p.State,
Error: p.Error,
Started: p.Started,
Stopped: p.Stopped,
AgentID: p.AgentID,
Platform: p.Platform,
Environ: p.Environ,
}
_, err = sess.Insert(asWorkflow)
if err != nil {
return err
}
_, err = sess.Delete(&oldStep018{ID: p.ID})
if err != nil {
return err
}
}
return dropTableColumns(sess, "steps", "step_agent_id", "step_platform", "step_environ")
},
}

View file

@ -52,6 +52,7 @@ var migrationTasks = []*task{
&dropOldCols, &dropOldCols,
&initLogsEntriesTable, &initLogsEntriesTable,
&migrateLogs2LogEntries, &migrateLogs2LogEntries,
&parentStepsToWorkflows,
} }
var allBeans = []interface{}{ var allBeans = []interface{}{
@ -70,6 +71,7 @@ var allBeans = []interface{}{
new(model.ServerConfig), new(model.ServerConfig),
new(model.Cron), new(model.Cron),
new(model.Redirection), new(model.Redirection),
new(model.Workflow),
} }
type migrations struct { type migrations struct {

View file

@ -55,21 +55,27 @@ func (s storage) StepList(pipeline *model.Pipeline) ([]*model.Step, error) {
Find(&stepList) Find(&stepList)
} }
func (s storage) StepCreate(steps []*model.Step) error { func (s storage) StepListFromWorkflowFind(workflow *model.Workflow) ([]*model.Step, error) {
sess := s.engine.NewSession() return s.stepListWorkflow(s.engine.NewSession(), workflow)
defer sess.Close() }
if err := sess.Begin(); err != nil {
return err
}
func (s storage) stepListWorkflow(sess *xorm.Session, workflow *model.Workflow) ([]*model.Step, error) {
stepList := make([]*model.Step, 0)
return stepList, sess.
Where("step_pipeline_id = ?", workflow.PipelineID).
Where("step_ppid = ?", workflow.PID).
OrderBy("step_pid").
Find(&stepList)
}
func (s storage) stepCreate(sess *xorm.Session, steps []*model.Step) error {
for i := range steps { for i := range steps {
// only Insert on single object ref set auto created ID back to object // only Insert on single object ref set auto created ID back to object
if _, err := sess.Insert(steps[i]); err != nil { if _, err := sess.Insert(steps[i]); err != nil {
return err return err
} }
} }
return nil
return sess.Commit()
} }
func (s storage) StepUpdate(step *model.Step) error { func (s storage) StepUpdate(step *model.Step) error {
@ -88,6 +94,10 @@ func (s storage) StepClear(pipeline *model.Pipeline) error {
return err return err
} }
if _, err := sess.Where("workflow_pipeline_id = ?", pipeline.ID).Delete(new(model.Workflow)); err != nil {
return err
}
return sess.Commit() return sess.Commit()
} }

View file

@ -26,6 +26,8 @@ import (
func TestStepFind(t *testing.T) { func TestStepFind(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline)) store, closer := newTestStore(t, new(model.Step), new(model.Pipeline))
sess := store.engine.NewSession()
defer closer() defer closer()
steps := []*model.Step{ steps := []*model.Step{
@ -38,14 +40,12 @@ func TestStepFind(t *testing.T) {
State: model.StatusSuccess, State: model.StatusSuccess,
Error: "pc load letter", Error: "pc load letter",
ExitCode: 255, ExitCode: 255,
AgentID: 1,
Platform: "linux/amd64",
Environ: map[string]string{"GOLANG": "tip"},
}, },
} }
assert.NoError(t, store.StepCreate(steps)) assert.NoError(t, store.stepCreate(sess, steps))
assert.EqualValues(t, 1, steps[0].ID) assert.EqualValues(t, 1, steps[0].ID)
assert.Error(t, store.StepCreate(steps)) assert.Error(t, store.stepCreate(sess, steps))
assert.NoError(t, sess.Close())
step, err := store.StepFind(&model.Pipeline{ID: 1000}, 1) step, err := store.StepFind(&model.Pipeline{ID: 1000}, 1)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
@ -58,7 +58,8 @@ func TestStepChild(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline)) store, closer := newTestStore(t, new(model.Step), new(model.Pipeline))
defer closer() defer closer()
err := store.StepCreate([]*model.Step{ sess := store.engine.NewSession()
err := store.stepCreate(sess, []*model.Step{
{ {
UUID: "ea6d4008-8ace-4f8a-ad03-53f1756465d9", UUID: "ea6d4008-8ace-4f8a-ad03-53f1756465d9",
PipelineID: 1, PipelineID: 1,
@ -79,6 +80,7 @@ func TestStepChild(t *testing.T) {
t.Errorf("Unexpected error: insert steps: %s", err) t.Errorf("Unexpected error: insert steps: %s", err)
return return
} }
_ = sess.Commit()
step, err := store.StepChild(&model.Pipeline{ID: 1}, 1, "build") step, err := store.StepChild(&model.Pipeline{ID: 1}, 1, "build")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -97,7 +99,8 @@ func TestStepList(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline)) store, closer := newTestStore(t, new(model.Step), new(model.Pipeline))
defer closer() defer closer()
err := store.StepCreate([]*model.Step{ sess := store.engine.NewSession()
err := store.stepCreate(sess, []*model.Step{
{ {
UUID: "2bf387f7-2913-4907-814c-c9ada88707c0", UUID: "2bf387f7-2913-4907-814c-c9ada88707c0",
PipelineID: 2, PipelineID: 2,
@ -125,6 +128,7 @@ func TestStepList(t *testing.T) {
t.Errorf("Unexpected error: insert steps: %s", err) t.Errorf("Unexpected error: insert steps: %s", err)
return return
} }
_ = sess.Commit()
steps, err := store.StepList(&model.Pipeline{ID: 1}) steps, err := store.StepList(&model.Pipeline{ID: 1})
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -148,14 +152,13 @@ func TestStepUpdate(t *testing.T) {
State: "pending", State: "pending",
Error: "pc load letter", Error: "pc load letter",
ExitCode: 255, ExitCode: 255,
AgentID: 1,
Platform: "linux/amd64",
Environ: map[string]string{"GOLANG": "tip"},
} }
if err := store.StepCreate([]*model.Step{step}); err != nil { sess := store.engine.NewSession()
if err := store.stepCreate(sess, []*model.Step{step}); err != nil {
t.Errorf("Unexpected error: insert step: %s", err) t.Errorf("Unexpected error: insert step: %s", err)
return return
} }
_ = sess.Commit()
step.State = "running" step.State = "running"
if err := store.StepUpdate(step); err != nil { if err := store.StepUpdate(step); err != nil {
t.Errorf("Unexpected error: update step: %s", err) t.Errorf("Unexpected error: update step: %s", err)
@ -175,7 +178,8 @@ func TestStepIndexes(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline)) store, closer := newTestStore(t, new(model.Step), new(model.Pipeline))
defer closer() defer closer()
if err := store.StepCreate([]*model.Step{ sess := store.engine.NewSession()
if err := store.stepCreate(sess, []*model.Step{
{ {
UUID: "4db7e5fc-5312-4d02-9e14-b51b9e3242cc", UUID: "4db7e5fc-5312-4d02-9e14-b51b9e3242cc",
PipelineID: 1, PipelineID: 1,
@ -190,7 +194,7 @@ func TestStepIndexes(t *testing.T) {
} }
// fail due to duplicate pid // fail due to duplicate pid
if err := store.StepCreate([]*model.Step{ if err := store.stepCreate(sess, []*model.Step{
{ {
UUID: "c1f33a9e-2a02-4579-95ec-90255d785a12", UUID: "c1f33a9e-2a02-4579-95ec-90255d785a12",
PipelineID: 1, PipelineID: 1,
@ -204,7 +208,7 @@ func TestStepIndexes(t *testing.T) {
} }
// fail due to duplicate uuid // fail due to duplicate uuid
if err := store.StepCreate([]*model.Step{ if err := store.stepCreate(sess, []*model.Step{
{ {
UUID: "4db7e5fc-5312-4d02-9e14-b51b9e3242cc", UUID: "4db7e5fc-5312-4d02-9e14-b51b9e3242cc",
PipelineID: 5, PipelineID: 5,
@ -216,13 +220,15 @@ func TestStepIndexes(t *testing.T) {
}); err == nil { }); err == nil {
t.Errorf("Unexpected error: duplicate pid") t.Errorf("Unexpected error: duplicate pid")
} }
_ = sess.Close()
} }
func TestStepByUUID(t *testing.T) { func TestStepByUUID(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline)) store, closer := newTestStore(t, new(model.Step), new(model.Pipeline))
defer closer() defer closer()
assert.NoError(t, store.StepCreate([]*model.Step{ sess := store.engine.NewSession()
assert.NoError(t, store.stepCreate(sess, []*model.Step{
{ {
UUID: "4db7e5fc-5312-4d02-9e14-b51b9e3242cc", UUID: "4db7e5fc-5312-4d02-9e14-b51b9e3242cc",
PipelineID: 1, PipelineID: 1,
@ -240,11 +246,9 @@ func TestStepByUUID(t *testing.T) {
State: "pending", State: "pending",
Error: "pc load letter", Error: "pc load letter",
ExitCode: 255, ExitCode: 255,
AgentID: 1,
Platform: "linux/amd64",
Environ: map[string]string{"GOLANG": "tip"},
}, },
})) }))
_ = sess.Close()
step, err := store.StepByUUID("4db7e5fc-5312-4d02-9e14-b51b9e3242cc") step, err := store.StepByUUID("4db7e5fc-5312-4d02-9e14-b51b9e3242cc")
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -0,0 +1,71 @@
package datastore
import (
"xorm.io/xorm"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func (s storage) WorkflowGetTree(pipeline *model.Pipeline) ([]*model.Workflow, error) {
sess := s.engine.NewSession()
wfList, err := s.workflowList(sess, pipeline)
if err != nil {
return nil, err
}
for _, wf := range wfList {
wf.Children, err = s.stepListWorkflow(sess, wf)
if err != nil {
return nil, err
}
}
return wfList, sess.Commit()
}
func (s storage) WorkflowsCreate(workflows []*model.Workflow) error {
sess := s.engine.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
for i := range workflows {
// only Insert on single object ref set auto created ID back to object
if err := s.stepCreate(sess, workflows[i].Children); err != nil {
return err
}
if _, err := sess.Insert(workflows[i]); err != nil {
return err
}
}
return sess.Commit()
}
func (s storage) WorkflowList(pipeline *model.Pipeline) ([]*model.Workflow, error) {
return s.workflowList(s.engine.NewSession(), pipeline)
}
// workflowList lists workflows without child steps
func (s storage) workflowList(sess *xorm.Session, pipeline *model.Pipeline) ([]*model.Workflow, error) {
var wfList []*model.Workflow
err := sess.Where("workflow_pipeline_id = ?", pipeline.ID).
OrderBy("workflow_pid").
Find(&wfList)
if err != nil {
return nil, err
}
return wfList, nil
}
func (s storage) WorkflowLoad(id int64) (*model.Workflow, error) {
workflow := new(model.Workflow)
return workflow, wrapGet(s.engine.ID(id).Get(workflow))
}
func (s storage) WorkflowUpdate(workflow *model.Workflow) error {
_, err := s.engine.ID(workflow.ID).AllCols().Update(workflow)
return err
}

View file

@ -0,0 +1,171 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datastore
import (
"testing"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func TestWorkflowLoad(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline), new(model.Workflow))
defer closer()
wf := &model.Workflow{
PipelineID: 1,
PID: 1,
Name: "woodpecker",
Children: []*model.Step{
{
UUID: "ea6d4008-8ace-4f8a-ad03-53f1756465d9",
PipelineID: 1,
PID: 2,
PPID: 1,
State: "success",
},
{
UUID: "2bf387f7-2913-4907-814c-c9ada88707c0",
PipelineID: 1,
PID: 3,
PPID: 1,
Name: "build",
State: "success",
},
},
}
err := store.WorkflowsCreate([]*model.Workflow{wf})
if err != nil {
t.Errorf("Unexpected error: insert steps: %s", err)
return
}
workflowGet, err := store.WorkflowLoad(1)
if err != nil {
t.Errorf("Unexpected error: insert steps: %s", err)
return
}
if got, want := workflowGet.PipelineID, int64(1); got != want {
t.Errorf("Want pipeline id %d, got %d", want, got)
}
if got, want := workflowGet.PID, 1; got != want {
t.Errorf("Want workflow pid %d, got %d", want, got)
}
// children are not loaded
if got, want := len(workflowGet.Children), 0; got != want {
t.Errorf("Want children len %d, got %d", want, got)
}
}
func TestWorkflowGetTree(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline), new(model.Workflow))
defer closer()
wf := &model.Workflow{
PipelineID: 1,
PID: 1,
Name: "woodpecker",
Children: []*model.Step{
{
UUID: "ea6d4008-8ace-4f8a-ad03-53f1756465d9",
PipelineID: 1,
PID: 2,
PPID: 1,
State: "success",
},
{
UUID: "2bf387f7-2913-4907-814c-c9ada88707c0",
PipelineID: 1,
PID: 3,
PPID: 1,
Name: "build",
State: "success",
},
},
}
err := store.WorkflowsCreate([]*model.Workflow{wf})
if err != nil {
t.Errorf("Unexpected error: insert steps: %s", err)
return
}
workflowsGet, err := store.WorkflowGetTree(&model.Pipeline{ID: 1})
if err != nil {
t.Error(err)
return
}
if got, want := len(workflowsGet), 1; got != want {
t.Errorf("Want workflow len %d, got %d", want, got)
return
}
workflowGet := workflowsGet[0]
if got, want := workflowGet.Name, "woodpecker"; got != want {
t.Errorf("Want workflow name %s, got %s", want, got)
}
if got, want := len(workflowGet.Children), 2; got != want {
t.Errorf("Want children len %d, got %d", want, got)
return
}
if got, want := workflowGet.Children[0].PID, 2; got != want {
t.Errorf("Want children len %d, got %d", want, got)
}
if got, want := workflowGet.Children[1].PID, 3; got != want {
t.Errorf("Want children len %d, got %d", want, got)
}
}
func TestWorkflowUpdate(t *testing.T) {
store, closer := newTestStore(t, new(model.Step), new(model.Pipeline), new(model.Workflow))
defer closer()
wf := &model.Workflow{
PipelineID: 1,
PID: 1,
Name: "woodpecker",
State: "pending",
}
err := store.WorkflowsCreate([]*model.Workflow{wf})
if err != nil {
t.Errorf("Unexpected error: insert steps: %s", err)
return
}
workflowGet, err := store.WorkflowLoad(1)
if err != nil {
t.Errorf("Unexpected error: insert steps: %s", err)
return
}
if got, want := workflowGet.State, model.StatusValue("pending"); got != want {
t.Errorf("Want workflow state %s, got %s", want, got)
}
wf.State = "success"
err = store.WorkflowUpdate(wf)
if err != nil {
t.Errorf("Unexpected error: insert steps: %s", err)
return
}
workflowGet, err = store.WorkflowLoad(1)
if err != nil {
t.Errorf("Unexpected error: insert steps: %s", err)
return
}
if got, want := workflowGet.State, model.StatusValue("success"); got != want {
t.Errorf("Want workflow state %s, got %s", want, got)
}
}

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.28.2. DO NOT EDIT. // Code generated by mockery v2.29.0. DO NOT EDIT.
package mocks package mocks
@ -1701,20 +1701,6 @@ func (_m *Store) StepClear(_a0 *model.Pipeline) error {
return r0 return r0
} }
// StepCreate provides a mock function with given fields: _a0
func (_m *Store) StepCreate(_a0 []*model.Step) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func([]*model.Step) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// StepFind provides a mock function with given fields: _a0, _a1 // StepFind provides a mock function with given fields: _a0, _a1
func (_m *Store) StepFind(_a0 *model.Pipeline, _a1 int) (*model.Step, error) { func (_m *Store) StepFind(_a0 *model.Pipeline, _a1 int) (*model.Step, error) {
ret := _m.Called(_a0, _a1) ret := _m.Called(_a0, _a1)
@ -1767,6 +1753,32 @@ func (_m *Store) StepList(_a0 *model.Pipeline) ([]*model.Step, error) {
return r0, r1 return r0, r1
} }
// StepListFromWorkflowFind provides a mock function with given fields: _a0
func (_m *Store) StepListFromWorkflowFind(_a0 *model.Workflow) ([]*model.Step, error) {
ret := _m.Called(_a0)
var r0 []*model.Step
var r1 error
if rf, ok := ret.Get(0).(func(*model.Workflow) ([]*model.Step, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(*model.Workflow) []*model.Step); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Step)
}
}
if rf, ok := ret.Get(1).(func(*model.Workflow) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// StepLoad provides a mock function with given fields: _a0 // StepLoad provides a mock function with given fields: _a0
func (_m *Store) StepLoad(_a0 int64) (*model.Step, error) { func (_m *Store) StepLoad(_a0 int64) (*model.Step, error) {
ret := _m.Called(_a0) ret := _m.Called(_a0)
@ -1929,6 +1941,86 @@ func (_m *Store) UserFeed(_a0 *model.User) ([]*model.Feed, error) {
return r0, r1 return r0, r1
} }
// WorkflowGetTree provides a mock function with given fields: _a0
func (_m *Store) WorkflowGetTree(_a0 *model.Pipeline) ([]*model.Workflow, error) {
ret := _m.Called(_a0)
var r0 []*model.Workflow
var r1 error
if rf, ok := ret.Get(0).(func(*model.Pipeline) ([]*model.Workflow, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(*model.Pipeline) []*model.Workflow); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Workflow)
}
}
if rf, ok := ret.Get(1).(func(*model.Pipeline) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// WorkflowLoad provides a mock function with given fields: _a0
func (_m *Store) WorkflowLoad(_a0 int64) (*model.Workflow, error) {
ret := _m.Called(_a0)
var r0 *model.Workflow
var r1 error
if rf, ok := ret.Get(0).(func(int64) (*model.Workflow, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(int64) *model.Workflow); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Workflow)
}
}
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// WorkflowUpdate provides a mock function with given fields: _a0
func (_m *Store) WorkflowUpdate(_a0 *model.Workflow) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Workflow) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// WorkflowsCreate provides a mock function with given fields: _a0
func (_m *Store) WorkflowsCreate(_a0 []*model.Workflow) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func([]*model.Workflow) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
type mockConstructorTestingTNewStore interface { type mockConstructorTestingTNewStore interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())

View file

@ -139,9 +139,9 @@ type Store interface {
StepByUUID(string) (*model.Step, error) StepByUUID(string) (*model.Step, error)
StepChild(*model.Pipeline, int, string) (*model.Step, error) StepChild(*model.Pipeline, int, string) (*model.Step, error)
StepList(*model.Pipeline) ([]*model.Step, error) StepList(*model.Pipeline) ([]*model.Step, error)
StepCreate([]*model.Step) error
StepUpdate(*model.Step) error StepUpdate(*model.Step) error
StepClear(*model.Pipeline) error StepClear(*model.Pipeline) error
StepListFromWorkflowFind(*model.Workflow) ([]*model.Step, error)
// Logs // Logs
LogFind(*model.Step) ([]*model.LogEntry, error) LogFind(*model.Step) ([]*model.LogEntry, error)
@ -177,6 +177,12 @@ type Store interface {
AgentUpdate(*model.Agent) error AgentUpdate(*model.Agent) error
AgentDelete(*model.Agent) error AgentDelete(*model.Agent) error
// Workflow
WorkflowGetTree(*model.Pipeline) ([]*model.Workflow, error)
WorkflowsCreate([]*model.Workflow) error
WorkflowLoad(int64) (*model.Workflow, error)
WorkflowUpdate(*model.Workflow) error
// Store operations // Store operations
Ping() error Ping() error
Close() error Close() error

View file

@ -104,7 +104,7 @@ const apiClient = useApiClient();
const loadedStepSlug = ref<string>(); const loadedStepSlug = ref<string>();
const stepSlug = computed(() => `${repo?.value.owner} - ${repo?.value.name} - ${pipeline.value.id} - ${stepId.value}`); const stepSlug = computed(() => `${repo?.value.owner} - ${repo?.value.name} - ${pipeline.value.id} - ${stepId.value}`);
const step = computed(() => pipeline.value && findStep(pipeline.value.steps || [], stepId.value)); const step = computed(() => pipeline.value && findStep(pipeline.value.workflows || [], stepId.value));
const stream = ref<EventSource>(); const stream = ref<EventSource>();
const log = ref<LogLine[]>(); const log = ref<LogLine[]>();
const consoleElement = ref<Element>(); const consoleElement = ref<Element>();

View file

@ -1,12 +1,12 @@
<template> <template>
<span v-if="step.start_time !== undefined" class="ml-auto text-sm">{{ duration }}</span> <span v-if="started" class="ml-auto text-sm">{{ duration }}</span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType, toRef } from 'vue'; import { computed, defineComponent, PropType, toRef } from 'vue';
import { useElapsedTime } from '~/compositions/useElapsedTime'; import { useElapsedTime } from '~/compositions/useElapsedTime';
import { PipelineStep } from '~/lib/api/types'; import { PipelineStep, PipelineWorkflow } from '~/lib/api/types';
import { durationAsNumber } from '~/utils/duration'; import { durationAsNumber } from '~/utils/duration';
export default defineComponent({ export default defineComponent({
@ -15,16 +15,22 @@ export default defineComponent({
props: { props: {
step: { step: {
type: Object as PropType<PipelineStep>, type: Object as PropType<PipelineStep>,
required: true, default: undefined,
},
workflow: {
type: Object as PropType<PipelineWorkflow>,
default: undefined,
}, },
}, },
setup(props) { setup(props) {
const step = toRef(props, 'step'); const step = toRef(props, 'step');
const workflow = toRef(props, 'workflow');
const durationRaw = computed(() => { const durationRaw = computed(() => {
const start = step.value.start_time || 0; const start = (step.value ? step.value?.start_time : workflow.value?.start_time) || 0;
const end = step.value.end_time || 0; const end = (step.value ? step.value?.end_time : workflow.value?.end_time) || 0;
if (end === 0 && start === 0) { if (end === 0 && start === 0) {
return undefined; return undefined;
@ -37,7 +43,7 @@ export default defineComponent({
return (end - start) * 1000; return (end - start) * 1000;
}); });
const running = computed(() => step.value.state === 'running'); const running = computed(() => (step.value ? step.value?.state : workflow.value?.state) === 'running');
const { time: durationElapsed } = useElapsedTime(running, durationRaw); const { time: durationElapsed } = useElapsedTime(running, durationRaw);
const duration = computed(() => { const duration = computed(() => {
@ -45,10 +51,11 @@ export default defineComponent({
return '-'; return '-';
} }
return durationAsNumber(durationElapsed.value); return durationAsNumber(durationElapsed.value || 0);
}); });
const started = computed(() => (step.value ? step.value?.start_time : workflow.value?.start_time) !== undefined);
return { duration }; return { started, duration };
}, },
}); });
</script> </script>

View file

@ -38,14 +38,14 @@
</div> </div>
</div> </div>
<div v-if="pipeline.steps === undefined || pipeline.steps.length === 0" class="m-auto mt-4"> <div v-if="pipeline.workflows === undefined || pipeline.workflows.length === 0" class="m-auto mt-4">
<span>{{ $t('repo.pipeline.no_pipeline_steps') }}</span> <span>{{ $t('repo.pipeline.no_pipeline_steps') }}</span>
</div> </div>
<div class="flex-grow min-h-0 w-full relative"> <div class="flex-grow min-h-0 w-full relative">
<div class="absolute top-0 left-0 right-0 h-full flex flex-col overflow-y-scroll gap-y-2"> <div class="absolute top-0 left-0 right-0 h-full flex flex-col overflow-y-scroll gap-y-2">
<div <div
v-for="workflow in pipeline.steps" v-for="workflow in pipeline.workflows"
:key="workflow.id" :key="workflow.id"
class="p-2 md:rounded-md bg-white shadow dark:border-b-dark-gray-600 dark:bg-dark-gray-700" class="p-2 md:rounded-md bg-white shadow dark:border-b-dark-gray-600 dark:bg-dark-gray-700"
> >
@ -56,7 +56,7 @@
</div> </div>
</div> </div>
<button <button
v-if="pipeline.steps && pipeline.steps.length > 1" v-if="pipeline.workflows && pipeline.workflows.length > 1"
type="button" type="button"
:title="workflow.name" :title="workflow.name"
class="flex items-center gap-2 py-2 px-1 hover-effect rounded-md" class="flex items-center gap-2 py-2 px-1 hover-effect rounded-md"
@ -71,7 +71,7 @@
<span class="truncate">{{ workflow.name }}</span> <span class="truncate">{{ workflow.name }}</span>
<PipelineStepDuration <PipelineStepDuration
v-if="workflow.start_time !== workflow.end_time" v-if="workflow.start_time !== workflow.end_time"
:step="workflow" :workflow="workflow"
class="mr-1 pr-2px" class="mr-1 pr-2px"
/> />
</button> </button>
@ -81,7 +81,7 @@
:class="{ :class="{
'max-h-screen': !workflowsCollapsed[workflow.id], 'max-h-screen': !workflowsCollapsed[workflow.id],
'max-h-0': workflowsCollapsed[workflow.id], 'max-h-0': workflowsCollapsed[workflow.id],
'ml-6': pipeline.steps && pipeline.steps.length > 1, 'ml-6': pipeline.workflows && pipeline.workflows.length > 1,
}" }"
> >
<button <button
@ -93,7 +93,7 @@
:class="{ :class="{
'bg-black bg-opacity-10 dark:bg-white dark:bg-opacity-5': selectedStepId && selectedStepId === step.pid, 'bg-black bg-opacity-10 dark:bg-white dark:bg-opacity-5': selectedStepId && selectedStepId === step.pid,
'mt-1': 'mt-1':
(pipeline.steps && pipeline.steps.length > 1) || (pipeline.workflows && pipeline.workflows.length > 1) ||
(workflow.children && step.pid !== workflow.children[0].pid), (workflow.children && step.pid !== workflow.children[0].pid),
}" }"
@click="$emit('update:selected-step-id', step.pid)" @click="$emit('update:selected-step-id', step.pid)"
@ -132,8 +132,8 @@ const pipeline = toRef(props, 'pipeline');
const { prettyRef } = usePipeline(pipeline); const { prettyRef } = usePipeline(pipeline);
const workflowsCollapsed = ref<Record<PipelineStep['id'], boolean>>( const workflowsCollapsed = ref<Record<PipelineStep['id'], boolean>>(
props.pipeline.steps && props.pipeline.steps.length > 1 props.pipeline.workflows && props.pipeline.workflows.length > 1
? (props.pipeline.steps || []).reduce( ? (props.pipeline.workflows || []).reduce(
(collapsed, workflow) => ({ (collapsed, workflow) => ({
...collapsed, ...collapsed,
[workflow.id]: ['success', 'skipped', 'blocked'].includes(workflow.state), [workflow.id]: ['success', 'skipped', 'blocked'].includes(workflow.state),

View file

@ -35,6 +35,6 @@ export default () => {
return; return;
} }
const { step } = data; const { step } = data;
pipelineStore.setStep(repo.id, pipeline.number, step); pipelineStore.setWorkflow(repo.id, pipeline.number, step);
}); });
}; };

View file

@ -7,7 +7,7 @@ import {
PipelineConfig, PipelineConfig,
PipelineFeed, PipelineFeed,
PipelineLog, PipelineLog,
PipelineStep, PipelineWorkflow,
PullRequest, PullRequest,
QueueInfo, QueueInfo,
Registry, Registry,
@ -289,7 +289,7 @@ export default class WoodpeckerClient extends ApiClient {
} }
// eslint-disable-next-line promise/prefer-await-to-callbacks // eslint-disable-next-line promise/prefer-await-to-callbacks
on(callback: (data: { pipeline?: Pipeline; repo?: Repo; step?: PipelineStep }) => void): EventSource { on(callback: (data: { pipeline?: Pipeline; repo?: Repo; step?: PipelineWorkflow }) => void): EventSource {
return this._subscribe('/stream/events', callback, { return this._subscribe('/stream/events', callback, {
reconnect: true, reconnect: true,
}); });

View file

@ -83,7 +83,7 @@ export type Pipeline = {
// The steps associated with this pipeline. // The steps associated with this pipeline.
// A pipeline will have multiple steps if a matrix pipeline was used or if a rebuild was requested. // A pipeline will have multiple steps if a matrix pipeline was used or if a rebuild was requested.
steps?: PipelineStep[]; workflows?: PipelineWorkflow[];
changed_files?: string[]; changed_files?: string[];
}; };
@ -100,6 +100,20 @@ export type PipelineStatus =
| 'started' | 'started'
| 'success'; | 'success';
export type PipelineWorkflow = {
id: number;
pipeline_id: number;
pid: number;
name: string;
state: PipelineStatus;
environ?: Record<string, string>;
start_time?: number;
end_time?: number;
agent_id?: number;
error?: string;
children: PipelineStep[];
};
export type PipelineStep = { export type PipelineStep = {
id: number; id: number;
uuid: string; uuid: string;
@ -109,12 +123,9 @@ export type PipelineStep = {
name: string; name: string;
state: PipelineStatus; state: PipelineStatus;
exit_code: number; exit_code: number;
environ?: Record<string, string>;
start_time?: number; start_time?: number;
end_time?: number; end_time?: number;
machine?: string;
error?: string; error?: string;
children?: PipelineStep[];
}; };
export type PipelineLog = { export type PipelineLog = {

View file

@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import { computed, reactive, Ref, ref } from 'vue'; import { computed, reactive, Ref, ref } from 'vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import { Pipeline, PipelineFeed, PipelineStep } from '~/lib/api/types'; import { Pipeline, PipelineFeed, PipelineWorkflow } from '~/lib/api/types';
import { useRepoStore } from '~/store/repos'; import { useRepoStore } from '~/store/repos';
import { comparePipelines, isPipelineActive } from '~/utils/helpers'; import { comparePipelines, isPipelineActive } from '~/utils/helpers';
@ -32,17 +32,17 @@ export const usePipelineStore = defineStore('pipelines', () => {
}); });
} }
function setStep(repoId: number, pipelineNumber: number, step: PipelineStep) { function setWorkflow(repoId: number, pipelineNumber: number, workflow: PipelineWorkflow) {
const pipeline = getPipeline(ref(repoId), ref(pipelineNumber.toString())).value; const pipeline = getPipeline(ref(repoId), ref(pipelineNumber.toString())).value;
if (!pipeline) { if (!pipeline) {
throw new Error("Can't find pipeline"); throw new Error("Can't find pipeline");
} }
if (!pipeline.steps) { if (!pipeline.workflows) {
pipeline.steps = []; pipeline.workflows = [];
} }
pipeline.steps = [...pipeline.steps.filter((p) => p.pid !== step.pid), step]; pipeline.workflows = [...pipeline.workflows.filter((p) => p.pid !== workflow.pid), workflow];
setPipeline(repoId, pipeline); setPipeline(repoId, pipeline);
} }
@ -89,7 +89,7 @@ export const usePipelineStore = defineStore('pipelines', () => {
return { return {
pipelines, pipelines,
setPipeline, setPipeline,
setStep, setWorkflow,
getRepoPipelines, getRepoPipelines,
getPipeline, getPipeline,
loadRepoPipelines, loadRepoPipelines,

View file

@ -1,16 +1,16 @@
import { Pipeline, PipelineStep, Repo } from '~/lib/api/types'; import { Pipeline, PipelineStep, PipelineWorkflow, Repo } from '~/lib/api/types';
export function findStep(steps: PipelineStep[], pid: number): PipelineStep | undefined { export function findStep(workflows: PipelineWorkflow[], pid: number): PipelineStep | undefined {
return steps.reduce((prev, step) => { return workflows.reduce((prev, workflow) => {
if (step.pid === pid) { const result = workflow.children.reduce((prevChild, step) => {
return step; if (step.pid === pid) {
} return step;
if (step.children) {
const result = findStep(step.children, pid);
if (result) {
return result;
} }
return prevChild;
}, undefined as PipelineStep | undefined);
if (result) {
return result;
} }
return prev; return prev;

View file

@ -2,7 +2,7 @@
<FluidContainer full-width class="flex flex-col flex-grow"> <FluidContainer full-width class="flex flex-col flex-grow">
<div class="flex w-full min-h-0 flex-grow"> <div class="flex w-full min-h-0 flex-grow">
<PipelineStepList <PipelineStepList
v-if="pipeline?.steps?.length || 0 > 0" v-if="pipeline?.workflows?.length || 0 > 0"
v-model:selected-step-id="selectedStepId" v-model:selected-step-id="selectedStepId"
:class="{ 'hidden md:flex': pipeline.status === 'blocked' }" :class="{ 'hidden md:flex': pipeline.status === 'blocked' }"
:pipeline="pipeline" :pipeline="pipeline"
@ -85,18 +85,18 @@ if (!repo || !repoPermissions || !pipeline) {
const stepId = toRef(props, 'stepId'); const stepId = toRef(props, 'stepId');
const defaultStepId = computed(() => { const defaultStepId = computed(() => {
if (!pipeline.value || !pipeline.value.steps || !pipeline.value.steps[0].children) { if (!pipeline.value || !pipeline.value.workflows || !pipeline.value.workflows[0].children) {
return null; return null;
} }
return pipeline.value.steps[0].children[0].pid; return pipeline.value.workflows[0].children[0].pid;
}); });
const selectedStepId = computed({ const selectedStepId = computed({
get() { get() {
if (stepId.value !== '' && stepId.value !== null && stepId.value !== undefined) { if (stepId.value !== '' && stepId.value !== null && stepId.value !== undefined) {
const id = parseInt(stepId.value, 10); const id = parseInt(stepId.value, 10);
const step = pipeline.value?.steps?.reduce( const step = pipeline.value?.workflows?.reduce(
(prev, p) => prev || p.children?.find((c) => c.pid === id), (prev, p) => prev || p.children?.find((c) => c.pid === id),
undefined as PipelineStep | undefined, undefined as PipelineStep | undefined,
); );
@ -125,7 +125,7 @@ const selectedStepId = computed({
}, },
}); });
const selectedStep = computed(() => findStep(pipeline.value.steps || [], selectedStepId.value || -1)); const selectedStep = computed(() => findStep(pipeline.value.workflows || [], selectedStepId.value || -1));
const error = computed(() => pipeline.value?.error || selectedStep.value?.error); const error = computed(() => pipeline.value?.error || selectedStep.value?.error);
const { doSubmit: approvePipeline, isLoading: isApprovingPipeline } = useAsyncAction(async () => { const { doSubmit: approvePipeline, isLoading: isApprovingPipeline } = useAsyncAction(async () => {

View file

@ -140,7 +140,7 @@ const { doSubmit: cancelPipeline, isLoading: isCancelingPipeline } = useAsyncAct
throw new Error('Unexpected: Repo is undefined'); throw new Error('Unexpected: Repo is undefined');
} }
if (!pipeline.value?.steps) { if (!pipeline.value?.workflows) {
throw new Error('Unexpected: Pipeline steps not loaded'); throw new Error('Unexpected: Pipeline steps not loaded');
} }

View file

@ -62,50 +62,61 @@ type (
// Pipeline defines a pipeline object. // Pipeline defines a pipeline object.
Pipeline struct { Pipeline struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Number int `json:"number"` Number int `json:"number"`
Parent int `json:"parent"` Parent int `json:"parent"`
Event string `json:"event"` Event string `json:"event"`
Status string `json:"status"` Status string `json:"status"`
Error string `json:"error"` Error string `json:"error"`
Enqueued int64 `json:"enqueued_at"` Enqueued int64 `json:"enqueued_at"`
Created int64 `json:"created_at"` Created int64 `json:"created_at"`
Started int64 `json:"started_at"` Started int64 `json:"started_at"`
Finished int64 `json:"finished_at"` Finished int64 `json:"finished_at"`
Deploy string `json:"deploy_to"` Deploy string `json:"deploy_to"`
Commit string `json:"commit"` Commit string `json:"commit"`
Branch string `json:"branch"` Branch string `json:"branch"`
Ref string `json:"ref"` Ref string `json:"ref"`
Refspec string `json:"refspec"` Refspec string `json:"refspec"`
CloneURL string `json:"clone_url"` CloneURL string `json:"clone_url"`
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message"` Message string `json:"message"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
Sender string `json:"sender"` Sender string `json:"sender"`
Author string `json:"author"` Author string `json:"author"`
Avatar string `json:"author_avatar"` Avatar string `json:"author_avatar"`
Email string `json:"author_email"` Email string `json:"author_email"`
Link string `json:"link_url"` Link string `json:"link_url"`
Reviewer string `json:"reviewed_by"` Reviewer string `json:"reviewed_by"`
Reviewed int64 `json:"reviewed_at"` Reviewed int64 `json:"reviewed_at"`
Steps []*Step `json:"steps,omitempty"` Workflows []*Workflow `json:"workflows,omitempty"`
}
// Workflow represents a workflow in the pipeline.
Workflow struct {
ID int64 `json:"id"`
PID int `json:"pid"`
Name string `json:"name"`
State string `json:"state"`
Error string `json:"error,omitempty"`
Started int64 `json:"start_time,omitempty"`
Stopped int64 `json:"end_time,omitempty"`
AgentID int64 `json:"agent_id,omitempty"`
Platform string `json:"platform,omitempty"`
Environ map[string]string `json:"environ,omitempty"`
Children []*Step `json:"children,omitempty"`
} }
// Step represents a process in the pipeline. // Step represents a process in the pipeline.
Step struct { Step struct {
ID int64 `json:"id"` ID int64 `json:"id"`
PID int `json:"pid"` PID int `json:"pid"`
PPID int `json:"ppid"` PPID int `json:"ppid"`
Name string `json:"name"` Name string `json:"name"`
State string `json:"state"` State string `json:"state"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
ExitCode int `json:"exit_code"` ExitCode int `json:"exit_code"`
Started int64 `json:"start_time,omitempty"` Started int64 `json:"start_time,omitempty"`
Stopped int64 `json:"end_time,omitempty"` Stopped int64 `json:"end_time,omitempty"`
Machine string `json:"machine,omitempty"`
Platform string `json:"platform,omitempty"`
Environ map[string]string `json:"environ,omitempty"`
Children []*Step `json:"children,omitempty"`
} }
// Registry represents a docker registry with credentials. // Registry represents a docker registry with credentials.