mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-22 08:27:06 +00:00
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:
parent
4c4fdff5f7
commit
5ff006614f
55 changed files with 912 additions and 342 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
7
go.mod
|
@ -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
14
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/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
77
pipeline/errors/error.go
Normal 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
|
||||||
|
}
|
158
pipeline/errors/error_test.go
Normal file
158
pipeline/errors/error_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// 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.
|
||||||
// You may obtain a copy of the License at
|
// 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
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// 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
|
// 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)
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -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"
|
||||||
},
|
},
|
|
@ -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) {
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -50,7 +50,8 @@ func TestGlobalEnvsubst(t *testing.T) {
|
||||||
steps:
|
steps:
|
||||||
build:
|
build:
|
||||||
image: ${IMAGE}
|
image: ${IMAGE}
|
||||||
yyy: ${CI_COMMIT_MESSAGE}
|
settings:
|
||||||
|
yyy: ${CI_COMMIT_MESSAGE}
|
||||||
`)},
|
`)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -85,7 +86,8 @@ func TestMissingGlobalEnvsubst(t *testing.T) {
|
||||||
steps:
|
steps:
|
||||||
build:
|
build:
|
||||||
image: ${IMAGE}
|
image: ${IMAGE}
|
||||||
yyy: ${CI_COMMIT_MESSAGE}
|
settings:
|
||||||
|
yyy: ${CI_COMMIT_MESSAGE}
|
||||||
`)},
|
`)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -117,13 +119,15 @@ bbb`,
|
||||||
steps:
|
steps:
|
||||||
xxx:
|
xxx:
|
||||||
image: scratch
|
image: scratch
|
||||||
yyy: ${CI_COMMIT_MESSAGE}
|
settings:
|
||||||
|
yyy: ${CI_COMMIT_MESSAGE}
|
||||||
`)},
|
`)},
|
||||||
{Data: []byte(`
|
{Data: []byte(`
|
||||||
steps:
|
steps:
|
||||||
build:
|
build:
|
||||||
image: scratch
|
image: scratch
|
||||||
yyy: ${CI_COMMIT_MESSAGE}
|
settings:
|
||||||
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -15,40 +15,44 @@
|
||||||
|
|
||||||
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'"`
|
||||||
Number int64 `json:"number" xorm:"UNIQUE(s) 'pipeline_number'"`
|
Number int64 `json:"number" xorm:"UNIQUE(s) 'pipeline_number'"`
|
||||||
Author string `json:"author" xorm:"INDEX 'pipeline_author'"`
|
Author string `json:"author" xorm:"INDEX 'pipeline_author'"`
|
||||||
ConfigID int64 `json:"-" xorm:"pipeline_config_id"`
|
ConfigID int64 `json:"-" xorm:"pipeline_config_id"`
|
||||||
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'"`
|
||||||
Started int64 `json:"started_at" xorm:"pipeline_started"`
|
Started int64 `json:"started_at" xorm:"pipeline_started"`
|
||||||
Finished int64 `json:"finished_at" xorm:"pipeline_finished"`
|
Finished int64 `json:"finished_at" xorm:"pipeline_finished"`
|
||||||
Deploy string `json:"deploy_to" xorm:"pipeline_deploy"`
|
Deploy string `json:"deploy_to" xorm:"pipeline_deploy"`
|
||||||
Commit string `json:"commit" xorm:"pipeline_commit"`
|
Commit string `json:"commit" xorm:"pipeline_commit"`
|
||||||
Branch string `json:"branch" xorm:"pipeline_branch"`
|
Branch string `json:"branch" xorm:"pipeline_branch"`
|
||||||
Ref string `json:"ref" xorm:"pipeline_ref"`
|
Ref string `json:"ref" xorm:"pipeline_ref"`
|
||||||
Refspec string `json:"refspec" xorm:"pipeline_refspec"`
|
Refspec string `json:"refspec" xorm:"pipeline_refspec"`
|
||||||
CloneURL string `json:"clone_url" xorm:"pipeline_clone_url"`
|
CloneURL string `json:"clone_url" xorm:"pipeline_clone_url"`
|
||||||
Title string `json:"title" xorm:"pipeline_title"`
|
Title string `json:"title" xorm:"pipeline_title"`
|
||||||
Message string `json:"message" xorm:"TEXT 'pipeline_message'"`
|
Message string `json:"message" xorm:"TEXT 'pipeline_message'"`
|
||||||
Timestamp int64 `json:"timestamp" xorm:"pipeline_timestamp"`
|
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
|
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"`
|
Avatar string `json:"author_avatar" xorm:"pipeline_avatar"`
|
||||||
Email string `json:"author_email" xorm:"pipeline_email"`
|
Email string `json:"author_email" xorm:"pipeline_email"`
|
||||||
Link string `json:"link_url" xorm:"pipeline_link"`
|
Link string `json:"link_url" xorm:"pipeline_link"`
|
||||||
Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"`
|
Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"`
|
||||||
Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"`
|
Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"`
|
||||||
Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"`
|
Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"`
|
||||||
ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"`
|
ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"`
|
||||||
AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"`
|
AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"`
|
||||||
PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"`
|
PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"`
|
||||||
} // @name Pipeline
|
} // @name Pipeline
|
||||||
|
|
||||||
// TableName return database table name for xorm
|
// TableName return database table name for xorm
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
return currentPipeline, nil, err
|
|
||||||
|
if pipeline_errors.HasBlockingErrors(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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
},
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ var migrationTasks = []*task{
|
||||||
&alterTableTasksUpdateColumnTaskDataType,
|
&alterTableTasksUpdateColumnTaskDataType,
|
||||||
&alterTableConfigUpdateColumnConfigDataType,
|
&alterTableConfigUpdateColumnConfigDataType,
|
||||||
&removePluginOnlyOptionFromSecretsTable,
|
&removePluginOnlyOptionFromSecretsTable,
|
||||||
|
&convertToNewPipelineErrorFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
var allBeans = []interface{}{
|
var allBeans = []interface{}{
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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>
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,54 +2,74 @@
|
||||||
<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">
|
||||||
<Icon name="status-error" class="w-16 h-16 text-wp-state-error-100" />
|
<Panel>
|
||||||
<div class="flex flex-wrap items-center justify-center gap-2 text-xl">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<span class="capitalize">{{ $t('repo.pipeline.execution_error') }}:</span>
|
<Icon name="status-error" class="w-16 h-16 text-wp-state-error-100" />
|
||||||
<span>{{ error }}</span>
|
<span class="text-xl">{{ $t('repo.pipeline.we_got_some_errors') }}</span>
|
||||||
</div>
|
<span class="whitespace-pre">{{ selectedStep?.error }}</span>
|
||||||
</PipelineInfo>
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<PipelineInfo v-else-if="pipeline.status === 'blocked'">
|
<Container v-else-if="pipeline.errors?.some((e) => !e.is_warning)" class="py-0">
|
||||||
<Icon name="status-blocked" class="w-16 h-16" />
|
<Panel>
|
||||||
<span class="text-xl">{{ $t('repo.pipeline.protected.awaits') }}</span>
|
<div class="flex flex-col items-center gap-4">
|
||||||
<div v-if="repoPermissions.push" class="flex gap-2 flex-wrap items-center justify-center">
|
<Icon name="status-error" class="w-16 h-16 text-wp-state-error-100" />
|
||||||
<Button
|
<span class="text-xl">{{ $t('repo.pipeline.we_got_some_errors') }}</span>
|
||||||
color="blue"
|
<Button color="red" :text="$t('repo.pipeline.show_errors')" :to="{ name: 'repo-pipeline-errors' }" />
|
||||||
:start-icon="forge ?? 'repo'"
|
</div>
|
||||||
:text="$t('repo.pipeline.protected.review')"
|
</Panel>
|
||||||
:to="pipeline.link_url"
|
</Container>
|
||||||
:title="message"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
color="green"
|
|
||||||
:text="$t('repo.pipeline.protected.approve')"
|
|
||||||
:is-loading="isApprovingPipeline"
|
|
||||||
@click="approvePipeline"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
color="red"
|
|
||||||
:text="$t('repo.pipeline.protected.decline')"
|
|
||||||
:is-loading="isDecliningPipeline"
|
|
||||||
@click="declinePipeline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PipelineInfo>
|
|
||||||
|
|
||||||
<PipelineInfo v-else-if="pipeline.status === 'declined'">
|
<Container v-else-if="pipeline.status === 'blocked'" class="py-0">
|
||||||
<Icon name="status-blocked" class="w-16 h-16" />
|
<Panel>
|
||||||
<p class="text-xl">{{ $t('repo.pipeline.protected.declined') }}</p>
|
<div class="flex flex-col items-center gap-4">
|
||||||
</PipelineInfo>
|
<Icon name="status-blocked" class="w-16 h-16" />
|
||||||
|
<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">
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
:start-icon="forge ?? 'repo'"
|
||||||
|
:text="$t('repo.pipeline.protected.review')"
|
||||||
|
:to="pipeline.link_url"
|
||||||
|
:title="message"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
:text="$t('repo.pipeline.protected.approve')"
|
||||||
|
:is-loading="isApprovingPipeline"
|
||||||
|
@click="approvePipeline"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
:text="$t('repo.pipeline.protected.decline')"
|
||||||
|
:is-loading="isDecliningPipeline"
|
||||||
|
@click="declinePipeline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container v-else-if="pipeline.status === 'declined'" class="py-0">
|
||||||
|
<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>
|
||||||
|
</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) {
|
||||||
|
|
31
web/src/views/repo/pipeline/PipelineErrors.vue
Normal file
31
web/src/views/repo/pipeline/PipelineErrors.vue
Normal 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>
|
|
@ -1,84 +1,97 @@
|
||||||
<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
|
||||||
:go-back="goBack"
|
:go-back="goBack"
|
||||||
:fluid-content="activeTab === 'tasks'"
|
:fluid-content="activeTab === 'tasks'"
|
||||||
full-width-header
|
full-width-header
|
||||||
>
|
>
|
||||||
<template #title>{{ repo.full_name }}</template>
|
<template #title>{{ repo.full_name }}</template>
|
||||||
|
|
||||||
<template #titleActions>
|
<template #titleActions>
|
||||||
<div class="flex md:items-center flex-col gap-2 md:flex-row md:justify-between min-w-0">
|
<div class="flex md:items-center flex-col gap-2 md:flex-row md:justify-between min-w-0">
|
||||||
<div class="flex content-start gap-2 min-w-0">
|
<div class="flex content-start gap-2 min-w-0">
|
||||||
<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>
|
|
||||||
|
|
||||||
<template v-if="repoPermissions.push && pipeline.status !== 'declined' && pipeline.status !== 'blocked'">
|
|
||||||
<div class="flex content-start gap-x-2">
|
|
||||||
<Button
|
|
||||||
v-if="pipeline.status === 'pending' || pipeline.status === 'running'"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
:text="$t('repo.pipeline.actions.cancel')"
|
|
||||||
:is-loading="isCancelingPipeline"
|
|
||||||
@click="cancelPipeline"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
class="flex-shrink-0"
|
|
||||||
:text="$t('repo.pipeline.actions.restart')"
|
|
||||||
:is-loading="isRestartingPipeline"
|
|
||||||
@click="restartPipeline"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
v-if="pipeline.status === 'success'"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
:text="$t('repo.pipeline.actions.deploy')"
|
|
||||||
@click="showDeployPipelinePopup = true"
|
|
||||||
/>
|
|
||||||
<DeployPipelinePopup
|
|
||||||
:pipeline-number="pipelineId"
|
|
||||||
:open="showDeployPipelinePopup"
|
|
||||||
@close="showDeployPipelinePopup = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #tabActions>
|
<template v-if="repoPermissions.push && pipeline.status !== 'declined' && pipeline.status !== 'blocked'">
|
||||||
<div class="flex gap-x-4">
|
<div class="flex content-start gap-x-2">
|
||||||
<div class="flex space-x-1 items-center flex-shrink-0" :title="created">
|
<Button
|
||||||
<Icon name="since" />
|
v-if="pipeline.status === 'pending' || pipeline.status === 'running'"
|
||||||
<span>{{ since }}</span>
|
class="flex-shrink-0"
|
||||||
</div>
|
:text="$t('repo.pipeline.actions.cancel')"
|
||||||
<div class="flex space-x-1 items-center flex-shrink-0">
|
:is-loading="isCancelingPipeline"
|
||||||
<Icon name="duration" />
|
@click="cancelPipeline"
|
||||||
<span>{{ duration }}</span>
|
/>
|
||||||
|
<Button
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:text="$t('repo.pipeline.actions.restart')"
|
||||||
|
:is-loading="isRestartingPipeline"
|
||||||
|
@click="restartPipeline"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="pipeline.status === 'success'"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
:text="$t('repo.pipeline.actions.deploy')"
|
||||||
|
@click="showDeployPipelinePopup = true"
|
||||||
|
/>
|
||||||
|
<DeployPipelinePopup
|
||||||
|
:pipeline-number="pipelineId"
|
||||||
|
:open="showDeployPipelinePopup"
|
||||||
|
@close="showDeployPipelinePopup = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tabActions>
|
||||||
|
<div class="flex gap-x-4">
|
||||||
|
<div class="flex space-x-1 items-center flex-shrink-0" :title="created">
|
||||||
|
<Icon name="since" />
|
||||||
|
<span>{{ since }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="flex space-x-1 items-center flex-shrink-0">
|
||||||
|
<Icon name="duration" />
|
||||||
|
<span>{{ duration }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" />
|
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" />
|
||||||
<Tab id="config" :title="$t('repo.pipeline.config')" />
|
<Tab
|
||||||
<Tab
|
v-if="pipeline.errors && pipeline.errors.length > 0"
|
||||||
v-if="
|
id="errors"
|
||||||
(pipeline.event === 'push' || pipeline.event === 'pull_request') &&
|
:title="
|
||||||
pipeline.changed_files &&
|
pipeline.errors.some((e) => !e.is_warning)
|
||||||
pipeline.changed_files.length > 0
|
? '❌ ' +
|
||||||
"
|
$t('repo.pipeline.errors', {
|
||||||
id="changed-files"
|
count: pipeline.errors?.length,
|
||||||
:title="$t('repo.pipeline.files', { files: pipeline.changed_files.length })"
|
})
|
||||||
/>
|
: '⚠️ ' +
|
||||||
<router-view />
|
$t('repo.pipeline.warnings', {
|
||||||
</Scaffold>
|
count: pipeline.errors?.length,
|
||||||
</template>
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Tab id="config" :title="$t('repo.pipeline.config')" />
|
||||||
|
<Tab
|
||||||
|
v-if="
|
||||||
|
(pipeline.event === 'push' || pipeline.event === 'pull_request') &&
|
||||||
|
pipeline.changed_files &&
|
||||||
|
pipeline.changed_files.length > 0
|
||||||
|
"
|
||||||
|
id="changed-files"
|
||||||
|
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<router-view />
|
||||||
|
</Scaffold>
|
||||||
</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' });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -60,36 +60,43 @@ 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"`
|
||||||
Number int64 `json:"number"`
|
Number int64 `json:"number"`
|
||||||
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"`
|
||||||
Started int64 `json:"started_at"`
|
Started int64 `json:"started_at"`
|
||||||
Finished int64 `json:"finished_at"`
|
Finished int64 `json:"finished_at"`
|
||||||
Deploy string `json:"deploy_to"`
|
Deploy string `json:"deploy_to"`
|
||||||
Commit string `json:"commit"`
|
Commit string `json:"commit"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
Ref string `json:"ref"`
|
Ref string `json:"ref"`
|
||||||
Refspec string `json:"refspec"`
|
Refspec string `json:"refspec"`
|
||||||
CloneURL string `json:"clone_url"`
|
CloneURL string `json:"clone_url"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
Sender string `json:"sender"`
|
Sender string `json:"sender"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Avatar string `json:"author_avatar"`
|
Avatar string `json:"author_avatar"`
|
||||||
Email string `json:"author_email"`
|
Email string `json:"author_email"`
|
||||||
Link string `json:"link_url"`
|
Link string `json:"link_url"`
|
||||||
Reviewer string `json:"reviewed_by"`
|
Reviewer string `json:"reviewed_by"`
|
||||||
Reviewed int64 `json:"reviewed_at"`
|
Reviewed int64 `json:"reviewed_at"`
|
||||||
Workflows []*Workflow `json:"workflows,omitempty"`
|
Workflows []*Workflow `json:"workflows,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow represents a workflow in the pipeline.
|
// Workflow represents a workflow in the pipeline.
|
||||||
|
|
Loading…
Reference in a new issue