diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d9aedf2691..51c367f54e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: rev: v2.3.0 hooks: - id: check-yaml - exclude: 'pipeline/schema/.woodpecker/test-merge-map-and-sequence.yml' + exclude: 'pipeline/frontend/yaml/linter/schema/.woodpecker/test-merge-map-and-sequence.yml' - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/golangci/golangci-lint diff --git a/cli/exec/exec.go b/cli/exec/exec.go index 7703d8f175..7df8a38590 100644 --- a/cli/exec/exec.go +++ b/cli/exec/exec.go @@ -167,7 +167,7 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error } // lint the yaml file - if lerr := linter.New(linter.WithTrusted(true)).Lint(conf); lerr != nil { + if lerr := linter.New(linter.WithTrusted(true)).Lint(confstr, conf); lerr != nil { return lerr } diff --git a/cli/lint/lint.go b/cli/lint/lint.go index 7edde54e46..643d855a39 100644 --- a/cli/lint/lint.go +++ b/cli/lint/lint.go @@ -15,15 +15,20 @@ package lint import ( + "errors" "fmt" "os" + "path" "path/filepath" "strings" + "github.com/muesli/termenv" "github.com/urfave/cli/v2" "github.com/woodpecker-ci/woodpecker/cli/common" - "github.com/woodpecker-ci/woodpecker/pipeline/schema" + pipeline_errors "github.com/woodpecker-ci/woodpecker/pipeline/errors" + "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml" + "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/linter" ) // Command exports the info command. @@ -68,21 +73,58 @@ func lintDir(c *cli.Context, dir string) error { } func lintFile(_ *cli.Context, file string) error { + output := termenv.NewOutput(os.Stdout) + fi, err := os.Open(file) if err != nil { return err } defer fi.Close() - configErrors, err := schema.Lint(fi) + buf, err := os.ReadFile(file) if err != nil { - fmt.Println("❌ Config is invalid") - for _, configError := range configErrors { - fmt.Println("In", configError.Field()+":", configError.Description()) - } return err } + rawConfig := string(buf) + + c, err := yaml.ParseString(rawConfig) + if err != nil { + return err + } + + err = linter.New(linter.WithTrusted(true)).Lint(string(buf), c) + if err != nil { + fmt.Printf("🔥 %s has errors:\n", output.String(path.Base(file)).Underline()) + + hasErrors := true + for _, err := range pipeline_errors.GetPipelineErrors(err) { + line := " " + + if err.IsWarning { + line = fmt.Sprintf("%s ⚠️ ", line) + } else { + line = fmt.Sprintf("%s ❌", line) + hasErrors = true + } + + if data := err.GetLinterData(); data != nil { + line = fmt.Sprintf("%s %s\t%s", line, output.String(data.Field).Bold(), err.Message) + } else { + line = fmt.Sprintf("%s %s", line, err.Message) + } + + // TODO: use table output + fmt.Printf("%s\n", line) + } + + if hasErrors { + return errors.New("config has errors") + } + + return nil + } + fmt.Println("✅ Config is valid") return nil } diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 0a9c40deed..efe3474730 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -3951,8 +3951,11 @@ const docTemplate = `{ "enqueued_at": { "type": "integer" }, - "error": { - "type": "string" + "errors": { + "type": "array", + "items": { + "$ref": "#/definitions/errors.PipelineError" + } }, "event": { "$ref": "#/definitions/WebhookEvent" @@ -4248,6 +4251,17 @@ const docTemplate = `{ "blocked", "declined" ], + "x-enum-comments": { + "StatusBlocked": "waiting for approval", + "StatusDeclined": "blocked and declined", + "StatusError": "error with the config / while parsing / some other system problem", + "StatusFailure": "failed to finish (exit code != 0)", + "StatusKilled": "killed by user", + "StatusPending": "pending to be executed", + "StatusRunning": "currently running", + "StatusSkipped": "skipped as another step failed", + "StatusSuccess": "successfully finished" + }, "x-enum-varnames": [ "StatusSkipped", "StatusPending", @@ -4407,6 +4421,42 @@ const docTemplate = `{ "EventManual" ] }, + "errors.PipelineError": { + "type": "object", + "properties": { + "data": {}, + "is_warning": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/errors.PipelineErrorType" + } + } + }, + "errors.PipelineErrorType": { + "type": "string", + "enum": [ + "linter", + "deprecation", + "compiler", + "generic" + ], + "x-enum-comments": { + "PipelineErrorTypeCompiler": "some error with the config semantics", + "PipelineErrorTypeDeprecation": "using some deprecated feature", + "PipelineErrorTypeGeneric": "some generic error", + "PipelineErrorTypeLinter": "some error with the config syntax" + }, + "x-enum-varnames": [ + "PipelineErrorTypeLinter", + "PipelineErrorTypeDeprecation", + "PipelineErrorTypeCompiler", + "PipelineErrorTypeGeneric" + ] + }, "model.Workflow": { "type": "object", "properties": { diff --git a/go.mod b/go.mod index fbcf7126c9..c856898c61 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.17 github.com/moby/moby v24.0.7+incompatible github.com/moby/term v0.5.0 + github.com/muesli/termenv v0.15.2 github.com/oklog/ulid/v2 v2.1.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.17.0 @@ -47,6 +48,7 @@ require ( github.com/urfave/cli/v2 v2.25.7 github.com/xanzy/go-gitlab v0.93.2 github.com/xeipuuv/gojsonschema v1.2.0 + go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.14.0 golang.org/x/net v0.17.0 golang.org/x/oauth2 v0.13.0 @@ -67,6 +69,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -107,9 +110,11 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/libdns/libdns v0.2.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mholt/acmez v1.2.0 // indirect github.com/miekg/dns v1.1.55 // indirect @@ -124,6 +129,7 @@ require ( github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -136,7 +142,6 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/atomic v1.11.0 // indirect - go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/mod v0.13.0 // indirect diff --git a/go.sum b/go.sum index 8f9600fc6a..922185c7c0 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/antonmedv/expr v1.15.3 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI= github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -175,8 +177,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= @@ -272,6 +272,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -292,6 +294,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= @@ -314,6 +318,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -350,6 +356,8 @@ github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwa github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -666,7 +674,5 @@ sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/xorm v1.3.3 h1:L5/GOhvgMcwJYYRjzPf3lTTTf6JcaTd1Mb9A/Iqvccw= -xorm.io/xorm v1.3.3/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo= xorm.io/xorm v1.3.4 h1:vWFKzR3DhGUDl5b4srhUjhDwjxkZAc4C7BFszpu0swI= xorm.io/xorm v1.3.4/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo= diff --git a/pipeline/errors/error.go b/pipeline/errors/error.go new file mode 100644 index 0000000000..7223740657 --- /dev/null +++ b/pipeline/errors/error.go @@ -0,0 +1,77 @@ +package errors + +import ( + "errors" + "fmt" + + "go.uber.org/multierr" +) + +type PipelineErrorType string + +const ( + PipelineErrorTypeLinter PipelineErrorType = "linter" // some error with the config syntax + PipelineErrorTypeDeprecation PipelineErrorType = "deprecation" // using some deprecated feature + PipelineErrorTypeCompiler PipelineErrorType = "compiler" // some error with the config semantics + PipelineErrorTypeGeneric PipelineErrorType = "generic" // some generic error +) + +type PipelineError struct { + Type PipelineErrorType `json:"type"` + Message string `json:"message"` + IsWarning bool `json:"is_warning"` + Data interface{} `json:"data"` +} + +type LinterErrorData struct { + Field string `json:"field"` +} + +func (e *PipelineError) Error() string { + return fmt.Sprintf("[%s] %s", e.Type, e.Message) +} + +func (e *PipelineError) GetLinterData() *LinterErrorData { + if e.Type != PipelineErrorTypeLinter { + return nil + } + + if data, ok := e.Data.(*LinterErrorData); ok { + return data + } + + return nil +} + +func GetPipelineErrors(err error) []*PipelineError { + var pipelineErrors []*PipelineError + for _, _err := range multierr.Errors(err) { + var err *PipelineError + if errors.As(_err, &err) { + pipelineErrors = append(pipelineErrors, err) + } else { + pipelineErrors = append(pipelineErrors, &PipelineError{ + Message: _err.Error(), + Type: PipelineErrorTypeGeneric, + }) + } + } + + return pipelineErrors +} + +func HasBlockingErrors(err error) bool { + if err == nil { + return false + } + + errs := GetPipelineErrors(err) + + for _, err := range errs { + if !err.IsWarning { + return true + } + } + + return false +} diff --git a/pipeline/errors/error_test.go b/pipeline/errors/error_test.go new file mode 100644 index 0000000000..489f372d1c --- /dev/null +++ b/pipeline/errors/error_test.go @@ -0,0 +1,158 @@ +package errors_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/multierr" + + pipeline_errors "github.com/woodpecker-ci/woodpecker/pipeline/errors" +) + +func TestGetPipelineErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + title string + err error + expected []*pipeline_errors.PipelineError + }{ + { + title: "nil error", + err: nil, + expected: nil, + }, + { + title: "warning", + err: &pipeline_errors.PipelineError{ + IsWarning: true, + }, + expected: []*pipeline_errors.PipelineError{ + { + IsWarning: true, + }, + }, + }, + { + title: "pipeline error", + err: &pipeline_errors.PipelineError{ + IsWarning: false, + }, + expected: []*pipeline_errors.PipelineError{ + { + IsWarning: false, + }, + }, + }, + { + title: "multiple warnings", + err: multierr.Combine( + &pipeline_errors.PipelineError{ + IsWarning: true, + }, + &pipeline_errors.PipelineError{ + IsWarning: true, + }, + ), + expected: []*pipeline_errors.PipelineError{ + { + IsWarning: true, + }, + { + IsWarning: true, + }, + }, + }, + { + title: "multiple errors and warnings", + err: multierr.Combine( + &pipeline_errors.PipelineError{ + IsWarning: true, + }, + &pipeline_errors.PipelineError{ + IsWarning: false, + }, + errors.New("some error"), + ), + expected: []*pipeline_errors.PipelineError{ + { + IsWarning: true, + }, + { + IsWarning: false, + }, + { + Type: pipeline_errors.PipelineErrorTypeGeneric, + IsWarning: false, + Message: "some error", + }, + }, + }, + } + + for _, test := range tests { + assert.Equalf(t, pipeline_errors.GetPipelineErrors(test.err), test.expected, test.title) + } +} + +func TestHasBlockingErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + title string + err error + expected bool + }{ + { + title: "nil error", + err: nil, + expected: false, + }, + { + title: "warning", + err: &pipeline_errors.PipelineError{ + IsWarning: true, + }, + expected: false, + }, + { + title: "pipeline error", + err: &pipeline_errors.PipelineError{ + IsWarning: false, + }, + expected: true, + }, + { + title: "multiple warnings", + err: multierr.Combine( + &pipeline_errors.PipelineError{ + IsWarning: true, + }, + &pipeline_errors.PipelineError{ + IsWarning: true, + }, + ), + expected: false, + }, + { + title: "multiple errors and warnings", + err: multierr.Combine( + &pipeline_errors.PipelineError{ + IsWarning: true, + }, + &pipeline_errors.PipelineError{ + IsWarning: false, + }, + errors.New("some error"), + ), + expected: true, + }, + } + + for _, test := range tests { + if pipeline_errors.HasBlockingErrors(test.err) != test.expected { + t.Error("Should only return true if there are blocking errors") + } + } +} diff --git a/pipeline/frontend/yaml/constraint/constraint.go b/pipeline/frontend/yaml/constraint/constraint.go index ab5cde4103..76313856fc 100644 --- a/pipeline/frontend/yaml/constraint/constraint.go +++ b/pipeline/frontend/yaml/constraint/constraint.go @@ -15,7 +15,6 @@ package constraint import ( - "errors" "fmt" "maps" "path" @@ -23,6 +22,7 @@ import ( "github.com/antonmedv/expr" "github.com/bmatcuk/doublestar/v4" + "go.uber.org/multierr" "gopkg.in/yaml.v3" "github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata" @@ -261,7 +261,7 @@ func (c *List) UnmarshalYAML(value *yaml.Node) error { if err1 != nil && err2 != nil { y, _ := yaml.Marshal(value) - return fmt.Errorf("Could not parse condition: %s: %w", y, errors.Join(err1, err2)) + return fmt.Errorf("Could not parse condition: %s: %w", y, multierr.Append(err1, err2)) } return nil diff --git a/pipeline/frontend/yaml/error.go b/pipeline/frontend/yaml/linter/error.go similarity index 51% rename from pipeline/frontend/yaml/error.go rename to pipeline/frontend/yaml/linter/error.go index 26600c636e..94ae0eb311 100644 --- a/pipeline/frontend/yaml/error.go +++ b/pipeline/frontend/yaml/linter/error.go @@ -1,10 +1,10 @@ -// Copyright 2022 Woodpecker Authors +// 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 +// 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, @@ -12,21 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package yaml +package linter -import "errors" +import ( + "github.com/woodpecker-ci/woodpecker/pipeline/errors" +) -// PipelineParseError is an error that occurs when the pipeline parsing fails. -type PipelineParseError struct { - Err error -} - -func (e PipelineParseError) Error() string { - return e.Err.Error() -} - -func (e PipelineParseError) Is(err error) bool { - target1 := PipelineParseError{} - target2 := &target1 - return errors.As(err, &target1) || errors.As(err, &target2) +func newLinterError(message, field string, isWarning bool) *errors.PipelineError { + return &errors.PipelineError{ + Type: errors.PipelineErrorTypeLinter, + Message: message, + Data: &errors.LinterErrorData{Field: field}, + IsWarning: isWarning, + } } diff --git a/pipeline/frontend/yaml/linter/linter.go b/pipeline/frontend/yaml/linter/linter.go index 0b55ab42bc..088d30d6f7 100644 --- a/pipeline/frontend/yaml/linter/linter.go +++ b/pipeline/frontend/yaml/linter/linter.go @@ -17,13 +17,10 @@ package linter import ( "fmt" - "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types" -) + "go.uber.org/multierr" -const ( - blockClone uint8 = iota - blockPipeline - blockServices + "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/linter/schema" + "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types" ) // A Linter lints a pipeline configuration. @@ -41,39 +38,59 @@ func New(opts ...Option) *Linter { } // Lint lints the configuration. -func (l *Linter) Lint(c *types.Workflow) error { +func (l *Linter) Lint(rawConfig string, c *types.Workflow) error { + var linterErr error + if len(c.Steps.ContainerList) == 0 { - return fmt.Errorf("Invalid or missing pipeline section") + linterErr = multierr.Append(linterErr, newLinterError("Invalid or missing steps section", "steps", false)) + } + + if err := l.lintContainers(c.Clone.ContainerList); err != nil { + linterErr = multierr.Append(linterErr, err) } - if err := l.lint(c.Clone.ContainerList, blockClone); err != nil { - return err + if err := l.lintContainers(c.Steps.ContainerList); err != nil { + linterErr = multierr.Append(linterErr, err) } - if err := l.lint(c.Steps.ContainerList, blockPipeline); err != nil { - return err + if err := l.lintContainers(c.Services.ContainerList); err != nil { + linterErr = multierr.Append(linterErr, err) } - return l.lint(c.Services.ContainerList, blockServices) + + if err := l.lintSchema(rawConfig); err != nil { + linterErr = multierr.Append(linterErr, err) + } + if err := l.lintDeprecations(c); err != nil { + linterErr = multierr.Append(linterErr, err) + } + if err := l.lintBadHabits(c); err != nil { + linterErr = multierr.Append(linterErr, err) + } + + return linterErr } -func (l *Linter) lint(containers []*types.Container, _ uint8) error { +func (l *Linter) lintContainers(containers []*types.Container) error { + var linterErr error + for _, container := range containers { if err := l.lintImage(container); err != nil { - return err + linterErr = multierr.Append(linterErr, err) } if !l.trusted { if err := l.lintTrusted(container); err != nil { - return err + linterErr = multierr.Append(linterErr, err) } } if err := l.lintCommands(container); err != nil { - return err + linterErr = multierr.Append(linterErr, err) } } - return nil + + return linterErr } func (l *Linter) lintImage(c *types.Container) error { if len(c.Image) == 0 { - return fmt.Errorf("Invalid or missing image") + return newLinterError("Invalid or missing image", fmt.Sprintf("steps.%s", c.Name), false) } return nil } @@ -87,47 +104,73 @@ func (l *Linter) lintCommands(c *types.Container) error { for key := range c.Settings { keys = append(keys, key) } - return fmt.Errorf("Cannot configure both commands and custom attributes %v", keys) + return newLinterError(fmt.Sprintf("Cannot configure both commands and custom attributes %v", keys), fmt.Sprintf("steps.%s", c.Name), false) } return nil } func (l *Linter) lintTrusted(c *types.Container) error { + yamlPath := fmt.Sprintf("steps.%s", c.Name) if c.Privileged { - return fmt.Errorf("Insufficient privileges to use privileged mode") + return newLinterError("Insufficient privileges to use privileged mode", yamlPath, false) } if c.ShmSize != 0 { - return fmt.Errorf("Insufficient privileges to override shm_size") + return newLinterError("Insufficient privileges to override shm_size", yamlPath, false) } if len(c.DNS) != 0 { - return fmt.Errorf("Insufficient privileges to use custom dns") + return newLinterError("Insufficient privileges to use custom dns", yamlPath, false) } if len(c.DNSSearch) != 0 { - return fmt.Errorf("Insufficient privileges to use dns_search") + return newLinterError("Insufficient privileges to use dns_search", yamlPath, false) } if len(c.Devices) != 0 { - return fmt.Errorf("Insufficient privileges to use devices") + return newLinterError("Insufficient privileges to use devices", yamlPath, false) } if len(c.ExtraHosts) != 0 { - return fmt.Errorf("Insufficient privileges to use extra_hosts") + return newLinterError("Insufficient privileges to use extra_hosts", yamlPath, false) } if len(c.NetworkMode) != 0 { - return fmt.Errorf("Insufficient privileges to use network_mode") + return newLinterError("Insufficient privileges to use network_mode", yamlPath, false) } if len(c.IpcMode) != 0 { - return fmt.Errorf("Insufficient privileges to use ipc_mode") + return newLinterError("Insufficient privileges to use ipc_mode", yamlPath, false) } if len(c.Sysctls) != 0 { - return fmt.Errorf("Insufficient privileges to use sysctls") + return newLinterError("Insufficient privileges to use sysctls", yamlPath, false) } if c.Networks.Networks != nil && len(c.Networks.Networks) != 0 { - return fmt.Errorf("Insufficient privileges to use networks") + return newLinterError("Insufficient privileges to use networks", yamlPath, false) } if c.Volumes.Volumes != nil && len(c.Volumes.Volumes) != 0 { - return fmt.Errorf("Insufficient privileges to use volumes") + return newLinterError("Insufficient privileges to use volumes", yamlPath, false) } if len(c.Tmpfs) != 0 { - return fmt.Errorf("Insufficient privileges to use tmpfs") + return newLinterError("Insufficient privileges to use tmpfs", yamlPath, false) + } + return nil +} + +func (l *Linter) lintSchema(rawConfig string) error { + var linterErr error + schemaErrors, err := schema.LintString(rawConfig) + if err != nil { + for _, schemaError := range schemaErrors { + linterErr = multierr.Append(linterErr, newLinterError( + schemaError.Description(), + schemaError.Field(), + true, // TODO: let pipelines fail if the schema is invalid + )) + } } + return linterErr +} + +func (l *Linter) lintDeprecations(_ *types.Workflow) error { + // TODO: add deprecation warnings + return nil +} + +func (l *Linter) lintBadHabits(_ *types.Workflow) error { + // TODO: add bad habit warnings return nil } diff --git a/pipeline/frontend/yaml/linter/linter_test.go b/pipeline/frontend/yaml/linter/linter_test.go index 6ffdffdef9..9c1c5ff400 100644 --- a/pipeline/frontend/yaml/linter/linter_test.go +++ b/pipeline/frontend/yaml/linter/linter_test.go @@ -17,6 +17,8 @@ package linter import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/woodpecker-ci/woodpecker/pipeline/errors" "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml" ) @@ -26,8 +28,6 @@ func TestLint(t *testing.T) { steps: build: image: docker - privileged: true - network_mode: host volumes: - /tmp:/tmp commands: @@ -35,8 +35,8 @@ steps: - go test publish: image: plugins/docker - repo: foo/bar settings: + repo: foo/bar foo: bar services: redis: @@ -47,8 +47,6 @@ services: steps: - name: build image: docker - privileged: true - network_mode: host volumes: - /tmp:/tmp commands: @@ -56,8 +54,8 @@ steps: - go test - name: publish image: plugins/docker - repo: foo/bar settings: + repo: foo/bar foo: bar `, }, { @@ -81,9 +79,10 @@ steps: t.Run(testd.Title, func(t *testing.T) { conf, err := yaml.ParseString(testd.Data) if err != nil { - t.Fatalf("Cannot unmarshal yaml %q. Error: %s", testd, err) + t.Fatalf("Cannot unmarshal yaml %q. Error: %s", testd.Title, err) } - if err := New(WithTrusted(true)).Lint(conf); err != nil { + + if err := New(WithTrusted(true)).Lint(testd.Data, conf); err != nil { t.Errorf("Expected lint returns no errors, got %q", err) } }) @@ -97,7 +96,7 @@ func TestLintErrors(t *testing.T) { }{ { from: "", - want: "Invalid or missing pipeline section", + want: "Invalid or missing steps section", }, { from: "steps: { build: { image: '' } }", @@ -156,11 +155,19 @@ func TestLintErrors(t *testing.T) { t.Fatalf("Cannot unmarshal yaml %q. Error: %s", test.from, err) } - lerr := New().Lint(conf) + lerr := New().Lint(test.from, conf) if lerr == nil { t.Errorf("Expected lint error for configuration %q", test.from) - } else if lerr.Error() != test.want { - t.Errorf("Want error %q, got %q", test.want, lerr.Error()) } + + lerrors := errors.GetPipelineErrors(lerr) + found := false + for _, lerr := range lerrors { + if lerr.Message == test.want { + found = true + break + } + } + assert.True(t, found, "Expected error %q, got %q", test.want, lerrors) } } diff --git a/pipeline/schema/.woodpecker/test-branches-array.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-branches-array.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-branches-array.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-branches-array.yml diff --git a/pipeline/schema/.woodpecker/test-branches-exclude-include.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-branches-exclude-include.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-branches-exclude-include.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-branches-exclude-include.yml diff --git a/pipeline/schema/.woodpecker/test-branches.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-branches.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-branches.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-branches.yml diff --git a/pipeline/schema/.woodpecker/test-broken.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-broken.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-broken.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-broken.yml diff --git a/pipeline/schema/.woodpecker/test-clone-skip.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-clone-skip.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-clone-skip.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-clone-skip.yml diff --git a/pipeline/schema/.woodpecker/test-clone.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-clone.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-clone.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-clone.yml diff --git a/pipeline/schema/.woodpecker/test-labels.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-labels.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-labels.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-labels.yml diff --git a/pipeline/schema/.woodpecker/test-matrix.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-matrix.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-matrix.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-matrix.yml diff --git a/pipeline/schema/.woodpecker/test-merge-map-and-sequence.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-merge-map-and-sequence.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-merge-map-and-sequence.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-merge-map-and-sequence.yml diff --git a/pipeline/schema/.woodpecker/test-multi.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-multi.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-multi.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-multi.yml diff --git a/pipeline/schema/.woodpecker/test-pipeline-when.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-pipeline-when.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-pipeline-when.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-pipeline-when.yml diff --git a/pipeline/schema/.woodpecker/test-platform.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-platform.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-platform.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-platform.yml diff --git a/pipeline/schema/.woodpecker/test-plugin.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-plugin.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-plugin.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-plugin.yml diff --git a/pipeline/schema/.woodpecker/test-run-on.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-run-on.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-run-on.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-run-on.yml diff --git a/pipeline/schema/.woodpecker/test-service.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-service.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-service.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-service.yml diff --git a/pipeline/schema/.woodpecker/test-step.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-step.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-step.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-step.yml diff --git a/pipeline/schema/.woodpecker/test-when.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-when.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-when.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-when.yml diff --git a/pipeline/schema/.woodpecker/test-workspace.yml b/pipeline/frontend/yaml/linter/schema/.woodpecker/test-workspace.yml similarity index 100% rename from pipeline/schema/.woodpecker/test-workspace.yml rename to pipeline/frontend/yaml/linter/schema/.woodpecker/test-workspace.yml diff --git a/pipeline/schema/schema.go b/pipeline/frontend/yaml/linter/schema/schema.go similarity index 93% rename from pipeline/schema/schema.go rename to pipeline/frontend/yaml/linter/schema/schema.go index 793c8be82d..da24cab37b 100644 --- a/pipeline/schema/schema.go +++ b/pipeline/frontend/yaml/linter/schema/schema.go @@ -15,6 +15,7 @@ package schema import ( + "bytes" _ "embed" "fmt" "io" @@ -62,3 +63,7 @@ func Lint(r io.Reader) ([]gojsonschema.ResultError, error) { return nil, nil } + +func LintString(s string) ([]gojsonschema.ResultError, error) { + return Lint(bytes.NewBufferString(s)) +} diff --git a/pipeline/schema/schema.json b/pipeline/frontend/yaml/linter/schema/schema.json similarity index 99% rename from pipeline/schema/schema.json rename to pipeline/frontend/yaml/linter/schema/schema.json index 1a33688da5..aa26dd69b7 100644 --- a/pipeline/schema/schema.json +++ b/pipeline/frontend/yaml/linter/schema/schema.json @@ -204,6 +204,10 @@ "additionalProperties": false, "required": ["image"], "properties": { + "name": { + "description": "The name of the step. Can be used if using the array style steps list.", + "type": "string" + }, "image": { "$ref": "#/definitions/step_image" }, diff --git a/pipeline/schema/schema_test.go b/pipeline/frontend/yaml/linter/schema/schema_test.go similarity index 97% rename from pipeline/schema/schema_test.go rename to pipeline/frontend/yaml/linter/schema/schema_test.go index 0a213d4a3b..119cd88e78 100644 --- a/pipeline/schema/schema_test.go +++ b/pipeline/frontend/yaml/linter/schema/schema_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/woodpecker-ci/woodpecker/pipeline/schema" + "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/linter/schema" ) func TestSchema(t *testing.T) { diff --git a/pipeline/frontend/yaml/matrix/matrix.go b/pipeline/frontend/yaml/matrix/matrix.go index d9b5454d26..e161ea6015 100644 --- a/pipeline/frontend/yaml/matrix/matrix.go +++ b/pipeline/frontend/yaml/matrix/matrix.go @@ -17,7 +17,7 @@ package matrix import ( "strings" - pipeline "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml" + "github.com/woodpecker-ci/woodpecker/pipeline/errors" "codeberg.org/6543/xyaml" ) @@ -116,7 +116,7 @@ func parse(raw []byte) (Matrix, error) { Matrix map[string][]string }{} if err := xyaml.Unmarshal(raw, &data); err != nil { - return nil, &pipeline.PipelineParseError{Err: err} + return nil, &errors.PipelineError{Message: err.Error(), Type: errors.PipelineErrorTypeCompiler} } return data.Matrix, nil } @@ -129,7 +129,7 @@ func parseList(raw []byte) ([]Axis, error) { }{} if err := xyaml.Unmarshal(raw, &data); err != nil { - return nil, &pipeline.PipelineParseError{Err: err} + return nil, &errors.PipelineError{Message: err.Error(), Type: errors.PipelineErrorTypeCompiler} } return data.Matrix.Include, nil } diff --git a/pipeline/stepBuilder.go b/pipeline/stepBuilder.go index c3c198b317..332b47a991 100644 --- a/pipeline/stepBuilder.go +++ b/pipeline/stepBuilder.go @@ -22,8 +22,11 @@ import ( "github.com/oklog/ulid/v2" "github.com/rs/zerolog/log" + "go.uber.org/multierr" backend_types "github.com/woodpecker-ci/woodpecker/pipeline/backend/types" + "github.com/woodpecker-ci/woodpecker/pipeline/errors" + pipeline_errors "github.com/woodpecker-ci/woodpecker/pipeline/errors" yaml_types "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" @@ -60,9 +63,7 @@ type Item struct { Config *backend_types.Config } -func (b *StepBuilder) Build() ([]*Item, error) { - var items []*Item - +func (b *StepBuilder) Build() (items []*Item, errorsAndWarnings error) { b.Yamls = forge_types.SortByName(b.Yamls) pidSequence := 1 @@ -86,9 +87,12 @@ func (b *StepBuilder) Build() ([]*Item, error) { AxisID: i + 1, } item, err := b.genItemForWorkflow(workflow, axis, string(y.Data)) - if err != nil { + if err != nil && pipeline_errors.HasBlockingErrors(err) { return nil, err + } else if err != nil { + errorsAndWarnings = multierr.Append(errorsAndWarnings, err) } + if item == nil { continue } @@ -104,13 +108,13 @@ func (b *StepBuilder) Build() ([]*Item, error) { // check if at least one step can start if slice is not empty if len(items) > 0 && !stepListContainsItemsToRun(items) { - return nil, fmt.Errorf("pipeline has no startpoint") + return nil, fmt.Errorf("pipeline has no steps to run") } - return items, nil + return items, errorsAndWarnings } -func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.Axis, data string) (*Item, error) { +func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.Axis, data string) (item *Item, errorsAndWarnings error) { workflowMetadata := frontend.MetadataFromStruct(b.Forge, b.Repo, b.Curr, b.Last, workflow, b.Link) environ := b.environmentVariables(workflowMetadata, axis) @@ -126,20 +130,21 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A // substitute vars substituted, err := frontend.EnvVarSubst(data, environ) if err != nil { - return nil, err + return nil, multierr.Append(errorsAndWarnings, err) } // parse yaml pipeline parsed, err := yaml.ParseString(substituted) if err != nil { - return nil, &yaml.PipelineParseError{Err: err} + return nil, &errors.PipelineError{Message: err.Error(), Type: errors.PipelineErrorTypeCompiler} } // lint pipeline - if err := linter.New( + errorsAndWarnings = multierr.Append(errorsAndWarnings, linter.New( linter.WithTrusted(b.Repo.IsTrusted), - ).Lint(parsed); err != nil { - return nil, &yaml.PipelineParseError{Err: err} + ).Lint(substituted, parsed)) + if pipeline_errors.HasBlockingErrors(errorsAndWarnings) { + return nil, errorsAndWarnings } // checking if filtered. @@ -152,19 +157,19 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A log.Debug().Str("pipeline", workflow.Name).Msg( "Pipeline config could not be parsed", ) - return nil, err + return nil, multierr.Append(errorsAndWarnings, err) } ir, err := b.toInternalRepresentation(parsed, environ, workflowMetadata, workflow.ID) if err != nil { - return nil, err + return nil, multierr.Append(errorsAndWarnings, err) } if len(ir.Stages) == 0 { return nil, nil } - item := &Item{ + item = &Item{ Workflow: workflow, Config: ir, Labels: parsed.Labels, @@ -175,7 +180,7 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A item.Labels = map[string]string{} } - return item, nil + return item, errorsAndWarnings } func stepListContainsItemsToRun(items []*Item) bool { diff --git a/pipeline/stepBuilder_test.go b/pipeline/stepBuilder_test.go index 2b9d1c4d42..6a687a7136 100644 --- a/pipeline/stepBuilder_test.go +++ b/pipeline/stepBuilder_test.go @@ -50,7 +50,8 @@ func TestGlobalEnvsubst(t *testing.T) { steps: build: image: ${IMAGE} - yyy: ${CI_COMMIT_MESSAGE} + settings: + yyy: ${CI_COMMIT_MESSAGE} `)}, }, } @@ -85,7 +86,8 @@ func TestMissingGlobalEnvsubst(t *testing.T) { steps: build: image: ${IMAGE} - yyy: ${CI_COMMIT_MESSAGE} + settings: + yyy: ${CI_COMMIT_MESSAGE} `)}, }, } @@ -117,13 +119,15 @@ bbb`, steps: xxx: image: scratch - yyy: ${CI_COMMIT_MESSAGE} + settings: + yyy: ${CI_COMMIT_MESSAGE} `)}, {Data: []byte(` steps: build: image: scratch - yyy: ${CI_COMMIT_MESSAGE} + settings: + yyy: ${CI_COMMIT_MESSAGE} `)}, }, } @@ -335,7 +339,7 @@ func TestRootWhenFilter(t *testing.T) { b := StepBuilder{ Forge: getMockForge(t), Repo: &model.Repo{}, - Curr: &model.Pipeline{Event: "tester"}, + Curr: &model.Pipeline{Event: "tag"}, Last: &model.Pipeline{}, Netrc: &model.Netrc{}, Secs: []*model.Secret{}, @@ -345,7 +349,7 @@ func TestRootWhenFilter(t *testing.T) { {Data: []byte(` when: event: - - tester + - tag steps: xxx: image: scratch diff --git a/server/model/const.go b/server/model/const.go index 3cda0b810b..337c6a1380 100644 --- a/server/model/const.go +++ b/server/model/const.go @@ -52,15 +52,15 @@ func ValidateWebhookEvent(s WebhookEvent) error { type StatusValue string // @name StatusValue const ( - StatusSkipped StatusValue = "skipped" - StatusPending StatusValue = "pending" - StatusRunning StatusValue = "running" - StatusSuccess StatusValue = "success" - StatusFailure StatusValue = "failure" - StatusKilled StatusValue = "killed" - StatusError StatusValue = "error" - StatusBlocked StatusValue = "blocked" - StatusDeclined StatusValue = "declined" + StatusSkipped StatusValue = "skipped" // skipped as another step failed + StatusPending StatusValue = "pending" // pending to be executed + StatusRunning StatusValue = "running" // currently running + StatusSuccess StatusValue = "success" // successfully finished + StatusFailure StatusValue = "failure" // failed to finish (exit code != 0) + StatusKilled StatusValue = "killed" // killed by user + StatusError StatusValue = "error" // error with the config / while parsing / some other system problem + StatusBlocked StatusValue = "blocked" // waiting for approval + StatusDeclined StatusValue = "declined" // blocked and declined ) // SCMKind represent different version control systems diff --git a/server/model/pipeline.go b/server/model/pipeline.go index 991839c69b..9963927135 100644 --- a/server/model/pipeline.go +++ b/server/model/pipeline.go @@ -15,40 +15,44 @@ package model +import ( + "github.com/woodpecker-ci/woodpecker/pipeline/errors" +) + type Pipeline struct { - ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` - RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'pipeline_repo_id'"` - Number int64 `json:"number" xorm:"UNIQUE(s) 'pipeline_number'"` - Author string `json:"author" xorm:"INDEX 'pipeline_author'"` - ConfigID int64 `json:"-" xorm:"pipeline_config_id"` - Parent int64 `json:"parent" xorm:"pipeline_parent"` - Event WebhookEvent `json:"event" xorm:"pipeline_event"` - Status StatusValue `json:"status" xorm:"INDEX 'pipeline_status'"` - Error string `json:"error" xorm:"LONGTEXT 'pipeline_error'"` - Enqueued int64 `json:"enqueued_at" xorm:"pipeline_enqueued"` - Created int64 `json:"created_at" xorm:"pipeline_created"` - Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"` - Started int64 `json:"started_at" xorm:"pipeline_started"` - Finished int64 `json:"finished_at" xorm:"pipeline_finished"` - Deploy string `json:"deploy_to" xorm:"pipeline_deploy"` - Commit string `json:"commit" xorm:"pipeline_commit"` - Branch string `json:"branch" xorm:"pipeline_branch"` - Ref string `json:"ref" xorm:"pipeline_ref"` - Refspec string `json:"refspec" xorm:"pipeline_refspec"` - CloneURL string `json:"clone_url" xorm:"pipeline_clone_url"` - Title string `json:"title" xorm:"pipeline_title"` - Message string `json:"message" xorm:"TEXT 'pipeline_message'"` - Timestamp int64 `json:"timestamp" xorm:"pipeline_timestamp"` - Sender string `json:"sender" xorm:"pipeline_sender"` // uses reported user for webhooks and name of cron for cron pipelines - Avatar string `json:"author_avatar" xorm:"pipeline_avatar"` - Email string `json:"author_email" xorm:"pipeline_email"` - Link string `json:"link_url" xorm:"pipeline_link"` - Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"` - Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"` - Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` - ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` - AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` - PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` + ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` + RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'pipeline_repo_id'"` + Number int64 `json:"number" xorm:"UNIQUE(s) 'pipeline_number'"` + Author string `json:"author" xorm:"INDEX 'pipeline_author'"` + ConfigID int64 `json:"-" xorm:"pipeline_config_id"` + Parent int64 `json:"parent" xorm:"pipeline_parent"` + Event WebhookEvent `json:"event" xorm:"pipeline_event"` + Status StatusValue `json:"status" xorm:"INDEX 'pipeline_status'"` + Errors []*errors.PipelineError `json:"errors" xorm:"json 'pipeline_errors'"` + Enqueued int64 `json:"enqueued_at" xorm:"pipeline_enqueued"` + Created int64 `json:"created_at" xorm:"pipeline_created"` + Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"` + Started int64 `json:"started_at" xorm:"pipeline_started"` + Finished int64 `json:"finished_at" xorm:"pipeline_finished"` + Deploy string `json:"deploy_to" xorm:"pipeline_deploy"` + Commit string `json:"commit" xorm:"pipeline_commit"` + Branch string `json:"branch" xorm:"pipeline_branch"` + Ref string `json:"ref" xorm:"pipeline_ref"` + Refspec string `json:"refspec" xorm:"pipeline_refspec"` + CloneURL string `json:"clone_url" xorm:"pipeline_clone_url"` + Title string `json:"title" xorm:"pipeline_title"` + Message string `json:"message" xorm:"TEXT 'pipeline_message'"` + Timestamp int64 `json:"timestamp" xorm:"pipeline_timestamp"` + Sender string `json:"sender" xorm:"pipeline_sender"` // uses reported user for webhooks and name of cron for cron pipelines + Avatar string `json:"author_avatar" xorm:"pipeline_avatar"` + Email string `json:"author_email" xorm:"pipeline_email"` + Link string `json:"link_url" xorm:"pipeline_link"` + Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"` + Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"` + Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` + ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` + AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` + PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` } // @name Pipeline // TableName return database table name for xorm diff --git a/server/pipeline/approve.go b/server/pipeline/approve.go index f0d708fa99..f5fe8aa301 100644 --- a/server/pipeline/approve.go +++ b/server/pipeline/approve.go @@ -20,6 +20,7 @@ import ( "github.com/rs/zerolog/log" + "github.com/woodpecker-ci/woodpecker/pipeline/errors" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/store" @@ -50,10 +51,12 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe } currentPipeline, pipelineItems, err := createPipelineItems(ctx, store, currentPipeline, user, repo, yamls, nil) - if err != nil { - msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName) + if errors.HasBlockingErrors(err) { + msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName) log.Error().Err(err).Msg(msg) return nil, err + } else if err != nil { + currentPipeline.Errors = errors.GetPipelineErrors(err) } currentPipeline, err = start(ctx, store, currentPipeline, user, repo, pipelineItems) diff --git a/server/pipeline/create.go b/server/pipeline/create.go index 8c143a27bd..e647a2cb04 100644 --- a/server/pipeline/create.go +++ b/server/pipeline/create.go @@ -22,6 +22,7 @@ import ( "github.com/rs/zerolog/log" + "github.com/woodpecker-ci/woodpecker/pipeline/errors" "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/forge" "github.com/woodpecker-ci/woodpecker/server/model" @@ -60,13 +61,15 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline if configFetchErr != nil { log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("cannot find config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login) - return nil, persistPipelineWithErr(ctx, _store, pipeline, repo, repoUser, fmt.Sprintf("pipeline definition not found in %s", repo.FullName)) + return nil, persistPipelineWithErr(ctx, _store, pipeline, repo, repoUser, fmt.Errorf("pipeline definition not found in %s", repo.FullName)) } pipelineItems, parseErr := parsePipeline(_store, pipeline, repoUser, repo, forgeYamlConfigs, nil) - if parseErr != nil { + if errors.HasBlockingErrors(parseErr) { log.Debug().Str("repo", repo.FullName).Err(parseErr).Msg("failed to parse yaml") - return nil, persistPipelineWithErr(ctx, _store, pipeline, repo, repoUser, fmt.Sprintf("failed to parse pipeline: %s", parseErr.Error())) + return nil, persistPipelineWithErr(ctx, _store, pipeline, repo, repoUser, parseErr) + } else if parseErr != nil { + pipeline.Errors = errors.GetPipelineErrors(parseErr) } if len(pipelineItems) == 0 { @@ -118,11 +121,11 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return pipeline, nil } -func persistPipelineWithErr(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err string) error { +func persistPipelineWithErr(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error { pipeline.Started = time.Now().Unix() pipeline.Finished = pipeline.Started pipeline.Status = model.StatusError - pipeline.Error = err + pipeline.Errors = errors.GetPipelineErrors(err) dbErr := _store.CreatePipeline(pipeline) if dbErr != nil { msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName) diff --git a/server/pipeline/items.go b/server/pipeline/items.go index 2f87e28155..912e0cb86e 100644 --- a/server/pipeline/items.go +++ b/server/pipeline/items.go @@ -22,6 +22,7 @@ import ( "github.com/rs/zerolog/log" "github.com/woodpecker-ci/woodpecker/pipeline" + pipeline_errors "github.com/woodpecker-ci/woodpecker/pipeline/errors" "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/compiler" "github.com/woodpecker-ci/woodpecker/server" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" @@ -82,12 +83,7 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod HTTPSProxy: server.Config.Pipeline.Proxy.HTTPS, }, } - pipelineItems, err := b.Build() - if err != nil { - return nil, err - } - - return pipelineItems, nil + return b.Build() } func createPipelineItems(c context.Context, store store.Store, @@ -102,12 +98,15 @@ func createPipelineItems(c context.Context, store store.Store, } else { updatePipelineStatus(c, currentPipeline, repo, user) } - return currentPipeline, nil, err + + if pipeline_errors.HasBlockingErrors(err) { + return currentPipeline, nil, err + } } currentPipeline = setPipelineStepsOnPipeline(currentPipeline, pipelineItems) - return currentPipeline, pipelineItems, nil + return currentPipeline, pipelineItems, err } // setPipelineStepsOnPipeline is the link between pipeline representation in "pipeline package" and server diff --git a/server/pipeline/pipelineStatus.go b/server/pipeline/pipelineStatus.go index 0c33a6cc0b..f88a47821d 100644 --- a/server/pipeline/pipelineStatus.go +++ b/server/pipeline/pipelineStatus.go @@ -18,6 +18,7 @@ package pipeline import ( "time" + "github.com/woodpecker-ci/woodpecker/pipeline/errors" "github.com/woodpecker-ci/woodpecker/server/model" ) @@ -48,7 +49,7 @@ func UpdateStatusToDone(store model.UpdatePipelineStore, pipeline model.Pipeline } func UpdateToStatusError(store model.UpdatePipelineStore, pipeline model.Pipeline, err error) (*model.Pipeline, error) { - pipeline.Error = err.Error() + pipeline.Errors = errors.GetPipelineErrors(err) pipeline.Status = model.StatusError pipeline.Started = time.Now().Unix() pipeline.Finished = pipeline.Started diff --git a/server/pipeline/pipelineStatus_test.go b/server/pipeline/pipelineStatus_test.go index a77d1a9168..8176974d2f 100644 --- a/server/pipeline/pipelineStatus_test.go +++ b/server/pipeline/pipelineStatus_test.go @@ -90,10 +90,12 @@ func TestUpdateToStatusError(t *testing.T) { now := time.Now().Unix() - pipeline, _ := UpdateToStatusError(&mockUpdatePipelineStore{}, model.Pipeline{}, errors.New("error")) + pipeline, _ := UpdateToStatusError(&mockUpdatePipelineStore{}, model.Pipeline{}, errors.New("this is an error")) - if pipeline.Error != "error" { - t.Errorf("Pipeline error not equals 'error' != '%s'", pipeline.Error) + if len(pipeline.Errors) != 1 { + t.Errorf("Expected one error, got %d", len(pipeline.Errors)) + } else if pipeline.Errors[0].Error() != "[generic] this is an error" { + t.Errorf("Pipeline error not equals '[generic] this is an error' != '%s'", pipeline.Errors[0].Error()) } else if model.StatusError != pipeline.Status { t.Errorf("Pipeline status not equals '%s' != '%s'", model.StatusError, pipeline.Status) } else if now > pipeline.Started { diff --git a/server/pipeline/restart.go b/server/pipeline/restart.go index 34a7ed93c0..9e2db9a7b2 100644 --- a/server/pipeline/restart.go +++ b/server/pipeline/restart.go @@ -22,7 +22,6 @@ import ( "github.com/rs/zerolog/log" - "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml" "github.com/woodpecker-ci/woodpecker/server" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" "github.com/woodpecker-ci/woodpecker/server/model" @@ -96,10 +95,7 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin newPipeline, pipelineItems, err := createPipelineItems(ctx, store, newPipeline, user, repo, pipelineFiles, envs) if err != nil { - if errors.Is(err, &yaml.PipelineParseError{}) { - return newPipeline, nil - } - msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName) + msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName) log.Error().Err(err).Msg(msg) return nil, fmt.Errorf(msg) } @@ -136,6 +132,6 @@ func createNewOutOfOld(old *model.Pipeline) *model.Pipeline { newPipeline.Started = 0 newPipeline.Finished = 0 newPipeline.Enqueued = time.Now().UTC().Unix() - newPipeline.Error = "" + newPipeline.Errors = nil return &newPipeline } diff --git a/server/store/datastore/migration/026_convert_to_new_pipeline_errors_format.go b/server/store/datastore/migration/026_convert_to_new_pipeline_errors_format.go new file mode 100644 index 0000000000..8a4bd687a9 --- /dev/null +++ b/server/store/datastore/migration/026_convert_to_new_pipeline_errors_format.go @@ -0,0 +1,85 @@ +// 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 migration + +import ( + "github.com/woodpecker-ci/woodpecker/pipeline/errors" + "github.com/woodpecker-ci/woodpecker/server/model" + "xorm.io/xorm" +) + +type oldPipeline026 struct { + ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` + Error string `json:"error" xorm:"LONGTEXT 'pipeline_error'"` +} + +func (oldPipeline026) TableName() string { + return "pipelines" +} + +type PipelineError026 struct { + Type string `json:"type"` + Message string `json:"message"` + IsWarning bool `json:"is_warning"` + Data interface{} `json:"data"` +} + +type newPipeline026 struct { + ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` + Errors []*errors.PipelineError `json:"errors" xorm:"json 'pipeline_errors'"` +} + +func (newPipeline026) TableName() string { + return "pipelines" +} + +var convertToNewPipelineErrorFormat = task{ + name: "convert-to-new-pipeline-error-format", + required: true, + fn: func(sess *xorm.Session) (err error) { + // make sure pipeline_error column exists + if err := sess.Sync(new(oldPipeline026)); err != nil { + return err + } + + // add new pipeline_errors column + if err := sess.Sync(new(model.Pipeline)); err != nil { + return err + } + + var oldPipelines []*oldPipeline026 + if err := sess.Find(&oldPipelines); err != nil { + return err + } + + for _, oldPipeline := range oldPipelines { + + var newPipeline newPipeline026 + newPipeline.ID = oldPipeline.ID + if oldPipeline.Error != "" { + newPipeline.Errors = []*errors.PipelineError{{ + Type: "generic", + Message: oldPipeline.Error, + }} + } + + if _, err := sess.ID(oldPipeline.ID).Cols("pipeline_errors").Update(&newPipeline); err != nil { + return err + } + } + + return dropTableColumns(sess, "pipelines", "pipeline_error") + }, +} diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go index ca35f73cad..3b4c493723 100644 --- a/server/store/datastore/migration/migration.go +++ b/server/store/datastore/migration/migration.go @@ -58,6 +58,7 @@ var migrationTasks = []*task{ &alterTableTasksUpdateColumnTaskDataType, &alterTableConfigUpdateColumnConfigDataType, &removePluginOnlyOptionFromSecretsTable, + &convertToNewPipelineErrorFormat, } var allBeans = []interface{}{ diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 754702d4fe..3ca1936a3f 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -230,7 +230,6 @@ "config": "Config", "files": "Changed files ({files})", "no_files": "No files have been changed.", - "execution_error": "Execution error", "no_pipelines": "No pipelines have been started yet.", "no_pipeline_steps": "No pipeline steps available!", "step_not_started": "This step hasn't started yet.", @@ -281,7 +280,11 @@ "error": "error", "failure": "failure", "killed": "killed" - } + }, + "errors": "Errors ({count})", + "warnings": "Warnings ({count})", + "show_errors": "Show errors", + "we_got_some_errors": "Oh no, we got some errors!" } }, "org": { diff --git a/web/src/components/layout/scaffold/Tab.vue b/web/src/components/layout/scaffold/Tab.vue index 1a6fbe3f41..9060820144 100644 --- a/web/src/components/layout/scaffold/Tab.vue +++ b/web/src/components/layout/scaffold/Tab.vue @@ -9,12 +9,10 @@ import { computed, onMounted, ref } from 'vue'; import { Tab, useTabsClient } from '~/compositions/useTabs'; -export interface Props { +const props = defineProps<{ id?: string; title: string; -} - -const props = defineProps(); +}>(); const { tabs, activeTab } = useTabsClient(); const tab = ref(); diff --git a/web/src/components/repo/pipeline/PipelineInfo.vue b/web/src/components/repo/pipeline/PipelineInfo.vue deleted file mode 100644 index f890f66aeb..0000000000 --- a/web/src/components/repo/pipeline/PipelineInfo.vue +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/web/src/lib/api/types/pipeline.ts b/web/src/lib/api/types/pipeline.ts index 411e5a44e7..d98eb8cd62 100644 --- a/web/src/lib/api/types/pipeline.ts +++ b/web/src/lib/api/types/pipeline.ts @@ -1,5 +1,12 @@ import { WebhookEvents } from './webhook'; +export type PipelineError = { + type: string; + message: string; + data?: unknown; + is_warning: boolean; +}; + // A pipeline for a repository. export type Pipeline = { id: number; @@ -15,7 +22,7 @@ export type Pipeline = { // The current status of the pipeline. status: PipelineStatus; - error: string; + errors?: PipelineError[]; // When the pipeline request was received. created_at: number; diff --git a/web/src/router.ts b/web/src/router.ts index 7c500aecab..b5d20a1c84 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -88,6 +88,12 @@ const routes: RouteRecordRaw[] = [ component: (): Component => import('~/views/repo/pipeline/PipelineConfig.vue'), props: true, }, + { + path: 'errors', + name: 'repo-pipeline-errors', + component: (): Component => import('~/views/repo/pipeline/PipelineErrors.vue'), + props: true, + }, ], }, { diff --git a/web/src/views/repo/pipeline/Pipeline.vue b/web/src/views/repo/pipeline/Pipeline.vue index e5c06b32ea..ab9372f25b 100644 --- a/web/src/views/repo/pipeline/Pipeline.vue +++ b/web/src/views/repo/pipeline/Pipeline.vue @@ -2,54 +2,74 @@
-
- - -
- {{ $t('repo.pipeline.execution_error') }}: - {{ error }} -
-
- - - - {{ $t('repo.pipeline.protected.awaits') }} -
-
-
- - - -

