mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-04-15 01:41:56 +00:00
Merge branch 'main' into pr/4861
This commit is contained in:
+19
-4
@@ -44,6 +44,8 @@ when:
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
path: *when_path
|
||||
- event: pull_request_metadata
|
||||
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
|
||||
|
||||
steps:
|
||||
vendor:
|
||||
@@ -52,7 +54,7 @@ steps:
|
||||
commands:
|
||||
- go mod vendor
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: [pull_request, pull_request_metadata]
|
||||
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
|
||||
- event: pull_request
|
||||
path: *when_path
|
||||
@@ -72,7 +74,7 @@ steps:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm build
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: [pull_request, pull_request_metadata]
|
||||
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
|
||||
- event: pull_request
|
||||
path: *when_path
|
||||
@@ -95,7 +97,7 @@ steps:
|
||||
PLATFORMS: linux|amd64
|
||||
TAGS: sqlite sqlite_unlock_notify netgo
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: [pull_request, pull_request_metadata]
|
||||
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
|
||||
- event: pull_request
|
||||
path: *when_path
|
||||
@@ -131,7 +133,7 @@ steps:
|
||||
logins: *publish_logins
|
||||
when: &when-preview
|
||||
evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "build_pr_images"'
|
||||
event: pull_request
|
||||
event: [pull_request, pull_request_metadata]
|
||||
|
||||
build-server-dryrun:
|
||||
depends_on:
|
||||
@@ -305,6 +307,19 @@ steps:
|
||||
# C L I #
|
||||
#########
|
||||
|
||||
build-cli-alpine-preview:
|
||||
depends_on:
|
||||
- vendor
|
||||
image: *buildx_plugin
|
||||
settings:
|
||||
repo: woodpeckerci/woodpecker-cli
|
||||
dockerfile: docker/Dockerfile.cli.alpine.multiarch.rootless
|
||||
platforms: *platforms_preview
|
||||
tag: pull_${CI_COMMIT_PULL_REQUEST}-alpine
|
||||
build_args: *build_args
|
||||
logins: *publish_logins
|
||||
when: *when-preview
|
||||
|
||||
build-cli-dryrun:
|
||||
depends_on:
|
||||
- vendor
|
||||
|
||||
@@ -151,7 +151,7 @@ services:
|
||||
when: *when
|
||||
|
||||
mysql:
|
||||
image: docker.io/mysql:9.4.0
|
||||
image: docker.io/mysql:9.5.0
|
||||
ports: ['3306']
|
||||
environment:
|
||||
MYSQL_DATABASE: test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# renovate: datasource=github-releases depName=mvdan/gofumpt
|
||||
GOFUMPT_VERSION := v0.9.1
|
||||
GOFUMPT_VERSION := v0.9.2
|
||||
# renovate: datasource=github-releases depName=golangci/golangci-lint
|
||||
GOLANGCI_LINT_VERSION := v2.5.0
|
||||
# renovate: datasource=docker depName=docker.io/techknowlogick/xgo
|
||||
|
||||
+14
-9
@@ -36,9 +36,9 @@ import (
|
||||
"go.woodpecker-ci.org/woodpecker/v3/cli/lint"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/docker"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/kubernetes"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/local"
|
||||
backend_docker "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/docker"
|
||||
backend_kubernetes "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/kubernetes"
|
||||
backend_local "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/local"
|
||||
backend_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml"
|
||||
@@ -56,13 +56,13 @@ var Command = &cli.Command{
|
||||
Usage: "execute a local pipeline",
|
||||
ArgsUsage: "[path/to/.woodpecker.yaml]",
|
||||
Action: run,
|
||||
Flags: utils.MergeSlices(flags, docker.Flags, kubernetes.Flags, local.Flags),
|
||||
Flags: utils.MergeSlices(flags, backend_docker.Flags, backend_kubernetes.Flags, backend_local.Flags),
|
||||
}
|
||||
|
||||
var backends = []backend_types.Backend{
|
||||
kubernetes.New(),
|
||||
docker.New(),
|
||||
local.New(),
|
||||
backend_kubernetes.New(),
|
||||
backend_docker.New(),
|
||||
backend_local.New(),
|
||||
}
|
||||
|
||||
func run(ctx context.Context, c *cli.Command) error {
|
||||
@@ -77,7 +77,7 @@ func execDir(ctx context.Context, c *cli.Command, dir string) error {
|
||||
} else {
|
||||
repoPath, _ = filepath.Abs(filepath.Dir(dir))
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if runtime.GOOS == "windows" && c.String("backend-engine") != "local" {
|
||||
repoPath = convertPathForWindows(repoPath)
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func execFile(ctx context.Context, c *cli.Command, file string) error {
|
||||
} else {
|
||||
repoPath, _ = filepath.Abs(filepath.Dir(file))
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if runtime.GOOS == "windows" && c.String("backend-engine") != "local" {
|
||||
repoPath = convertPathForWindows(repoPath)
|
||||
}
|
||||
return runExec(ctx, c, file, repoPath, true)
|
||||
@@ -130,6 +130,11 @@ func runExec(ctx context.Context, c *cli.Command, file, repoPath string, singleE
|
||||
return err
|
||||
}
|
||||
|
||||
// if we use the local backend we should signal to run at $repoPath
|
||||
if c.String("backend-engine") == "local" {
|
||||
backend_local.CLIWorkaroundExecAtDir = repoPath
|
||||
}
|
||||
|
||||
axes, err := matrix.ParseString(string(dat))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse matrix fail")
|
||||
|
||||
@@ -289,6 +289,16 @@
|
||||
"name": "SonarQube",
|
||||
"docs": "https://raw.githubusercontent.com/j04n-f/woodpecker-sonar-plugin/refs/heads/main/docs.md",
|
||||
"verified": false
|
||||
},
|
||||
{
|
||||
"name": "Github App Tokens",
|
||||
"docs": "https://raw.githubusercontent.com/yyewolf/woodpecker-plugins/refs/heads/main/github-app-token/docs.md",
|
||||
"verified": false
|
||||
},
|
||||
{
|
||||
"name": "Github Comment",
|
||||
"docs": "https://raw.githubusercontent.com/yyewolf/woodpecker-plugins/refs/heads/main/github-comment/docs.md",
|
||||
"verified": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+717
-717
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ go 1.24.4
|
||||
|
||||
require (
|
||||
al.essio.dev/pkg/shellescape v1.6.0
|
||||
code.gitea.io/sdk/gitea v0.22.0
|
||||
code.gitea.io/sdk/gitea v0.22.1
|
||||
codeberg.org/6543/go-yaml2json v1.0.0
|
||||
codeberg.org/6543/xyaml v1.1.0
|
||||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0
|
||||
@@ -50,7 +50,7 @@ require (
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/urfave/cli-docs/v3 v3.1.1-0.20251020155410-9c1a23c69f4e
|
||||
github.com/urfave/cli-docs/v3 v3.1.1-0.20251022123016-72b87d11c482
|
||||
github.com/urfave/cli/v3 v3.5.0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/yaronf/httpsign v0.3.3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
|
||||
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
||||
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
||||
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
codeberg.org/6543/go-yaml2json v1.0.0 h1:heGqo9VEi7gY2yNqjj7X4ADs5nzlFIbGsJtgYDLrnig=
|
||||
codeberg.org/6543/go-yaml2json v1.0.0/go.mod h1:mz61q14LWF4ZABrgMEDMmk3t9dPi6zgR1uBh2VKV2RQ=
|
||||
codeberg.org/6543/xyaml v1.1.0 h1:0PWTy8OUqshshjrrnAXFWXSPUEa8R49DIh2ah07SxFc=
|
||||
@@ -557,8 +557,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/urfave/cli-docs/v3 v3.1.1-0.20251020155410-9c1a23c69f4e h1:x8sO82pyawSRZ3Te4tpeX66PAZ02SnGk6J3A0N72a30=
|
||||
github.com/urfave/cli-docs/v3 v3.1.1-0.20251020155410-9c1a23c69f4e/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
||||
github.com/urfave/cli-docs/v3 v3.1.1-0.20251022123016-72b87d11c482 h1:xlSy8R55vuHbUM1c2mwfQ5MFeVTnm59BwQeWibPUD5A=
|
||||
github.com/urfave/cli-docs/v3 v3.1.1-0.20251022123016-72b87d11c482/go.mod h1:cjSVza4yCaqQet06zO6QhYqXQYjGRqbUj8zok6mHDRU=
|
||||
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
|
||||
github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw=
|
||||
|
||||
@@ -52,6 +52,8 @@ type local struct {
|
||||
os, arch string
|
||||
}
|
||||
|
||||
var CLIWorkaroundExecAtDir string // To handle edge case for running local backend via cli exec
|
||||
|
||||
// New returns a new local Backend.
|
||||
func New() types.Backend {
|
||||
return &local{
|
||||
@@ -100,17 +102,34 @@ func (e *local) SetupWorkflow(_ context.Context, _ *types.Config, taskUUID strin
|
||||
}
|
||||
|
||||
state := &workflowState{
|
||||
baseDir: baseDir,
|
||||
workspaceDir: filepath.Join(baseDir, "workspace"),
|
||||
homeDir: filepath.Join(baseDir, "home"),
|
||||
baseDir: baseDir,
|
||||
homeDir: filepath.Join(baseDir, "home"),
|
||||
}
|
||||
e.workflows.Store(taskUUID, state)
|
||||
|
||||
if err := os.Mkdir(state.homeDir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Mkdir(state.workspaceDir, 0o700); err != nil {
|
||||
return err
|
||||
// normal workspace setup case
|
||||
if CLIWorkaroundExecAtDir == "" {
|
||||
state.workspaceDir = filepath.Join(baseDir, "workspace")
|
||||
if err := os.Mkdir(state.workspaceDir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
} else
|
||||
// setup workspace via internal flag signaled from cli exec to a specific dir
|
||||
{
|
||||
state.workspaceDir = CLIWorkaroundExecAtDir
|
||||
if stat, err := os.Stat(CLIWorkaroundExecAtDir); os.IsNotExist(err) {
|
||||
log.Debug().Msgf("create workspace directory '%s' set by internal flag", CLIWorkaroundExecAtDir)
|
||||
if err := os.Mkdir(state.workspaceDir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
//nolint:forbidigo
|
||||
log.Fatal().Msg("This should never happen! internalExecDir was set to an non directory path!")
|
||||
}
|
||||
}
|
||||
|
||||
e.workflows.Store(taskUUID, state)
|
||||
|
||||
@@ -280,7 +280,7 @@ func TestCompilerCompile(t *testing.T) {
|
||||
Name: "step",
|
||||
Image: "bash",
|
||||
Commands: []string{"env"},
|
||||
Environment: yaml_base_types.EnvironmentMap{
|
||||
Environment: map[string]any{
|
||||
"MISSING": map[string]any{"from_secret": "missing"},
|
||||
},
|
||||
}}}},
|
||||
@@ -375,7 +375,7 @@ func TestCompilerCompileWithFromSecret(t *testing.T) {
|
||||
Name: "step",
|
||||
Image: "bash",
|
||||
Commands: []string{"env"},
|
||||
Environment: yaml_base_types.EnvironmentMap{
|
||||
Environment: map[string]any{
|
||||
"SECRET": map[string]any{"from_secret": "secret_name"},
|
||||
},
|
||||
}}}},
|
||||
|
||||
@@ -19,11 +19,8 @@ import (
|
||||
"maps"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/expr-lang/expr"
|
||||
"go.uber.org/multierr"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
|
||||
@@ -38,38 +35,18 @@ type (
|
||||
}
|
||||
|
||||
Constraint struct {
|
||||
Ref List
|
||||
Repo List
|
||||
Instance List
|
||||
Platform List
|
||||
Branch List
|
||||
Cron List
|
||||
Status List
|
||||
Matrix Map
|
||||
Local yamlBaseTypes.BoolTrue
|
||||
Path Path
|
||||
Evaluate string `yaml:"evaluate,omitempty"`
|
||||
Event yamlBaseTypes.StringOrSlice
|
||||
}
|
||||
|
||||
// List defines a runtime constraint for exclude & include string slices.
|
||||
List struct {
|
||||
Include []string
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
// Map defines a runtime constraint for exclude & include map strings.
|
||||
Map struct {
|
||||
Include map[string]string
|
||||
Exclude map[string]string
|
||||
}
|
||||
|
||||
// Path defines a runtime constrain for exclude & include paths.
|
||||
Path struct {
|
||||
Include []string
|
||||
Exclude []string
|
||||
IgnoreMessage string `yaml:"ignore_message,omitempty"`
|
||||
OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"`
|
||||
Ref List `yaml:"ref,omitempty"`
|
||||
Repo List `yaml:"repo,omitempty"`
|
||||
Instance List `yaml:"instance,omitempty"`
|
||||
Platform List `yaml:"platform,omitempty"`
|
||||
Branch List `yaml:"branch,omitempty"`
|
||||
Cron List `yaml:"cron,omitempty"`
|
||||
Status List `yaml:"status,omitempty"`
|
||||
Matrix Map `yaml:"matrix,omitempty"`
|
||||
Local yamlBaseTypes.BoolTrue `yaml:"local,omitempty"`
|
||||
Path Path `yaml:"path,omitempty"`
|
||||
Evaluate string `yaml:"evaluate,omitempty"`
|
||||
Event yamlBaseTypes.StringOrSlice `yaml:"event,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -153,6 +130,18 @@ func (when *When) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements custom Yaml marshaling.
|
||||
func (when When) MarshalYAML() (any, error) {
|
||||
switch len(when.Constraints) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return when.Constraints[0], nil
|
||||
default:
|
||||
return when.Constraints, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Match returns true if all constraints match the given input. If a single
|
||||
// constraint fails a false value is returned.
|
||||
func (c *Constraint) Match(m metadata.Metadata, global bool, env map[string]string) (bool, error) {
|
||||
@@ -204,203 +193,3 @@ func (c *Constraint) Match(m metadata.Metadata, global bool, env map[string]stri
|
||||
|
||||
return match, nil
|
||||
}
|
||||
|
||||
// IsEmpty return true if a constraint has no conditions.
|
||||
func (c List) IsEmpty() bool {
|
||||
return len(c.Include) == 0 && len(c.Exclude) == 0
|
||||
}
|
||||
|
||||
// Match returns true if the string matches the include patterns and does not
|
||||
// match any of the exclude patterns.
|
||||
func (c *List) Match(v string) bool {
|
||||
if c.Excludes(v) {
|
||||
return false
|
||||
}
|
||||
if c.Includes(v) {
|
||||
return true
|
||||
}
|
||||
if len(c.Include) == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Includes returns true if the string matches the include patterns.
|
||||
func (c *List) Includes(v string) bool {
|
||||
for _, pattern := range c.Include {
|
||||
if ok, _ := doublestar.Match(pattern, v); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Excludes returns true if the string matches the exclude patterns.
|
||||
func (c *List) Excludes(v string) bool {
|
||||
for _, pattern := range c.Exclude {
|
||||
if ok, _ := doublestar.Match(pattern, v); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshal the constraint.
|
||||
func (c *List) UnmarshalYAML(value *yaml.Node) error {
|
||||
out1 := struct {
|
||||
Include yamlBaseTypes.StringOrSlice
|
||||
Exclude yamlBaseTypes.StringOrSlice
|
||||
}{}
|
||||
|
||||
var out2 yamlBaseTypes.StringOrSlice
|
||||
|
||||
err1 := value.Decode(&out1)
|
||||
err2 := value.Decode(&out2)
|
||||
|
||||
c.Exclude = out1.Exclude
|
||||
c.Include = append( //nolint:gocritic
|
||||
out1.Include,
|
||||
out2...,
|
||||
)
|
||||
|
||||
if err1 != nil && err2 != nil {
|
||||
y, _ := yaml.Marshal(value)
|
||||
return fmt.Errorf("could not parse condition: %s: %w", y, multierr.Append(err1, err2))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match returns true if the params matches the include key values and does not
|
||||
// match any of the exclude key values.
|
||||
func (c *Map) Match(params map[string]string) bool {
|
||||
// when no includes or excludes automatically match
|
||||
if len(c.Include) == 0 && len(c.Exclude) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Exclusions are processed first. So we can include everything and then
|
||||
// selectively include others.
|
||||
if len(c.Exclude) != 0 {
|
||||
var matches int
|
||||
|
||||
for key, val := range c.Exclude {
|
||||
if ok, _ := doublestar.Match(val, params[key]); ok {
|
||||
matches++
|
||||
}
|
||||
}
|
||||
if matches == len(c.Exclude) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for key, val := range c.Include {
|
||||
if ok, _ := doublestar.Match(val, params[key]); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshal the constraint map.
|
||||
func (c *Map) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
out1 := struct {
|
||||
Include map[string]string
|
||||
Exclude map[string]string
|
||||
}{
|
||||
Include: map[string]string{},
|
||||
Exclude: map[string]string{},
|
||||
}
|
||||
|
||||
out2 := map[string]string{}
|
||||
|
||||
_ = unmarshal(&out1) // it contains include and exclude statement
|
||||
_ = unmarshal(&out2) // it contains no include/exclude statement, assume include as default
|
||||
|
||||
c.Include = out1.Include
|
||||
c.Exclude = out1.Exclude
|
||||
for k, v := range out2 {
|
||||
c.Include[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshal the constraint.
|
||||
func (c *Path) UnmarshalYAML(value *yaml.Node) error {
|
||||
out1 := struct {
|
||||
Include yamlBaseTypes.StringOrSlice `yaml:"include,omitempty"`
|
||||
Exclude yamlBaseTypes.StringOrSlice `yaml:"exclude,omitempty"`
|
||||
IgnoreMessage string `yaml:"ignore_message,omitempty"`
|
||||
OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"`
|
||||
}{}
|
||||
|
||||
var out2 yamlBaseTypes.StringOrSlice
|
||||
|
||||
err1 := value.Decode(&out1)
|
||||
err2 := value.Decode(&out2)
|
||||
|
||||
c.Exclude = out1.Exclude
|
||||
c.IgnoreMessage = out1.IgnoreMessage
|
||||
c.OnEmpty = out1.OnEmpty
|
||||
c.Include = append( //nolint:gocritic
|
||||
out1.Include,
|
||||
out2...,
|
||||
)
|
||||
|
||||
if err1 != nil && err2 != nil {
|
||||
y, _ := yaml.Marshal(value)
|
||||
return fmt.Errorf("could not parse condition: %s", y)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match returns true if file paths in string slice matches the include and not exclude patterns
|
||||
// or if commit message contains ignore message.
|
||||
func (c *Path) Match(v []string, message string) bool {
|
||||
// ignore file pattern matches if the commit message contains a pattern
|
||||
if len(c.IgnoreMessage) > 0 && strings.Contains(strings.ToLower(message), strings.ToLower(c.IgnoreMessage)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// return value based on 'on_empty', if there are no commit files (empty commit)
|
||||
if len(v) == 0 {
|
||||
return c.OnEmpty.Bool()
|
||||
}
|
||||
|
||||
if len(c.Exclude) > 0 && c.Excludes(v) {
|
||||
return false
|
||||
}
|
||||
if len(c.Include) > 0 && !c.Includes(v) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Includes returns true if the string matches any of the include patterns.
|
||||
func (c *Path) Includes(v []string) bool {
|
||||
for _, pattern := range c.Include {
|
||||
for _, file := range v {
|
||||
if ok, _ := doublestar.Match(pattern, file); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Excludes returns true if all of the strings match any of the exclude patterns.
|
||||
func (c *Path) Excludes(v []string) bool {
|
||||
for _, file := range v {
|
||||
matched := false
|
||||
for _, pattern := range c.Exclude {
|
||||
if ok, _ := doublestar.Match(pattern, file); ok {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -23,397 +23,6 @@ import (
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
|
||||
)
|
||||
|
||||
func TestConstraint(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
with string
|
||||
want bool
|
||||
}{
|
||||
// string value
|
||||
{
|
||||
conf: "main",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "main",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "feature/*",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// slice value
|
||||
{
|
||||
conf: "[ main, feature/* ]",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "[ main, feature/* ]",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "[ main, feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// includes block
|
||||
{
|
||||
conf: "include: main",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: main",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: feature/*",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: feature/*",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: [ main, feature/* ]",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: [ main, feature/* ]",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: [ main, feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// excludes block
|
||||
{
|
||||
conf: "exclude: main",
|
||||
with: "develop",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: main",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: feature/*",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: feature/*",
|
||||
with: "feature/foo",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ main, develop ]",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/*, bar ]",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/*, bar ]",
|
||||
with: "feature/foo",
|
||||
want: false,
|
||||
},
|
||||
// include and exclude blocks
|
||||
{
|
||||
conf: "{ include: [ main, feature/* ], exclude: [ develop ] }",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ main, feature/* ], exclude: [ feature/bar ] }",
|
||||
with: "feature/bar",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ main, feature/* ], exclude: [ main, develop ] }",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
// empty blocks
|
||||
{
|
||||
conf: "",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
c := parseConstraint(t, test.conf)
|
||||
assert.Equal(t, test.want, c.Match(test.with))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraintList(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
with []string
|
||||
message string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
conf: "",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "CHANGELOG.md",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'*.md'",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "['*.md']",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'docs/*'",
|
||||
with: []string{"docs/README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'docs/*'",
|
||||
with: []string{"docs/sub/README.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "'docs/**'",
|
||||
with: []string{"docs/README.md", "docs/sub/README.md", "docs/sub-sub/README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'docs/**'",
|
||||
with: []string{"README.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ README.md ] }",
|
||||
with: []string{"CHANGELOG.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ README.md ] }",
|
||||
with: []string{"design.md"},
|
||||
want: true,
|
||||
},
|
||||
// include and exclude blocks
|
||||
{
|
||||
conf: "{ include: [ '*.md', '*.ini' ], exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ '*.md' ], exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"CHANGELOG.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ '*.md' ], exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"README.md", "CHANGELOG.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"README.md", "CHANGELOG.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ CHANGELOG.md, docs/**/*.md ] }",
|
||||
with: []string{"docs/main.md", "CHANGELOG.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ CHANGELOG.md, docs/**/*.md ] }",
|
||||
with: []string{"docs/main.md", "CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
// commit message ignore matches
|
||||
{
|
||||
conf: "{ include: [ README.md ], ignore_message: '[ALL]' }",
|
||||
with: []string{"CHANGELOG.md"},
|
||||
message: "Build them [ALL]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ '*.php' ], ignore_message: '[ALL]' }",
|
||||
with: []string{"myfile.php"},
|
||||
message: "Build them [ALL]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ ignore_message: '[ALL]' }",
|
||||
with: []string{},
|
||||
message: "Build them [ALL]",
|
||||
want: true,
|
||||
},
|
||||
// empty commit
|
||||
{
|
||||
conf: "{ include: [ README.md ] }",
|
||||
with: []string{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ README.md ], on_empty: false }",
|
||||
with: []string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ README.md ], on_empty: true }",
|
||||
with: []string{},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
c := parseConstraintPath(t, test.conf)
|
||||
assert.Equal(t, test.want, c.Match(test.with, test.message))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraintMap(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
with map[string]string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
conf: "GOLANG: 1.7",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "GOLANG: tip",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.* }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, BRANCH: release/**/test }",
|
||||
with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1//test"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, BRANCH: release/**/test }",
|
||||
with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1/qest"},
|
||||
want: false,
|
||||
},
|
||||
// include syntax
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7 }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: tip }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: false,
|
||||
},
|
||||
// exclude syntax
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7 }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: tip }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: true,
|
||||
},
|
||||
// exclude AND include values
|
||||
{
|
||||
conf: "{ include: { GOLANG: 1.7 }, exclude: { GOLANG: 1.7 } }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
// blanks
|
||||
{
|
||||
conf: "",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "GOLANG: 1.7",
|
||||
with: map[string]string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.0 }",
|
||||
with: map[string]string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
c := parseConstraintMap(t, test.conf)
|
||||
assert.Equal(t, test.want, c.Match(test.with), "config: '%s', with: '%s'", test.conf, test.with)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraintStatusSuccess(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
@@ -576,21 +185,3 @@ func parseConstraints(t *testing.T, s string) *When {
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(s), c))
|
||||
return c
|
||||
}
|
||||
|
||||
func parseConstraint(t *testing.T, s string) *List {
|
||||
c := &List{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(s), c))
|
||||
return c
|
||||
}
|
||||
|
||||
func parseConstraintMap(t *testing.T, s string) *Map {
|
||||
c := &Map{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(s), c))
|
||||
return c
|
||||
}
|
||||
|
||||
func parseConstraintPath(t *testing.T, s string) *Path {
|
||||
c := &Path{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(s), c))
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2025 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 constraint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"go.uber.org/multierr"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
yamlBaseTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
|
||||
)
|
||||
|
||||
// List defines a runtime constraint for exclude & include string slices.
|
||||
type List struct {
|
||||
Include []string
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
// IsEmpty return true if a constraint has no conditions.
|
||||
func (c List) IsEmpty() bool {
|
||||
return len(c.Include) == 0 && len(c.Exclude) == 0
|
||||
}
|
||||
|
||||
// Match returns true if the string matches the include patterns and does not
|
||||
// match any of the exclude patterns.
|
||||
func (c *List) Match(v string) bool {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
if c.Excludes(v) {
|
||||
return false
|
||||
}
|
||||
if c.Includes(v) {
|
||||
return true
|
||||
}
|
||||
if len(c.Include) == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Includes returns true if the string matches the include patterns.
|
||||
func (c *List) Includes(v string) bool {
|
||||
for _, pattern := range c.Include {
|
||||
if ok, _ := doublestar.Match(pattern, v); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Excludes returns true if the string matches the exclude patterns.
|
||||
func (c *List) Excludes(v string) bool {
|
||||
for _, pattern := range c.Exclude {
|
||||
if ok, _ := doublestar.Match(pattern, v); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshal the constraint.
|
||||
func (c *List) UnmarshalYAML(value *yaml.Node) error {
|
||||
out1 := struct {
|
||||
Include yamlBaseTypes.StringOrSlice
|
||||
Exclude yamlBaseTypes.StringOrSlice
|
||||
}{}
|
||||
|
||||
var out2 yamlBaseTypes.StringOrSlice
|
||||
|
||||
err1 := value.Decode(&out1)
|
||||
err2 := value.Decode(&out2)
|
||||
|
||||
c.Exclude = out1.Exclude
|
||||
c.Include = append( //nolint:gocritic
|
||||
out1.Include,
|
||||
out2...,
|
||||
)
|
||||
|
||||
if err1 != nil && err2 != nil {
|
||||
y, _ := yaml.Marshal(value)
|
||||
return fmt.Errorf("could not parse condition: %s: %w", y, multierr.Append(err1, err2))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements custom Yaml marshaling.
|
||||
func (c List) MarshalYAML() (any, error) {
|
||||
switch {
|
||||
case len(c.Include) == 0 && len(c.Exclude) == 0:
|
||||
return nil, nil
|
||||
case len(c.Exclude) == 0:
|
||||
return yamlBaseTypes.StringOrSlice(c.Include), nil
|
||||
default:
|
||||
// we can not return type List as it would lead to infinite recursion :/
|
||||
return struct {
|
||||
Include yamlBaseTypes.StringOrSlice `yaml:"include,omitempty"`
|
||||
Exclude yamlBaseTypes.StringOrSlice `yaml:"exclude,omitempty"`
|
||||
}{
|
||||
Include: c.Include,
|
||||
Exclude: c.Exclude,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright 2025 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 constraint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestConstraintList(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
with string
|
||||
want bool
|
||||
}{
|
||||
// string value
|
||||
{
|
||||
conf: "main",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "main",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "feature/*",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// slice value
|
||||
{
|
||||
conf: "[ main, feature/* ]",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "[ main, feature/* ]",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "[ main, feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// includes block
|
||||
{
|
||||
conf: "include: main",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: main",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: feature/*",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: feature/*",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: [ main, feature/* ]",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: [ main, feature/* ]",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: [ main, feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// excludes block
|
||||
{
|
||||
conf: "exclude: main",
|
||||
with: "develop",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: main",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: feature/*",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: feature/*",
|
||||
with: "feature/foo",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ main, develop ]",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/*, bar ]",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/*, bar ]",
|
||||
with: "feature/foo",
|
||||
want: false,
|
||||
},
|
||||
// include and exclude blocks
|
||||
{
|
||||
conf: "{ include: [ main, feature/* ], exclude: [ develop ] }",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ main, feature/* ], exclude: [ feature/bar ] }",
|
||||
with: "feature/bar",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ main, feature/* ], exclude: [ main, develop ] }",
|
||||
with: "main",
|
||||
want: false,
|
||||
},
|
||||
// empty blocks
|
||||
{
|
||||
conf: "",
|
||||
with: "main",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
c := parseConstraintList(t, test.conf)
|
||||
assert.Equal(t, test.want, c.Match(test.with))
|
||||
}
|
||||
}
|
||||
|
||||
func parseConstraintList(t *testing.T, s string) *List {
|
||||
c := &List{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(s), c))
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2025 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 constraint
|
||||
|
||||
import "github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
// Map defines a runtime constraint for exclude & include map strings.
|
||||
type Map struct {
|
||||
Include map[string]string `yaml:"include,omitempty"`
|
||||
Exclude map[string]string `yaml:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
// Match returns true if the params matches the include key values and does not
|
||||
// match any of the exclude key values.
|
||||
func (c *Map) Match(params map[string]string) bool {
|
||||
// when no includes or excludes automatically match
|
||||
if c == nil || len(c.Include) == 0 && len(c.Exclude) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Exclusions are processed first. So we can include everything and then
|
||||
// selectively include others.
|
||||
if len(c.Exclude) != 0 {
|
||||
var matches int
|
||||
|
||||
for key, val := range c.Exclude {
|
||||
if ok, _ := doublestar.Match(val, params[key]); ok {
|
||||
matches++
|
||||
}
|
||||
}
|
||||
if matches == len(c.Exclude) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for key, val := range c.Include {
|
||||
if ok, _ := doublestar.Match(val, params[key]); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshal the constraint map.
|
||||
func (c *Map) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
out1 := struct {
|
||||
Include map[string]string
|
||||
Exclude map[string]string
|
||||
}{
|
||||
Include: map[string]string{},
|
||||
Exclude: map[string]string{},
|
||||
}
|
||||
|
||||
out2 := map[string]string{}
|
||||
|
||||
_ = unmarshal(&out1) // it contains include and exclude statement
|
||||
_ = unmarshal(&out2) // it contains no include/exclude statement, assume include as default
|
||||
|
||||
c.Include = out1.Include
|
||||
c.Exclude = out1.Exclude
|
||||
for k, v := range out2 {
|
||||
c.Include[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements custom Yaml marshaling.
|
||||
func (c Map) MarshalYAML() (any, error) {
|
||||
switch {
|
||||
case len(c.Include) == 0 && len(c.Exclude) == 0:
|
||||
return nil, nil
|
||||
case len(c.Exclude) == 0:
|
||||
return c.Include, nil
|
||||
case len(c.Include) == 0 && len(c.Exclude) != 0:
|
||||
return struct {
|
||||
Exclude map[string]string
|
||||
}{Exclude: c.Exclude}, nil
|
||||
default:
|
||||
// we can not return type Map as it would lead to infinite recursion :/
|
||||
return struct {
|
||||
Include map[string]string `yaml:"include,omitempty"`
|
||||
Exclude map[string]string `yaml:"exclude,omitempty"`
|
||||
}{
|
||||
Include: c.Include,
|
||||
Exclude: c.Exclude,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright 2025 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 constraint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestConstraintMap(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
with map[string]string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
conf: "GOLANG: 1.7",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "GOLANG: tip",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.* }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, BRANCH: release/**/test }",
|
||||
with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1//test"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, BRANCH: release/**/test }",
|
||||
with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1/qest"},
|
||||
want: false,
|
||||
},
|
||||
// include syntax
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7 }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: tip }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: false,
|
||||
},
|
||||
// exclude syntax
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7 }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: tip }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: true,
|
||||
},
|
||||
// exclude AND include values
|
||||
{
|
||||
conf: "{ include: { GOLANG: 1.7 }, exclude: { GOLANG: 1.7 } }",
|
||||
with: map[string]string{"GOLANG": "1.7"},
|
||||
want: false,
|
||||
},
|
||||
// blanks
|
||||
{
|
||||
conf: "",
|
||||
with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "GOLANG: 1.7",
|
||||
with: map[string]string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ GOLANG: 1.7, REDIS: 3.0 }",
|
||||
with: map[string]string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }",
|
||||
with: map[string]string{},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
c := parseConstraintMap(t, test.conf)
|
||||
assert.Equal(t, test.want, c.Match(test.with), "config: '%s', with: '%s'", test.conf, test.with)
|
||||
}
|
||||
}
|
||||
|
||||
func parseConstraintMap(t *testing.T, s string) *Map {
|
||||
c := &Map{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(s), c))
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2025 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 constraint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
yamlBaseTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
|
||||
)
|
||||
|
||||
// Path defines a runtime constrain for exclude & include paths.
|
||||
type Path struct {
|
||||
Include []string `yaml:"include,omitempty"`
|
||||
Exclude []string `yaml:"exclude,omitempty"`
|
||||
IgnoreMessage string `yaml:"ignore_message,omitempty"`
|
||||
OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshal the constraint.
|
||||
func (c *Path) UnmarshalYAML(value *yaml.Node) error {
|
||||
out1 := struct {
|
||||
Include yamlBaseTypes.StringOrSlice `yaml:"include"`
|
||||
Exclude yamlBaseTypes.StringOrSlice `yaml:"exclude"`
|
||||
IgnoreMessage string `yaml:"ignore_message"`
|
||||
OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty"`
|
||||
}{}
|
||||
|
||||
var out2 yamlBaseTypes.StringOrSlice
|
||||
|
||||
err1 := value.Decode(&out1)
|
||||
err2 := value.Decode(&out2)
|
||||
|
||||
c.Exclude = out1.Exclude
|
||||
c.IgnoreMessage = out1.IgnoreMessage
|
||||
c.OnEmpty = out1.OnEmpty
|
||||
c.Include = append( //nolint:gocritic
|
||||
out1.Include,
|
||||
out2...,
|
||||
)
|
||||
|
||||
if err1 != nil && err2 != nil {
|
||||
y, _ := yaml.Marshal(value)
|
||||
return fmt.Errorf("could not parse condition: %s", y)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements custom Yaml marshaling.
|
||||
func (c Path) MarshalYAML() (any, error) {
|
||||
// if only Include is set return simple syntax
|
||||
if len(c.Exclude) == 0 &&
|
||||
len(c.IgnoreMessage) == 0 &&
|
||||
c.OnEmpty.Bool() {
|
||||
if len(c.Include) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return yamlBaseTypes.StringOrSlice(c.Include), nil
|
||||
}
|
||||
// we can not return type Path as it would lead to infinite recursion :/
|
||||
return struct {
|
||||
Include yamlBaseTypes.StringOrSlice `yaml:"include,omitempty"`
|
||||
Exclude yamlBaseTypes.StringOrSlice `yaml:"exclude,omitempty"`
|
||||
IgnoreMessage string `yaml:"ignore_message,omitempty"`
|
||||
OnEmpty yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"`
|
||||
}{
|
||||
Include: c.Include,
|
||||
Exclude: c.Exclude,
|
||||
IgnoreMessage: c.IgnoreMessage,
|
||||
OnEmpty: c.OnEmpty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Match returns true if file paths in string slice matches the include and not exclude patterns
|
||||
// or if commit message contains ignore message.
|
||||
func (c *Path) Match(v []string, message string) bool {
|
||||
// ignore file pattern matches if the commit message contains a pattern
|
||||
if len(c.IgnoreMessage) > 0 && strings.Contains(strings.ToLower(message), strings.ToLower(c.IgnoreMessage)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// return value based on 'on_empty', if there are no commit files (empty commit)
|
||||
if len(v) == 0 {
|
||||
return c.OnEmpty.Bool()
|
||||
}
|
||||
|
||||
if len(c.Exclude) > 0 && c.Excludes(v) {
|
||||
return false
|
||||
}
|
||||
if len(c.Include) > 0 && !c.Includes(v) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Includes returns true if the string matches any of the include patterns.
|
||||
func (c *Path) Includes(v []string) bool {
|
||||
for _, pattern := range c.Include {
|
||||
for _, file := range v {
|
||||
if ok, _ := doublestar.Match(pattern, file); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Excludes returns true if all of the strings match any of the exclude patterns.
|
||||
func (c *Path) Excludes(v []string) bool {
|
||||
for _, file := range v {
|
||||
matched := false
|
||||
for _, pattern := range c.Exclude {
|
||||
if ok, _ := doublestar.Match(pattern, file); ok {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2025 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 constraint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestConstraintPath(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
with []string
|
||||
message string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
conf: "",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "CHANGELOG.md",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'*.md'",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "['*.md']",
|
||||
with: []string{"CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'docs/*'",
|
||||
with: []string{"docs/README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'docs/*'",
|
||||
with: []string{"docs/sub/README.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "'docs/**'",
|
||||
with: []string{"docs/README.md", "docs/sub/README.md", "docs/sub-sub/README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "'docs/**'",
|
||||
with: []string{"README.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ README.md ] }",
|
||||
with: []string{"CHANGELOG.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ README.md ] }",
|
||||
with: []string{"design.md"},
|
||||
want: true,
|
||||
},
|
||||
// include and exclude blocks
|
||||
{
|
||||
conf: "{ include: [ '*.md', '*.ini' ], exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"README.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ '*.md' ], exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"CHANGELOG.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ '*.md' ], exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"README.md", "CHANGELOG.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ CHANGELOG.md ] }",
|
||||
with: []string{"README.md", "CHANGELOG.md"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ CHANGELOG.md, docs/**/*.md ] }",
|
||||
with: []string{"docs/main.md", "CHANGELOG.md"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ CHANGELOG.md, docs/**/*.md ] }",
|
||||
with: []string{"docs/main.md", "CHANGELOG.md", "README.md"},
|
||||
want: true,
|
||||
},
|
||||
// commit message ignore matches
|
||||
{
|
||||
conf: "{ include: [ README.md ], ignore_message: '[ALL]' }",
|
||||
with: []string{"CHANGELOG.md"},
|
||||
message: "Build them [ALL]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ exclude: [ '*.php' ], ignore_message: '[ALL]' }",
|
||||
with: []string{"myfile.php"},
|
||||
message: "Build them [ALL]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ ignore_message: '[ALL]' }",
|
||||
with: []string{},
|
||||
message: "Build them [ALL]",
|
||||
want: true,
|
||||
},
|
||||
// empty commit
|
||||
{
|
||||
conf: "{ include: [ README.md ] }",
|
||||
with: []string{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ README.md ], on_empty: false }",
|
||||
with: []string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ README.md ], on_empty: true }",
|
||||
with: []string{},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
c := parseConstraintPath(t, test.conf)
|
||||
assert.Equal(t, test.want, c.Match(test.with, test.message))
|
||||
}
|
||||
}
|
||||
|
||||
func parseConstraintPath(t *testing.T, s string) *Path {
|
||||
c := &Path{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(s), c))
|
||||
return c
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
|
||||
yaml_base_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
|
||||
@@ -69,7 +71,7 @@ func TestParse(t *testing.T) {
|
||||
assert.Empty(t, out.Steps.ContainerList[0].When.Constraints)
|
||||
assert.Equal(t, "notify_success", out.Steps.ContainerList[1].Name)
|
||||
assert.Equal(t, "plugins/slack", out.Steps.ContainerList[1].Image)
|
||||
assert.Equal(t, yaml_base_types.StringOrSlice{"success"}, out.Steps.ContainerList[1].When.Constraints[0].Event)
|
||||
assert.Equal(t, yaml_base_types.StringOrSlice{"push"}, out.Steps.ContainerList[1].When.Constraints[0].Event)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,9 +172,6 @@ when:
|
||||
- tester2
|
||||
- branch:
|
||||
- tester
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
workspace:
|
||||
path: src/github.com/octocat/hello-world
|
||||
base: /go
|
||||
@@ -189,6 +188,7 @@ steps:
|
||||
- go build
|
||||
when:
|
||||
event: push
|
||||
depends_on: []
|
||||
notify:
|
||||
image: slack
|
||||
channel: dev
|
||||
@@ -217,38 +217,86 @@ steps:
|
||||
`
|
||||
|
||||
var sampleVarYaml = `
|
||||
_slack: &SLACK
|
||||
variables: &SLACK
|
||||
image: plugins/slack
|
||||
steps:
|
||||
notify_fail: *SLACK
|
||||
notify_success:
|
||||
<< : *SLACK
|
||||
when:
|
||||
event: success
|
||||
event: push
|
||||
echo:
|
||||
when:
|
||||
- path: wow.sh
|
||||
repo: "test"
|
||||
branch:
|
||||
exclude: main
|
||||
- path:
|
||||
- test.yaml
|
||||
- test.zig
|
||||
- path:
|
||||
exclude: a
|
||||
on_empty: true
|
||||
- ref: ref/tags/v1
|
||||
path:
|
||||
env:
|
||||
image: print
|
||||
environment:
|
||||
DRIVER: next
|
||||
PLATFORM: linux
|
||||
`
|
||||
|
||||
var sampleSliceYaml = `
|
||||
steps:
|
||||
nil_slice:
|
||||
image: plugins/slack
|
||||
empty_slice:
|
||||
image: plugins/slack
|
||||
depends_on: []
|
||||
`
|
||||
func TestReSerialize(t *testing.T) {
|
||||
work1, err := ParseString(sampleVarYaml)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
workBin, err := yaml.Marshal(work1)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assert.EqualValues(t, `steps:
|
||||
- name: notify_fail
|
||||
image: plugins/slack
|
||||
- name: notify_success
|
||||
image: plugins/slack
|
||||
when:
|
||||
event: push
|
||||
- name: echo
|
||||
when:
|
||||
- repo: test
|
||||
branch:
|
||||
exclude: main
|
||||
path: wow.sh
|
||||
- path:
|
||||
- test.yaml
|
||||
- test.zig
|
||||
- path:
|
||||
exclude: a
|
||||
- ref: ref/tags/v1
|
||||
- name: env
|
||||
image: print
|
||||
environment:
|
||||
DRIVER: next
|
||||
PLATFORM: linux
|
||||
skip_clone: false
|
||||
`, string(workBin))
|
||||
}
|
||||
|
||||
func TestSlice(t *testing.T) {
|
||||
t.Run("should marshal a not set slice to nil", func(t *testing.T) {
|
||||
out, err := ParseString(sampleSliceYaml)
|
||||
assert.NoError(t, err)
|
||||
out, err := ParseString(sampleYaml)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should marshal a not set slice to nil", func(t *testing.T) {
|
||||
assert.Equal(t, "test", out.Steps.ContainerList[0].Name)
|
||||
assert.Nil(t, out.Steps.ContainerList[0].DependsOn)
|
||||
assert.Empty(t, out.Steps.ContainerList[0].DependsOn)
|
||||
})
|
||||
|
||||
t.Run("should marshal an empty slice", func(t *testing.T) {
|
||||
out, err := ParseString(sampleSliceYaml)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "build", out.Steps.ContainerList[1].Name)
|
||||
assert.NotNil(t, out.Steps.ContainerList[1].DependsOn)
|
||||
assert.Empty(t, (out.Steps.ContainerList[1].DependsOn))
|
||||
})
|
||||
|
||||
@@ -42,7 +42,16 @@ func (b *BoolTrue) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements custom Yaml marshaling.
|
||||
func (b BoolTrue) MarshalYAML() (any, error) {
|
||||
return b.Bool(), nil
|
||||
}
|
||||
|
||||
// Bool returns the bool value.
|
||||
func (b BoolTrue) Bool() bool {
|
||||
return !b.value
|
||||
}
|
||||
|
||||
func ToBoolTrue(v bool) BoolTrue {
|
||||
return BoolTrue{value: !v}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,27 @@ func TestBoolTrue(t *testing.T) {
|
||||
err := yaml.Unmarshal(in, &out)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("marshal", func(t *testing.T) {
|
||||
t.Run("marshal empty", func(t *testing.T) {
|
||||
in := &BoolTrue{}
|
||||
out, err := yaml.Marshal(&in)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "true\n", string(out))
|
||||
})
|
||||
|
||||
t.Run("marshal true", func(t *testing.T) {
|
||||
in := ToBoolTrue(true)
|
||||
out, err := yaml.Marshal(&in)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "true\n", string(out))
|
||||
})
|
||||
|
||||
t.Run("marshal false", func(t *testing.T) {
|
||||
in := ToBoolTrue(false)
|
||||
out, err := yaml.Marshal(&in)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "false\n", string(out))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
// TODO: delete file after v3.0.0 release
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type EnvironmentMap map[string]any
|
||||
|
||||
// UnmarshalYAML implements the Unmarshaler interface.
|
||||
func (s *EnvironmentMap) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var mapType map[string]any
|
||||
err := unmarshal(&mapType)
|
||||
if err == nil {
|
||||
*s = mapType
|
||||
return nil
|
||||
}
|
||||
|
||||
var sliceType []any
|
||||
if err := unmarshal(&sliceType); err == nil {
|
||||
return fmt.Errorf("list syntax for 'environment' has been removed, use map syntax instead (https://woodpecker-ci.org/docs/usage/environment)")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
// TODO: delete file after v3.0.0 release
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type StructMap struct {
|
||||
Foos EnvironmentMap `yaml:"foos,omitempty"`
|
||||
}
|
||||
|
||||
func TestEnvironmentMapYaml(t *testing.T) {
|
||||
str := `{foos: [bar=baz, far=faz]}`
|
||||
s := StructMap{}
|
||||
err := yaml.Unmarshal([]byte(str), &s)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualValues(t, "list syntax for 'environment' has been removed, use map syntax instead (https://woodpecker-ci.org/docs/usage/environment)", err.Error())
|
||||
}
|
||||
|
||||
s.Foos = EnvironmentMap{"bar": "baz", "far": "faz"}
|
||||
d, err := yaml.Marshal(&s)
|
||||
assert.NoError(t, err)
|
||||
str = `foos:
|
||||
bar: baz
|
||||
far: faz
|
||||
`
|
||||
assert.EqualValues(t, str, string(d))
|
||||
|
||||
s2 := StructMap{}
|
||||
assert.NoError(t, yaml.Unmarshal(d, &s2))
|
||||
|
||||
assert.Equal(t, EnvironmentMap{"bar": "baz", "far": "faz"}, s2.Foos)
|
||||
}
|
||||
@@ -44,6 +44,16 @@ func (s *StringOrSlice) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
return errors.New("failed to unmarshal StringOrSlice")
|
||||
}
|
||||
|
||||
// MarshalYAML implements custom Yaml marshaling.
|
||||
func (s StringOrSlice) MarshalYAML() (any, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
} else if len(s) == 1 {
|
||||
return s[0], nil
|
||||
}
|
||||
return []string(s), nil
|
||||
}
|
||||
|
||||
func toStrings(s []any) ([]string, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -22,10 +22,49 @@ import (
|
||||
)
|
||||
|
||||
type StructStringOrSlice struct {
|
||||
Foo StringOrSlice
|
||||
Foo StringOrSlice `yaml:"foo"`
|
||||
Bar StringOrSlice `yaml:"bar,omitempty"`
|
||||
}
|
||||
|
||||
func TestStringOrSliceYaml(t *testing.T) {
|
||||
t.Run("unmarshal", func(t *testing.T) {
|
||||
str := `{foo: [bar, baz]}`
|
||||
|
||||
s := StructStringOrSlice{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(str), &s))
|
||||
|
||||
assert.Equal(t, StringOrSlice{"bar", "baz"}, s.Foo)
|
||||
|
||||
d, err := yaml.Marshal(&s)
|
||||
assert.Nil(t, err)
|
||||
|
||||
s2 := StructStringOrSlice{}
|
||||
assert.NoError(t, yaml.Unmarshal(d, &s2))
|
||||
|
||||
assert.Equal(t, StringOrSlice{"bar", "baz"}, s2.Foo)
|
||||
})
|
||||
|
||||
t.Run("marshal", func(t *testing.T) {
|
||||
str := StructStringOrSlice{}
|
||||
out, err := yaml.Marshal(str)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "foo: null\n", string(out))
|
||||
|
||||
str = StructStringOrSlice{Foo: []string{"a\""}}
|
||||
out, err = yaml.Marshal(str)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "foo: a\"\n", string(out))
|
||||
|
||||
str = StructStringOrSlice{Foo: []string{"a", "b", "c"}}
|
||||
out, err = yaml.Marshal(str)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, `foo:
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
`, string(out))
|
||||
})
|
||||
|
||||
str := `{foo: [bar, "baz"]}`
|
||||
s := StructStringOrSlice{}
|
||||
assert.NoError(t, yaml.Unmarshal([]byte(str), &s))
|
||||
|
||||
@@ -33,13 +33,14 @@ type (
|
||||
// Container defines a container.
|
||||
Container struct {
|
||||
// common
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Pull bool `yaml:"pull,omitempty"`
|
||||
Commands base.StringOrSlice `yaml:"commands,omitempty"`
|
||||
Entrypoint base.StringOrSlice `yaml:"entrypoint,omitempty"`
|
||||
Directory string `yaml:"directory,omitempty"`
|
||||
Settings map[string]any `yaml:"settings"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Pull bool `yaml:"pull,omitempty"`
|
||||
Commands base.StringOrSlice `yaml:"commands,omitempty"`
|
||||
Entrypoint base.StringOrSlice `yaml:"entrypoint,omitempty"`
|
||||
Directory string `yaml:"directory,omitempty"`
|
||||
Settings map[string]any `yaml:"settings,omitempty"`
|
||||
Environment map[string]any `yaml:"environment,omitempty"`
|
||||
// flow control
|
||||
DependsOn base.StringOrSlice `yaml:"depends_on,omitempty"`
|
||||
When constraint.When `yaml:"when,omitempty"`
|
||||
@@ -56,9 +57,6 @@ type (
|
||||
|
||||
// ACTIVE DEVELOPMENT BELOW
|
||||
|
||||
// TODO: remove base.EnvironmentMap and use map[string]any after v3.0.0 release
|
||||
Environment base.EnvironmentMap `yaml:"environment,omitempty"`
|
||||
|
||||
// Remove after v3.1.0
|
||||
Secrets []any `yaml:"secrets,omitempty"`
|
||||
|
||||
@@ -121,6 +119,11 @@ func (c *ContainerList) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML implements custom Yaml marshaling.
|
||||
func (c ContainerList) MarshalYAML() (any, error) {
|
||||
return c.ContainerList, nil
|
||||
}
|
||||
|
||||
func (c *Container) IsPlugin() bool {
|
||||
return len(c.Commands) == 0 &&
|
||||
len(c.Entrypoint) == 0 &&
|
||||
|
||||
@@ -210,6 +210,7 @@ func HandleAuth(c *gin.Context) {
|
||||
// insert the user into the database
|
||||
if err := _store.CreateUser(user); err != nil {
|
||||
log.Error().Err(err).Msgf("cannot insert %s", user.Login)
|
||||
log.Trace().Msgf("user was: %#v", user)
|
||||
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func parseHook(r *http.Request) (*model.Repo, *model.Pipeline, error) {
|
||||
}
|
||||
|
||||
// parsePushHook parses a push hook and returns the Repo and Pipeline details.
|
||||
// If the commit type is unsupported nil values are returned.
|
||||
// If the commit type is unsupported it returns an ErrIgnoreEvent error.
|
||||
func parsePushHook(payload []byte) (*model.Repo, *model.Pipeline, error) {
|
||||
hook := internal.PushHook{}
|
||||
|
||||
@@ -71,7 +71,7 @@ func parsePushHook(payload []byte) (*model.Repo, *model.Pipeline, error) {
|
||||
}
|
||||
return convertRepo(&hook.Repo, &internal.RepoPerm{}), convertPushHook(&hook, &change), nil
|
||||
}
|
||||
return nil, nil, nil
|
||||
return nil, nil, &types.ErrIgnoreEvent{Event: "push", Reason: "BB reports no Changes"}
|
||||
}
|
||||
|
||||
// parsePullHook parses a pull request hook and returns the Repo and Pipeline
|
||||
|
||||
@@ -107,7 +107,7 @@ func Test_parseHook(t *testing.T) {
|
||||
r, b, err := parseHook(req)
|
||||
assert.Nil(t, r)
|
||||
assert.Nil(t, b)
|
||||
assert.NoError(t, err)
|
||||
assert.ErrorIs(t, err, &types.ErrIgnoreEvent{})
|
||||
})
|
||||
|
||||
t.Run("push hook", func(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
bb "github.com/neticdk/go-bitbucket/bitbucket"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ func parseHook(r *http.Request, baseURL string) (*HookResult, error) {
|
||||
result.Repo = convertRepo(&e.PullRequest.Target.Repository, nil, "")
|
||||
result.Pipeline = convertPullRequestEvent(e, baseURL)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported webhook event type: %T", e)
|
||||
return nil, &types.ErrIgnoreEvent{Event: fmt.Sprintf("%T", e), Reason: "unsupported webhook event type"}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -120,16 +120,16 @@ func (c *Gitea) Login(ctx context.Context, req *forge_types.OAuthRequest) (*mode
|
||||
|
||||
token, err := config.Exchange(oauth2Ctx, req.Code)
|
||||
if err != nil {
|
||||
return nil, redirectURL, err
|
||||
return nil, redirectURL, fmt.Errorf("oauth2 config exchange failed: %w", err)
|
||||
}
|
||||
|
||||
client, err := c.newClientToken(ctx, token.AccessToken)
|
||||
if err != nil {
|
||||
return nil, redirectURL, err
|
||||
return nil, redirectURL, fmt.Errorf("client creation with new access token failed: %w", err)
|
||||
}
|
||||
account, _, err := client.GetMyUserInfo()
|
||||
if err != nil {
|
||||
return nil, redirectURL, err
|
||||
return nil, redirectURL, fmt.Errorf("fetching user info failed: %w", err)
|
||||
}
|
||||
|
||||
return &model.User{
|
||||
|
||||
@@ -633,7 +633,7 @@ func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if pipeline.Event == model.EventPush {
|
||||
} else if pipeline != nil && pipeline.Event == model.EventPush {
|
||||
// GitHub has removed commit summaries from Events API payloads from 7th October 2025 onwards.
|
||||
pipeline, err = c.loadChangedFilesFromCommits(ctx, repo, pipeline, prevCommit, currCommit)
|
||||
if err != nil {
|
||||
|
||||
@@ -127,15 +127,9 @@ func setupGitLab(forge *model.Forge) (forge.Forge, error) {
|
||||
}
|
||||
|
||||
func setupGitHub(forge *model.Forge) (forge.Forge, error) {
|
||||
mergeRef, ok := forge.AdditionalOptions["merge-ref"].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing merge-ref")
|
||||
}
|
||||
|
||||
publicOnly, ok := forge.AdditionalOptions["public-only"].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing public-only")
|
||||
}
|
||||
// get additional config and be false by default
|
||||
mergeRef, _ := forge.AdditionalOptions["merge-ref"].(bool)
|
||||
publicOnly, _ := forge.AdditionalOptions["public-only"].(bool)
|
||||
|
||||
opts := github.Opts{
|
||||
URL: forge.URL,
|
||||
|
||||
@@ -50,7 +50,11 @@ func createFilterFunc(agentFilter rpc.Filter) queue.FilterFn {
|
||||
// all task labels are required to be present for an agent to match
|
||||
agentLabelValue, ok := agentFilter.Labels[taskLabel]
|
||||
if !ok {
|
||||
return false, 0
|
||||
// Check for required label
|
||||
agentLabelValue, ok = agentFilter.Labels["!"+taskLabel]
|
||||
if !ok {
|
||||
return false, 0
|
||||
}
|
||||
}
|
||||
|
||||
switch agentLabelValue {
|
||||
|
||||
@@ -119,6 +119,17 @@ func TestCreateFilterFunc(t *testing.T) {
|
||||
wantMatched: true,
|
||||
wantScore: 2,
|
||||
},
|
||||
{
|
||||
name: "Required label matches without shebang",
|
||||
agentFilter: rpc.Filter{
|
||||
Labels: map[string]string{"!org-id": "123", "platform": "linux", "extra": "value"},
|
||||
},
|
||||
task: &model.Task{
|
||||
Labels: map[string]string{"org-id": "123", "platform": "linux", "empty": ""},
|
||||
},
|
||||
wantMatched: true,
|
||||
wantScore: 20,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -34,14 +34,14 @@ type User struct {
|
||||
// required: true
|
||||
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
||||
|
||||
ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id UNIQUE(forge)"`
|
||||
ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id UNIQUE(forge_id) UNIQUE(forge_login)"`
|
||||
|
||||
ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id UNIQUE(forge)"`
|
||||
ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id UNIQUE(forge_id)"`
|
||||
|
||||
// Login is the username for this user.
|
||||
//
|
||||
// required: true
|
||||
Login string `json:"login" xorm:"UNIQUE 'login'"`
|
||||
Login string `json:"login" xorm:"'login' UNIQUE(forge_login)"`
|
||||
|
||||
// AccessToken is the oauth2 access token.
|
||||
AccessToken string `json:"-" xorm:"TEXT 'access_token'"`
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Supported database drivers
|
||||
// Supported database drivers.
|
||||
const (
|
||||
DriverMysql = "mysql"
|
||||
DriverPostgres = "postgres"
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@
|
||||
"dependencies": {
|
||||
"@kyvg/vue3-notification": "^3.4.1",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"ansi_up": "^6.0.6",
|
||||
"dompurify": "^3.2.6",
|
||||
"fuse.js": "^7.1.0",
|
||||
@@ -44,7 +44,7 @@
|
||||
"@intlify/eslint-plugin-vue-i18n": "4.1.0",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "4.1.14",
|
||||
"@tailwindcss/vite": "4.1.16",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
@@ -66,7 +66,7 @@
|
||||
"vite": "^7.0.5",
|
||||
"vite-plugin-prismjs": "^0.0.11",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest": "^4.0.0",
|
||||
"vue-tsc": "^3.0.3"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
Generated
+505
-584
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user