Enhance linter and errors (#1572)

Co-authored-by: 6543 <m.huber@kithara.com>
Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com>
This commit is contained in:
Anbraten 2023-11-03 11:44:03 +01:00 committed by GitHub
parent 4c4fdff5f7
commit 5ff006614f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 912 additions and 342 deletions

View file

@ -7,7 +7,7 @@ repos:
rev: v2.3.0 rev: v2.3.0
hooks: hooks:
- id: check-yaml - 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: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint

View file

@ -167,7 +167,7 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error
} }
// lint the yaml file // 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 return lerr
} }

View file

@ -15,15 +15,20 @@
package lint package lint
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/muesli/termenv"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/woodpecker-ci/woodpecker/cli/common" "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. // Command exports the info command.
@ -68,21 +73,58 @@ func lintDir(c *cli.Context, dir string) error {
} }
func lintFile(_ *cli.Context, file string) error { func lintFile(_ *cli.Context, file string) error {
output := termenv.NewOutput(os.Stdout)
fi, err := os.Open(file) fi, err := os.Open(file)
if err != nil { if err != nil {
return err return err
} }
defer fi.Close() defer fi.Close()
configErrors, err := schema.Lint(fi) buf, err := os.ReadFile(file)
if err != nil { if err != nil {
fmt.Println("❌ Config is invalid")
for _, configError := range configErrors {
fmt.Println("In", configError.Field()+":", configError.Description())
}
return err 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") fmt.Println("✅ Config is valid")
return nil return nil
} }

View file

@ -3951,8 +3951,11 @@ const docTemplate = `{
"enqueued_at": { "enqueued_at": {
"type": "integer" "type": "integer"
}, },
"error": { "errors": {
"type": "string" "type": "array",
"items": {
"$ref": "#/definitions/errors.PipelineError"
}
}, },
"event": { "event": {
"$ref": "#/definitions/WebhookEvent" "$ref": "#/definitions/WebhookEvent"
@ -4248,6 +4251,17 @@ const docTemplate = `{
"blocked", "blocked",
"declined" "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": [ "x-enum-varnames": [
"StatusSkipped", "StatusSkipped",
"StatusPending", "StatusPending",
@ -4407,6 +4421,42 @@ const docTemplate = `{
"EventManual" "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": { "model.Workflow": {
"type": "object", "type": "object",
"properties": { "properties": {

7
go.mod
View file

@ -34,6 +34,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.17
github.com/moby/moby v24.0.7+incompatible github.com/moby/moby v24.0.7+incompatible
github.com/moby/term v0.5.0 github.com/moby/term v0.5.0
github.com/muesli/termenv v0.15.2
github.com/oklog/ulid/v2 v2.1.0 github.com/oklog/ulid/v2 v2.1.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.17.0 github.com/prometheus/client_golang v1.17.0
@ -47,6 +48,7 @@ require (
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.25.7
github.com/xanzy/go-gitlab v0.93.2 github.com/xanzy/go-gitlab v0.93.2
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.14.0 golang.org/x/crypto v0.14.0
golang.org/x/net v0.17.0 golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.13.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/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.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/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // 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/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/libdns/libdns v0.2.1 // 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/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // 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/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mholt/acmez v1.2.0 // indirect github.com/mholt/acmez v1.2.0 // indirect
github.com/miekg/dns v1.1.55 // 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/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // 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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/pflag v1.0.5 // 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/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/atomic v1.11.0 // 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 go.uber.org/zap v1.24.0 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/mod v0.13.0 // indirect golang.org/x/mod v0.13.0 // indirect

14
go.sum
View file

@ -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/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 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI=
github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= 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 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= 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.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 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= 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/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 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= 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-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/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= 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.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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.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.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= 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/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 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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= 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/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 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 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.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= 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 h1:vWFKzR3DhGUDl5b4srhUjhDwjxkZAc4C7BFszpu0swI=
xorm.io/xorm v1.3.4/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo= xorm.io/xorm v1.3.4/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=

77
pipeline/errors/error.go Normal file
View file

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

View file

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

View file

@ -15,7 +15,6 @@
package constraint package constraint
import ( import (
"errors"
"fmt" "fmt"
"maps" "maps"
"path" "path"
@ -23,6 +22,7 @@ import (
"github.com/antonmedv/expr" "github.com/antonmedv/expr"
"github.com/bmatcuk/doublestar/v4" "github.com/bmatcuk/doublestar/v4"
"go.uber.org/multierr"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata" "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 { if err1 != nil && err2 != nil {
y, _ := yaml.Marshal(value) 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 return nil

View file

@ -1,4 +1,4 @@
// Copyright 2022 Woodpecker Authors // Copyright 2023 Woodpecker Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,21 +12,17 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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. func newLinterError(message, field string, isWarning bool) *errors.PipelineError {
type PipelineParseError struct { return &errors.PipelineError{
Err error Type: errors.PipelineErrorTypeLinter,
} Message: message,
Data: &errors.LinterErrorData{Field: field},
func (e PipelineParseError) Error() string { IsWarning: isWarning,
return e.Err.Error() }
}
func (e PipelineParseError) Is(err error) bool {
target1 := PipelineParseError{}
target2 := &target1
return errors.As(err, &target1) || errors.As(err, &target2)
} }

View file

@ -17,13 +17,10 @@ package linter
import ( import (
"fmt" "fmt"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types" "go.uber.org/multierr"
)
const ( "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/linter/schema"
blockClone uint8 = iota "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types"
blockPipeline
blockServices
) )
// A Linter lints a pipeline configuration. // A Linter lints a pipeline configuration.
@ -41,39 +38,59 @@ func New(opts ...Option) *Linter {
} }
// Lint lints the configuration. // 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 { 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.lint(c.Clone.ContainerList, blockClone); err != nil {
return err if err := l.lintContainers(c.Clone.ContainerList); err != nil {
linterErr = multierr.Append(linterErr, err)
} }
if err := l.lint(c.Steps.ContainerList, blockPipeline); err != nil { if err := l.lintContainers(c.Steps.ContainerList); err != nil {
return err linterErr = multierr.Append(linterErr, err)
} }
return l.lint(c.Services.ContainerList, blockServices) if err := l.lintContainers(c.Services.ContainerList); err != nil {
linterErr = multierr.Append(linterErr, err)
}
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 { for _, container := range containers {
if err := l.lintImage(container); err != nil { if err := l.lintImage(container); err != nil {
return err linterErr = multierr.Append(linterErr, err)
} }
if !l.trusted { if !l.trusted {
if err := l.lintTrusted(container); err != nil { if err := l.lintTrusted(container); err != nil {
return err linterErr = multierr.Append(linterErr, err)
} }
} }
if err := l.lintCommands(container); err != nil { 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 { func (l *Linter) lintImage(c *types.Container) error {
if len(c.Image) == 0 { 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 return nil
} }
@ -87,47 +104,73 @@ func (l *Linter) lintCommands(c *types.Container) error {
for key := range c.Settings { for key := range c.Settings {
keys = append(keys, key) 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 return nil
} }
func (l *Linter) lintTrusted(c *types.Container) error { func (l *Linter) lintTrusted(c *types.Container) error {
yamlPath := fmt.Sprintf("steps.%s", c.Name)
if c.Privileged { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { if len(c.Tmpfs) != 0 {
return fmt.Errorf("Insufficient privileges to use tmpfs") return newLinterError("Insufficient privileges to use tmpfs", yamlPath, false)
} }
return nil 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
}

View file

@ -17,6 +17,8 @@ package linter
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/woodpecker-ci/woodpecker/pipeline/errors"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml" "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml"
) )
@ -26,8 +28,6 @@ func TestLint(t *testing.T) {
steps: steps:
build: build:
image: docker image: docker
privileged: true
network_mode: host
volumes: volumes:
- /tmp:/tmp - /tmp:/tmp
commands: commands:
@ -35,8 +35,8 @@ steps:
- go test - go test
publish: publish:
image: plugins/docker image: plugins/docker
repo: foo/bar
settings: settings:
repo: foo/bar
foo: bar foo: bar
services: services:
redis: redis:
@ -47,8 +47,6 @@ services:
steps: steps:
- name: build - name: build
image: docker image: docker
privileged: true
network_mode: host
volumes: volumes:
- /tmp:/tmp - /tmp:/tmp
commands: commands:
@ -56,8 +54,8 @@ steps:
- go test - go test
- name: publish - name: publish
image: plugins/docker image: plugins/docker
repo: foo/bar
settings: settings:
repo: foo/bar
foo: bar foo: bar
`, `,
}, { }, {
@ -81,9 +79,10 @@ steps:
t.Run(testd.Title, func(t *testing.T) { t.Run(testd.Title, func(t *testing.T) {
conf, err := yaml.ParseString(testd.Data) conf, err := yaml.ParseString(testd.Data)
if err != nil { 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) t.Errorf("Expected lint returns no errors, got %q", err)
} }
}) })
@ -97,7 +96,7 @@ func TestLintErrors(t *testing.T) {
}{ }{
{ {
from: "", from: "",
want: "Invalid or missing pipeline section", want: "Invalid or missing steps section",
}, },
{ {
from: "steps: { build: { image: '' } }", from: "steps: { build: { image: '' } }",
@ -156,11 +155,19 @@ func TestLintErrors(t *testing.T) {
t.Fatalf("Cannot unmarshal yaml %q. Error: %s", test.from, err) t.Fatalf("Cannot unmarshal yaml %q. Error: %s", test.from, err)
} }
lerr := New().Lint(conf) lerr := New().Lint(test.from, conf)
if lerr == nil { if lerr == nil {
t.Errorf("Expected lint error for configuration %q", test.from) 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)
} }
} }

View file

@ -15,6 +15,7 @@
package schema package schema
import ( import (
"bytes"
_ "embed" _ "embed"
"fmt" "fmt"
"io" "io"
@ -62,3 +63,7 @@ func Lint(r io.Reader) ([]gojsonschema.ResultError, error) {
return nil, nil return nil, nil
} }
func LintString(s string) ([]gojsonschema.ResultError, error) {
return Lint(bytes.NewBufferString(s))
}

View file

@ -204,6 +204,10 @@
"additionalProperties": false, "additionalProperties": false,
"required": ["image"], "required": ["image"],
"properties": { "properties": {
"name": {
"description": "The name of the step. Can be used if using the array style steps list.",
"type": "string"
},
"image": { "image": {
"$ref": "#/definitions/step_image" "$ref": "#/definitions/step_image"
}, },

View file

@ -21,7 +21,7 @@ import (
"github.com/stretchr/testify/assert" "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) { func TestSchema(t *testing.T) {

View file

@ -17,7 +17,7 @@ package matrix
import ( import (
"strings" "strings"
pipeline "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml" "github.com/woodpecker-ci/woodpecker/pipeline/errors"
"codeberg.org/6543/xyaml" "codeberg.org/6543/xyaml"
) )
@ -116,7 +116,7 @@ func parse(raw []byte) (Matrix, error) {
Matrix map[string][]string Matrix map[string][]string
}{} }{}
if err := xyaml.Unmarshal(raw, &data); err != nil { 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 return data.Matrix, nil
} }
@ -129,7 +129,7 @@ func parseList(raw []byte) ([]Axis, error) {
}{} }{}
if err := xyaml.Unmarshal(raw, &data); err != nil { 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 return data.Matrix.Include, nil
} }

View file

@ -22,8 +22,11 @@ import (
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"go.uber.org/multierr"
backend_types "github.com/woodpecker-ci/woodpecker/pipeline/backend/types" 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" yaml_types "github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types"
forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types"
@ -60,9 +63,7 @@ type Item struct {
Config *backend_types.Config Config *backend_types.Config
} }
func (b *StepBuilder) Build() ([]*Item, error) { func (b *StepBuilder) Build() (items []*Item, errorsAndWarnings error) {
var items []*Item
b.Yamls = forge_types.SortByName(b.Yamls) b.Yamls = forge_types.SortByName(b.Yamls)
pidSequence := 1 pidSequence := 1
@ -86,9 +87,12 @@ func (b *StepBuilder) Build() ([]*Item, error) {
AxisID: i + 1, AxisID: i + 1,
} }
item, err := b.genItemForWorkflow(workflow, axis, string(y.Data)) item, err := b.genItemForWorkflow(workflow, axis, string(y.Data))
if err != nil { if err != nil && pipeline_errors.HasBlockingErrors(err) {
return nil, err return nil, err
} else if err != nil {
errorsAndWarnings = multierr.Append(errorsAndWarnings, err)
} }
if item == nil { if item == nil {
continue continue
} }
@ -104,13 +108,13 @@ func (b *StepBuilder) Build() ([]*Item, error) {
// check if at least one step can start if slice is not empty // check if at least one step can start if slice is not empty
if len(items) > 0 && !stepListContainsItemsToRun(items) { 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) workflowMetadata := frontend.MetadataFromStruct(b.Forge, b.Repo, b.Curr, b.Last, workflow, b.Link)
environ := b.environmentVariables(workflowMetadata, axis) environ := b.environmentVariables(workflowMetadata, axis)
@ -126,20 +130,21 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A
// substitute vars // substitute vars
substituted, err := frontend.EnvVarSubst(data, environ) substituted, err := frontend.EnvVarSubst(data, environ)
if err != nil { if err != nil {
return nil, err return nil, multierr.Append(errorsAndWarnings, err)
} }
// parse yaml pipeline // parse yaml pipeline
parsed, err := yaml.ParseString(substituted) parsed, err := yaml.ParseString(substituted)
if err != nil { if err != nil {
return nil, &yaml.PipelineParseError{Err: err} return nil, &errors.PipelineError{Message: err.Error(), Type: errors.PipelineErrorTypeCompiler}
} }
// lint pipeline // lint pipeline
if err := linter.New( errorsAndWarnings = multierr.Append(errorsAndWarnings, linter.New(
linter.WithTrusted(b.Repo.IsTrusted), linter.WithTrusted(b.Repo.IsTrusted),
).Lint(parsed); err != nil { ).Lint(substituted, parsed))
return nil, &yaml.PipelineParseError{Err: err} if pipeline_errors.HasBlockingErrors(errorsAndWarnings) {
return nil, errorsAndWarnings
} }
// checking if filtered. // checking if filtered.
@ -152,19 +157,19 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A
log.Debug().Str("pipeline", workflow.Name).Msg( log.Debug().Str("pipeline", workflow.Name).Msg(
"Pipeline config could not be parsed", "Pipeline config could not be parsed",
) )
return nil, err return nil, multierr.Append(errorsAndWarnings, err)
} }
ir, err := b.toInternalRepresentation(parsed, environ, workflowMetadata, workflow.ID) ir, err := b.toInternalRepresentation(parsed, environ, workflowMetadata, workflow.ID)
if err != nil { if err != nil {
return nil, err return nil, multierr.Append(errorsAndWarnings, err)
} }
if len(ir.Stages) == 0 { if len(ir.Stages) == 0 {
return nil, nil return nil, nil
} }
item := &Item{ item = &Item{
Workflow: workflow, Workflow: workflow,
Config: ir, Config: ir,
Labels: parsed.Labels, Labels: parsed.Labels,
@ -175,7 +180,7 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A
item.Labels = map[string]string{} item.Labels = map[string]string{}
} }
return item, nil return item, errorsAndWarnings
} }
func stepListContainsItemsToRun(items []*Item) bool { func stepListContainsItemsToRun(items []*Item) bool {

View file

@ -50,6 +50,7 @@ func TestGlobalEnvsubst(t *testing.T) {
steps: steps:
build: build:
image: ${IMAGE} image: ${IMAGE}
settings:
yyy: ${CI_COMMIT_MESSAGE} yyy: ${CI_COMMIT_MESSAGE}
`)}, `)},
}, },
@ -85,6 +86,7 @@ func TestMissingGlobalEnvsubst(t *testing.T) {
steps: steps:
build: build:
image: ${IMAGE} image: ${IMAGE}
settings:
yyy: ${CI_COMMIT_MESSAGE} yyy: ${CI_COMMIT_MESSAGE}
`)}, `)},
}, },
@ -117,12 +119,14 @@ bbb`,
steps: steps:
xxx: xxx:
image: scratch image: scratch
settings:
yyy: ${CI_COMMIT_MESSAGE} yyy: ${CI_COMMIT_MESSAGE}
`)}, `)},
{Data: []byte(` {Data: []byte(`
steps: steps:
build: build:
image: scratch image: scratch
settings:
yyy: ${CI_COMMIT_MESSAGE} yyy: ${CI_COMMIT_MESSAGE}
`)}, `)},
}, },
@ -335,7 +339,7 @@ func TestRootWhenFilter(t *testing.T) {
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
Repo: &model.Repo{}, Repo: &model.Repo{},
Curr: &model.Pipeline{Event: "tester"}, Curr: &model.Pipeline{Event: "tag"},
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
Secs: []*model.Secret{}, Secs: []*model.Secret{},
@ -345,7 +349,7 @@ func TestRootWhenFilter(t *testing.T) {
{Data: []byte(` {Data: []byte(`
when: when:
event: event:
- tester - tag
steps: steps:
xxx: xxx:
image: scratch image: scratch

View file

@ -52,15 +52,15 @@ func ValidateWebhookEvent(s WebhookEvent) error {
type StatusValue string // @name StatusValue type StatusValue string // @name StatusValue
const ( const (
StatusSkipped StatusValue = "skipped" StatusSkipped StatusValue = "skipped" // skipped as another step failed
StatusPending StatusValue = "pending" StatusPending StatusValue = "pending" // pending to be executed
StatusRunning StatusValue = "running" StatusRunning StatusValue = "running" // currently running
StatusSuccess StatusValue = "success" StatusSuccess StatusValue = "success" // successfully finished
StatusFailure StatusValue = "failure" StatusFailure StatusValue = "failure" // failed to finish (exit code != 0)
StatusKilled StatusValue = "killed" StatusKilled StatusValue = "killed" // killed by user
StatusError StatusValue = "error" StatusError StatusValue = "error" // error with the config / while parsing / some other system problem
StatusBlocked StatusValue = "blocked" StatusBlocked StatusValue = "blocked" // waiting for approval
StatusDeclined StatusValue = "declined" StatusDeclined StatusValue = "declined" // blocked and declined
) )
// SCMKind represent different version control systems // SCMKind represent different version control systems

View file

@ -15,6 +15,10 @@
package model package model
import (
"github.com/woodpecker-ci/woodpecker/pipeline/errors"
)
type Pipeline struct { type Pipeline struct {
ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"`
RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'pipeline_repo_id'"` RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'pipeline_repo_id'"`
@ -24,7 +28,7 @@ type Pipeline struct {
Parent int64 `json:"parent" xorm:"pipeline_parent"` Parent int64 `json:"parent" xorm:"pipeline_parent"`
Event WebhookEvent `json:"event" xorm:"pipeline_event"` Event WebhookEvent `json:"event" xorm:"pipeline_event"`
Status StatusValue `json:"status" xorm:"INDEX 'pipeline_status'"` Status StatusValue `json:"status" xorm:"INDEX 'pipeline_status'"`
Error string `json:"error" xorm:"LONGTEXT 'pipeline_error'"` Errors []*errors.PipelineError `json:"errors" xorm:"json 'pipeline_errors'"`
Enqueued int64 `json:"enqueued_at" xorm:"pipeline_enqueued"` Enqueued int64 `json:"enqueued_at" xorm:"pipeline_enqueued"`
Created int64 `json:"created_at" xorm:"pipeline_created"` Created int64 `json:"created_at" xorm:"pipeline_created"`
Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"` Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"`

View file

@ -20,6 +20,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/pipeline/errors"
forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types"
"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"
@ -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) currentPipeline, pipelineItems, err := createPipelineItems(ctx, store, currentPipeline, user, repo, yamls, nil)
if err != nil { if errors.HasBlockingErrors(err) {
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) log.Error().Err(err).Msg(msg)
return nil, err return nil, err
} else if err != nil {
currentPipeline.Errors = errors.GetPipelineErrors(err)
} }
currentPipeline, err = start(ctx, store, currentPipeline, user, repo, pipelineItems) currentPipeline, err = start(ctx, store, currentPipeline, user, repo, pipelineItems)

View file

@ -22,6 +22,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/pipeline/errors"
"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/model" "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 { 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) 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) 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") 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 { if len(pipelineItems) == 0 {
@ -118,11 +121,11 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
return pipeline, nil 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.Started = time.Now().Unix()
pipeline.Finished = pipeline.Started pipeline.Finished = pipeline.Started
pipeline.Status = model.StatusError pipeline.Status = model.StatusError
pipeline.Error = err pipeline.Errors = errors.GetPipelineErrors(err)
dbErr := _store.CreatePipeline(pipeline) dbErr := _store.CreatePipeline(pipeline)
if dbErr != nil { if dbErr != nil {
msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName) msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName)

View file

@ -22,6 +22,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/pipeline" "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/pipeline/frontend/yaml/compiler"
"github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server"
forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" 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, HTTPSProxy: server.Config.Pipeline.Proxy.HTTPS,
}, },
} }
pipelineItems, err := b.Build() return b.Build()
if err != nil {
return nil, err
}
return pipelineItems, nil
} }
func createPipelineItems(c context.Context, store store.Store, func createPipelineItems(c context.Context, store store.Store,
@ -102,12 +98,15 @@ func createPipelineItems(c context.Context, store store.Store,
} else { } else {
updatePipelineStatus(c, currentPipeline, repo, user) updatePipelineStatus(c, currentPipeline, repo, user)
} }
if pipeline_errors.HasBlockingErrors(err) {
return currentPipeline, nil, err return currentPipeline, nil, err
} }
}
currentPipeline = setPipelineStepsOnPipeline(currentPipeline, pipelineItems) currentPipeline = setPipelineStepsOnPipeline(currentPipeline, pipelineItems)
return currentPipeline, pipelineItems, nil return currentPipeline, pipelineItems, err
} }
// setPipelineStepsOnPipeline is the link between pipeline representation in "pipeline package" and server // setPipelineStepsOnPipeline is the link between pipeline representation in "pipeline package" and server