{{ $t('repo.pipeline.protected.declined') }}

-
+
+ + +
+ + {{ $t('repo.pipeline.we_got_some_errors') }} + {{ selectedStep?.error }} +
+
+
+ + + +
+ + {{ $t('repo.pipeline.we_got_some_errors') }} +
+
+
+ + + +
+ + {{ $t('repo.pipeline.protected.awaits') }} +
+
+
+
+
+ + + +
+ +

{{ $t('repo.pipeline.protected.declined') }}

+
+
+
{ - if (!pipeline.value || !pipeline.value.workflows || !pipeline.value.workflows[0].children) { - return null; - } - - return pipeline.value.workflows[0].children[0].pid; -}); +const defaultStepId = computed(() => pipeline.value?.workflows?.[0].children?.[0].pid ?? null); const selectedStepId = computed({ get() { if (stepId.value !== '' && stepId.value !== null && stepId.value !== undefined) { const id = parseInt(stepId.value, 10); - const step = pipeline.value?.workflows?.reduce( - (prev, p) => prev || p.children?.find((c) => c.pid === id), - undefined as PipelineStep | undefined, - ); + const step = pipeline.value?.workflows?.find((p) => p.children?.find((c) => c.pid === id)); if (step) { return step.pid; } @@ -128,7 +139,7 @@ const selectedStepId = computed({ return null; }, set(_selectedStepId: number | null) { - if (!_selectedStepId) { + if (_selectedStepId === null) { router.replace({ params: { ...route.params, stepId: '' } }); return; } @@ -141,7 +152,6 @@ const { forge } = useConfig(); const { message } = usePipeline(pipeline); const selectedStep = computed(() => findStep(pipeline.value.workflows || [], selectedStepId.value || -1)); -const error = computed(() => pipeline.value?.error || selectedStep.value?.error); const { doSubmit: approvePipeline, isLoading: isApprovingPipeline } = useAsyncAction(async () => { if (!repo) { diff --git a/web/src/views/repo/pipeline/PipelineErrors.vue b/web/src/views/repo/pipeline/PipelineErrors.vue new file mode 100644 index 0000000000..4f0dea7a73 --- /dev/null +++ b/web/src/views/repo/pipeline/PipelineErrors.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/web/src/views/repo/pipeline/PipelineWrapper.vue b/web/src/views/repo/pipeline/PipelineWrapper.vue index d6cd78324a..aec2511513 100644 --- a/web/src/views/repo/pipeline/PipelineWrapper.vue +++ b/web/src/views/repo/pipeline/PipelineWrapper.vue @@ -1,84 +1,97 @@