View file

@ -18,6 +18,7 @@ package pipeline
import ( import (
"time" "time"
"github.com/woodpecker-ci/woodpecker/pipeline/errors"
"github.com/woodpecker-ci/woodpecker/server/model" "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) { 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.Status = model.StatusError
pipeline.Started = time.Now().Unix() pipeline.Started = time.Now().Unix()
pipeline.Finished = pipeline.Started pipeline.Finished = pipeline.Started

View file

@ -90,10 +90,12 @@ func TestUpdateToStatusError(t *testing.T) {
now := time.Now().Unix() 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" { if len(pipeline.Errors) != 1 {
t.Errorf("Pipeline error not equals 'error' != '%s'", pipeline.Error) 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 { } else if model.StatusError != pipeline.Status {
t.Errorf("Pipeline status not equals '%s' != '%s'", model.StatusError, pipeline.Status) t.Errorf("Pipeline status not equals '%s' != '%s'", model.StatusError, pipeline.Status)
} else if now > pipeline.Started { } else if now > pipeline.Started {

View file

@ -22,7 +22,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml"
"github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server"
forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types"
"github.com/woodpecker-ci/woodpecker/server/model" "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) newPipeline, pipelineItems, err := createPipelineItems(ctx, store, newPipeline, user, repo, pipelineFiles, envs)
if err != nil { if err != nil {
if errors.Is(err, &yaml.PipelineParseError{}) { msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName)
return newPipeline, nil
}
msg := fmt.Sprintf("failure to createBuildItems for %s", repo.FullName)
log.Error().Err(err).Msg(msg) log.Error().Err(err).Msg(msg)
return nil, fmt.Errorf(msg) return nil, fmt.Errorf(msg)
} }
@ -136,6 +132,6 @@ func createNewOutOfOld(old *model.Pipeline) *model.Pipeline {
newPipeline.Started = 0 newPipeline.Started = 0
newPipeline.Finished = 0 newPipeline.Finished = 0
newPipeline.Enqueued = time.Now().UTC().Unix() newPipeline.Enqueued = time.Now().UTC().Unix()
newPipeline.Error = "" newPipeline.Errors = nil
return &newPipeline return &newPipeline
} }

View file

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

View file

@ -58,6 +58,7 @@ var migrationTasks = []*task{
&alterTableTasksUpdateColumnTaskDataType, &alterTableTasksUpdateColumnTaskDataType,
&alterTableConfigUpdateColumnConfigDataType, &alterTableConfigUpdateColumnConfigDataType,
&removePluginOnlyOptionFromSecretsTable, &removePluginOnlyOptionFromSecretsTable,
&convertToNewPipelineErrorFormat,
} }
var allBeans = []interface{}{ var allBeans = []interface{}{

View file

@ -230,7 +230,6 @@
"config": "Config", "config": "Config",
"files": "Changed files ({files})", "files": "Changed files ({files})",
"no_files": "No files have been changed.", "no_files": "No files have been changed.",
"execution_error": "Execution error",
"no_pipelines": "No pipelines have been started yet.", "no_pipelines": "No pipelines have been started yet.",
"no_pipeline_steps": "No pipeline steps available!", "no_pipeline_steps": "No pipeline steps available!",
"step_not_started": "This step hasn't started yet.", "step_not_started": "This step hasn't started yet.",
@ -281,7 +280,11 @@
"error": "error", "error": "error",
"failure": "failure", "failure": "failure",
"killed": "killed" "killed": "killed"
} },
"errors": "Errors ({count})",
"warnings": "Warnings ({count})",
"show_errors": "Show errors",
"we_got_some_errors": "Oh no, we got some errors!"
} }
}, },
"org": { "org": {

View file

@ -9,12 +9,10 @@ import { computed, onMounted, ref } from 'vue';
import { Tab, useTabsClient } from '~/compositions/useTabs'; import { Tab, useTabsClient } from '~/compositions/useTabs';
export interface Props { const props = defineProps<{
id?: string; id?: string;
title: string; title: string;
} }>();
const props = defineProps<Props>();
const { tabs, activeTab } = useTabsClient(); const { tabs, activeTab } = useTabsClient();
const tab = ref<Tab>(); const tab = ref<Tab>();

View file

@ -1,9 +0,0 @@
<template>
<div class="w-full md:px-4 text-wp-text-100">
<div
class="flex flex-col px-4 py-8 gap-4 justify-center items-center text-center flex-shrink-0 rounded-md border bg-wp-background-100 border-wp-background-400 dark:bg-wp-background-200"
>
<slot />
</div>
</div>
</template>

View file

@ -1,5 +1,12 @@
import { WebhookEvents } from './webhook'; import { WebhookEvents } from './webhook';
export type PipelineError = {
type: string;
message: string;
data?: unknown;
is_warning: boolean;
};
// A pipeline for a repository. // A pipeline for a repository.
export type Pipeline = { export type Pipeline = {
id: number; id: number;
@ -15,7 +22,7 @@ export type Pipeline = {
// The current status of the pipeline. // The current status of the pipeline.
status: PipelineStatus; status: PipelineStatus;
error: string; errors?: PipelineError[];
// When the pipeline request was received. // When the pipeline request was received.
created_at: number; created_at: number;

View file

@ -88,6 +88,12 @@ const routes: RouteRecordRaw[] = [
component: (): Component => import('~/views/repo/pipeline/PipelineConfig.vue'), component: (): Component => import('~/views/repo/pipeline/PipelineConfig.vue'),
props: true, props: true,
}, },
{
path: 'errors',
name: 'repo-pipeline-errors',
component: (): Component => import('~/views/repo/pipeline/PipelineErrors.vue'),
props: true,
},
], ],
}, },
{ {

View file

@ -2,22 +2,36 @@
<Container full-width class="flex flex-col flex-grow md:min-h-xs"> <Container full-width class="flex flex-col flex-grow md:min-h-xs">
<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?.workflows?.length || 0 > 0" v-if="pipeline?.workflows && pipeline?.workflows?.length > 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"
/> />
<div class="flex flex-grow relative"> <div class="flex items-start justify-center flex-grow relative">
<PipelineInfo v-if="error"> <Container v-if="selectedStep?.error" class="py-0">
<Panel>
<div class="flex flex-col items-center gap-4">
<Icon name="status-error" class="w-16 h-16 text-wp-state-error-100" /> <Icon name="status-error" class="w-16 h-16 text-wp-state-error-100" />
<div class="flex flex-wrap items-center justify-center gap-2 text-xl"> <span class="text-xl">{{ $t('repo.pipeline.we_got_some_errors') }}</span>
<span class="capitalize">{{ $t('repo.pipeline.execution_error') }}:</span> <span class="whitespace-pre">{{ selectedStep?.error }}</span>
<span>{{ error }}</span>
</div> </div>
</PipelineInfo> </Panel>
</Container>
<PipelineInfo v-else-if="pipeline.status === 'blocked'"> <Container v-else-if="pipeline.errors?.some((e) => !e.is_warning)" class="py-0">
<Panel>
<div class="flex flex-col items-center gap-4">
<Icon name="status-error" class="w-16 h-16 text-wp-state-error-100" />
<span class="text-xl">{{ $t('repo.pipeline.we_got_some_errors') }}</span>
<Button color="red" :text="$t('repo.pipeline.show_errors')" :to="{ name: 'repo-pipeline-errors' }" />
</div>
</Panel>
</Container>
<Container v-else-if="pipeline.status === 'blocked'" class="py-0">
<Panel>
<div class="flex flex-col items-center gap-4">
<Icon name="status-blocked" class="w-16 h-16" /> <Icon name="status-blocked" class="w-16 h-16" />
<span class="text-xl">{{ $t('repo.pipeline.protected.awaits') }}</span> <span class="text-xl">{{ $t('repo.pipeline.protected.awaits') }}</span>
<div v-if="repoPermissions.push" class="flex gap-2 flex-wrap items-center justify-center"> <div v-if="repoPermissions.push" class="flex gap-2 flex-wrap items-center justify-center">
@ -41,15 +55,21 @@
@click="declinePipeline" @click="declinePipeline"
/> />
</div> </div>
</PipelineInfo> </div>
</Panel>
</Container>
<PipelineInfo v-else-if="pipeline.status === 'declined'"> <Container v-else-if="pipeline.status === 'declined'" class="py-0">
<Icon name="status-blocked" class="w-16 h-16" /> <Panel>
<div class="flex flex-col items-center gap-4">
<Icon name="status-declined" class="w-16 h-16 text-wp-state-error-100" />
<p class="text-xl">{{ $t('repo.pipeline.protected.declined') }}</p> <p class="text-xl">{{ $t('repo.pipeline.protected.declined') }}</p>
</PipelineInfo> </div>
</Panel>
</Container>
<PipelineLog <PipelineLog
v-else-if="selectedStepId" v-else-if="selectedStepId !== null"
v-model:step-id="selectedStepId" v-model:step-id="selectedStepId"
:pipeline="pipeline" :pipeline="pipeline"
class="fixed top-0 left-0 w-full h-full md:absolute" class="fixed top-0 left-0 w-full h-full md:absolute"
@ -74,7 +94,7 @@ import { useAsyncAction } from '~/compositions/useAsyncAction';
import useConfig from '~/compositions/useConfig'; import useConfig from '~/compositions/useConfig';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import usePipeline from '~/compositions/usePipeline'; import usePipeline from '~/compositions/usePipeline';
import { Pipeline, PipelineStep, Repo, RepoPermissions } from '~/lib/api/types'; import { Pipeline, Repo, RepoPermissions } from '~/lib/api/types';
import { findStep } from '~/utils/helpers'; import { findStep } from '~/utils/helpers';
const props = defineProps<{ const props = defineProps<{
@ -96,22 +116,13 @@ if (!repo || !repoPermissions || !pipeline) {
const stepId = toRef(props, 'stepId'); const stepId = toRef(props, 'stepId');
const defaultStepId = computed(() => { const defaultStepId = computed(() => pipeline.value?.workflows?.[0].children?.[0].pid ?? null);
if (!pipeline.value || !pipeline.value.workflows || !pipeline.value.workflows[0].children) {
return null;
}
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?.workflows?.reduce( const step = pipeline.value?.workflows?.find((p) => p.children?.find((c) => c.pid === id));
(prev, p) => prev || p.children?.find((c) => c.pid === id),
undefined as PipelineStep | undefined,
);
if (step) { if (step) {
return step.pid; return step.pid;
} }
@ -128,7 +139,7 @@ const selectedStepId = computed({
return null; return null;
}, },
set(_selectedStepId: number | null) { set(_selectedStepId: number | null) {
if (!_selectedStepId) { if (_selectedStepId === null) {
router.replace({ params: { ...route.params, stepId: '' } }); router.replace({ params: { ...route.params, stepId: '' } });
return; return;
} }
@ -141,7 +152,6 @@ const { forge } = useConfig();
const { message } = usePipeline(pipeline); const { message } = usePipeline(pipeline);
const selectedStep = computed(() => findStep(pipeline.value.workflows || [], selectedStepId.value || -1)); 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 () => { const { doSubmit: approvePipeline, isLoading: isApprovingPipeline } = useAsyncAction(async () => {
if (!repo) { if (!repo) {

View file

@ -0,0 +1,31 @@
<template>
<Panel>
<div class="grid justify-center gap-2 text-left grid-3-1">
<template v-for="(error, i) in pipeline.errors" :key="i">
<span>{{ error.is_warning ? '⚠️' : '❌' }}</span>
<span>[{{ error.type }}]</span>
<span v-if="error.type === 'linter'" class="underline">{{ (error.data as any)?.field }}</span>
<span v-else />
<span class="ml-4">{{ error.message }}</span>
</template>
</div>
</Panel>
</template>
<script lang="ts" setup>
import { inject, Ref } from 'vue';
import Panel from '~/components/layout/Panel.vue';
import { Pipeline } from '~/lib/api/types';
const pipeline = inject<Ref<Pipeline>>('pipeline');
if (!pipeline) {
throw new Error('Unexpected: "pipeline" should be provided at this place');
}
</script>
<style scoped>
.grid-3-1 {
grid-template-columns: auto auto auto 1fr;
}
</style>

View file

@ -1,6 +1,6 @@
<template> <template>
<template v-if="pipeline && repo">
<Scaffold <Scaffold
v-if="pipeline && repo"
v-model:activeTab="activeTab" v-model:activeTab="activeTab"
enable-tabs enable-tabs
disable-hash-mode disable-hash-mode
@ -16,9 +16,7 @@
<PipelineStatusIcon :status="pipeline.status" class="flex flex-shrink-0" /> <PipelineStatusIcon :status="pipeline.status" class="flex flex-shrink-0" />
<span class="flex-shrink-0 text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span> <span class="flex-shrink-0 text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span>
<span class="hidden md:inline-block">-</span> <span class="hidden md:inline-block">-</span>
<span class="min-w-0 whitespace-nowrap overflow-hidden overflow-ellipsis" :title="message">{{ <span class="min-w-0 whitespace-nowrap overflow-hidden overflow-ellipsis" :title="message">{{ title }}</span>
title
}}</span>
</div> </div>
<template v-if="repoPermissions.push && pipeline.status !== 'declined' && pipeline.status !== 'blocked'"> <template v-if="repoPermissions.push && pipeline.status !== 'declined' && pipeline.status !== 'blocked'">
@ -66,6 +64,21 @@
</template> </template>
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" /> <Tab id="tasks" :title="$t('repo.pipeline.tasks')" />
<Tab
v-if="pipeline.errors && pipeline.errors.length > 0"
id="errors"
:title="
pipeline.errors.some((e) => !e.is_warning)
? '❌ ' +
$t('repo.pipeline.errors', {
count: pipeline.errors?.length,
})
: '⚠️ ' +
$t('repo.pipeline.warnings', {
count: pipeline.errors?.length,
})
"
/>
<Tab id="config" :title="$t('repo.pipeline.config')" /> <Tab id="config" :title="$t('repo.pipeline.config')" />
<Tab <Tab
v-if=" v-if="
@ -74,11 +87,11 @@
pipeline.changed_files.length > 0 pipeline.changed_files.length > 0
" "
id="changed-files" id="changed-files"
:title="$t('repo.pipeline.files', { files: pipeline.changed_files.length })" :title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })"
/> />
<router-view /> <router-view />
</Scaffold> </Scaffold>
</template>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -182,6 +195,10 @@ const activeTab = computed({
return 'config'; return 'config';
} }
if (route.name === 'repo-pipeline-errors') {
return 'errors';
}
return 'tasks'; return 'tasks';
}, },
set(tab: string) { set(tab: string) {
@ -196,6 +213,10 @@ const activeTab = computed({
if (tab === 'config') { if (tab === 'config') {
router.replace({ name: 'repo-pipeline-config' }); router.replace({ name: 'repo-pipeline-config' });
} }
if (tab === 'errors') {
router.replace({ name: 'repo-pipeline-errors' });
}
}, },
}); });

View file

@ -60,6 +60,13 @@ type (
PipelineCounter *int `json:"pipeline_counter,omitempty"` PipelineCounter *int `json:"pipeline_counter,omitempty"`
} }
PipelineError struct {
Type string `json:"type"`
Message string `json:"message"`
IsWarning bool `json:"is_warning"`
Data interface{} `json:"data"`
}
// Pipeline defines a pipeline object. // Pipeline defines a pipeline object.
Pipeline struct { Pipeline struct {
ID int64 `json:"id"` ID int64 `json:"id"`
@ -67,7 +74,7 @@ type (
Parent int64 `json:"parent"` Parent int64 `json:"parent"`
Event string `json:"event"` Event string `json:"event"`
Status string `json:"status"` Status string `json:"status"`
Error string `json:"error"` Errors PipelineError `json:"errors"`
Enqueued int64 `json:"enqueued_at"` Enqueued int64 `json:"enqueued_at"`
Created int64 `json:"created_at"` Created int64 `json:"created_at"`
Updated int64 `json:"updated_at"` Updated int64 `json:"updated_at"`