diff --git a/.cspell.json b/.cspell.json index 7e65ae9d0..30b77e051 100644 --- a/.cspell.json +++ b/.cspell.json @@ -79,7 +79,22 @@ "HEALTHCHECK", "devx", "gomod", - "laszlocph" + "laszlocph", + "charmbracelet", + "GODEBUG", + "netdns", + "BUILDPLATFORM", + "repology", + "WORKDIR", + "corepack", + "binutils", + "nocolor", + "logfile", + "Keyfunc", + "protoc", + "PROTOC", + "GOBIN", + "GOPATH" ], "ignorePaths": [ "**/node_modules/**/*", @@ -103,10 +118,8 @@ "agent/", "cli/", "cmd/", - "docker/", "docs/", "pipeline/", - "shared/", "server/" ], "enableFiletypes": ["dockercompose"] diff --git a/.github/renovate.json b/.github/renovate.json index b38fc1202..d3ea2d642 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -2,18 +2,28 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>woodpecker-ci/renovate-config"], "packageRules": [ + { + "matchCurrentVersion": "<1.0.0", + "matchPackageNames": ["github.com/distribution/reference"], + "matchUpdateTypes": ["major", "minor"], + "dependencyDashboardApproval": true + }, + { + "matchPackageNames": ["github.com/charmbracelet/huh/spinner"], + "enabled": false + }, { "matchManagers": ["docker-compose"], - "matchFileNames": ["docker-compose.gitpod.yml"], + "matchFileNames": ["docker-compose.gitpod.yaml"], "addLabels": ["devx"] }, { - "groupName": "golang (lang)", + "groupName": "golang-lang", "matchPackagePatterns": ["^golang$", "xgo"], "matchUpdateTypes": ["minor", "patch"] }, { - "groupName": "golang (packages)", + "groupName": "golang-packages", "matchManagers": ["gomod"], "matchUpdateTypes": ["minor", "patch"] }, diff --git a/.gitignore b/.gitignore index e4bc51c2e..90d70c085 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ extras/ /build/ /dist/ /data/ +datastore/migration/testfiles/ docs/venv diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 000000000..cc734bcae --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,2 @@ +--- +disable-version-string: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 258f58202..fa3427fe3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,12 +5,12 @@ repos: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/golangci/golangci-lint - rev: v1.57.1 + rev: v1.57.2 hooks: - id: golangci-lint - repo: https://github.com/igorshubovych/markdownlint-cli @@ -24,7 +24,7 @@ repos: - id: checkmake exclude: '^docker/Dockerfile.make$' # actually a Dockerfile and not a makefile - repo: https://github.com/hadolint/hadolint - rev: v2.12.1-beta + rev: v2.12.0 hooks: - id: hadolint - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/.woodpecker/binaries.yaml b/.woodpecker/binaries.yaml index 73c589f0b..2109c4840 100644 --- a/.woodpecker/binaries.yaml +++ b/.woodpecker/binaries.yaml @@ -2,7 +2,7 @@ when: event: tag variables: - - &golang_image 'docker.io/golang:1.22.1' + - &golang_image 'docker.io/golang:1.22.2' - &node_image 'docker.io/node:21-alpine' - &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.1' - &xgo_version 'go-1.21.2' diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index d7807c14c..592a2030a 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -1,9 +1,9 @@ variables: - - &golang_image 'docker.io/golang:1.22.1' + - &golang_image 'docker.io/golang:1.22.2' - &node_image 'docker.io/node:21-alpine' - &xgo_image 'docker.io/techknowlogick/xgo:go-1.22.1' - &xgo_version 'go-1.21.2' - - &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:3.2.0' + - &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:3.2.1' - &platforms_release 'linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/386,linux/amd64,linux/ppc64le,linux/riscv64,linux/s390x,freebsd/arm64,freebsd/amd64,openbsd/arm64,openbsd/amd64' - &platforms_server 'linux/arm/v7,linux/arm64/v8,linux/amd64,linux/ppc64le,linux/riscv64' - &platforms_preview 'linux/amd64' diff --git a/Makefile b/Makefile index c0029f25b..e4a33838f 100644 --- a/Makefile +++ b/Makefile @@ -58,8 +58,6 @@ ifeq (in_docker,$(firstword $(MAKECMDGOALS))) -e TARGETOS="$(TARGETOS)" \ -e TARGETARCH="$(TARGETARCH)" \ -e CGO_ENABLED="$(CGO_ENABLED)" \ - -e GOPATH=/tmp/go \ - -e HOME=/tmp/home \ -v $(PWD):/build --rm woodpecker/make:local make $(MAKE_ARGS) else @@ -110,7 +108,7 @@ clean-all: clean ## Clean all artifacts rm -rf docs/docs/40-cli.md docs/swagger.json .PHONY: generate -generate: generate-swagger ## Run all code generations +generate: install-tools generate-swagger ## Run all code generations go generate ./... generate-swagger: install-tools ## Run swagger code generation @@ -137,6 +135,15 @@ install-tools: ## Install development tools fi ; \ hash addlicense > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ go install github.com/google/addlicense@latest; \ + fi ; \ + hash mockery > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go install github.com/vektra/mockery/v2@latest; \ + fi ; \ + hash protoc-gen-go > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest; \ + fi ; \ + hash protoc-gen-go-grpc > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest; \ fi ui-dependencies: ## Install UI dependencies diff --git a/agent/rpc/client_grpc.go b/agent/rpc/client_grpc.go index 5648d31cf..b0960b16e 100644 --- a/agent/rpc/client_grpc.go +++ b/agent/rpc/client_grpc.go @@ -53,6 +53,7 @@ func (c *client) Close() error { func (c *client) newBackOff() backoff.BackOff { b := backoff.NewExponentialBackOff() + b.MaxElapsedTime = 0 b.MaxInterval = 10 * time.Second //nolint: gomnd b.InitialInterval = 10 * time.Millisecond //nolint: gomnd return b diff --git a/agent/runner.go b/agent/runner.go index 22a8729ef..20fc1d40e 100644 --- a/agent/runner.go +++ b/agent/runner.go @@ -50,7 +50,7 @@ func NewRunner(workEngine rpc.Peer, f rpc.Filter, h string, state *State, backen } } -func (r *Runner) Run(runnerCtx context.Context) error { +func (r *Runner) Run(runnerCtx context.Context) error { //nolint:contextcheck log.Debug().Msg("request next execution") meta, _ := metadata.FromOutgoingContext(runnerCtx) diff --git a/cli/common/hooks.go b/cli/common/hooks.go index 5ff2a1242..952742cba 100644 --- a/cli/common/hooks.go +++ b/cli/common/hooks.go @@ -37,7 +37,7 @@ func Before(c *cli.Context) error { log.Debug().Msg("Checking for updates ...") - newVersion, err := update.CheckForUpdate(waitForUpdateCheck, true) + newVersion, err := update.CheckForUpdate(waitForUpdateCheck, false) if err != nil { log.Error().Err(err).Msgf("Failed to check for updates") return @@ -57,8 +57,8 @@ func After(_ *cli.Context) error { if waitForUpdateCheck != nil { select { case <-waitForUpdateCheck.Done(): - // When the actual command already finished, we still wait 250ms for the update check to finish - case <-time.After(time.Millisecond * 250): + // When the actual command already finished, we still wait 500ms for the update check to finish + case <-time.After(time.Millisecond * 500): log.Debug().Msg("Update check stopped due to timeout") cancelWaitForUpdate(errors.New("update check timeout")) } diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index 77405dc66..50d32bec0 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -30,9 +30,12 @@ func Load(c *cli.Context) error { return err } - if config == nil && !c.IsSet("server-url") && !c.IsSet("token") { - log.Info().Msg("The woodpecker-cli is not yet set up. Please run `woodpecker-cli setup`") - return errors.New("woodpecker-cli is not setup") + if config == nil { + config = &Config{ + LogLevel: "info", + ServerURL: c.String("server-url"), + Token: c.String("token"), + } } if !c.IsSet("server") { @@ -56,6 +59,11 @@ func Load(c *cli.Context) error { } } + if config.ServerURL == "" || config.Token == "" { + log.Info().Msg("The woodpecker-cli is not yet set up. Please run `woodpecker-cli setup` or provide the required environment variables / flags.") + return errors.New("woodpecker-cli is not configured") + } + return nil } diff --git a/cli/lint/lint.go b/cli/lint/lint.go index 7d2a59d74..a11927d77 100644 --- a/cli/lint/lint.go +++ b/cli/lint/lint.go @@ -114,7 +114,7 @@ func lintFile(_ *cli.Context, file string) error { hasErrors = true } - if data := err.GetLinterData(); data != nil { + if data := pipeline_errors.GetLinterData(err); 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) diff --git a/cli/setup/setup.go b/cli/setup/setup.go index b114cf750..bdacd0127 100644 --- a/cli/setup/setup.go +++ b/cli/setup/setup.go @@ -13,8 +13,10 @@ import ( // Command exports the setup command. var Command = &cli.Command{ - Name: "setup", - Usage: "setup the woodpecker-cli for the first time", + Name: "setup", + Usage: "setup the woodpecker-cli for the first time", + Args: true, + ArgsUsage: "[server-url]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "server-url", @@ -45,6 +47,9 @@ func setup(c *cli.Context) error { } serverURL := c.String("server-url") + if serverURL == "" { + serverURL = c.Args().First() + } if serverURL == "" { serverURL, err = ui.Ask("Enter the URL of the woodpecker server", "https://ci.woodpecker-ci.org", true) diff --git a/cli/setup/token_fetcher.go b/cli/setup/token_fetcher.go index e96772cac..59be6b08d 100644 --- a/cli/setup/token_fetcher.go +++ b/cli/setup/token_fetcher.go @@ -10,6 +10,7 @@ import ( "runtime" "time" + "github.com/charmbracelet/huh/spinner" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) @@ -37,13 +38,27 @@ func receiveTokenFromUI(c context.Context, serverURL string) (string, error) { return "", err } + spinnerCtx, spinnerDone := context.WithCancelCause(c) + go func() { + err = spinner.New(). + Title("Waiting for token ..."). + Context(spinnerCtx). + Run() + if err != nil { + return + } + }() + // wait for token to be received or timeout select { case token := <-tokenReceived: + spinnerDone(nil) return token, nil case <-c.Done(): + spinnerDone(nil) return "", c.Err() case <-time.After(5 * time.Minute): + spinnerDone(nil) return "", errors.New("timed out waiting for token") } } diff --git a/cli/setup/ui/ask.go b/cli/setup/ui/ask.go index 2fa8c539e..eea4b2b00 100644 --- a/cli/setup/ui/ask.go +++ b/cli/setup/ui/ask.go @@ -1,79 +1,26 @@ package ui import ( - "fmt" + "errors" "strings" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" ) -type askModel struct { - prompt string - required bool - textInput textinput.Model - err error -} - -func (m askModel) Init() tea.Cmd { - return textinput.Blink -} - -func (m askModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyEnter: - if !m.required || (m.required && strings.TrimSpace(m.textInput.Value()) != "") { - return m, tea.Quit - } - case tea.KeyCtrlC, tea.KeyEsc: - return m, tea.Quit - } - default: - return m, cmd - } - - m.textInput, cmd = m.textInput.Update(msg) - return m, cmd -} - -func (m askModel) View() string { - return fmt.Sprintf( - "%s\n\n%s\n\n%s", - m.prompt, - m.textInput.View(), - "(esc to quit)", - ) + "\n" -} - func Ask(prompt, placeholder string, required bool) (string, error) { - ti := textinput.New() - ti.Placeholder = placeholder - ti.Focus() - ti.CharLimit = 156 - ti.Width = 40 - - p := tea.NewProgram(askModel{ - prompt: prompt, - textInput: ti, - required: required, - err: nil, - }) - - _m, err := p.Run() + var input string + err := huh.NewInput(). + Title(prompt). + Value(&input). + Placeholder(placeholder).Validate(func(s string) error { + if required && strings.TrimSpace(s) == "" { + return errors.New("required") + } + return nil + }).Run() if err != nil { return "", err } - m, ok := _m.(askModel) - if !ok { - return "", fmt.Errorf("unexpected model: %T", _m) - } - - text := strings.TrimSpace(m.textInput.Value()) - - return text, nil + return strings.TrimSpace(input), nil } diff --git a/cli/setup/ui/confirm.go b/cli/setup/ui/confirm.go index 350fdb2e4..1a7dfa494 100644 --- a/cli/setup/ui/confirm.go +++ b/cli/setup/ui/confirm.go @@ -1,71 +1,19 @@ package ui import ( - "fmt" - - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" ) -type confirmModel struct { - confirmed bool - prompt string - err error -} - -func (m confirmModel) Init() tea.Cmd { - return textinput.Blink -} - -func (m confirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.Runes != nil { - switch msg.Runes[0] { - case 'y': - m.confirmed = true - return m, tea.Quit - case 'n': - m.confirmed = false - return m, tea.Quit - } - } - - switch msg.Type { - case tea.KeyCtrlC, tea.KeyEsc: - return m, tea.Quit - } - default: - return m, nil - } - - return m, cmd -} - -func (m confirmModel) View() string { - return fmt.Sprintf( - "%s y / n (esc to quit)", - m.prompt, - ) + "\n" -} - func Confirm(prompt string) (bool, error) { - p := tea.NewProgram(confirmModel{ - prompt: prompt, - err: nil, - }) - - _m, err := p.Run() + var confirm bool + err := huh.NewConfirm(). + Title(prompt). + Affirmative("Yes!"). + Negative("No."). + Value(&confirm).Run() if err != nil { return false, err } - m, ok := _m.(confirmModel) - if !ok { - return false, fmt.Errorf("unexpected model: %T", _m) - } - - return m.confirmed, nil + return confirm, err } diff --git a/cli/update/types.go b/cli/update/types.go index 35ae99181..7f2bf6b77 100644 --- a/cli/update/types.go +++ b/cli/update/types.go @@ -1,11 +1,9 @@ package update -type GithubRelease struct { - TagName string `json:"tag_name"` - Assets []struct { - Name string `json:"name"` - BrowserDownloadURL string `json:"browser_download_url"` - } `json:"assets"` +type VersionData struct { + Latest string `json:"latest"` + Next string `json:"next"` + RC string `json:"rc"` } type NewVersion struct { @@ -13,4 +11,7 @@ type NewVersion struct { AssetURL string } -const githubReleaseURL = "https://api.github.com/repos/woodpecker-ci/woodpecker/releases/latest" +const ( + woodpeckerVersionURL = "https://woodpecker-ci.org/version.json" + githubBinaryURL = "https://github.com/woodpecker-ci/woodpecker/releases/download/v%s/woodpecker-cli_%s_%s.tar.gz" +) diff --git a/cli/update/updater.go b/cli/update/updater.go index dce5b5d1c..41d6eb3b8 100644 --- a/cli/update/updater.go +++ b/cli/update/updater.go @@ -10,6 +10,7 @@ import ( "os" "path" "runtime" + "strings" "github.com/rs/zerolog/log" @@ -17,14 +18,18 @@ import ( ) func CheckForUpdate(ctx context.Context, force bool) (*NewVersion, error) { + return checkForUpdate(ctx, woodpeckerVersionURL, force) +} + +func checkForUpdate(ctx context.Context, versionURL string, force bool) (*NewVersion, error) { log.Debug().Msgf("Current version: %s", version.String()) - if version.String() == "dev" && !force { - log.Debug().Msgf("Skipping update check for development version") + if (version.String() == "dev" || strings.HasPrefix(version.String(), "next-")) && !force { + log.Debug().Msgf("Skipping update check for development & next versions") return nil, nil } - req, err := http.NewRequestWithContext(ctx, "GET", githubReleaseURL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", versionURL, nil) if err != nil { return nil, err } @@ -39,34 +44,32 @@ func CheckForUpdate(ctx context.Context, force bool) (*NewVersion, error) { return nil, errors.New("failed to fetch the latest release") } - var release GithubRelease - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + var versionData VersionData + if err := json.NewDecoder(resp.Body).Decode(&versionData); err != nil { return nil, err } + upstreamVersion := versionData.Latest + if strings.HasPrefix(version.String(), "next-") { + upstreamVersion = versionData.Next + } else if strings.HasSuffix(version.String(), "rc-") { + upstreamVersion = versionData.RC + } + + installedVersion := strings.TrimPrefix(version.Version, "v") + upstreamVersion = strings.TrimPrefix(upstreamVersion, "v") + // using the latest release - if release.TagName == version.String() && !force { + if installedVersion == upstreamVersion && !force { + log.Debug().Msgf("No new version available") return nil, nil } - log.Debug().Msgf("Latest version: %s", release.TagName) - - assetURL := "" - fileName := fmt.Sprintf("woodpecker-cli_%s_%s.tar.gz", runtime.GOOS, runtime.GOARCH) - for _, asset := range release.Assets { - if fileName == asset.Name { - assetURL = asset.BrowserDownloadURL - log.Debug().Msgf("Found asset for the current OS and arch: %s", assetURL) - break - } - } - - if assetURL == "" { - return nil, errors.New("no asset found for the current OS") - } + log.Debug().Msgf("New version available: %s", upstreamVersion) + assetURL := fmt.Sprintf(githubBinaryURL, upstreamVersion, runtime.GOOS, runtime.GOARCH) return &NewVersion{ - Version: release.TagName, + Version: upstreamVersion, AssetURL: assetURL, }, nil } diff --git a/cli/update/updater_test.go b/cli/update/updater_test.go new file mode 100644 index 000000000..a2855ac8e --- /dev/null +++ b/cli/update/updater_test.go @@ -0,0 +1,61 @@ +package update + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "go.woodpecker-ci.org/woodpecker/v2/version" +) + +func TestCheckForUpdate(t *testing.T) { + version.Version = "1.0.0" + fixtureHandler := func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/version.json" { + http.NotFound(w, r) + return + } + + _, _ = io.WriteString(w, `{"latest": "1.0.1", "next": "1.0.2", "rc": "1.0.3"}`) + } + ts := httptest.NewServer(http.HandlerFunc(fixtureHandler)) + defer ts.Close() + + newVersion, err := checkForUpdate(context.Background(), ts.URL+"/version.json", false) + if err != nil { + t.Fatalf("Failed to check for updates: %v", err) + } + + if newVersion == nil || newVersion.Version != "1.0.1" { + t.Fatalf("Expected a new version 1.0.1, got: %s", newVersion) + } +} + +func TestDownloadNewVersion(t *testing.T) { + downloadFilePath := "/woodpecker-cli_linux_amd64.tar.gz" + + fixtureHandler := func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != downloadFilePath { + http.NotFound(w, r) + return + } + + _, _ = io.WriteString(w, `blob`) + } + ts := httptest.NewServer(http.HandlerFunc(fixtureHandler)) + defer ts.Close() + + file, err := downloadNewVersion(context.Background(), ts.URL+downloadFilePath) + if err != nil { + t.Fatalf("Failed to download new version: %v", err) + } + + if file == "" { + t.Fatalf("Expected a file path, got: %s", file) + } + + _ = os.Remove(file) +} diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 6c1d65c4e..1195f9829 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -2012,6 +2012,53 @@ const docTemplate = `{ } } }, + "/repos/{repo_id}/logs/{number}/{stepId}": { + "delete": { + "produces": [ + "text/plain" + ], + "tags": [ + "Pipeline logs" + ], + "summary": "Deletes step log", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cpersonal access token\u003e", + "description": "Insert your personal access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "the repository id", + "name": "repo_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "the number of the pipeline", + "name": "number", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "the step id", + "name": "stepId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/repos/{repo_id}/move": { "post": { "produces": [ @@ -3844,6 +3891,9 @@ const docTemplate = `{ "Org": { "type": "object", "properties": { + "forge_id": { + "type": "integer" + }, "id": { "type": "integer" }, @@ -3922,7 +3972,7 @@ const docTemplate = `{ "errors": { "type": "array", "items": { - "$ref": "#/definitions/errors.PipelineError" + "$ref": "#/definitions/types.PipelineError" } }, "event": { @@ -4048,6 +4098,9 @@ const docTemplate = `{ "active": { "type": "boolean" }, + "allow_deploy": { + "type": "boolean" + }, "allow_pr": { "type": "boolean" }, @@ -4072,6 +4125,9 @@ const docTemplate = `{ "default_branch": { "type": "string" }, + "forge_id": { + "type": "integer" + }, "forge_remote_id": { "description": "ForgeRemoteID is the unique identifier for the repository on the forge.", "type": "string" @@ -4123,6 +4179,9 @@ const docTemplate = `{ "RepoPatch": { "type": "object", "properties": { + "allow_deploy": { + "type": "boolean" + }, "allow_pr": { "type": "boolean" }, @@ -4365,6 +4424,9 @@ const docTemplate = `{ "description": "Email is the email address for this user.\n\nrequired: true", "type": "string" }, + "forge_id": { + "type": "integer" + }, "id": { "description": "the id for this user.\n\nrequired: true", "type": "integer" @@ -4402,45 +4464,6 @@ const docTemplate = `{ "EventManual" ] }, - "errors.PipelineError": { - "type": "object", - "properties": { - "data": {}, - "is_warning": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/errors.PipelineErrorType" - } - } - }, - "errors.PipelineErrorType": { - "type": "string", - "enum": [ - "linter", - "deprecation", - "compiler", - "generic", - "bad_habit" - ], - "x-enum-comments": { - "PipelineErrorTypeBadHabit": "some bad-habit error", - "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", - "PipelineErrorTypeBadHabit" - ] - }, "model.Workflow": { "type": "object", "properties": { @@ -4487,6 +4510,45 @@ const docTemplate = `{ "$ref": "#/definitions/StatusValue" } } + }, + "types.PipelineError": { + "type": "object", + "properties": { + "data": {}, + "is_warning": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/types.PipelineErrorType" + } + } + }, + "types.PipelineErrorType": { + "type": "string", + "enum": [ + "linter", + "deprecation", + "compiler", + "generic", + "bad_habit" + ], + "x-enum-comments": { + "PipelineErrorTypeBadHabit": "some bad-habit error", + "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", + "PipelineErrorTypeBadHabit" + ] } } }` diff --git a/cmd/server/flags.go b/cmd/server/flags.go index 152070ba5..9789a8089 100644 --- a/cmd/server/flags.go +++ b/cmd/server/flags.go @@ -246,11 +246,6 @@ var flags = append([]cli.Flag{ Usage: "Disable version check in admin web ui.", Name: "skip-version-check", }, - &cli.StringSliceFlag{ - EnvVars: []string{"WOODPECKER_ADDONS"}, - Name: "addons", - Usage: "list of addon files", - }, // // backend options for pipeline compiler // @@ -309,6 +304,35 @@ var flags = append([]cli.Flag{ Usage: "set the cpus allowed to execute containers", }, // + &cli.StringFlag{ + Name: "forge-url", + Usage: "url of the forge", + EnvVars: []string{"WOODPECKER_FORGE_URL", "WOODPECKER_GITHUB_URL", "WOODPECKER_GITLAB_URL", "WOODPECKER_GITEA_URL", "WOODPECKER_BITBUCKET_URL"}, + }, + &cli.StringFlag{ + Name: "forge-oauth-client", + Usage: "oauth2 client id", + EnvVars: []string{"WOODPECKER_FORGE_CLIENT", "WOODPECKER_GITHUB_CLIENT", "WOODPECKER_GITLAB_CLIENT", "WOODPECKER_GITEA_CLIENT", "WOODPECKER_BITBUCKET_CLIENT", "WOODPECKER_BITBUCKET_DC_CLIENT_ID"}, + }, + &cli.StringFlag{ + Name: "forge-oauth-secret", + Usage: "oauth2 client secret", + EnvVars: []string{"WOODPECKER_FORGE_SECRET", "WOODPECKER_GITHUB_SECRET", "WOODPECKER_GITLAB_SECRET", "WOODPECKER_GITEA_SECRET", "WOODPECKER_BITBUCKET_SECRET", "WOODPECKER_BITBUCKET_DC_CLIENT_SECRET"}, + }, + &cli.BoolFlag{ + Name: "forge-skip-verify", + Usage: "skip ssl verification", + EnvVars: []string{"WOODPECKER_FORGE_SKIP_VERIFY", "WOODPECKER_GITHUB_SKIP_VERIFY", "WOODPECKER_GITLAB_SKIP_VERIFY", "WOODPECKER_GITEA_SKIP_VERIFY", "WOODPECKER_BITBUCKET_SKIP_VERIFY"}, + }, + // + // Addon + // + &cli.StringFlag{ + EnvVars: []string{"WOODPECKER_ADDON_FORGE"}, + Name: "addon-forge", + Usage: "path to forge addon executable", + }, + // // GitHub // &cli.BoolFlag{ @@ -316,24 +340,6 @@ var flags = append([]cli.Flag{ Name: "github", Usage: "github driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_URL"}, - Name: "github-server", - Usage: "github server address", - Value: "https://github.com", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_CLIENT"}, - Name: "github-client", - Usage: "github oauth2 client id", - FilePath: os.Getenv("WOODPECKER_GITHUB_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_SECRET"}, - Name: "github-secret", - Usage: "github oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_GITHUB_SECRET_FILE"), - }, &cli.BoolFlag{ EnvVars: []string{"WOODPECKER_GITHUB_MERGE_REF"}, Name: "github-merge-ref", @@ -341,9 +347,10 @@ var flags = append([]cli.Flag{ Value: true, }, &cli.BoolFlag{ - EnvVars: []string{"WOODPECKER_GITHUB_SKIP_VERIFY"}, - Name: "github-skip-verify", - Usage: "github skip ssl verification", + EnvVars: []string{"WOODPECKER_GITHUB_PUBLIC_ONLY"}, + Name: "github-public-only", + Usage: "github tokens should only get access to public repos", + Value: false, }, // // Gitea @@ -353,29 +360,6 @@ var flags = append([]cli.Flag{ Name: "gitea", Usage: "gitea driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITEA_URL"}, - Name: "gitea-server", - Usage: "gitea server address", - Value: "https://try.gitea.io", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITEA_CLIENT"}, - Name: "gitea-client", - Usage: "gitea oauth2 client id", - FilePath: os.Getenv("WOODPECKER_GITEA_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITEA_SECRET"}, - Name: "gitea-secret", - Usage: "gitea oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_GITEA_SECRET_FILE"), - }, - &cli.BoolFlag{ - EnvVars: []string{"WOODPECKER_GITEA_SKIP_VERIFY"}, - Name: "gitea-skip-verify", - Usage: "gitea skip ssl verification", - }, &cli.StringFlag{ EnvVars: []string{"WOODPECKER_DEV_GITEA_OAUTH_URL"}, Name: "gitea-oauth-server", @@ -389,18 +373,6 @@ var flags = append([]cli.Flag{ Name: "bitbucket", Usage: "bitbucket driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_CLIENT"}, - Name: "bitbucket-client", - Usage: "bitbucket oauth2 client id", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_SECRET"}, - Name: "bitbucket-secret", - Usage: "bitbucket oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_SECRET_FILE"), - }, // // Gitlab // @@ -409,29 +381,6 @@ var flags = append([]cli.Flag{ Name: "gitlab", Usage: "gitlab driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_URL"}, - Name: "gitlab-server", - Usage: "gitlab server address", - Value: "https://gitlab.com", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_CLIENT"}, - Name: "gitlab-client", - Usage: "gitlab oauth2 client id", - FilePath: os.Getenv("WOODPECKER_GITLAB_CLIENT_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_SECRET"}, - Name: "gitlab-secret", - Usage: "gitlab oauth2 client secret", - FilePath: os.Getenv("WOODPECKER_GITLAB_SECRET_FILE"), - }, - &cli.BoolFlag{ - EnvVars: []string{"WOODPECKER_GITLAB_SKIP_VERIFY"}, - Name: "gitlab-skip-verify", - Usage: "gitlab skip ssl verification", - }, // // Bitbucket DataCenter/Server (previously Stash) // @@ -440,23 +389,6 @@ var flags = append([]cli.Flag{ Name: "bitbucket-dc", Usage: "Bitbucket DataCenter/Server driver is enabled", }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_DC_URL"}, - Name: "bitbucket-dc-server", - Usage: "Bitbucket DataCenter/Server server address", - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_DC_CLIENT_ID"}, - Name: "bitbucket-dc-client-id", - Usage: "Bitbucket DataCenter/Server OAuth 2.0 client id", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_DC_CLIENT_ID_FILE"), - }, - &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_BITBUCKET_DC_CLIENT_SECRET"}, - Name: "bitbucket-dc-client-secret", - Usage: "Bitbucket DataCenter/Server OAuth 2.0 client secret", - FilePath: os.Getenv("WOODPECKER_BITBUCKET_DC_CLIENT_SECRET_FILE"), - }, &cli.StringFlag{ EnvVars: []string{"WOODPECKER_BITBUCKET_DC_GIT_USERNAME"}, Name: "bitbucket-dc-git-username", diff --git a/cmd/server/server.go b/cmd/server/server.go index 3eb3b4bbc..39ddc2580 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -38,7 +38,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc/proto" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/cron" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/setup" woodpeckerGrpcServer "go.woodpecker-ci.org/woodpecker/v2/server/grpc" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -82,11 +82,6 @@ func run(c *cli.Context) error { ) } - _forge, err := setupForge(c) - if err != nil { - return fmt.Errorf("can't setup forge: %w", err) - } - _store, err := setupStore(c) if err != nil { return fmt.Errorf("can't setup store: %w", err) @@ -97,7 +92,7 @@ func run(c *cli.Context) error { } }() - err = setupEvilGlobals(c, _store, _forge) + err = setupEvilGlobals(c, _store) if err != nil { return fmt.Errorf("can't setup globals: %w", err) } @@ -107,7 +102,7 @@ func run(c *cli.Context) error { setupMetrics(&g, _store) g.Go(func() error { - return cron.Start(c.Context, _store, _forge) + return cron.Start(c.Context, _store) }) // start the grpc server @@ -130,7 +125,6 @@ func run(c *cli.Context) error { ) woodpeckerServer := woodpeckerGrpcServer.NewWoodpeckerServer( - _forge, server.Config.Services.Queue, server.Config.Services.Logs, server.Config.Services.Pubsub, @@ -270,17 +264,13 @@ func run(c *cli.Context) error { return g.Wait() } -func setupEvilGlobals(c *cli.Context, s store.Store, f forge.Forge) error { - // forge - server.Config.Services.Forge = f - +func setupEvilGlobals(c *cli.Context, s store.Store) error { // services server.Config.Services.Queue = setupQueue(c, s) server.Config.Services.Logs = logging.New() server.Config.Services.Pubsub = pubsub.New() - server.Config.Services.Membership = setupMembershipService(c, f) - - serviceMangager, err := services.NewManager(c, s) + server.Config.Services.Membership = setupMembershipService(c, s) + serviceMangager, err := services.NewManager(c, s, setup.Forge) if err != nil { return fmt.Errorf("could not setup service manager: %w", err) } diff --git a/cmd/server/setup.go b/cmd/server/setup.go index fa3bba2d8..8df86cc86 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -18,9 +18,7 @@ package main import ( "context" "fmt" - "net/url" "os" - "strings" "time" "github.com/prometheus/client_golang/prometheus" @@ -31,17 +29,9 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/cache" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucket" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucketdatacenter" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/github" - "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab" "go.woodpecker-ci.org/woodpecker/v2/server/queue" "go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store/datastore" - "go.woodpecker-ci.org/woodpecker/v2/shared/addon" - addonTypes "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types" ) func setupStore(c *cli.Context) (store.Store, error) { @@ -101,108 +91,8 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue { return queue.WithTaskStore(queue.New(c.Context), s) } -func setupMembershipService(_ *cli.Context, r forge.Forge) cache.MembershipService { - return cache.NewMembershipService(r) -} - -// setupForge helper function to set up the forge from the CLI arguments. -func setupForge(c *cli.Context) (forge.Forge, error) { - addonForge, err := addon.Load[forge.Forge](c.StringSlice("addons"), addonTypes.TypeForge) - if err != nil { - return nil, err - } - if addonForge != nil { - return addonForge.Value, nil - } - - switch { - case c.Bool("github"): - return setupGitHub(c) - case c.Bool("gitlab"): - return setupGitLab(c) - case c.Bool("bitbucket"): - return setupBitbucket(c) - case c.Bool("bitbucket-dc"): - return setupBitbucketDatacenter(c) - case c.Bool("gitea"): - return setupGitea(c) - default: - return nil, fmt.Errorf("version control system not configured") - } -} - -// setupBitbucket helper function to setup the Bitbucket forge from the CLI arguments. -func setupBitbucket(c *cli.Context) (forge.Forge, error) { - opts := &bitbucket.Opts{ - Client: c.String("bitbucket-client"), - Secret: c.String("bitbucket-secret"), - } - log.Trace().Msgf("forge (bitbucket) opts: %#v", opts) - return bitbucket.New(opts) -} - -// setupGitea helper function to set up the Gitea forge from the CLI arguments. -func setupGitea(c *cli.Context) (forge.Forge, error) { - server, err := url.Parse(c.String("gitea-server")) - if err != nil { - return nil, err - } - oauth2Server := c.String("gitea-oauth-server") - if oauth2Server != "" { - oauth2URL, err := url.Parse(oauth2Server) - if err != nil { - return nil, err - } - oauth2Server = strings.TrimRight(oauth2URL.String(), "/") - } - opts := gitea.Opts{ - URL: strings.TrimRight(server.String(), "/"), - OAuth2URL: oauth2Server, - Client: c.String("gitea-client"), - Secret: c.String("gitea-secret"), - SkipVerify: c.Bool("gitea-skip-verify"), - } - if len(opts.URL) == 0 { - return nil, fmt.Errorf("WOODPECKER_GITEA_URL must be set") - } - log.Trace().Msgf("forge (gitea) opts: %#v", opts) - return gitea.New(opts) -} - -// setupBitbucketDatacenter helper function to setup the Bitbucket DataCenter/Server forge from the CLI arguments. -func setupBitbucketDatacenter(c *cli.Context) (forge.Forge, error) { - opts := bitbucketdatacenter.Opts{ - URL: c.String("bitbucket-dc-server"), - Username: c.String("bitbucket-dc-git-username"), - Password: c.String("bitbucket-dc-git-password"), - ClientID: c.String("bitbucket-dc-client-id"), - ClientSecret: c.String("bitbucket-dc-client-secret"), - } - log.Trace().Msgf("Forge (bitbucketdatacenter) opts: %#v", opts) - return bitbucketdatacenter.New(opts) -} - -// setupGitLab helper function to setup the GitLab forge from the CLI arguments. -func setupGitLab(c *cli.Context) (forge.Forge, error) { - return gitlab.New(gitlab.Opts{ - URL: c.String("gitlab-server"), - ClientID: c.String("gitlab-client"), - ClientSecret: c.String("gitlab-secret"), - SkipVerify: c.Bool("gitlab-skip-verify"), - }) -} - -// setupGitHub helper function to setup the GitHub forge from the CLI arguments. -func setupGitHub(c *cli.Context) (forge.Forge, error) { - opts := github.Opts{ - URL: c.String("github-server"), - Client: c.String("github-client"), - Secret: c.String("github-secret"), - SkipVerify: c.Bool("github-skip-verify"), - MergeRef: c.Bool("github-merge-ref"), - } - log.Trace().Msgf("forge (github) opts: %#v", opts) - return github.New(opts) +func setupMembershipService(_ *cli.Context, _store store.Store) cache.MembershipService { + return cache.NewMembershipService(_store) } func setupMetrics(g *errgroup.Group, _store store.Store) { diff --git a/docker/Dockerfile.make b/docker/Dockerfile.make index 5f508a562..84f8e3140 100644 --- a/docker/Dockerfile.make +++ b/docker/Dockerfile.make @@ -1,17 +1,19 @@ # docker build --rm -f docker/Dockerfile.make -t woodpecker/make:local . -FROM docker.io/golang:1.22-alpine3.18 as golang_image -FROM docker.io/node:21-alpine3.18 +FROM docker.io/golang:1.22-alpine3.19 as golang_image +FROM docker.io/node:21-alpine3.19 -# renovate: datasource=repology depName=alpine_3_18/make versioning=loose -ENV MAKE_VERSION="4.4.1-r1" -# renovate: datasource=repology depName=alpine_3_18/gcc versioning=loose -ENV GCC_VERSION="12.2.1_git20220924-r10" -# renovate: datasource=repology depName=alpine_3_18/binutils-gold versioning=loose -ENV BINUTILS_GOLD_VERSION="2.40-r7" -# renovate: datasource=repology depName=alpine_3_18/musl-dev versioning=loose -ENV MUSL_DEV_VERSION="1.2.4-r2" +# renovate: datasource=repology depName=alpine_3_19/make versioning=loose +ENV MAKE_VERSION="4.4.1-r2" +# renovate: datasource=repology depName=alpine_3_19/gcc versioning=loose +ENV GCC_VERSION="13.2.1_git20231014-r0" +# renovate: datasource=repology depName=alpine_3_19/binutils-gold versioning=loose +ENV BINUTILS_GOLD_VERSION="2.41-r0" +# renovate: datasource=repology depName=alpine_3_19/musl-dev versioning=loose +ENV MUSL_DEV_VERSION="1.2.4_git20230717-r4" +# renovate: datasource=repology depName=alpine_3_19/protoc versioning=loose +ENV PROTOC_VERSION="24.4-r0" -RUN apk add --no-cache --update make=${MAKE_VERSION} gcc=${GCC_VERSION} binutils-gold=${BINUTILS_GOLD_VERSION} musl-dev=${MUSL_DEV_VERSION} && \ +RUN apk add --no-cache --update make=${MAKE_VERSION} gcc=${GCC_VERSION} binutils-gold=${BINUTILS_GOLD_VERSION} musl-dev=${MUSL_DEV_VERSION} protoc=${PROTOC_VERSION} && \ corepack enable # Build packages. @@ -20,9 +22,12 @@ COPY Makefile / ENV PATH=$PATH:/usr/local/go/bin # Cache tools -RUN make install-tools && \ - mv /root/go/bin/* /usr/local/go/bin/ && \ - chmod 755 /usr/local/go/bin/* +RUN GOBIN=/usr/local/go/bin make install-tools && \ + rm -rf /Makefile + +ENV GOPATH=/tmp/go +ENV HOME=/tmp/home +ENV PATH=$PATH:/usr/local/go/bin:/tmp/go/bin WORKDIR /build RUN chmod -R 777 /root diff --git a/docs/docs/10-intro.md b/docs/docs/10-intro.md index 2d7f70950..309c6f1af 100644 --- a/docs/docs/10-intro.md +++ b/docs/docs/10-intro.md @@ -75,7 +75,7 @@ kubectl apply -f $PLUGIN_TEMPLATE ```yaml title=".woodpecker.yaml" steps: - deploy-to-k8s: + - name: deploy-to-k8s image: laszlocloud/my-k8s-plugin settings: template: config/k8s/service.yaml diff --git a/docs/docs/20-usage/20-workflow-syntax.md b/docs/docs/20-usage/20-workflow-syntax.md index 4e7759209..8effc3973 100644 --- a/docs/docs/20-usage/20-workflow-syntax.md +++ b/docs/docs/20-usage/20-workflow-syntax.md @@ -50,7 +50,8 @@ git commit -m "updated README [CI SKIP]" ## Steps -Every step of your workflow executes commands inside a specified container. The defined commands are executed serially. +Every step of your workflow executes commands inside a specified container.
+The defined steps are executed in sequence by default, if they should run in parallel you can use [`depends_on`](./20-workflow-syntax.md#depends_on).
The associated commit is checked out with git to a workspace which is mounted to every step of the workflow as the working directory. ```diff @@ -484,6 +485,19 @@ Normally steps of a workflow are executed serially in the order in which they ar - go test ``` +:::note +You can define a step to start immediately without dependencies by adding an empty `depends_on: []`. By setting `depends_on` on a single step all other steps will be immediately executed as well if no further dependencies are specified. + +```yaml +steps: + - name: check code format + image: mstruebing/editorconfig-checker + depends_on: [] # enable parallel steps + ... +``` + +::: + ### `volumes` Woodpecker gives the ability to define Docker volumes in the YAML. You can use this parameter to mount files or folders on the host machine into your containers. diff --git a/docs/docs/20-usage/51-plugins/20-creating-plugins.md b/docs/docs/20-usage/51-plugins/20-creating-plugins.md index 44e0167ee..8a0ea5920 100644 --- a/docs/docs/20-usage/51-plugins/20-creating-plugins.md +++ b/docs/docs/20-usage/51-plugins/20-creating-plugins.md @@ -135,5 +135,5 @@ docker run --rm \ These should also be built for different OS/architectures. - Use [built-in env vars](../50-environment.md#built-in-environment-variables) where possible. - Do not use any configuration except settings (and internal env vars). This means: Don't require using [`environment`](../50-environment.md) and don't require specific secret names. -- Add a `docs.md` file, listing all your settings and plugin metadata ([example](https://codeberg.org/woodpecker-plugins/plugin-docker-buildx/src/branch/main/docs.md)). -- Add your plugin to the [plugin index](/plugins) using your `docs.md` ([the example above in the index](https://woodpecker-ci.org/plugins/Docker%20Buildx)). +- Add a `docs.md` file, listing all your settings and plugin metadata ([example](https://github.com/woodpecker-ci/plugin-git/blob/main/docs.md)). +- Add your plugin to the [plugin index](/plugins) using your `docs.md` ([the example above in the index](https://woodpecker-ci.org/plugins/Git%20Clone)). diff --git a/docs/docs/20-usage/71-project-settings.md b/docs/docs/20-usage/71-project-settings.md index 573e85eee..24bdbe605 100644 --- a/docs/docs/20-usage/71-project-settings.md +++ b/docs/docs/20-usage/71-project-settings.md @@ -16,6 +16,15 @@ Your Version-Control-System will notify Woodpecker about events via webhooks. If Enables handling webhook's pull request event. If disabled, then pipeline won't run for pull requests. +## Allow deployments + +Enables a pipeline to be started with the `deploy` event from a successful pipeline. + +:::danger +Only activate this option if you trust all users who have push access to your repository. +Otherwise, these users will be able to steal secrets that are only available for `deploy` events. +::: + ## Protected Every pipeline initiated by an webhook event needs to be approved by a project members with push permissions before being executed. diff --git a/docs/docs/30-administration/10-server-config.md b/docs/docs/30-administration/10-server-config.md index 9f50065ed..70a262c85 100644 --- a/docs/docs/30-administration/10-server-config.md +++ b/docs/docs/30-administration/10-server-config.md @@ -473,12 +473,6 @@ Supported variables: - `owner`: the repo's owner - `repo`: the repo's name -### `WOODPECKER_ADDONS` - -> Default: empty - -List of addon files. See [addons](./75-addons/75-overview.md). - --- ### `WOODPECKER_LIMIT_MEM_SWAP` @@ -559,4 +553,8 @@ See [Bitbucket configuration](./11-forges/50-bitbucket.md#configuration) ### `WOODPECKER_GITLAB_...` -See [Gitlab configuration](./11-forges/40-gitlab.md#configuration) +See [GitLab configuration](./11-forges/40-gitlab.md#configuration) + +### `WOODPECKER_ADDON_FORGE` + +See [addon forges](./11-forges/100-addon.md). diff --git a/docs/docs/30-administration/11-forges/100-addon.md b/docs/docs/30-administration/11-forges/100-addon.md new file mode 100644 index 000000000..5c98149ac --- /dev/null +++ b/docs/docs/30-administration/11-forges/100-addon.md @@ -0,0 +1,68 @@ +# Addon forges + +If the forge you're using does not comply with [Woodpecker's requirements](../../92-development/02-core-ideas.md#forge) or your setup is too specific to be added to Woodpecker's core, you can write your own forge using an addon forge. + +:::warning +Addon forges are still experimental. Their implementation can change and break at any time. +::: + +:::danger +You need to trust the author of the addon forge you use. It can access authentication codes and other possibly sensitive information. +::: + +## Usage + +To use an addon forge, download the correct addon version. Then, you can add the following to your configuration: + +```ini +WOODPECKER_ADDON_FORGE=/path/to/your/addon/forge/file +``` + +In case you run Woodpecker as container, you probably want to mount the addon binary to `/opt/addons/`. + +### Bug reports + +If you experience bugs, please check which component has the issue. If it's the addon, **do not raise an issue in the main repository**, but rather use the separate addon repositories. To check which component is responsible for the bug, look at the logs. Logs from addons are marked with a special field `addon` containing their addon file name. + +## List of addon forges + +If you wrote or found an addon forge, please add it here so others can find it! + +_Be the first one to add your addon forge!_ + +## Creating addon forges + +Addons use RPC to communicate to the server and are implemented using the [`go-plugin` library](https://github.com/hashicorp/go-plugin). + +### Writing your code + +This example will use the Go language. + +Directly import Woodpecker's Go packages (`go.woodpecker-ci.org/woodpecker/woodpecker/v2`) and use the interfaces and types defined there. + +In the `main` function, just call `"go.woodpecker-ci.org/woodpecker/v2/server/forge/addon".Serve` with a `"go.woodpecker-ci.org/woodpecker/v2/server/forge".Forge` as argument. +This will take care of connecting the addon forge to the server. + +### Example structure + +```go +package main + +import ( + "context" + "net/http" + + "go.woodpecker-ci.org/woodpecker/v2/server/forge/addon" + forgeTypes "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +func main() { + addon.Serve(config{}) +} + +type config struct { +} + +// `config` must implement `"go.woodpecker-ci.org/woodpecker/v2/server/forge".Forge`. You must directly use Woodpecker's packages - see imports above. +``` diff --git a/docs/docs/30-administration/11-forges/20-github.md b/docs/docs/30-administration/11-forges/20-github.md index f3656f8fd..338e555fd 100644 --- a/docs/docs/30-administration/11-forges/20-github.md +++ b/docs/docs/30-administration/11-forges/20-github.md @@ -81,3 +81,9 @@ Read the value for `WOODPECKER_GITHUB_SECRET` from the specified filepath. > Default: `false` Configure if SSL verification should be skipped. + +### `WOODPECKER_GITHUB_PUBLIC_ONLY` + +> Default: `false` + +Configures the GitHub OAuth client to only obtain a token that can manage public repositories. diff --git a/docs/docs/30-administration/11-forges/60-bitbucket_datacenter.md b/docs/docs/30-administration/11-forges/60-bitbucket_datacenter.md index e85242c05..9304d13a1 100644 --- a/docs/docs/30-administration/11-forges/60-bitbucket_datacenter.md +++ b/docs/docs/30-administration/11-forges/60-bitbucket_datacenter.md @@ -10,24 +10,23 @@ Woodpecker comes with experimental support for Bitbucket Datacenter / Server, fo To enable Bitbucket Server you should configure the Woodpecker container using the following environment variables: -```diff -# docker-compose.yml -version: '3' +```diff title="docker-compose.yaml" + version: '3' -services: - woodpecker-server: - [...] - environment: - - [...] -+ - WOODPECKER_BITBUCKET_DC=true -+ - WOODPECKER_BITBUCKET_DC_GIT_USERNAME=foo -+ - WOODPECKER_BITBUCKET_DC_GIT_PASSWORD=bar -+ - WOODPECKER_BITBUCKET_DC_CLIENT_ID=xxx -+ - WOODPECKER_BITBUCKET_DC_CLIENT_SECRET=yyy -+ - WOODPECKER_BITBUCKET_DC_URL=http://stash.mycompany.com + services: + woodpecker-server: + [...] + environment: + - [...] ++ - WOODPECKER_BITBUCKET_DC=true ++ - WOODPECKER_BITBUCKET_DC_GIT_USERNAME=foo ++ - WOODPECKER_BITBUCKET_DC_GIT_PASSWORD=bar ++ - WOODPECKER_BITBUCKET_DC_CLIENT_ID=xxx ++ - WOODPECKER_BITBUCKET_DC_CLIENT_SECRET=yyy ++ - WOODPECKER_BITBUCKET_DC_URL=http://stash.mycompany.com - woodpecker-agent: - [...] + woodpecker-agent: + [...] ``` ## Service Account diff --git a/docs/docs/30-administration/22-backends/40-kubernetes.md b/docs/docs/30-administration/22-backends/40-kubernetes.md index e878b6a4f..262ce4c5f 100644 --- a/docs/docs/30-administration/22-backends/40-kubernetes.md +++ b/docs/docs/30-administration/22-backends/40-kubernetes.md @@ -40,6 +40,11 @@ steps: You can use [Limit Ranges](https://kubernetes.io/docs/concepts/policy/limit-range/) if you want to set the limits by per-namespace basis. +### Runtime class + +`runtimeClassName` specifies the name of the RuntimeClass which will be used to run this pod. If no `runtimeClassName` is specified, the default RuntimeHandler will be used. +See the [kubernetes documentation](https://kubernetes.io/docs/concepts/containers/runtime-class/) for more information on specifying runtime classes. + ### Service account `serviceAccountName` specifies the name of the ServiceAccount which the pod will mount. This service account must be created externally. diff --git a/docs/docs/30-administration/75-addons/20-creating-addons.md b/docs/docs/30-administration/75-addons/20-creating-addons.md deleted file mode 100644 index 283c456f4..000000000 --- a/docs/docs/30-administration/75-addons/20-creating-addons.md +++ /dev/null @@ -1,97 +0,0 @@ -# Creating addons - -Addons are written in Go. - -## Writing your code - -An addon consists of two variables/functions in Go. - -1. The `Type` variable. Specifies the type of the addon and must be directly accessed from `shared/addons/types/types.go`. -2. The `Addon` function which is the main point of your addon. - This function takes the `zerolog` logger you should use to log errors, warnings, etc. as argument. - - It returns two values: - - 1. The actual addon. For type reference see [table below](#return-types). - 2. An error. If this error is not `nil`, Woodpecker exits. - -Directly import Woodpecker's Go package (`go.woodpecker-ci.org/woodpecker/woodpecker/v2`) and use the interfaces and types defined there. - -### Return types - -| Addon type | Return type | -| ---------- | -------------------------------------------------------------------- | -| `Forge` | `"go.woodpecker-ci.org/woodpecker/woodpecker/v2/server/forge".Forge` | - -### Using configurations - -If you write a plugin for the server (`Forge` and the services), you can access the server config. - -Therefore, use the `"go.woodpecker-ci.org/woodpecker/v2/server".Config` variable. - -:::warning -The config is not available when your addon is initialized, i.e., the `Addon` function is called. -Only use the config in the interface methods. -::: - -## Compiling - -After you write your addon code, compile your addon: - -```sh -go build -buildmode plugin -``` - -The output file is your addon that is now ready to be used. - -## Restrictions - -Addons must directly depend on Woodpecker's core (`go.woodpecker-ci.org/woodpecker/woodpecker/v2`). -The addon must have been built with **exactly the same code** as the Woodpecker instance you'd like to use it on. This means: If you build your addon with a specific commit from Woodpecker `next`, you can likely only use it with the Woodpecker version compiled from this commit. -Also, if you change something inside Woodpecker without committing, it might fail because you need to recompile your addon with this code first. - -In addition to this, addons are only supported on Linux, FreeBSD, and macOS. - -:::info -It is recommended to at least support the latest version of Woodpecker. -::: - -### Compile for different versions - -As long as there are no changes to Woodpecker's interfaces, -or they are backwards-compatible, you can compile the addon for multiple versions -by changing the version of `go.woodpecker-ci.org/woodpecker/woodpecker/v2` using `go get` before compiling. - -## Logging - -The entrypoint receives a `zerolog.Logger` as input. **Do not use any other logging solution.** This logger follows the configuration of the Woodpecker instance and adds a special field `addon` to the log entries which allows users to find out which component is writing the log messages. - -## Example structure - -```go -package main - -import ( - "context" - "net/http" - - "github.com/rs/zerolog" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" - forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" - "go.woodpecker-ci.org/woodpecker/v2/server/model" - addon_types "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types" -) - -var Type = addon_types.TypeForge - -func Addon(logger zerolog.Logger) (forge.Forge, error) { - logger.Info().Msg("hello world from addon") - return &config{l: logger}, nil -} - -type config struct { - l zerolog.Logger -} - -// In this case, `config` must implement `forge.Forge`. You must directly use Woodpecker's packages - see imports above. -``` diff --git a/docs/docs/30-administration/75-addons/75-overview.md b/docs/docs/30-administration/75-addons/75-overview.md deleted file mode 100644 index 747dc4b36..000000000 --- a/docs/docs/30-administration/75-addons/75-overview.md +++ /dev/null @@ -1,40 +0,0 @@ -# Addons - -:::warning -Addons are still experimental. Their implementation can change and break at any time. -::: - -:::danger -You need to trust the author of the addons you use. Depending on their type, addons can access forge authentication codes, your secrets or other sensitive information. -::: - -To adapt Woodpecker to your needs beyond the [configuration](../10-server-config.md), Woodpecker has its own **addon** system, built ontop of [Go's internal plugin system](https://go.dev/pkg/plugin). - -Addons can be used for: - -- Forges - -## Restrictions - -Addons are restricted by how Go plugins work. This includes the following restrictions: - -- only supported on Linux, FreeBSD, and macOS -- addons must have been built for the correct Woodpecker version. If an addon is not provided specifically for this version, you likely won't be able to use it. - -## Usage - -To use an addon, download the addon version built for your Woodpecker version. Then, you can add the following to your configuration: - -```ini -WOODPECKER_ADDONS=/path/to/your/addon/file.so -``` - -In case you run Woodpecker as container, you probably want to mount the addon binaries to `/opt/addons/`. - -You can list multiple addons, Woodpecker will automatically determine their type. If you specify multiple addons with the same type, only the first one will be used. - -Using an addon always overwrites Woodpecker's internal setup. This means, that a forge addon will be used if specified, no matter what's configured for the forges natively supported by Woodpecker. - -### Bug reports - -If you experience bugs, please check which component has the issue. If it's the addon, **do not raise an issue in the main repository**, but rather use the separate addon repositories. To check which component is responsible for the bug, look at the logs. Logs from addons are marked with a special field `addon` containing their addon file name. diff --git a/docs/docs/30-administration/75-addons/_category_.yaml b/docs/docs/30-administration/75-addons/_category_.yaml deleted file mode 100644 index 4cd7380c5..000000000 --- a/docs/docs/30-administration/75-addons/_category_.yaml +++ /dev/null @@ -1,6 +0,0 @@ -label: 'Addons' -collapsible: true -collapsed: true -link: - type: 'doc' - id: 'overview' diff --git a/docs/docs/92-development/02-core-ideas.md b/docs/docs/92-development/02-core-ideas.md index 2f80661f4..8e0d6e292 100644 --- a/docs/docs/92-development/02-core-ideas.md +++ b/docs/docs/92-development/02-core-ideas.md @@ -8,7 +8,7 @@ ## Addons and extensions If you are wondering whether your contribution will be accepted to be merged in the Woodpecker core, or whether it's better to write an -[addon](../30-administration/75-addons/75-overview.md), [extension](../30-administration/100-external-configuration-api.md) or an +[addon forge](../30-administration/11-forges/100-addon.md), [extension](../30-administration/100-external-configuration-api.md) or an [external custom backend](../30-administration/22-backends/50-custom-backends.md), please check these points: - Is your change very specific to your setup and unlikely to be used by anyone else? diff --git a/docs/plugins/woodpecker-plugins/plugins.json b/docs/plugins/woodpecker-plugins/plugins.json index 8480438cd..a1c68db2f 100644 --- a/docs/plugins/woodpecker-plugins/plugins.json +++ b/docs/plugins/woodpecker-plugins/plugins.json @@ -194,6 +194,11 @@ "name": "Forge deployments", "docs": "https://raw.githubusercontent.com/woodpecker-ci/plugin-deployments/main/docs.md", "verified": true + }, + { + "name": "Twine", + "docs": "https://gitea.elara.ws/music-kraken/plugin-twine/raw/branch/master/docs.md", + "verified": false } ] } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 4ffd8ec46..ef93a5fbf 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -20,13 +20,13 @@ importers: version: 3.1.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) '@docusaurus/preset-classic': specifier: ^3.1.0 - version: 3.1.1(@algolia/client-search@4.22.1)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3) + version: 3.1.1(@algolia/client-search@4.22.1)(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3) '@easyops-cn/docusaurus-search-local': specifier: ^0.40.1 version: 0.40.1(@docusaurus/theme-common@3.1.1)(@docusaurus/types@3.1.1)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) '@mdx-js/react': specifier: ^3.0.0 - version: 3.0.1(@types/react@18.2.69)(react@18.2.0) + version: 3.0.1(@types/react@18.2.70)(react@18.2.0) '@svgr/webpack': specifier: ^8.1.0 version: 8.1.0(typescript@5.4.3) @@ -69,7 +69,7 @@ importers: version: 20.11.30 '@types/react': specifier: ^18.2.67 - version: 18.2.69 + version: 18.2.70 '@types/react-helmet': specifier: ^6.1.11 version: 6.1.11 @@ -100,7 +100,7 @@ importers: version: 3.1.1(react-dom@18.2.0)(react@18.2.0) '@docusaurus/theme-classic': specifier: ^3.0.0 - version: 3.1.1(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) + version: 3.1.1(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) '@docusaurus/types': specifier: ^3.0.0 version: 3.1.1(react-dom@18.2.0)(react@18.2.0) @@ -1481,7 +1481,7 @@ packages: resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==} dev: false - /@docsearch/react@3.6.0(@algolia/client-search@4.22.1)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0): + /@docsearch/react@3.6.0(@algolia/client-search@4.22.1)(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0): resolution: {integrity: sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==} peerDependencies: '@types/react': '>= 16.8.0 < 19.0.0' @@ -1501,7 +1501,7 @@ packages: '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.22.1)(algoliasearch@4.22.1)(search-insights@2.13.0) '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.22.1)(algoliasearch@4.22.1) '@docsearch/css': 3.6.0 - '@types/react': 18.2.69 + '@types/react': 18.2.70 algoliasearch: 4.22.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -1676,7 +1676,7 @@ packages: '@docusaurus/react-loadable': 5.5.2(react@18.2.0) '@docusaurus/types': 3.1.1(react-dom@18.2.0)(react@18.2.0) '@types/history': 4.7.11 - '@types/react': 18.2.69 + '@types/react': 18.2.70 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 18.2.0 @@ -1979,7 +1979,7 @@ packages: - webpack-cli dev: false - /@docusaurus/preset-classic@3.1.1(@algolia/client-search@4.22.1)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3): + /@docusaurus/preset-classic@3.1.1(@algolia/client-search@4.22.1)(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3): resolution: {integrity: sha512-jG4ys/hWYf69iaN/xOmF+3kjs4Nnz1Ay3CjFLDtYa8KdxbmUhArA9HmP26ru5N0wbVWhY+6kmpYhTJpez5wTyg==} engines: {node: '>=18.0'} peerDependencies: @@ -1995,9 +1995,9 @@ packages: '@docusaurus/plugin-google-gtag': 3.1.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) '@docusaurus/plugin-google-tag-manager': 3.1.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) '@docusaurus/plugin-sitemap': 3.1.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) - '@docusaurus/theme-classic': 3.1.1(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) + '@docusaurus/theme-classic': 3.1.1(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) '@docusaurus/theme-common': 3.1.1(@docusaurus/types@3.1.1)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) - '@docusaurus/theme-search-algolia': 3.1.1(@algolia/client-search@4.22.1)(@docusaurus/types@3.1.1)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3) + '@docusaurus/theme-search-algolia': 3.1.1(@algolia/client-search@4.22.1)(@docusaurus/types@3.1.1)(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3) '@docusaurus/types': 3.1.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2028,11 +2028,11 @@ packages: peerDependencies: react: '*' dependencies: - '@types/react': 18.2.69 + '@types/react': 18.2.70 prop-types: 15.8.1 react: 18.2.0 - /@docusaurus/theme-classic@3.1.1(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3): + /@docusaurus/theme-classic@3.1.1(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3): resolution: {integrity: sha512-GiPE/jbWM8Qv1A14lk6s9fhc0LhPEQ00eIczRO4QL2nAQJZXkjPG6zaVx+1cZxPFWbAsqSjKe2lqkwF3fGkQ7Q==} engines: {node: '>=18.0'} peerDependencies: @@ -2051,7 +2051,7 @@ packages: '@docusaurus/utils': 3.1.1(@docusaurus/types@3.1.1) '@docusaurus/utils-common': 3.1.1(@docusaurus/types@3.1.1) '@docusaurus/utils-validation': 3.1.1(@docusaurus/types@3.1.1) - '@mdx-js/react': 3.0.1(@types/react@18.2.69)(react@18.2.0) + '@mdx-js/react': 3.0.1(@types/react@18.2.70)(react@18.2.0) clsx: 2.1.0 copy-text-to-clipboard: 3.2.0 infima: 0.2.0-alpha.43 @@ -2100,7 +2100,7 @@ packages: '@docusaurus/utils': 3.1.1(@docusaurus/types@3.1.1) '@docusaurus/utils-common': 3.1.1(@docusaurus/types@3.1.1) '@types/history': 4.7.11 - '@types/react': 18.2.69 + '@types/react': 18.2.70 '@types/react-router-config': 5.0.11 clsx: 2.1.0 parse-numeric-range: 1.3.0 @@ -2128,14 +2128,14 @@ packages: - vue-template-compiler - webpack-cli - /@docusaurus/theme-search-algolia@3.1.1(@algolia/client-search@4.22.1)(@docusaurus/types@3.1.1)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3): + /@docusaurus/theme-search-algolia@3.1.1(@algolia/client-search@4.22.1)(@docusaurus/types@3.1.1)(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0)(typescript@5.4.3): resolution: {integrity: sha512-tBH9VY5EpRctVdaAhT+b1BY8y5dyHVZGFXyCHgTrvcXQy5CV4q7serEX7U3SveNT9zksmchPyct6i1sFDC4Z5g==} engines: {node: '>=18.0'} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@docsearch/react': 3.6.0(@algolia/client-search@4.22.1)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0) + '@docsearch/react': 3.6.0(@algolia/client-search@4.22.1)(@types/react@18.2.70)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0) '@docusaurus/core': 3.1.1(@docusaurus/types@3.1.1)(debug@4.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) '@docusaurus/logger': 3.1.1 '@docusaurus/plugin-content-docs': 3.1.1(debug@4.3.4)(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) @@ -2195,7 +2195,7 @@ packages: dependencies: '@mdx-js/mdx': 3.0.1 '@types/history': 4.7.11 - '@types/react': 18.2.69 + '@types/react': 18.2.70 commander: 5.1.0 joi: 17.12.2 react: 18.2.0 @@ -2656,14 +2656,14 @@ packages: transitivePeerDependencies: - supports-color - /@mdx-js/react@3.0.1(@types/react@18.2.69)(react@18.2.0): + /@mdx-js/react@3.0.1(@types/react@18.2.70)(react@18.2.0): resolution: {integrity: sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==} peerDependencies: '@types/react': '>=16' react: '>=16' dependencies: '@types/mdx': 2.0.12 - '@types/react': 18.2.69 + '@types/react': 18.2.70 react: 18.2.0 /@napi-rs/wasm-runtime@0.1.2: @@ -3406,31 +3406,31 @@ packages: /@types/react-helmet@6.1.11: resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==} dependencies: - '@types/react': 18.2.69 + '@types/react': 18.2.70 dev: true /@types/react-router-config@5.0.11: resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.69 + '@types/react': 18.2.70 '@types/react-router': 5.1.20 /@types/react-router-dom@5.3.3: resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.69 + '@types/react': 18.2.70 '@types/react-router': 5.1.20 /@types/react-router@5.1.20: resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.69 + '@types/react': 18.2.70 - /@types/react@18.2.69: - resolution: {integrity: sha512-W1HOMUWY/1Yyw0ba5TkCV+oqynRjG7BnteBB+B7JmAK7iw3l2SW+VGOxL+akPweix6jk2NNJtyJKpn4TkpfK3Q==} + /@types/react@18.2.70: + resolution: {integrity: sha512-hjlM2hho2vqklPhopNkXkdkeq6Lv8WSZTpr7956zY+3WS5cfYUewtCzsJLsbW5dEv3lfSeQ4W14ZFeKC437JRQ==} dependencies: '@types/prop-types': 15.7.12 '@types/scheduler': 0.16.8 @@ -3963,7 +3963,7 @@ packages: hasBin: true dependencies: caniuse-lite: 1.0.30001600 - electron-to-chromium: 1.4.715 + electron-to-chromium: 1.4.717 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) @@ -4926,8 +4926,8 @@ packages: /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - /electron-to-chromium@1.4.715: - resolution: {integrity: sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==} + /electron-to-chromium@1.4.717: + resolution: {integrity: sha512-6Fmg8QkkumNOwuZ/5mIbMU9WI3H2fmn5ajcVya64I5Yr5CcNmO7vcLt0Y7c96DCiMO5/9G+4sI2r6eEvdg1F7A==} /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4983,8 +4983,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - /es-module-lexer@1.4.2: - resolution: {integrity: sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw==} + /es-module-lexer@1.5.0: + resolution: {integrity: sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==} /es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} @@ -4996,7 +4996,7 @@ packages: webpack: ^4.40.0 || ^5.0.0 dependencies: esbuild: 0.20.2 - get-tsconfig: 4.7.2 + get-tsconfig: 4.7.3 loader-utils: 2.0.4 webpack: 5.91.0 webpack-sources: 1.4.3 @@ -5163,8 +5163,8 @@ packages: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - /express@4.19.1: - resolution: {integrity: sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==} + /express@4.19.2: + resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} dependencies: accepts: 1.3.8 @@ -5469,8 +5469,8 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - /get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + /get-tsconfig@4.7.3: + resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} dependencies: resolve-pkg-maps: 1.0.0 dev: false @@ -9605,7 +9605,7 @@ packages: compression: 1.7.4 connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.19.1 + express: 4.19.2 graceful-fs: 4.2.11 html-entities: 2.5.2 http-proxy-middleware: 2.0.6(@types/express@4.17.21)(debug@4.3.4) @@ -9667,7 +9667,7 @@ packages: browserslist: 4.23.0 chrome-trace-event: 1.0.3 enhanced-resolve: 5.16.0 - es-module-lexer: 1.4.2 + es-module-lexer: 1.5.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 diff --git a/docs/versioned_docs/version-2.4/20-usage/20-workflow-syntax.md b/docs/versioned_docs/version-2.4/20-usage/20-workflow-syntax.md index b9aad7826..59ae06444 100644 --- a/docs/versioned_docs/version-2.4/20-usage/20-workflow-syntax.md +++ b/docs/versioned_docs/version-2.4/20-usage/20-workflow-syntax.md @@ -50,7 +50,8 @@ git commit -m "updated README [CI SKIP]" ## Steps -Every step of your workflow executes commands inside a specified container. The defined commands are executed serially. +Every step of your workflow executes commands inside a specified container.
+The defined steps are executed in sequence by default, if they should run in parallel you can use [`depends_on`](./20-workflow-syntax.md#depends_on).
The associated commit is checked out with git to a workspace which is mounted to every step of the workflow as the working directory. ```diff @@ -478,6 +479,19 @@ Normally steps of a workflow are executed serially in the order in which they ar - go test ``` +:::note +You can define a step to start immediately without dependencies by adding an empty `depends_on: []`. By setting `depends_on` on a single step all other steps will be immediately executed as well if no further dependencies are specified. + +```yaml +steps: + - name: check code format + image: mstruebing/editorconfig-checker + depends_on: [] # enable parallel steps + ... +``` + +::: + ### `volumes` Woodpecker gives the ability to define Docker volumes in the YAML. You can use this parameter to mount files or folders on the host machine into your containers. diff --git a/go.mod b/go.mod index cb7e010d0..6d19445c7 100644 --- a/go.mod +++ b/go.mod @@ -11,25 +11,27 @@ require ( github.com/alessio/shellescape v1.4.2 github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/caddyserver/certmagic v0.20.0 - github.com/cenkalti/backoff/v4 v4.2.1 - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/charmbracelet/huh v0.3.0 + github.com/charmbracelet/huh/spinner v0.0.0-20240327025511-ec643317aa10 github.com/distribution/reference v0.5.0 github.com/docker/cli v24.0.9+incompatible github.com/docker/docker v24.0.9+incompatible github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/drone/envsubst v1.0.3 - github.com/expr-lang/expr v1.16.2 + github.com/expr-lang/expr v1.16.3 github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.9.1 github.com/go-ap/httpsig v0.0.0-20221203064646-3647b4d88fdf - github.com/go-sql-driver/mysql v1.8.0 + github.com/go-sql-driver/mysql v1.8.1 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/go-github/v60 v60.0.0 + github.com/google/go-github/v61 v61.0.0 github.com/google/tink/go v1.7.0 github.com/gorilla/securecookie v1.1.2 + github.com/hashicorp/go-hclog v1.2.0 + github.com/hashicorp/go-plugin v1.4.3 github.com/jellydator/ttlcache/v3 v3.2.0 github.com/joho/godotenv v1.5.1 github.com/kinbiko/jsonassert v1.1.1 @@ -81,7 +83,10 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.10.2 // indirect + github.com/catppuccin/go v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/charmbracelet/bubbles v0.18.0 // indirect + github.com/charmbracelet/bubbletea v0.25.0 // indirect github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect @@ -116,9 +121,9 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -134,13 +139,15 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mholt/acmez v1.2.0 // indirect github.com/miekg/dns v1.1.57 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect diff --git a/go.sum b/go.sum index 6edbfdcb3..f1b7585a9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8= code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM= codeberg.org/6543/go-yaml2json v1.0.0 h1:heGqo9VEi7gY2yNqjj7X4ADs5nzlFIbGsJtgYDLrnig= @@ -40,14 +41,21 @@ github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZF github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= +github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= +github.com/charmbracelet/huh/spinner v0.0.0-20240327025511-ec643317aa10 h1:/HZJSyFVH5rB1MlCDfkhQhRbLPD2Er29ngWXiUQ8bik= +github.com/charmbracelet/huh/spinner v0.0.0-20240327025511-ec643317aa10/go.mod h1:nrBG0YEHaxdbqHXW1xvG1hPqkuac9Eg7RTMvogiXuz0= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -60,6 +68,7 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= @@ -102,8 +111,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s= -github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/expr-lang/expr v1.16.3 h1:NLldf786GffptcXNxxJx5dQ+FzeWDKChBDqOOwyK8to= +github.com/expr-lang/expr v1.16.3/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -146,8 +157,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4= -github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= @@ -167,7 +178,12 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -185,8 +201,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8= -github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk= +github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= +github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -205,12 +221,17 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= @@ -257,6 +278,8 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE= github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= @@ -331,6 +354,9 @@ github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/moby v24.0.9+incompatible h1:Z/hFbZJqC5Fmuf6jesMLdHU71CMAgdiSJ1ZYey+bFmg= @@ -345,8 +371,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= @@ -357,6 +383,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/neticdk/go-bitbucket v1.0.0 h1:FPvHEgPHoDwD2VHbpyu2R2gnoWQ867RxZd2FivS4wSw= github.com/neticdk/go-bitbucket v1.0.0/go.mod h1:IrHeWO1CrNi0DlOvfhAA9bGRSeNSUB6/SAfzmwbA5aU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -382,6 +410,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= @@ -513,6 +542,10 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -522,7 +555,11 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -538,9 +575,11 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -548,6 +587,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -603,8 +643,11 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -625,10 +668,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= @@ -658,6 +710,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= diff --git a/pipeline/backend/docker/docker.go b/pipeline/backend/docker/docker.go index 8a92deea5..2c7a0e835 100644 --- a/pipeline/backend/docker/docker.go +++ b/pipeline/backend/docker/docker.go @@ -213,7 +213,7 @@ func (e *docker) StartStep(ctx context.Context, step *backend.Step, taskUUID str } // add default volumes to the host configuration - hostConfig.Binds = utils.DedupStrings(append(hostConfig.Binds, e.volumes...)) + hostConfig.Binds = utils.DeduplicateStrings(append(hostConfig.Binds, e.volumes...)) _, err := e.client.ContainerCreate(ctx, config, hostConfig, nil, nil, containerName) if client.IsErrNotFound(err) { diff --git a/pipeline/backend/kubernetes/backend_options.go b/pipeline/backend/kubernetes/backend_options.go index 689f835fe..3b1109a2c 100644 --- a/pipeline/backend/kubernetes/backend_options.go +++ b/pipeline/backend/kubernetes/backend_options.go @@ -9,6 +9,7 @@ import ( // BackendOptions defines all the advanced options for the kubernetes backend type BackendOptions struct { Resources Resources `mapstructure:"resources"` + RuntimeClassName *string `mapstructure:"runtimeClassName"` ServiceAccountName string `mapstructure:"serviceAccountName"` NodeSelector map[string]string `mapstructure:"nodeSelector"` Tolerations []Toleration `mapstructure:"tolerations"` diff --git a/pipeline/backend/kubernetes/kubernetes.go b/pipeline/backend/kubernetes/kubernetes.go index 3567100a6..3019bba7a 100644 --- a/pipeline/backend/kubernetes/kubernetes.go +++ b/pipeline/backend/kubernetes/kubernetes.go @@ -316,6 +316,9 @@ func (e *kube) TailStep(ctx context.Context, step *types.Step, taskUUID string) } if pod.Name == podName { + if isImagePullBackOffState(pod) { + up <- true + } switch pod.Status.Phase { case v1.PodRunning, v1.PodSucceeded, v1.PodFailed: up <- true diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index 7a70aa5b0..331efae92 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -117,6 +117,7 @@ func podSpec(step *types.Step, config *config, options BackendOptions) (v1.PodSp var err error spec := v1.PodSpec{ RestartPolicy: v1.RestartPolicyNever, + RuntimeClassName: options.RuntimeClassName, ServiceAccountName: options.ServiceAccountName, ImagePullSecrets: imagePullSecretsReferences(config.ImagePullSecretNames), HostAliases: hostAliases(step.ExtraHosts), @@ -135,9 +136,11 @@ func podSpec(step *types.Step, config *config, options BackendOptions) (v1.PodSp func podContainer(step *types.Step, podName, goos string, options BackendOptions) (v1.Container, error) { var err error container := v1.Container{ - Name: podName, - Image: step.Image, - WorkingDir: step.WorkingDir, + Name: podName, + Image: step.Image, + WorkingDir: step.WorkingDir, + Ports: containerPorts(step.Ports), + SecurityContext: containerSecurityContext(options.SecurityContext, step.Privileged), } if step.Pull { @@ -155,8 +158,6 @@ func podContainer(step *types.Step, podName, goos string, options BackendOptions } container.Env = mapToEnvVars(step.Environment) - container.Ports = containerPorts(step.Ports) - container.SecurityContext = containerSecurityContext(options.SecurityContext, step.Privileged) container.Resources, err = resourceRequirements(options.Resources) if err != nil { diff --git a/pipeline/backend/kubernetes/pod_test.go b/pipeline/backend/kubernetes/pod_test.go index bf0972743..bf4f35a41 100644 --- a/pipeline/backend/kubernetes/pod_test.go +++ b/pipeline/backend/kubernetes/pod_test.go @@ -245,6 +245,7 @@ func TestFullPod(t *testing.T) { "nodeSelector": { "storage": "ssd" }, + "runtimeClassName": "runc", "serviceAccountName": "wp-svc-acc", "securityContext": { "runAsUser": 101, @@ -289,6 +290,7 @@ func TestFullPod(t *testing.T) { "status": {} }` + runtimeClass := "runc" hostAliases := []types.HostAlias{ {Name: "cloudflare", IP: "1.1.1.1"}, {Name: "cf.v6", IP: "2606:4700:4700::64"}, @@ -333,6 +335,7 @@ func TestFullPod(t *testing.T) { SecurityContext: SecurityContextConfig{RunAsNonRoot: false}, }, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64", BackendOptions{ NodeSelector: map[string]string{"storage": "ssd"}, + RuntimeClassName: &runtimeClass, ServiceAccountName: "wp-svc-acc", Tolerations: []Toleration{{Key: "net-port", Value: "100Mbit", Effect: TaintEffectNoSchedule}}, Resources: Resources{ diff --git a/pipeline/errors/error.go b/pipeline/errors/error.go index 1a0ae167c..f8bba4c67 100644 --- a/pipeline/errors/error.go +++ b/pipeline/errors/error.go @@ -2,28 +2,12 @@ package errors import ( "errors" - "fmt" "go.uber.org/multierr" + + "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" ) -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 - PipelineErrorTypeBadHabit PipelineErrorType = "bad_habit" // some bad-habit error -) - -type PipelineError struct { - Type PipelineErrorType `json:"type"` - Message string `json:"message"` - IsWarning bool `json:"is_warning"` - Data any `json:"data"` -} - type LinterErrorData struct { File string `json:"file"` Field string `json:"field"` @@ -35,12 +19,8 @@ type DeprecationErrorData struct { Docs string `json:"docs"` } -func (e *PipelineError) Error() string { - return fmt.Sprintf("[%s] %s", e.Type, e.Message) -} - -func (e *PipelineError) GetLinterData() *LinterErrorData { - if e.Type != PipelineErrorTypeLinter { +func GetLinterData(e *types.PipelineError) *LinterErrorData { + if e.Type != types.PipelineErrorTypeLinter { return nil } @@ -51,16 +31,16 @@ func (e *PipelineError) GetLinterData() *LinterErrorData { return nil } -func GetPipelineErrors(err error) []*PipelineError { - var pipelineErrors []*PipelineError +func GetPipelineErrors(err error) []*types.PipelineError { + var pipelineErrors []*types.PipelineError for _, _err := range multierr.Errors(err) { - var err *PipelineError + var err *types.PipelineError if errors.As(_err, &err) { pipelineErrors = append(pipelineErrors, err) } else { - pipelineErrors = append(pipelineErrors, &PipelineError{ + pipelineErrors = append(pipelineErrors, &types.PipelineError{ Message: _err.Error(), - Type: PipelineErrorTypeGeneric, + Type: types.PipelineErrorTypeGeneric, }) } } diff --git a/pipeline/errors/error_test.go b/pipeline/errors/error_test.go index b6b328e7e..8648c4048 100644 --- a/pipeline/errors/error_test.go +++ b/pipeline/errors/error_test.go @@ -8,6 +8,7 @@ import ( "go.uber.org/multierr" pipeline_errors "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" + "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" ) func TestGetPipelineErrors(t *testing.T) { @@ -16,7 +17,7 @@ func TestGetPipelineErrors(t *testing.T) { tests := []struct { title string err error - expected []*pipeline_errors.PipelineError + expected []*types.PipelineError }{ { title: "nil error", @@ -25,10 +26,10 @@ func TestGetPipelineErrors(t *testing.T) { }, { title: "warning", - err: &pipeline_errors.PipelineError{ + err: &types.PipelineError{ IsWarning: true, }, - expected: []*pipeline_errors.PipelineError{ + expected: []*types.PipelineError{ { IsWarning: true, }, @@ -36,10 +37,10 @@ func TestGetPipelineErrors(t *testing.T) { }, { title: "pipeline error", - err: &pipeline_errors.PipelineError{ + err: &types.PipelineError{ IsWarning: false, }, - expected: []*pipeline_errors.PipelineError{ + expected: []*types.PipelineError{ { IsWarning: false, }, @@ -48,14 +49,14 @@ func TestGetPipelineErrors(t *testing.T) { { title: "multiple warnings", err: multierr.Combine( - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: true, }, - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: true, }, ), - expected: []*pipeline_errors.PipelineError{ + expected: []*types.PipelineError{ { IsWarning: true, }, @@ -67,15 +68,15 @@ func TestGetPipelineErrors(t *testing.T) { { title: "multiple errors and warnings", err: multierr.Combine( - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: true, }, - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: false, }, errors.New("some error"), ), - expected: []*pipeline_errors.PipelineError{ + expected: []*types.PipelineError{ { IsWarning: true, }, @@ -83,7 +84,7 @@ func TestGetPipelineErrors(t *testing.T) { IsWarning: false, }, { - Type: pipeline_errors.PipelineErrorTypeGeneric, + Type: types.PipelineErrorTypeGeneric, IsWarning: false, Message: "some error", }, @@ -111,14 +112,14 @@ func TestHasBlockingErrors(t *testing.T) { }, { title: "warning", - err: &pipeline_errors.PipelineError{ + err: &types.PipelineError{ IsWarning: true, }, expected: false, }, { title: "pipeline error", - err: &pipeline_errors.PipelineError{ + err: &types.PipelineError{ IsWarning: false, }, expected: true, @@ -126,10 +127,10 @@ func TestHasBlockingErrors(t *testing.T) { { title: "multiple warnings", err: multierr.Combine( - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: true, }, - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: true, }, ), @@ -138,10 +139,10 @@ func TestHasBlockingErrors(t *testing.T) { { title: "multiple errors and warnings", err: multierr.Combine( - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: true, }, - &pipeline_errors.PipelineError{ + &types.PipelineError{ IsWarning: false, }, errors.New("some error"), diff --git a/pipeline/errors/types/errors.go b/pipeline/errors/types/errors.go new file mode 100644 index 000000000..752eb6904 --- /dev/null +++ b/pipeline/errors/types/errors.go @@ -0,0 +1,24 @@ +package types + +import "fmt" + +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 + PipelineErrorTypeBadHabit PipelineErrorType = "bad_habit" // some bad-habit error +) + +type PipelineError struct { + Type PipelineErrorType `json:"type"` + Message string `json:"message"` + IsWarning bool `json:"is_warning"` + Data any `json:"data"` +} + +func (e *PipelineError) Error() string { + return fmt.Sprintf("[%s] %s", e.Type, e.Message) +} diff --git a/pipeline/frontend/yaml/compiler/cacher.go b/pipeline/frontend/yaml/compiler/cacher.go deleted file mode 100644 index 93c279f31..000000000 --- a/pipeline/frontend/yaml/compiler/cacher.go +++ /dev/null @@ -1,117 +0,0 @@ -// 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 compiler - -import ( - "path" - "strings" - - yaml_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types" -) - -// Cacher defines a compiler transform that can be used -// to implement default caching for a repository. -type Cacher interface { - Restore(repo, branch string, mounts []string) *yaml_types.Container - Rebuild(repo, branch string, mounts []string) *yaml_types.Container -} - -type volumeCacher struct { - base string -} - -func (c *volumeCacher) Restore(repo, branch string, mounts []string) *yaml_types.Container { - return &yaml_types.Container{ - Name: "rebuild_cache", - Image: "plugins/volume-cache:1.0.0", - Settings: map[string]any{ - "mount": mounts, - "path": "/cache", - "restore": true, - "file": strings.ReplaceAll(branch, "/", "_") + ".tar", - "fallback_to": "main.tar", - }, - Volumes: yaml_types.Volumes{ - Volumes: []*yaml_types.Volume{ - { - Source: path.Join(c.base, repo), - Destination: "/cache", - // TODO add access mode - }, - }, - }, - } -} - -func (c *volumeCacher) Rebuild(repo, branch string, mounts []string) *yaml_types.Container { - return &yaml_types.Container{ - Name: "rebuild_cache", - Image: "plugins/volume-cache:1.0.0", - Settings: map[string]any{ - "mount": mounts, - "path": "/cache", - "rebuild": true, - "flush": true, - "file": strings.ReplaceAll(branch, "/", "_") + ".tar", - }, - Volumes: yaml_types.Volumes{ - Volumes: []*yaml_types.Volume{ - { - Source: path.Join(c.base, repo), - Destination: "/cache", - // TODO add access mode - }, - }, - }, - } -} - -type s3Cacher struct { - bucket string - access string - secret string - region string -} - -func (c *s3Cacher) Restore(_, _ string, mounts []string) *yaml_types.Container { - return &yaml_types.Container{ - Name: "rebuild_cache", - Image: "plugins/s3-cache:latest", - Settings: map[string]any{ - "mount": mounts, - "access_key": c.access, - "secret_key": c.secret, - "bucket": c.bucket, - "region": c.region, - "rebuild": true, - }, - } -} - -func (c *s3Cacher) Rebuild(_, _ string, mounts []string) *yaml_types.Container { - return &yaml_types.Container{ - Name: "rebuild_cache", - Image: "plugins/s3-cache:latest", - Settings: map[string]any{ - "mount": mounts, - "access_key": c.access, - "secret_key": c.secret, - "bucket": c.bucket, - "region": c.region, - "rebuild": true, - "flush": true, - }, - } -} diff --git a/pipeline/frontend/yaml/compiler/compiler.go b/pipeline/frontend/yaml/compiler/compiler.go index 8c36df2f7..2a7803349 100644 --- a/pipeline/frontend/yaml/compiler/compiler.go +++ b/pipeline/frontend/yaml/compiler/compiler.go @@ -16,7 +16,6 @@ package compiler import ( "fmt" - "path" backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata" @@ -34,7 +33,6 @@ type Registry struct { Hostname string Username string Password string - Token string } type Secret struct { @@ -68,7 +66,7 @@ func (s *Secret) Match(event string) bool { if len(s.Events) == 0 { return true } - // tread all pull events the same way + // treat all pull events the same way if event == "pull_request_closed" { event = "pull_request" } @@ -82,8 +80,6 @@ func (s *Secret) Match(event string) bool { return false } -type secretMap map[string]Secret - type ResourceLimit struct { MemSwapLimit int64 MemLimit int64 @@ -106,8 +102,7 @@ type Compiler struct { path string metadata metadata.Metadata registries []Registry - secrets secretMap - cacher Cacher + secrets map[string]Secret reslimit ResourceLimit defaultCloneImage string trustedPipeline bool @@ -224,11 +219,6 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er } } - err := c.setupCache(conf, config) - if err != nil { - return nil, err - } - // add services steps if len(conf.Services.ContainerList) != 0 { stage := new(backend_types.Stage) @@ -297,48 +287,5 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er config.Stages = append(config.Stages, stepStages...) - err = c.setupCacheRebuild(conf, config) - if err != nil { - return nil, err - } - return config, nil } - -func (c *Compiler) setupCache(conf *yaml_types.Workflow, ir *backend_types.Config) error { - if c.local || len(conf.Cache) == 0 || c.cacher == nil { - return nil - } - - container := c.cacher.Restore(path.Join(c.metadata.Repo.Owner, c.metadata.Repo.Name), c.metadata.Curr.Commit.Branch, conf.Cache) - step, err := c.createProcess(container, backend_types.StepTypeCache) - if err != nil { - return err - } - - stage := new(backend_types.Stage) - stage.Steps = append(stage.Steps, step) - - ir.Stages = append(ir.Stages, stage) - - return nil -} - -func (c *Compiler) setupCacheRebuild(conf *yaml_types.Workflow, ir *backend_types.Config) error { - if c.local || len(conf.Cache) == 0 || c.metadata.Curr.Event != metadata.EventPush || c.cacher == nil { - return nil - } - container := c.cacher.Rebuild(path.Join(c.metadata.Repo.Owner, c.metadata.Repo.Name), c.metadata.Curr.Commit.Branch, conf.Cache) - - step, err := c.createProcess(container, backend_types.StepTypeCache) - if err != nil { - return err - } - - stage := new(backend_types.Stage) - stage.Steps = append(stage.Steps, step) - - ir.Stages = append(ir.Stages, stage) - - return nil -} diff --git a/pipeline/frontend/yaml/compiler/option.go b/pipeline/frontend/yaml/compiler/option.go index 3fb651756..e4730d497 100644 --- a/pipeline/frontend/yaml/compiler/option.go +++ b/pipeline/frontend/yaml/compiler/option.go @@ -149,34 +149,6 @@ func WithEnviron(env map[string]string) Option { } } -// WithCacher configures the compiler with default cache settings. -func WithCacher(cacher Cacher) Option { - return func(compiler *Compiler) { - compiler.cacher = cacher - } -} - -// WithVolumeCacher configures the compiler with default local volume -// caching enabled. -func WithVolumeCacher(base string) Option { - return func(compiler *Compiler) { - compiler.cacher = &volumeCacher{base: base} - } -} - -// WithS3Cacher configures the compiler with default amazon s3 -// caching enabled. -func WithS3Cacher(access, secret, region, bucket string) Option { - return func(compiler *Compiler) { - compiler.cacher = &s3Cacher{ - access: access, - secret: secret, - bucket: bucket, - region: region, - } - } -} - // WithNetworks configures the compiler with additional networks // to be connected to pipeline containers func WithNetworks(networks ...string) Option { diff --git a/pipeline/frontend/yaml/compiler/option_test.go b/pipeline/frontend/yaml/compiler/option_test.go index 77b26638b..4f8e08e5d 100644 --- a/pipeline/frontend/yaml/compiler/option_test.go +++ b/pipeline/frontend/yaml/compiler/option_test.go @@ -166,30 +166,9 @@ func TestWithEnviron(t *testing.T) { assert.Equal(t, "true", compiler.env["SHOW"]) } -func TestWithVolumeCacher(t *testing.T) { - compiler := New( - WithVolumeCacher("/cache"), - ) - cacher, ok := compiler.cacher.(*volumeCacher) - assert.True(t, ok) - assert.Equal(t, "/cache", cacher.base) -} - func TestWithDefaultCloneImage(t *testing.T) { compiler := New( WithDefaultCloneImage("not-an-image"), ) assert.Equal(t, "not-an-image", compiler.defaultCloneImage) } - -func TestWithS3Cacher(t *testing.T) { - compiler := New( - WithS3Cacher("some-access-key", "some-secret-key", "some-region", "some-bucket"), - ) - cacher, ok := compiler.cacher.(*s3Cacher) - assert.True(t, ok) - assert.Equal(t, "some-bucket", cacher.bucket) - assert.Equal(t, "some-access-key", cacher.access) - assert.Equal(t, "some-region", cacher.region) - assert.Equal(t, "some-secret-key", cacher.secret) -} diff --git a/pipeline/frontend/yaml/linter/error.go b/pipeline/frontend/yaml/linter/error.go index c28a44ae8..09e243016 100644 --- a/pipeline/frontend/yaml/linter/error.go +++ b/pipeline/frontend/yaml/linter/error.go @@ -16,11 +16,12 @@ package linter import ( "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" + errorTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" ) -func newLinterError(message, file, field string, isWarning bool) *errors.PipelineError { - return &errors.PipelineError{ - Type: errors.PipelineErrorTypeLinter, +func newLinterError(message, file, field string, isWarning bool) *errorTypes.PipelineError { + return &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeLinter, Message: message, Data: &errors.LinterErrorData{File: file, Field: field}, IsWarning: isWarning, diff --git a/pipeline/frontend/yaml/linter/linter.go b/pipeline/frontend/yaml/linter/linter.go index dd22c9fff..646091972 100644 --- a/pipeline/frontend/yaml/linter/linter.go +++ b/pipeline/frontend/yaml/linter/linter.go @@ -21,6 +21,7 @@ import ( "go.uber.org/multierr" "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" + errorTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter/schema" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types" ) @@ -210,8 +211,8 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { } if parsed.PipelineDoNotUseIt.ContainerList != nil { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeDeprecation, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeDeprecation, Message: "Please use 'steps:' instead of deprecated 'pipeline:' list", Data: errors.DeprecationErrorData{ File: config.File, @@ -223,8 +224,8 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { } if parsed.PlatformDoNotUseIt != "" { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeDeprecation, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeDeprecation, Message: "Please use labels instead of deprecated 'platform' filters", Data: errors.DeprecationErrorData{ File: config.File, @@ -236,8 +237,8 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { } if parsed.BranchesDoNotUseIt != nil { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeDeprecation, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeDeprecation, Message: "Please use global when instead of deprecated 'branches' filter", Data: errors.DeprecationErrorData{ File: config.File, @@ -250,8 +251,8 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { for _, step := range parsed.Steps.ContainerList { if step.Group != "" { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeDeprecation, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeDeprecation, Message: "Please use depends_on instead of deprecated 'group' setting", Data: errors.DeprecationErrorData{ File: config.File, @@ -265,8 +266,8 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { for i, c := range parsed.When.Constraints { if len(c.Event.Exclude) != 0 { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeDeprecation, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeDeprecation, Message: "Please only use allow lists for events", Data: errors.DeprecationErrorData{ File: config.File, @@ -281,8 +282,8 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { for _, step := range parsed.Steps.ContainerList { for i, c := range step.When.Constraints { if len(c.Event.Exclude) != 0 { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeDeprecation, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeDeprecation, Message: "Please only use allow lists for events", Data: errors.DeprecationErrorData{ File: config.File, @@ -298,8 +299,8 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) { for _, step := range parsed.Steps.ContainerList { for i, c := range step.Secrets.Secrets { if c.Source != c.Target { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeDeprecation, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeDeprecation, Message: "Secrets alternative names are deprecated, use environment with from_secret", Data: errors.DeprecationErrorData{ File: config.File, @@ -348,8 +349,8 @@ func (l *Linter) lintBadHabits(config *WorkflowConfig) (err error) { } } if field != "" { - err = multierr.Append(err, &errors.PipelineError{ - Type: errors.PipelineErrorTypeBadHabit, + err = multierr.Append(err, &errorTypes.PipelineError{ + Type: errorTypes.PipelineErrorTypeBadHabit, Message: "Please set an event filter on all when branches", Data: errors.LinterErrorData{ File: config.File, diff --git a/pipeline/frontend/yaml/linter/schema/schema.json b/pipeline/frontend/yaml/linter/schema/schema.json index 15b9f0f36..13d7af690 100644 --- a/pipeline/frontend/yaml/linter/schema/schema.json +++ b/pipeline/frontend/yaml/linter/schema/schema.json @@ -696,6 +696,10 @@ }, "securityContext": { "$ref": "#/definitions/step_backend_kubernetes_security_context" + }, + "runtimeClassName": { + "description": "Read more: https://woodpecker-ci.org/docs/administration/backends/kubernetes#runtimeclassname", + "type": "string" } } }, diff --git a/pipeline/frontend/yaml/matrix/matrix.go b/pipeline/frontend/yaml/matrix/matrix.go index b89dfc5d9..373c7bec9 100644 --- a/pipeline/frontend/yaml/matrix/matrix.go +++ b/pipeline/frontend/yaml/matrix/matrix.go @@ -19,7 +19,7 @@ import ( "codeberg.org/6543/xyaml" - "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" + errorTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" ) const ( @@ -116,7 +116,7 @@ func parse(raw []byte) (Matrix, error) { Matrix map[string][]string }{} if err := xyaml.Unmarshal(raw, &data); err != nil { - return nil, &errors.PipelineError{Message: err.Error(), Type: errors.PipelineErrorTypeCompiler} + return nil, &errorTypes.PipelineError{Message: err.Error(), Type: errorTypes.PipelineErrorTypeCompiler} } return data.Matrix, nil } @@ -129,7 +129,7 @@ func parseList(raw []byte) ([]Axis, error) { }{} if err := xyaml.Unmarshal(raw, &data); err != nil { - return nil, &errors.PipelineError{Message: err.Error(), Type: errors.PipelineErrorTypeCompiler} + return nil, &errorTypes.PipelineError{Message: err.Error(), Type: errorTypes.PipelineErrorTypeCompiler} } return data.Matrix.Include, nil } diff --git a/pipeline/frontend/yaml/types/workflow.go b/pipeline/frontend/yaml/types/workflow.go index 1c5f98a07..4f291dec3 100644 --- a/pipeline/frontend/yaml/types/workflow.go +++ b/pipeline/frontend/yaml/types/workflow.go @@ -16,7 +16,6 @@ package types import ( "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/constraint" - "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types/base" ) type ( @@ -33,9 +32,8 @@ type ( SkipClone bool `yaml:"skip_clone"` // Undocumented - Cache base.StringOrSlice `yaml:"cache,omitempty"` - Networks WorkflowNetworks `yaml:"networks,omitempty"` - Volumes WorkflowVolumes `yaml:"volumes,omitempty"` + Networks WorkflowNetworks `yaml:"networks,omitempty"` + Volumes WorkflowVolumes `yaml:"volumes,omitempty"` // Deprecated PlatformDoNotUseIt string `yaml:"platform,omitempty"` // TODO: remove in next major version diff --git a/pipeline/rpc/proto/generate.go b/pipeline/rpc/proto/generate.go index a0f827ec1..8a3ce2ccf 100644 --- a/pipeline/rpc/proto/generate.go +++ b/pipeline/rpc/proto/generate.go @@ -16,8 +16,3 @@ package proto //go:generate protoc --go_out=paths=source_relative:. woodpecker.proto //go:generate protoc --go-grpc_out=paths=source_relative:. woodpecker.proto - -// install protoc: https://grpc.io/docs/protoc-installation/ -// and get needed binary's: -// go install google.golang.org/protobuf/cmd/protoc-gen-go@latest -// go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest diff --git a/pipeline/rpc/proto/woodpecker.pb.go b/pipeline/rpc/proto/woodpecker.pb.go index f49b64d33..c5cf5f47d 100644 --- a/pipeline/rpc/proto/woodpecker.pb.go +++ b/pipeline/rpc/proto/woodpecker.pb.go @@ -15,8 +15,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.1 +// protoc-gen-go v1.33.0 +// protoc v4.24.4 // source: woodpecker.proto package proto diff --git a/pipeline/rpc/proto/woodpecker_grpc.pb.go b/pipeline/rpc/proto/woodpecker_grpc.pb.go index 837ce74e0..f591f4dfe 100644 --- a/pipeline/rpc/proto/woodpecker_grpc.pb.go +++ b/pipeline/rpc/proto/woodpecker_grpc.pb.go @@ -1,22 +1,7 @@ -// Copyright 2021 Woodpecker Authors -// Copyright 2011 Drone.IO Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.1 +// - protoc v4.24.4 // source: woodpecker.proto package proto @@ -74,7 +59,7 @@ func NewWoodpeckerClient(cc grpc.ClientConnInterface) WoodpeckerClient { func (c *woodpeckerClient) Version(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*VersionResponse, error) { out := new(VersionResponse) - err := c.cc.Invoke(ctx, Woodpecker_Version_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Version", in, out, opts...) if err != nil { return nil, err } @@ -83,7 +68,7 @@ func (c *woodpeckerClient) Version(ctx context.Context, in *Empty, opts ...grpc. func (c *woodpeckerClient) Next(ctx context.Context, in *NextRequest, opts ...grpc.CallOption) (*NextResponse, error) { out := new(NextResponse) - err := c.cc.Invoke(ctx, Woodpecker_Next_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Next", in, out, opts...) if err != nil { return nil, err } @@ -92,7 +77,7 @@ func (c *woodpeckerClient) Next(ctx context.Context, in *NextRequest, opts ...gr func (c *woodpeckerClient) Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Init_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Init", in, out, opts...) if err != nil { return nil, err } @@ -101,7 +86,7 @@ func (c *woodpeckerClient) Init(ctx context.Context, in *InitRequest, opts ...gr func (c *woodpeckerClient) Wait(ctx context.Context, in *WaitRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Wait_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Wait", in, out, opts...) if err != nil { return nil, err } @@ -110,7 +95,7 @@ func (c *woodpeckerClient) Wait(ctx context.Context, in *WaitRequest, opts ...gr func (c *woodpeckerClient) Done(ctx context.Context, in *DoneRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Done_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Done", in, out, opts...) if err != nil { return nil, err } @@ -119,7 +104,7 @@ func (c *woodpeckerClient) Done(ctx context.Context, in *DoneRequest, opts ...gr func (c *woodpeckerClient) Extend(ctx context.Context, in *ExtendRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Extend_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Extend", in, out, opts...) if err != nil { return nil, err } @@ -128,7 +113,7 @@ func (c *woodpeckerClient) Extend(ctx context.Context, in *ExtendRequest, opts . func (c *woodpeckerClient) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Update_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Update", in, out, opts...) if err != nil { return nil, err } @@ -137,7 +122,7 @@ func (c *woodpeckerClient) Update(ctx context.Context, in *UpdateRequest, opts . func (c *woodpeckerClient) Log(ctx context.Context, in *LogRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_Log_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/Log", in, out, opts...) if err != nil { return nil, err } @@ -146,7 +131,7 @@ func (c *woodpeckerClient) Log(ctx context.Context, in *LogRequest, opts ...grpc func (c *woodpeckerClient) RegisterAgent(ctx context.Context, in *RegisterAgentRequest, opts ...grpc.CallOption) (*RegisterAgentResponse, error) { out := new(RegisterAgentResponse) - err := c.cc.Invoke(ctx, Woodpecker_RegisterAgent_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/RegisterAgent", in, out, opts...) if err != nil { return nil, err } @@ -164,7 +149,7 @@ func (c *woodpeckerClient) UnregisterAgent(ctx context.Context, in *Empty, opts func (c *woodpeckerClient) ReportHealth(ctx context.Context, in *ReportHealthRequest, opts ...grpc.CallOption) (*Empty, error) { out := new(Empty) - err := c.cc.Invoke(ctx, Woodpecker_ReportHealth_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.Woodpecker/ReportHealth", in, out, opts...) if err != nil { return nil, err } @@ -249,7 +234,7 @@ func _Woodpecker_Version_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Version_FullMethodName, + FullMethod: "/proto.Woodpecker/Version", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Version(ctx, req.(*Empty)) @@ -267,7 +252,7 @@ func _Woodpecker_Next_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Next_FullMethodName, + FullMethod: "/proto.Woodpecker/Next", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Next(ctx, req.(*NextRequest)) @@ -285,7 +270,7 @@ func _Woodpecker_Init_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Init_FullMethodName, + FullMethod: "/proto.Woodpecker/Init", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Init(ctx, req.(*InitRequest)) @@ -303,7 +288,7 @@ func _Woodpecker_Wait_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Wait_FullMethodName, + FullMethod: "/proto.Woodpecker/Wait", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Wait(ctx, req.(*WaitRequest)) @@ -321,7 +306,7 @@ func _Woodpecker_Done_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Done_FullMethodName, + FullMethod: "/proto.Woodpecker/Done", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Done(ctx, req.(*DoneRequest)) @@ -339,7 +324,7 @@ func _Woodpecker_Extend_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Extend_FullMethodName, + FullMethod: "/proto.Woodpecker/Extend", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Extend(ctx, req.(*ExtendRequest)) @@ -357,7 +342,7 @@ func _Woodpecker_Update_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Update_FullMethodName, + FullMethod: "/proto.Woodpecker/Update", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Update(ctx, req.(*UpdateRequest)) @@ -375,7 +360,7 @@ func _Woodpecker_Log_Handler(srv interface{}, ctx context.Context, dec func(inte } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_Log_FullMethodName, + FullMethod: "/proto.Woodpecker/Log", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).Log(ctx, req.(*LogRequest)) @@ -393,7 +378,7 @@ func _Woodpecker_RegisterAgent_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_RegisterAgent_FullMethodName, + FullMethod: "/proto.Woodpecker/RegisterAgent", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).RegisterAgent(ctx, req.(*RegisterAgentRequest)) @@ -429,7 +414,7 @@ func _Woodpecker_ReportHealth_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Woodpecker_ReportHealth_FullMethodName, + FullMethod: "/proto.Woodpecker/ReportHealth", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerServer).ReportHealth(ctx, req.(*ReportHealthRequest)) @@ -493,10 +478,6 @@ var Woodpecker_ServiceDesc = grpc.ServiceDesc{ Metadata: "woodpecker.proto", } -const ( - WoodpeckerAuth_Auth_FullMethodName = "/proto.WoodpeckerAuth/Auth" -) - // WoodpeckerAuthClient is the client API for WoodpeckerAuth service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -514,7 +495,7 @@ func NewWoodpeckerAuthClient(cc grpc.ClientConnInterface) WoodpeckerAuthClient { func (c *woodpeckerAuthClient) Auth(ctx context.Context, in *AuthRequest, opts ...grpc.CallOption) (*AuthResponse, error) { out := new(AuthResponse) - err := c.cc.Invoke(ctx, WoodpeckerAuth_Auth_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, "/proto.WoodpeckerAuth/Auth", in, out, opts...) if err != nil { return nil, err } @@ -559,7 +540,7 @@ func _WoodpeckerAuth_Auth_Handler(srv interface{}, ctx context.Context, dec func } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: WoodpeckerAuth_Auth_FullMethodName, + FullMethod: "/proto.WoodpeckerAuth/Auth", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(WoodpeckerAuthServer).Auth(ctx, req.(*AuthRequest)) diff --git a/server/api/cron.go b/server/api/cron.go index c7bcb6371..7a9f797fc 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -20,6 +20,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" cronScheduler "go.woodpecker-ci.org/woodpecker/v2/server/cron" @@ -80,7 +81,7 @@ func RunCron(c *gin.Context) { return } - repo, newPipeline, err := cronScheduler.CreatePipeline(c, _store, server.Config.Services.Forge, cron) + repo, newPipeline, err := cronScheduler.CreatePipeline(c, _store, cron) if err != nil { c.String(http.StatusInternalServerError, "Error creating pipeline for cron %q. %s", id, err) return @@ -109,7 +110,12 @@ func PostCron(c *gin.Context) { repo := session.Repo(c) user := session.User(c) _store := store.FromContext(c) - forge := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } in := new(model.Cron) if err := c.Bind(in); err != nil { @@ -137,7 +143,7 @@ func PostCron(c *gin.Context) { if in.Branch != "" { // check if branch exists on forge - _, err := forge.BranchHead(c, user, repo, in.Branch) + _, err := _forge.BranchHead(c, user, repo, in.Branch) if err != nil { c.String(http.StatusBadRequest, "Error inserting cron. branch not resolved: %s", err) return @@ -166,7 +172,12 @@ func PatchCron(c *gin.Context) { repo := session.Repo(c) user := session.User(c) _store := store.FromContext(c) - forge := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } id, err := strconv.ParseInt(c.Param("cron"), 10, 64) if err != nil { @@ -188,7 +199,7 @@ func PatchCron(c *gin.Context) { } if in.Branch != "" { // check if branch exists on forge - _, err := forge.BranchHead(c, user, repo, in.Branch) + _, err := _forge.BranchHead(c, user, repo, in.Branch) if err != nil { c.String(http.StatusBadRequest, "Error inserting cron. branch not resolved: %s", err) return diff --git a/server/api/helper.go b/server/api/helper.go index 51646e370..8b621487b 100644 --- a/server/api/helper.go +++ b/server/api/helper.go @@ -19,6 +19,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/forge" @@ -54,7 +55,12 @@ func handleDBError(c *gin.Context, err error) { // If the forge has a refresh token, the current access token may be stale. // Therefore, we should refresh prior to dispatching the job. func refreshUserToken(c *gin.Context, user *model.User) { - _forge := server.Config.Services.Forge _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } forge.Refresh(c, _forge, _store, user) } diff --git a/server/api/hook.go b/server/api/hook.go index 4318ae480..b2e7251ba 100644 --- a/server/api/hook.go +++ b/server/api/hook.go @@ -104,7 +104,13 @@ func BlockTilQueueHasRunningItem(c *gin.Context) { // @Param hook body object true "the webhook payload; forge is automatically detected" func PostHook(c *gin.Context) { _store := store.FromContext(c) - _forge := server.Config.Services.Forge + + _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get the forge for the specific repo somehow + if err != nil { + log.Error().Err(err).Msg("Cannot get main forge") + c.AbortWithStatus(http.StatusInternalServerError) + return + } // // 1. Parse webhook diff --git a/server/api/login.go b/server/api/login.go index 4daea4a57..349861ce1 100644 --- a/server/api/login.go +++ b/server/api/login.go @@ -43,7 +43,12 @@ func HandleLogin(c *gin.Context) { func HandleAuth(c *gin.Context) { _store := store.FromContext(c) - _forge := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeMain() + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + forgeID := int64(1) // TODO: replace with forge id when multiple forges are supported // when dealing with redirects, we may need to adjust the content type. I // cannot, however, remember why, so need to revisit this line. @@ -68,12 +73,12 @@ func HandleAuth(c *gin.Context) { // get the user from the database u, err := _store.GetUserRemoteID(tmpuser.ForgeRemoteID, tmpuser.Login) - if err != nil { - if !errors.Is(err, types.RecordNotExist) { - _ = c.AbortWithError(http.StatusInternalServerError, err) - return - } + if err != nil && !errors.Is(err, types.RecordNotExist) { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + if errors.Is(err, types.RecordNotExist) { // if self-registration is disabled we should return a not authorized error if !server.Config.Permissions.Open && !server.Config.Permissions.Admins.IsAdmin(tmpuser) { log.Error().Msgf("cannot register %s. registration closed", tmpuser.Login) @@ -100,6 +105,7 @@ func HandleAuth(c *gin.Context) { Secret: tmpuser.Secret, Email: tmpuser.Email, Avatar: tmpuser.Avatar, + ForgeID: forgeID, Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), ), @@ -129,6 +135,7 @@ func HandleAuth(c *gin.Context) { Name: u.Login, IsUser: true, Private: false, + ForgeID: u.ForgeID, } if err := _store.OrgCreate(org); err != nil { log.Error().Err(err).Msgf("on user creation, could create org for user") @@ -228,14 +235,21 @@ func GetLogout(c *gin.Context) { func GetLoginToken(c *gin.Context) { _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeMain() // TODO: get selected forge from auth request + if err != nil { + log.Error().Err(err).Msg("Cannot get main forge") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + in := &tokenPayload{} - err := c.Bind(in) + err = c.Bind(in) if err != nil { _ = c.AbortWithError(http.StatusBadRequest, err) return } - login, err := server.Config.Services.Forge.Auth(c, in.Access, in.Refresh) + login, err := _forge.Auth(c, in.Access, in.Refresh) if err != nil { _ = c.AbortWithError(http.StatusUnauthorized, err) return diff --git a/server/api/org.go b/server/api/org.go index 09d677221..c30d55ebe 100644 --- a/server/api/org.go +++ b/server/api/org.go @@ -68,6 +68,13 @@ func GetOrgPermissions(c *gin.Context) { user := session.User(c) _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64) if err != nil { c.String(http.StatusBadRequest, "Error parsing org id. %s", err) @@ -96,7 +103,7 @@ func GetOrgPermissions(c *gin.Context) { return } - perm, err := server.Config.Services.Membership.Get(c, user, org.Name) + perm, err := server.Config.Services.Membership.Get(c, _forge, user, org.Name) if err != nil { c.String(http.StatusInternalServerError, "Error getting membership for %d. %s", orgID, err) return @@ -116,6 +123,13 @@ func GetOrgPermissions(c *gin.Context) { // @Param org_full_name path string true "the organizations full-name / slug" func LookupOrg(c *gin.Context) { _store := store.FromContext(c) + user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } orgFullName := strings.TrimLeft(c.Param("org_full_name"), "/") @@ -137,7 +151,7 @@ func LookupOrg(c *gin.Context) { c.AbortWithStatus(http.StatusNotFound) return } else if !user.Admin { - perm, err := server.Config.Services.Membership.Get(c, user, org.Name) + perm, err := server.Config.Services.Membership.Get(c, _forge, user, org.Name) if err != nil { log.Error().Err(err).Msg("failed to check membership") c.Status(http.StatusInternalServerError) diff --git a/server/api/pipeline.go b/server/api/pipeline.go index 2a98f6a9c..8803ab95f 100644 --- a/server/api/pipeline.go +++ b/server/api/pipeline.go @@ -27,6 +27,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -48,10 +49,16 @@ import ( func CreatePipeline(c *gin.Context) { _store := store.FromContext(c) repo := session.Repo(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } // parse create options var opts model.PipelineOptions - err := json.NewDecoder(c.Request.Body).Decode(&opts) + err = json.NewDecoder(c.Request.Body).Decode(&opts) if err != nil { _ = c.AbortWithError(http.StatusBadRequest, err) return @@ -59,7 +66,7 @@ func CreatePipeline(c *gin.Context) { user := session.User(c) - lastCommit, _ := server.Config.Services.Forge.BranchHead(c, user, repo, opts.Branch) + lastCommit, _ := _forge.BranchHead(c, user, repo, opts.Branch) tmpPipeline := createTmpPipeline(model.EventManual, lastCommit, user, &opts) @@ -224,6 +231,66 @@ func GetStepLogs(c *gin.Context) { c.JSON(http.StatusOK, logs) } +// DeleteStepLogs +// +// @Summary Deletes step log +// @Router /repos/{repo_id}/logs/{number}/{stepId} [delete] +// @Produce plain +// @Success 204 +// @Tags Pipeline logs +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param repo_id path int true "the repository id" +// @Param number path int true "the number of the pipeline" +// @Param stepId path int true "the step id" +func DeleteStepLogs(c *gin.Context) { + _store := store.FromContext(c) + repo := session.Repo(c) + + pipelineNumber, err := strconv.ParseInt(c.Params.ByName("number"), 10, 64) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + + _pipeline, err := _store.GetPipelineNumber(repo, pipelineNumber) + if err != nil { + handleDBError(c, err) + return + } + + stepID, err := strconv.ParseInt(c.Params.ByName("stepId"), 10, 64) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + + _step, err := _store.StepLoad(stepID) + if err != nil { + handleDBError(c, err) + return + } + + if _step.PipelineID != _pipeline.ID { + // make sure we cannot read arbitrary logs by id + _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("step with id %d is not part of repo %s", stepID, repo.FullName)) + return + } + + switch _step.State { + case model.StatusRunning, model.StatusPending: + c.String(http.StatusUnprocessableEntity, "Cannot delete logs for a pending or running step") + return + } + + err = _store.LogDelete(_step) + if err != nil { + handleDBError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + // GetPipelineConfig // // @Summary Pipeline configuration @@ -272,6 +339,13 @@ func CancelPipeline(c *gin.Context) { _store := store.FromContext(c) repo := session.Repo(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + num, _ := strconv.ParseInt(c.Params.ByName("number"), 10, 64) pl, err := _store.GetPipelineNumber(repo, num) @@ -280,7 +354,7 @@ func CancelPipeline(c *gin.Context) { return } - if err := pipeline.Cancel(c, _store, repo, user, pl); err != nil { + if err := pipeline.Cancel(c, _forge, _store, repo, user, pl); err != nil { handlePipelineErr(c, err) } else { c.Status(http.StatusNoContent) @@ -407,23 +481,22 @@ func PostPipeline(c *gin.Context) { refreshUserToken(c, user) // make Deploy overridable - pl.Deploy = c.DefaultQuery("deploy_to", pl.Deploy) // make Event overridable to deploy // TODO refactor to use own proper API for deploy if event, ok := c.GetQuery("event"); ok { - // only allow deploy from push, tag and release - if pl.Event != model.EventPush && pl.Event != model.EventTag && pl.Event != model.EventRelease { - _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("can only deploy push, tag and release pipelines")) - return - } - pl.Event = model.WebhookEvent(event) - if pl.Event != model.EventDeploy { _ = c.AbortWithError(http.StatusBadRequest, model.ErrInvalidWebhookEvent) return } + + if !repo.AllowDeploy { + _ = c.AbortWithError(http.StatusForbidden, fmt.Errorf("repo does not allow deployments")) + return + } + + pl.Deploy = c.DefaultQuery("deploy_to", pl.Deploy) } // Read query string parameters into pipelineParams, exclude reserved params diff --git a/server/api/repo.go b/server/api/repo.go index e812312ea..6d03dcede 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -45,9 +45,14 @@ import ( // @Param Authorization header string true "Insert your personal access token" default(Bearer ) // @Param forge_remote_id query string true "the id of a repository at the forge" func PostRepo(c *gin.Context) { - forge := server.Config.Services.Forge _store := store.FromContext(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } forgeRemoteID := model.ForgeRemoteID(c.Query("forge_remote_id")) if !forgeRemoteID.IsValid() { @@ -67,7 +72,7 @@ func PostRepo(c *gin.Context) { return } - from, err := forge.Repo(c, user, forgeRemoteID, "", "") + from, err := _forge.Repo(c, user, forgeRemoteID, "", "") if err != nil { c.String(http.StatusInternalServerError, "Could not fetch repository from forge.") return @@ -86,6 +91,7 @@ func PostRepo(c *gin.Context) { } else { repo = from repo.AllowPull = true + repo.AllowDeploy = false repo.NetrcOnlyTrusted = true repo.CancelPreviousPipelineEvents = server.Config.Pipeline.DefaultCancelPreviousPipelineEvents } @@ -137,7 +143,7 @@ func PostRepo(c *gin.Context) { // create an org if it doesn't exist yet if errors.Is(err, types.RecordNotExist) { - org, err = forge.Org(c, user, repo.Owner) + org, err = _forge.Org(c, user, repo.Owner) if err != nil { msg := "could not fetch organization from forge." log.Error().Err(err).Msg(msg) @@ -145,6 +151,7 @@ func PostRepo(c *gin.Context) { return } + org.ForgeID = user.ForgeID err = _store.OrgCreate(org) if err != nil { msg := "could not create organization in store." @@ -156,7 +163,7 @@ func PostRepo(c *gin.Context) { repo.OrgID = org.ID - err = forge.Activate(c, user, repo, hookURL) + err = _forge.Activate(c, user, repo, hookURL) if err != nil { msg := "could not create webhook in forge." log.Error().Err(err).Msg(msg) @@ -167,6 +174,7 @@ func PostRepo(c *gin.Context) { if enabledOnce { err = _store.UpdateRepo(repo) } else { + repo.ForgeID = user.ForgeID // TODO: allow to use other connected forges of the user err = _store.CreateRepo(repo) } if err != nil { @@ -223,6 +231,9 @@ func PatchRepo(c *gin.Context) { if in.AllowPull != nil { repo.AllowPull = *in.AllowPull } + if in.AllowDeploy != nil { + repo.AllowDeploy = *in.AllowDeploy + } if in.IsGated != nil { repo.IsGated = *in.IsGated } @@ -338,9 +349,14 @@ func GetRepoPermissions(c *gin.Context) { func GetRepoBranches(c *gin.Context) { repo := session.Repo(c) user := session.User(c) - f := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } - branches, err := f.Branches(c, user, repo, session.Pagination(c)) + branches, err := _forge.Branches(c, user, repo, session.Pagination(c)) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -363,9 +379,14 @@ func GetRepoBranches(c *gin.Context) { func GetRepoPullRequests(c *gin.Context) { repo := session.Repo(c) user := session.User(c) - f := server.Config.Services.Forge + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } - prs, err := f.PullRequests(c, user, repo, session.Pagination(c)) + prs, err := _forge.PullRequests(c, user, repo, session.Pagination(c)) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -386,9 +407,14 @@ func GetRepoPullRequests(c *gin.Context) { func DeleteRepo(c *gin.Context) { remove, _ := strconv.ParseBool(c.Query("remove")) _store := store.FromContext(c) - repo := session.Repo(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } repo.IsActive = false repo.UserID = 0 @@ -405,7 +431,7 @@ func DeleteRepo(c *gin.Context) { } } - if err := server.Config.Services.Forge.Deactivate(c, user, repo, server.Config.Server.WebhookHost); err != nil { + if err := _forge.Deactivate(c, user, repo, server.Config.Server.WebhookHost); err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return } @@ -441,10 +467,15 @@ func RepairRepo(c *gin.Context) { // @Param repo_id path int true "the repository id" // @Param to query string true "the username to move the repository to" func MoveRepo(c *gin.Context) { - forge := server.Config.Services.Forge _store := store.FromContext(c) repo := session.Repo(c) user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } to, exists := c.GetQuery("to") if !exists { @@ -459,7 +490,7 @@ func MoveRepo(c *gin.Context) { return } - from, err := forge.Repo(c, user, "", owner, name) + from, err := _forge.Repo(c, user, "", owner, name) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -504,10 +535,10 @@ func MoveRepo(c *gin.Context) { sig, ) - if err := forge.Deactivate(c, user, repo, host); err != nil { + if err := _forge.Deactivate(c, user, repo, host); err != nil { log.Trace().Err(err).Msgf("deactivate repo '%s' for move to activate later, got an error", repo.FullName) } - if err := forge.Activate(c, user, repo, hookURL); err != nil { + if err := _forge.Activate(c, user, repo, hookURL); err != nil { c.String(http.StatusInternalServerError, err.Error()) return } @@ -567,8 +598,13 @@ func RepairAllRepos(c *gin.Context) { } func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { - forge := server.Config.Services.Forge _store := store.FromContext(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } user, err := _store.GetUser(repo.UserID) if err != nil { @@ -599,7 +635,7 @@ func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { sig, ) - from, err := forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) + from, err := _forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) if err != nil { log.Error().Err(err).Msgf("get repo '%s/%s' from forge", repo.Owner, repo.Name) if !skipOnErr { @@ -632,10 +668,10 @@ func repairRepo(c *gin.Context, repo *model.Repo, withPerms, skipOnErr bool) { } } - if err := forge.Deactivate(c, user, repo, host); err != nil { + if err := _forge.Deactivate(c, user, repo, host); err != nil { log.Trace().Err(err).Msgf("deactivate repo '%s' to repair failed", repo.FullName) } - if err := forge.Activate(c, user, repo, hookURL); err != nil { + if err := _forge.Activate(c, user, repo, hookURL); err != nil { c.String(http.StatusInternalServerError, err.Error()) return } diff --git a/server/api/user.go b/server/api/user.go index f33d4d32e..d24849bf3 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" + "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -86,9 +87,14 @@ func GetFeed(c *gin.Context) { // @Param all query bool false "query all repos, including inactive ones" func GetRepos(c *gin.Context) { _store := store.FromContext(c) - _forge := server.Config.Services.Forge - user := session.User(c) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + all, _ := strconv.ParseBool(c.Query("all")) if all { diff --git a/server/api/users.go b/server/api/users.go index 30fd8278f..c35339eef 100644 --- a/server/api/users.go +++ b/server/api/users.go @@ -132,6 +132,8 @@ func PostUser(c *gin.Context) { Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), ), + ForgeID: 1, // TODO: replace with forge id when multiple forges are supported + ForgeRemoteID: model.ForgeRemoteID("0"), // TODO: search for the user in the forge and get the remote id } if err = user.Validate(); err != nil { c.String(http.StatusBadRequest, err.Error()) diff --git a/server/cache/membership.go b/server/cache/membership.go index 45b6af5d1..38926540f 100644 --- a/server/cache/membership.go +++ b/server/cache/membership.go @@ -23,39 +23,39 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" + "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // MembershipService is a service to check for user membership. type MembershipService interface { // Get returns if the user is a member of the organization. - Get(ctx context.Context, u *model.User, org string) (*model.OrgPerm, error) + Get(ctx context.Context, _forge forge.Forge, u *model.User, org string) (*model.OrgPerm, error) } type membershipCache struct { - forge forge.Forge cache *ttlcache.Cache[string, *model.OrgPerm] + store store.Store ttl time.Duration } // NewMembershipService creates a new membership service. -func NewMembershipService(f forge.Forge) MembershipService { - //nolint:gomnd +func NewMembershipService(_store store.Store) MembershipService { return &membershipCache{ - ttl: 10 * time.Minute, - forge: f, + ttl: 10 * time.Minute, //nolint: gomnd + store: _store, cache: ttlcache.New(ttlcache.WithDisableTouchOnHit[string, *model.OrgPerm]()), } } // Get returns if the user is a member of the organization. -func (c *membershipCache) Get(ctx context.Context, u *model.User, org string) (*model.OrgPerm, error) { +func (c *membershipCache) Get(ctx context.Context, _forge forge.Forge, u *model.User, org string) (*model.OrgPerm, error) { key := fmt.Sprintf("%s-%s", u.ForgeRemoteID, org) item := c.cache.Get(key) if item != nil && !item.IsExpired() { return item.Value(), nil } - perm, err := c.forge.OrgMembership(ctx, u, org) + perm, err := _forge.OrgMembership(ctx, u, org) if err != nil { return nil, err } diff --git a/server/config.go b/server/config.go index 84b56c29a..e8ac177ee 100644 --- a/server/config.go +++ b/server/config.go @@ -21,7 +21,6 @@ import ( "time" "go.woodpecker-ci.org/woodpecker/v2/server/cache" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pubsub" @@ -35,9 +34,8 @@ var Config = struct { Pubsub *pubsub.Publisher Queue queue.Queue Logs logging.Log - Forge forge.Forge Membership cache.MembershipService - Manager *services.Manager + Manager services.Manager } Server struct { Key string diff --git a/server/cron/cron.go b/server/cron/cron.go index bce8374d5..b3331f46c 100644 --- a/server/cron/cron.go +++ b/server/cron/cron.go @@ -22,6 +22,7 @@ import ( "github.com/robfig/cron" "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pipeline" @@ -37,7 +38,7 @@ const ( ) // Start starts the cron scheduler loop -func Start(ctx context.Context, store store.Store, forge forge.Forge) error { +func Start(ctx context.Context, store store.Store) error { for { select { case <-ctx.Done(): @@ -54,7 +55,7 @@ func Start(ctx context.Context, store store.Store, forge forge.Forge) error { } for _, cron := range crons { - if err := runCron(store, forge, cron, now); err != nil { + if err := runCron(ctx, store, cron, now); err != nil { log.Error().Err(err).Int64("cronID", cron.ID).Msg("run cron failed") } } @@ -77,9 +78,8 @@ func CalcNewNext(schedule string, now time.Time) (time.Time, error) { return c.Next(now), nil } -func runCron(store store.Store, forge forge.Forge, cron *model.Cron, now time.Time) error { +func runCron(ctx context.Context, store store.Store, cron *model.Cron, now time.Time) error { log.Trace().Msgf("cron: run id[%d]", cron.ID) - ctx := context.Background() newNext, err := CalcNewNext(cron.Schedule, now) if err != nil { @@ -96,7 +96,7 @@ func runCron(store store.Store, forge forge.Forge, cron *model.Cron, now time.Ti return nil } - repo, newPipeline, err := CreatePipeline(ctx, store, forge, cron) + repo, newPipeline, err := CreatePipeline(ctx, store, cron) if err != nil { return err } @@ -105,12 +105,17 @@ func runCron(store store.Store, forge forge.Forge, cron *model.Cron, now time.Ti return err } -func CreatePipeline(ctx context.Context, store store.Store, f forge.Forge, cron *model.Cron) (*model.Repo, *model.Pipeline, error) { +func CreatePipeline(ctx context.Context, store store.Store, cron *model.Cron) (*model.Repo, *model.Pipeline, error) { repo, err := store.GetRepo(cron.RepoID) if err != nil { return nil, nil, err } + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + return nil, nil, err + } + if cron.Branch == "" { // fallback to the repos default branch cron.Branch = repo.Branch @@ -124,9 +129,9 @@ func CreatePipeline(ctx context.Context, store store.Store, f forge.Forge, cron // If the forge has a refresh token, the current access token // may be stale. Therefore, we should refresh prior to dispatching // the pipeline. - forge.Refresh(ctx, f, store, creator) + forge.Refresh(ctx, _forge, store, creator) - commit, err := f.BranchHead(ctx, creator, repo, cron.Branch) + commit, err := _forge.BranchHead(ctx, creator, repo, cron.Branch) if err != nil { return nil, nil, err } diff --git a/server/cron/cron_test.go b/server/cron/cron_test.go index 0b170cfe9..61f1a2538 100644 --- a/server/cron/cron_test.go +++ b/server/cron/cron_test.go @@ -22,13 +22,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "go.woodpecker-ci.org/woodpecker/v2/server" mocks_forge "go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks" "go.woodpecker-ci.org/woodpecker/v2/server/model" + mocks_manager "go.woodpecker-ci.org/woodpecker/v2/server/services/mocks" mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks" ) -func TestCreateBuild(t *testing.T) { - forge := mocks_forge.NewForge(t) +func TestCreatePipeline(t *testing.T) { + _manager := mocks_manager.NewManager(t) + _forge := mocks_forge.NewForge(t) store := mocks_store.NewStore(t) ctx := context.Background() @@ -47,12 +50,14 @@ func TestCreateBuild(t *testing.T) { // mock things store.On("GetRepo", mock.Anything).Return(repo1, nil) store.On("GetUser", mock.Anything).Return(creator, nil) - forge.On("BranchHead", mock.Anything, creator, repo1, "default").Return(&model.Commit{ + _forge.On("BranchHead", mock.Anything, creator, repo1, "default").Return(&model.Commit{ ForgeURL: "https://example.com/sha1", SHA: "sha1", }, nil) + _manager.On("ForgeFromRepo", repo1).Return(_forge, nil) + server.Config.Services.Manager = _manager - _, pipeline, err := CreatePipeline(ctx, store, forge, &model.Cron{ + _, pipeline, err := CreatePipeline(ctx, store, &model.Cron{ Name: "test", }) assert.NoError(t, err) diff --git a/server/forge/addon/args.go b/server/forge/addon/args.go new file mode 100644 index 000000000..038e8b0e0 --- /dev/null +++ b/server/forge/addon/args.go @@ -0,0 +1,154 @@ +// 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. + +package addon + +import ( + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +type argumentsAuth struct { + Token string `json:"token"` + Secret string `json:"secret"` +} + +type argumentsRepo struct { + U *modelUser `json:"u"` + RemoteID model.ForgeRemoteID `json:"remote_id"` + Owner string `json:"owner"` + Name string `json:"name"` +} + +type argumentsFileDir struct { + U *modelUser `json:"u"` + R *modelRepo `json:"r"` + B *model.Pipeline `json:"b"` + F string `json:"f"` +} + +type argumentsStatus struct { + U *modelUser `json:"u"` + R *modelRepo `json:"r"` + B *model.Pipeline `json:"b"` + P *model.Workflow `json:"p"` +} + +type argumentsNetrc struct { + U *modelUser `json:"u"` + R *modelRepo `json:"r"` +} + +type argumentsActivateDeactivate struct { + U *modelUser `json:"u"` + R *modelRepo `json:"r"` + Link string `json:"link"` +} + +type argumentsBranchesPullRequests struct { + U *modelUser `json:"u"` + R *modelRepo `json:"r"` + P *model.ListOptions `json:"p"` +} + +type argumentsBranchHead struct { + U *modelUser `json:"u"` + R *modelRepo `json:"r"` + Branch string `json:"branch"` +} + +type argumentsOrgMembershipOrg struct { + U *modelUser `json:"u"` + Org string `json:"org"` +} + +type responseHook struct { + Repo *modelRepo `json:"repo"` + Pipeline *model.Pipeline `json:"pipeline"` +} + +type responseLogin struct { + User *modelUser `json:"user"` + RedirectURL string `json:"redirect_url"` +} + +type httpRequest struct { + Method string `json:"method"` + URL string `json:"url"` + Header map[string][]string `json:"header"` + Form map[string][]string `json:"form"` + Body []byte `json:"body"` +} + +// modelUser is an extension of model.User to marshal all fields to JSON +type modelUser struct { + User *model.User `json:"user"` + + ForgeRemoteID model.ForgeRemoteID `json:"forge_remote_id"` + + // Token is the oauth2 token. + Token string `json:"token"` + + // Secret is the oauth2 token secret. + Secret string `json:"secret"` + + // Expiry is the token and secret expiration timestamp. + Expiry int64 `json:"expiry"` + + // Hash is a unique token used to sign tokens. + Hash string `json:"hash"` +} + +func (m *modelUser) asModel() *model.User { + m.User.ForgeRemoteID = m.ForgeRemoteID + m.User.Token = m.Token + m.User.Secret = m.Secret + m.User.Expiry = m.Expiry + m.User.Hash = m.Hash + return m.User +} + +func modelUserFromModel(u *model.User) *modelUser { + return &modelUser{ + User: u, + ForgeRemoteID: u.ForgeRemoteID, + Token: u.Token, + Secret: u.Secret, + Expiry: u.Expiry, + Hash: u.Hash, + } +} + +// modelRepo is an extension of model.Repo to marshal all fields to JSON +type modelRepo struct { + Repo *model.Repo `json:"repo"` + UserID int64 `json:"user_id"` + Hash string `json:"hash"` + Perm *model.Perm `json:"perm"` +} + +func (m *modelRepo) asModel() *model.Repo { + m.Repo.UserID = m.UserID + m.Repo.Hash = m.Hash + m.Repo.Perm = m.Perm + return m.Repo +} + +func modelRepoFromModel(r *model.Repo) *modelRepo { + return &modelRepo{ + Repo: r, + UserID: r.UserID, + Hash: r.Hash, + Perm: r.Perm, + } +} diff --git a/server/forge/addon/client.go b/server/forge/addon/client.go new file mode 100644 index 000000000..323b0e8ab --- /dev/null +++ b/server/forge/addon/client.go @@ -0,0 +1,377 @@ +// 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. + +package addon + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/rpc" + "os/exec" + + "github.com/hashicorp/go-plugin" + "github.com/rs/zerolog/log" + + "go.woodpecker-ci.org/woodpecker/v2/server/forge" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +// make sure RPC implements forge.Forge +var _ forge.Forge = new(RPC) + +func Load(file string) (forge.Forge, error) { + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: HandshakeConfig, + Plugins: map[string]plugin.Plugin{ + pluginKey: &Plugin{}, + }, + Cmd: exec.Command(file), + Logger: &clientLogger{ + logger: log.With().Str("addon", file).Logger(), + }, + }) + // TODO defer client.Kill() + + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + raw, err := rpcClient.Dispense(pluginKey) + if err != nil { + return nil, err + } + + extension, _ := raw.(forge.Forge) + return extension, nil +} + +type RPC struct { + client *rpc.Client +} + +func (g *RPC) Name() string { + var resp string + _ = g.client.Call("Plugin.Name", nil, &resp) + return resp +} + +func (g *RPC) URL() string { + var resp string + _ = g.client.Call("Plugin.URL", nil, &resp) + return resp +} + +func (g *RPC) Login(_ context.Context, r *types.OAuthRequest) (*model.User, string, error) { + args, err := json.Marshal(r) + if err != nil { + return nil, "", err + } + var jsonResp []byte + err = g.client.Call("Plugin.Login", args, &jsonResp) + if err != nil { + return nil, "", err + } + + var resp responseLogin + err = json.Unmarshal(jsonResp, &resp) + if err != nil { + return nil, "", err + } + + return resp.User.asModel(), resp.RedirectURL, nil +} + +func (g *RPC) Auth(_ context.Context, token, secret string) (string, error) { + args, err := json.Marshal(&argumentsAuth{ + Token: token, + Secret: secret, + }) + if err != nil { + return "", err + } + var resp string + return resp, g.client.Call("Plugin.Auth", args, &resp) +} + +func (g *RPC) Teams(_ context.Context, u *model.User) ([]*model.Team, error) { + args, err := json.Marshal(modelUserFromModel(u)) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Teams", args, &jsonResp) + if err != nil { + return nil, err + } + + var resp []*model.Team + return resp, json.Unmarshal(jsonResp, &resp) +} + +func (g *RPC) Repo(_ context.Context, u *model.User, remoteID model.ForgeRemoteID, owner, name string) (*model.Repo, error) { + args, err := json.Marshal(&argumentsRepo{ + U: modelUserFromModel(u), + RemoteID: remoteID, + Owner: owner, + Name: name, + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Repo", args, &jsonResp) + if err != nil { + return nil, err + } + + var resp *modelRepo + err = json.Unmarshal(jsonResp, resp) + if err != nil { + return nil, err + } + return resp.asModel(), nil +} + +func (g *RPC) Repos(_ context.Context, u *model.User) ([]*model.Repo, error) { + args, err := json.Marshal(modelUserFromModel(u)) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Repos", args, &jsonResp) + if err != nil { + return nil, err + } + + var resp []*modelRepo + err = json.Unmarshal(jsonResp, &resp) + if err != nil { + return nil, err + } + var modelRepos []*model.Repo + for _, repo := range resp { + modelRepos = append(modelRepos, repo.asModel()) + } + return modelRepos, nil +} + +func (g *RPC) File(_ context.Context, u *model.User, r *model.Repo, b *model.Pipeline, f string) ([]byte, error) { + args, err := json.Marshal(&argumentsFileDir{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + B: b, + F: f, + }) + if err != nil { + return nil, err + } + var resp []byte + return resp, g.client.Call("Plugin.File", args, &resp) +} + +func (g *RPC) Dir(_ context.Context, u *model.User, r *model.Repo, b *model.Pipeline, f string) ([]*types.FileMeta, error) { + args, err := json.Marshal(&argumentsFileDir{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + B: b, + F: f, + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Dir", args, &jsonResp) + if err != nil { + return nil, err + } + var resp []*types.FileMeta + return resp, json.Unmarshal(jsonResp, &resp) +} + +func (g *RPC) Status(_ context.Context, u *model.User, r *model.Repo, b *model.Pipeline, p *model.Workflow) error { + args, err := json.Marshal(&argumentsStatus{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + B: b, + P: p, + }) + if err != nil { + return err + } + var jsonResp []byte + return g.client.Call("Plugin.Status", args, &jsonResp) +} + +func (g *RPC) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + args, err := json.Marshal(&argumentsNetrc{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Netrc", args, &jsonResp) + if err != nil { + return nil, err + } + var resp *model.Netrc + return resp, json.Unmarshal(jsonResp, &resp) +} + +func (g *RPC) Activate(_ context.Context, u *model.User, r *model.Repo, link string) error { + args, err := json.Marshal(&argumentsActivateDeactivate{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + Link: link, + }) + if err != nil { + return err + } + var jsonResp []byte + return g.client.Call("Plugin.Activate", args, &jsonResp) +} + +func (g *RPC) Deactivate(_ context.Context, u *model.User, r *model.Repo, link string) error { + args, err := json.Marshal(&argumentsActivateDeactivate{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + Link: link, + }) + if err != nil { + return err + } + var jsonResp []byte + return g.client.Call("Plugin.Deactivate", args, &jsonResp) +} + +func (g *RPC) Branches(_ context.Context, u *model.User, r *model.Repo, p *model.ListOptions) ([]string, error) { + args, err := json.Marshal(&argumentsBranchesPullRequests{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + P: p, + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Branches", args, &jsonResp) + if err != nil { + return nil, err + } + var resp []string + return resp, json.Unmarshal(jsonResp, &resp) +} + +func (g *RPC) BranchHead(_ context.Context, u *model.User, r *model.Repo, branch string) (*model.Commit, error) { + args, err := json.Marshal(&argumentsBranchHead{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + Branch: branch, + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.BranchHead", args, &jsonResp) + if err != nil { + return nil, err + } + var resp *model.Commit + return resp, json.Unmarshal(jsonResp, &resp) +} + +func (g *RPC) PullRequests(_ context.Context, u *model.User, r *model.Repo, p *model.ListOptions) ([]*model.PullRequest, error) { + args, err := json.Marshal(&argumentsBranchesPullRequests{ + U: modelUserFromModel(u), + R: modelRepoFromModel(r), + P: p, + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.PullRequests", args, &jsonResp) + if err != nil { + return nil, err + } + var resp []*model.PullRequest + return resp, json.Unmarshal(jsonResp, &resp) +} + +func (g *RPC) Hook(_ context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) { + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, nil, err + } + args, err := json.Marshal(&httpRequest{ + Method: r.Method, + URL: r.URL.String(), + Header: r.Header, + Form: r.Form, + Body: body, + }) + if err != nil { + return nil, nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Hook", args, &jsonResp) + if err != nil { + return nil, nil, err + } + var resp responseHook + err = json.Unmarshal(jsonResp, &resp) + if err != nil { + return nil, nil, err + } + return resp.Repo.asModel(), resp.Pipeline, nil +} + +func (g *RPC) OrgMembership(_ context.Context, u *model.User, org string) (*model.OrgPerm, error) { + args, err := json.Marshal(&argumentsOrgMembershipOrg{ + U: modelUserFromModel(u), + Org: org, + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.OrgMembership", args, &jsonResp) + if err != nil { + return nil, err + } + var resp *model.OrgPerm + return resp, json.Unmarshal(jsonResp, &resp) +} + +func (g *RPC) Org(_ context.Context, u *model.User, org string) (*model.Org, error) { + args, err := json.Marshal(&argumentsOrgMembershipOrg{ + U: modelUserFromModel(u), + Org: org, + }) + if err != nil { + return nil, err + } + var jsonResp []byte + err = g.client.Call("Plugin.Org", args, &jsonResp) + if err != nil { + return nil, err + } + var resp *model.Org + return resp, json.Unmarshal(jsonResp, &resp) +} diff --git a/server/forge/addon/logger.go b/server/forge/addon/logger.go new file mode 100644 index 000000000..84ebd71b9 --- /dev/null +++ b/server/forge/addon/logger.go @@ -0,0 +1,165 @@ +// 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. + +package addon + +import ( + "bytes" + "io" + stdlog "log" + + "github.com/hashicorp/go-hclog" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +type clientLogger struct { + logger zerolog.Logger + name string + withArgs []any +} + +func convertLvl(level hclog.Level) zerolog.Level { + switch level { + case hclog.Error: + return zerolog.ErrorLevel + case hclog.Warn: + return zerolog.WarnLevel + case hclog.Info: + return zerolog.InfoLevel + case hclog.Debug: + return zerolog.DebugLevel + case hclog.Trace: + return zerolog.TraceLevel + } + return zerolog.NoLevel +} + +func (c *clientLogger) applyArgs(args []any) *zerolog.Logger { + var key string + logger := c.logger.With() + args = append(args, c.withArgs) + for i, arg := range args { + switch { + case key != "": + logger.Any(key, arg) + key = "" + case i == len(args)-1: + logger.Any(hclog.MissingKey, arg) + default: + + key, _ = arg.(string) + } + } + l := logger.Logger() + return &l +} + +func (c *clientLogger) Log(level hclog.Level, msg string, args ...any) { + c.applyArgs(args).WithLevel(convertLvl(level)).Msg(msg) +} + +func (c *clientLogger) Trace(msg string, args ...any) { + c.applyArgs(args).Trace().Msg(msg) +} + +func (c *clientLogger) Debug(msg string, args ...any) { + c.applyArgs(args).Debug().Msg(msg) +} + +func (c *clientLogger) Info(msg string, args ...any) { + c.applyArgs(args).Info().Msg(msg) +} + +func (c *clientLogger) Warn(msg string, args ...any) { + c.applyArgs(args).Warn().Msg(msg) +} + +func (c *clientLogger) Error(msg string, args ...any) { + c.applyArgs(args).Error().Msg(msg) +} + +func (c *clientLogger) IsTrace() bool { + return log.Logger.GetLevel() >= zerolog.TraceLevel +} + +func (c *clientLogger) IsDebug() bool { + return log.Logger.GetLevel() >= zerolog.DebugLevel +} + +func (c *clientLogger) IsInfo() bool { + return log.Logger.GetLevel() >= zerolog.InfoLevel +} + +func (c *clientLogger) IsWarn() bool { + return log.Logger.GetLevel() >= zerolog.WarnLevel +} + +func (c *clientLogger) IsError() bool { + return log.Logger.GetLevel() >= zerolog.ErrorLevel +} + +func (c *clientLogger) ImpliedArgs() []any { + return c.withArgs +} + +func (c *clientLogger) With(args ...any) hclog.Logger { + return &clientLogger{ + logger: c.logger, + name: c.name, + withArgs: args, + } +} + +func (c *clientLogger) Name() string { + return c.name +} + +func (c *clientLogger) Named(name string) hclog.Logger { + curr := c.name + if curr != "" { + curr = c.name + "." + } + return c.ResetNamed(curr + name) +} + +func (c *clientLogger) ResetNamed(name string) hclog.Logger { + return &clientLogger{ + logger: c.logger, + name: name, + withArgs: c.withArgs, + } +} + +func (c *clientLogger) SetLevel(level hclog.Level) { + c.logger = c.logger.Level(convertLvl(level)) +} + +func (c *clientLogger) StandardLogger(opts *hclog.StandardLoggerOptions) *stdlog.Logger { + return stdlog.New(c.StandardWriter(opts), "", 0) +} + +func (c *clientLogger) StandardWriter(*hclog.StandardLoggerOptions) io.Writer { + return ioAdapter{logger: c.logger} +} + +type ioAdapter struct { + logger zerolog.Logger +} + +func (i ioAdapter) Write(p []byte) (n int, err error) { + str := string(bytes.TrimRight(p, " \t\n")) + i.logger.Log().Msg(str) + return len(p), nil +} diff --git a/server/forge/addon/plugin.go b/server/forge/addon/plugin.go new file mode 100644 index 000000000..21099cf68 --- /dev/null +++ b/server/forge/addon/plugin.go @@ -0,0 +1,43 @@ +// 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. + +package addon + +import ( + "net/rpc" + + "github.com/hashicorp/go-plugin" + + "go.woodpecker-ci.org/woodpecker/v2/server/forge" +) + +const pluginKey = "forge" + +var HandshakeConfig = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "WOODPECKER_FORGE_ADDON_PLUGIN", + MagicCookieValue: "woodpecker-plugin-magic-cookie-value", +} + +type Plugin struct { + Impl forge.Forge +} + +func (p *Plugin) Server(*plugin.MuxBroker) (any, error) { + return &RPCServer{Impl: p.Impl}, nil +} + +func (*Plugin) Client(_ *plugin.MuxBroker, c *rpc.Client) (any, error) { + return &RPC{client: c}, nil +} diff --git a/server/forge/addon/server.go b/server/forge/addon/server.go new file mode 100644 index 000000000..a65967eaa --- /dev/null +++ b/server/forge/addon/server.go @@ -0,0 +1,278 @@ +// 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. + +package addon + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "github.com/hashicorp/go-plugin" + + "go.woodpecker-ci.org/woodpecker/v2/server/forge" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" +) + +func Serve(impl forge.Forge) { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: HandshakeConfig, + Plugins: map[string]plugin.Plugin{ + pluginKey: &Plugin{Impl: impl}, + }, + }) +} + +func mkCtx() context.Context { + return context.Background() +} + +type RPCServer struct { + Impl forge.Forge +} + +func (s *RPCServer) Name(_ []byte, resp *string) error { + *resp = s.Impl.Name() + return nil +} + +func (s *RPCServer) URL(_ []byte, resp *string) error { + *resp = s.Impl.URL() + return nil +} + +func (s *RPCServer) Teams(args []byte, resp *[]byte) error { + var a *modelUser + err := json.Unmarshal(args, a) + if err != nil { + return err + } + teams, err := s.Impl.Teams(mkCtx(), a.asModel()) + if err != nil { + return err + } + *resp, err = json.Marshal(teams) + return err +} + +func (s *RPCServer) Repo(args []byte, resp *[]byte) error { + var a argumentsRepo + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + repos, err := s.Impl.Repo(mkCtx(), a.U.asModel(), a.RemoteID, a.Owner, a.Name) + if err != nil { + return err + } + *resp, err = json.Marshal(modelRepoFromModel(repos)) + return err +} + +func (s *RPCServer) Repos(args []byte, resp *[]byte) error { + var a *modelUser + err := json.Unmarshal(args, a) + if err != nil { + return err + } + repos, err := s.Impl.Repos(mkCtx(), a.asModel()) + if err != nil { + return err + } + var modelRepos []*modelRepo + for _, repo := range repos { + modelRepos = append(modelRepos, modelRepoFromModel(repo)) + } + *resp, err = json.Marshal(modelRepos) + return err +} + +func (s *RPCServer) File(args []byte, resp *[]byte) error { + var a argumentsFileDir + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + *resp, err = s.Impl.File(mkCtx(), a.U.asModel(), a.R.asModel(), a.B, a.F) + return err +} + +func (s *RPCServer) Dir(args []byte, resp *[]byte) error { + var a argumentsFileDir + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + meta, err := s.Impl.Dir(mkCtx(), a.U.asModel(), a.R.asModel(), a.B, a.F) + if err != nil { + return err + } + *resp, err = json.Marshal(meta) + return err +} + +func (s *RPCServer) Status(args []byte, resp *[]byte) error { + var a argumentsStatus + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + *resp = []byte{} + return s.Impl.Status(mkCtx(), a.U.asModel(), a.R.asModel(), a.B, a.P) +} + +func (s *RPCServer) Netrc(args []byte, resp *[]byte) error { + var a argumentsNetrc + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + netrc, err := s.Impl.Netrc(a.U.asModel(), a.R.asModel()) + if err != nil { + return err + } + *resp, err = json.Marshal(netrc) + return err +} + +func (s *RPCServer) Activate(args []byte, resp *[]byte) error { + var a argumentsActivateDeactivate + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + *resp = []byte{} + return s.Impl.Activate(mkCtx(), a.U.asModel(), a.R.asModel(), a.Link) +} + +func (s *RPCServer) Deactivate(args []byte, resp *[]byte) error { + var a argumentsActivateDeactivate + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + *resp = []byte{} + return s.Impl.Deactivate(mkCtx(), a.U.asModel(), a.R.asModel(), a.Link) +} + +func (s *RPCServer) Branches(args []byte, resp *[]byte) error { + var a argumentsBranchesPullRequests + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + branches, err := s.Impl.Branches(mkCtx(), a.U.asModel(), a.R.asModel(), a.P) + if err != nil { + return err + } + *resp, err = json.Marshal(branches) + return err +} + +func (s *RPCServer) BranchHead(args []byte, resp *[]byte) error { + var a argumentsBranchHead + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + commit, err := s.Impl.BranchHead(mkCtx(), a.U.asModel(), a.R.asModel(), a.Branch) + if err != nil { + return err + } + *resp, err = json.Marshal(commit) + return err +} + +func (s *RPCServer) PullRequests(args []byte, resp *[]byte) error { + var a argumentsBranchesPullRequests + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + prs, err := s.Impl.PullRequests(mkCtx(), a.U.asModel(), a.R.asModel(), a.P) + if err != nil { + return err + } + *resp, err = json.Marshal(prs) + return err +} + +func (s *RPCServer) OrgMembership(args []byte, resp *[]byte) error { + var a argumentsOrgMembershipOrg + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + org, err := s.Impl.OrgMembership(mkCtx(), a.U.asModel(), a.Org) + if err != nil { + return err + } + *resp, err = json.Marshal(org) + return err +} + +func (s *RPCServer) Org(args []byte, resp *[]byte) error { + var a argumentsOrgMembershipOrg + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + org, err := s.Impl.Org(mkCtx(), a.U.asModel(), a.Org) + if err != nil { + return err + } + *resp, err = json.Marshal(org) + return err +} + +func (s *RPCServer) Hook(args []byte, resp *[]byte) error { + var a httpRequest + err := json.Unmarshal(args, &a) + if err != nil { + return err + } + req, err := http.NewRequest(a.Method, a.URL, bytes.NewBuffer(a.Body)) + if err != nil { + return err + } + req.Header = a.Header + req.Form = a.Form + repo, pipeline, err := s.Impl.Hook(mkCtx(), req) + if err != nil { + return err + } + *resp, err = json.Marshal(&responseHook{ + Repo: modelRepoFromModel(repo), + Pipeline: pipeline, + }) + return err +} + +func (s *RPCServer) Login(args []byte, resp *[]byte) error { + var a *types.OAuthRequest + err := json.Unmarshal(args, a) + if err != nil { + return err + } + user, red, err := s.Impl.Login(mkCtx(), a) + if err != nil { + return err + } + *resp, err = json.Marshal(&responseLogin{ + User: modelUserFromModel(user), + RedirectURL: red, + }) + return err +} diff --git a/server/forge/bitbucket/bitbucket.go b/server/forge/bitbucket/bitbucket.go index c1590fde7..d3c96920e 100644 --- a/server/forge/bitbucket/bitbucket.go +++ b/server/forge/bitbucket/bitbucket.go @@ -17,6 +17,7 @@ package bitbucket import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -232,9 +233,15 @@ func (c *config) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error func (c *config) File(ctx context.Context, u *model.User, r *model.Repo, p *model.Pipeline, f string) ([]byte, error) { config, err := c.newClient(ctx, u).FindSource(r.Owner, r.Name, p.Commit, f) if err != nil { + var rspErr internal.Error + if ok := errors.As(err, &rspErr); ok && rspErr.Status == http.StatusNotFound { + return nil, &forge_types.ErrConfigNotFound{ + Configs: []string{f}, + } + } return nil, err } - return []byte(*config), err + return []byte(*config), nil } // Dir fetches a folder from the bitbucket repository @@ -245,6 +252,12 @@ func (c *config) Dir(ctx context.Context, u *model.User, r *model.Repo, p *model for { filesResp, err := client.GetRepoFiles(r.Owner, r.Name, p.Commit, f, page) if err != nil { + var rspErr internal.Error + if ok := errors.As(err, &rspErr); ok && rspErr.Status == http.StatusNotFound { + return nil, &forge_types.ErrConfigNotFound{ + Configs: []string{f}, + } + } return nil, err } for _, file := range filesResp.Values { diff --git a/server/forge/bitbucket/bitbucket_test.go b/server/forge/bitbucket/bitbucket_test.go index 0f3823e68..dd3842964 100644 --- a/server/forge/bitbucket/bitbucket_test.go +++ b/server/forge/bitbucket/bitbucket_test.go @@ -18,6 +18,7 @@ package bitbucket import ( "bytes" "context" + "errors" "net/http" "net/http/httptest" "testing" @@ -179,6 +180,7 @@ func Test_bitbucket(t *testing.T) { g.It("Should handle not found error", func() { _, err := c.File(ctx, fakeUser, fakeRepo, fakePipeline, "file_not_found") g.Assert(err).IsNotNil() + g.Assert(errors.Is(err, &types.ErrConfigNotFound{})).IsTrue() }) }) @@ -222,8 +224,9 @@ func Test_bitbucket(t *testing.T) { g.Assert(string(files[0].Data)).Equal("dummy payload") }) g.It("Should handle not found errors", func() { - _, err := c.Dir(ctx, fakeUser, fakeRepo, fakePipeline, "/dir_not_found") + _, err := c.Dir(ctx, fakeUser, fakeRepo, fakePipeline, "dir_not_found/") g.Assert(err).IsNotNil() + g.Assert(errors.Is(err, &types.ErrConfigNotFound{})).IsTrue() }) }) diff --git a/server/forge/forge.go b/server/forge/forge.go index a823bc422..bb5db187d 100644 --- a/server/forge/forge.go +++ b/server/forge/forge.go @@ -15,7 +15,6 @@ package forge -//go:generate go install github.com/vektra/mockery/v2@latest //go:generate mockery --name Forge --output mocks --case underscore import ( diff --git a/server/forge/gitea/helper.go b/server/forge/gitea/helper.go index 3cdaca8eb..0236ceb3b 100644 --- a/server/forge/gitea/helper.go +++ b/server/forge/gitea/helper.go @@ -118,7 +118,7 @@ func getChangedFilesFromPushHook(hook *pushHook) []string { files = append(files, hook.HeadCommit.Removed...) files = append(files, hook.HeadCommit.Modified...) - return utils.DedupStrings(files) + return utils.DeduplicateStrings(files) } // helper function that extracts the Pipeline data from a Gitea tag hook diff --git a/server/forge/github/convert.go b/server/forge/github/convert.go index 5fcc352ab..d7dd9e531 100644 --- a/server/forge/github/convert.go +++ b/server/forge/github/convert.go @@ -18,7 +18,7 @@ package github import ( "fmt" - "github.com/google/go-github/v60/github" + "github.com/google/go-github/v61/github" "go.woodpecker-ci.org/woodpecker/v2/server/model" ) diff --git a/server/forge/github/convert_test.go b/server/forge/github/convert_test.go index 7894cd00b..0d74417ea 100644 --- a/server/forge/github/convert_test.go +++ b/server/forge/github/convert_test.go @@ -19,7 +19,7 @@ import ( "testing" "github.com/franela/goblin" - "github.com/google/go-github/v60/github" + "github.com/google/go-github/v61/github" "go.woodpecker-ci.org/woodpecker/v2/server/model" ) diff --git a/server/forge/github/github.go b/server/forge/github/github.go index 278b91401..3e0ae25dc 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -26,7 +26,7 @@ import ( "strconv" "strings" - "github.com/google/go-github/v60/github" + "github.com/google/go-github/v61/github" "github.com/rs/zerolog/log" "golang.org/x/oauth2" @@ -51,6 +51,7 @@ type Opts struct { Secret string // GitHub oauth client secret. SkipVerify bool // Skip ssl verification. MergeRef bool // Clone pull requests using the merge ref. + OnlyPublic bool // Only obtain OAuth tokens with access to public repos. } // New returns a Forge implementation that integrates with a GitHub Cloud or @@ -63,6 +64,7 @@ func New(opts Opts) (forge.Forge, error) { Secret: opts.Secret, SkipVerify: opts.SkipVerify, MergeRef: opts.MergeRef, + OnlyPublic: opts.OnlyPublic, } if opts.URL != defaultURL { r.url = strings.TrimSuffix(opts.URL, "/") @@ -79,6 +81,7 @@ type client struct { Secret string SkipVerify bool MergeRef bool + OnlyPublic bool } // Name returns the string name of this driver @@ -405,10 +408,17 @@ func (c *client) newContext(ctx context.Context) context.Context { // helper function to return the GitHub oauth2 config func (c *client) newConfig() *oauth2.Config { + scopes := []string{"user:email", "read:org"} + if c.OnlyPublic { + scopes = append(scopes, []string{"admin:repo_hook", "repo:status"}...) + } else { + scopes = append(scopes, "repo") + } + return &oauth2.Config{ ClientID: c.Client, ClientSecret: c.Secret, - Scopes: []string{"repo", "user:email", "read:org"}, + Scopes: scopes, Endpoint: oauth2.Endpoint{ AuthURL: fmt.Sprintf("%s/login/oauth/authorize", c.url), TokenURL: fmt.Sprintf("%s/login/oauth/access_token", c.url), @@ -620,7 +630,7 @@ func (c *client) loadChangedFilesFromPullRequest(ctx context.Context, pull *gith opts.Page = resp.NextPage } - return utils.DedupStrings(fileList), nil + return utils.DeduplicateStrings(fileList), nil }) return pipeline, err diff --git a/server/forge/github/parse.go b/server/forge/github/parse.go index 4d47b10cc..2a14a5a91 100644 --- a/server/forge/github/parse.go +++ b/server/forge/github/parse.go @@ -22,7 +22,7 @@ import ( "net/http" "strings" - "github.com/google/go-github/v60/github" + "github.com/google/go-github/v61/github" "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -215,5 +215,5 @@ func getChangedFilesFromCommits(commits []*github.HeadCommit) []string { files = append(files, cm.Removed...) files = append(files, cm.Modified...) } - return utils.DedupStrings(files) + return utils.DeduplicateStrings(files) } diff --git a/server/forge/gitlab/convert.go b/server/forge/gitlab/convert.go index 9e9adc7cd..9c3077cfa 100644 --- a/server/forge/gitlab/convert.go +++ b/server/forge/gitlab/convert.go @@ -189,7 +189,7 @@ func convertPushHook(hook *gitlab.PushEvent) (*model.Repo, *model.Pipeline, erro files = append(files, cm.Removed...) files = append(files, cm.Modified...) } - pipeline.ChangedFiles = utils.DedupStrings(files) + pipeline.ChangedFiles = utils.DeduplicateStrings(files) return repo, pipeline, nil } diff --git a/server/forge/gitlab/gitlab.go b/server/forge/gitlab/gitlab.go index 79c51462b..4f05659a5 100644 --- a/server/forge/gitlab/gitlab.go +++ b/server/forge/gitlab/gitlab.go @@ -808,7 +808,7 @@ func (g *GitLab) loadChangedFilesFromMergeRequest(ctx context.Context, tmpRepo * for _, file := range changes { files = append(files, file.NewPath, file.OldPath) } - pipeline.ChangedFiles = utils.DedupStrings(files) + pipeline.ChangedFiles = utils.DeduplicateStrings(files) return pipeline, nil } diff --git a/server/forge/mocks/forge.go b/server/forge/mocks/forge.go index e47fd1490..a926ca16a 100644 --- a/server/forge/mocks/forge.go +++ b/server/forge/mocks/forge.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery. DO NOT EDIT. package mocks diff --git a/server/forge/setup/setup.go b/server/forge/setup/setup.go new file mode 100644 index 000000000..d5ce8f742 --- /dev/null +++ b/server/forge/setup/setup.go @@ -0,0 +1,134 @@ +package setup + +import ( + "fmt" + "net/url" + "strings" + + "github.com/rs/zerolog/log" + + "go.woodpecker-ci.org/woodpecker/v2/server/forge" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/addon" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucket" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucketdatacenter" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/github" + "go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab" + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +func Forge(forge *model.Forge) (forge.Forge, error) { + switch forge.Type { + case model.ForgeTypeAddon: + return setupAddon(forge) + case model.ForgeTypeGithub: + return setupGitHub(forge) + case model.ForgeTypeGitlab: + return setupGitLab(forge) + case model.ForgeTypeBitbucket: + return setupBitbucket(forge) + case model.ForgeTypeGitea: + return setupGitea(forge) + case model.ForgeTypeBitbucketDatacenter: + return setupBitbucketDatacenter(forge) + default: + return nil, fmt.Errorf("forge not configured") + } +} + +func setupBitbucket(forge *model.Forge) (forge.Forge, error) { + opts := &bitbucket.Opts{ + Client: forge.Client, + Secret: forge.ClientSecret, + } + log.Trace().Msgf("Forge (bitbucket) opts: %#v", opts) + return bitbucket.New(opts) +} + +func setupGitea(forge *model.Forge) (forge.Forge, error) { + server, err := url.Parse(forge.URL) + if err != nil { + return nil, err + } + + oauthURL, ok := forge.AdditionalOptions["oauth-server"].(string) + if !ok { + return nil, fmt.Errorf("missing oauth-server") + } + + opts := gitea.Opts{ + URL: strings.TrimRight(server.String(), "/"), + Client: forge.Client, + Secret: forge.ClientSecret, + SkipVerify: forge.SkipVerify, + OAuth2URL: oauthURL, + } + if len(opts.URL) == 0 { + return nil, fmt.Errorf("WOODPECKER_GITEA_URL must be set") + } + log.Trace().Msgf("Forge (gitea) opts: %#v", opts) + return gitea.New(opts) +} + +func setupGitLab(forge *model.Forge) (forge.Forge, error) { + return gitlab.New(gitlab.Opts{ + URL: forge.URL, + ClientID: forge.Client, + ClientSecret: forge.ClientSecret, + SkipVerify: forge.SkipVerify, + }) +} + +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") + } + + opts := github.Opts{ + URL: forge.URL, + Client: forge.Client, + Secret: forge.ClientSecret, + SkipVerify: forge.SkipVerify, + MergeRef: mergeRef, + OnlyPublic: publicOnly, + } + log.Trace().Msgf("Forge (github) opts: %#v", opts) + return github.New(opts) +} + +func setupBitbucketDatacenter(forge *model.Forge) (forge.Forge, error) { + gitUsername, ok := forge.AdditionalOptions["git-username"].(string) + if !ok { + return nil, fmt.Errorf("missing git-username") + } + gitPassword, ok := forge.AdditionalOptions["git-password"].(string) + if !ok { + return nil, fmt.Errorf("missing git-password") + } + + opts := bitbucketdatacenter.Opts{ + URL: forge.URL, + ClientID: forge.Client, + ClientSecret: forge.ClientSecret, + Username: gitUsername, + Password: gitPassword, + } + log.Trace().Msgf("Forge (bitbucketdatacenter) opts: %#v", opts) + return bitbucketdatacenter.New(opts) +} + +func setupAddon(forge *model.Forge) (forge.Forge, error) { + executable, ok := forge.AdditionalOptions["executable"].(string) + if !ok { + return nil, fmt.Errorf("missing git-username") + } + + log.Trace().Msgf("Forge (addon) executable: %#v", executable) + return addon.Load(executable) +} diff --git a/server/grpc/rpc.go b/server/grpc/rpc.go index b84a150d8..7a6ccd0c4 100644 --- a/server/grpc/rpc.go +++ b/server/grpc/rpc.go @@ -31,6 +31,7 @@ import ( grpcMetadata "google.golang.org/grpc/metadata" "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" + "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/model" @@ -41,7 +42,6 @@ import ( ) type RPC struct { - forge forge.Forge queue queue.Queue pubsub *pubsub.Publisher logger logging.Log @@ -525,11 +525,17 @@ func (s *RPC) updateForgeStatus(ctx context.Context, repo *model.Repo, pipeline return } - forge.Refresh(ctx, s.forge, s.store, user) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msgf("can not get forge for repo '%s'", repo.FullName) + return + } + + forge.Refresh(ctx, _forge, s.store, user) // only do status updates for parent steps if workflow != nil { - err = s.forge.Status(ctx, user, repo, pipeline, workflow) + err = _forge.Status(ctx, user, repo, pipeline, workflow) if err != nil { log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number) } diff --git a/server/grpc/server.go b/server/grpc/server.go index 83a983351..81b4dc8b1 100644 --- a/server/grpc/server.go +++ b/server/grpc/server.go @@ -23,7 +23,6 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc" "go.woodpecker-ci.org/woodpecker/v2/pipeline/rpc/proto" - "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/logging" "go.woodpecker-ci.org/woodpecker/v2/server/pubsub" "go.woodpecker-ci.org/woodpecker/v2/server/queue" @@ -37,7 +36,7 @@ type WoodpeckerServer struct { peer RPC } -func NewWoodpeckerServer(forge forge.Forge, queue queue.Queue, logger logging.Log, pubsub *pubsub.Publisher, store store.Store) proto.WoodpeckerServer { +func NewWoodpeckerServer(queue queue.Queue, logger logging.Log, pubsub *pubsub.Publisher, store store.Store) proto.WoodpeckerServer { pipelineTime := promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "woodpecker", Name: "pipeline_time", @@ -49,7 +48,6 @@ func NewWoodpeckerServer(forge forge.Forge, queue queue.Queue, logger logging.Lo Help: "Pipeline count.", }, []string{"repo", "branch", "status", "pipeline"}) peer := RPC{ - forge: forge, store: store, queue: queue, pubsub: pubsub, diff --git a/server/model/forge.go b/server/model/forge.go new file mode 100644 index 000000000..8190fc34d --- /dev/null +++ b/server/model/forge.go @@ -0,0 +1,36 @@ +// 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. + +package model + +type ForgeType string + +const ( + ForgeTypeGithub ForgeType = "github" + ForgeTypeGitlab ForgeType = "gitlab" + ForgeTypeGitea ForgeType = "gitea" + ForgeTypeBitbucket ForgeType = "bitbucket" + ForgeTypeBitbucketDatacenter ForgeType = "bitbucket-dc" + ForgeTypeAddon ForgeType = "addon" +) + +type Forge struct { + ID int64 `xorm:"pk autoincr 'id'"` + Type ForgeType `xorm:"VARCHAR(250)"` + URL string `xorm:"VARCHAR(500) 'url'"` + Client string `xorm:"VARCHAR(250)"` + ClientSecret string `xorm:"VARCHAR(250)"` + SkipVerify bool `xorm:"bool"` + AdditionalOptions map[string]any `xorm:"json"` +} diff --git a/server/model/org.go b/server/model/org.go index db093bb64..0cf2ae3d0 100644 --- a/server/model/org.go +++ b/server/model/org.go @@ -16,11 +16,12 @@ package model // Org represents an organization. type Org struct { - ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"` - Name string `json:"name" xorm:"UNIQUE 'name'"` - IsUser bool `json:"is_user" xorm:"is_user"` + ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` + Name string `json:"name" xorm:"UNIQUE 'name'"` + IsUser bool `json:"is_user" xorm:"is_user"` // if name lookup has to check for membership or not - Private bool `json:"-" xorm:"private"` + Private bool `json:"-" xorm:"private"` } // @name Org // TableName return database table name for xorm diff --git a/server/model/pipeline.go b/server/model/pipeline.go index 1a0a7226d..72fea8192 100644 --- a/server/model/pipeline.go +++ b/server/model/pipeline.go @@ -16,41 +16,41 @@ package model import ( - "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" + "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" ) type Pipeline struct { - ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` - RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'pipeline_repo_id'"` - Number int64 `json:"number" xorm:"UNIQUE(s) 'pipeline_number'"` - Author string `json:"author" xorm:"INDEX 'pipeline_author'"` - Parent int64 `json:"parent" xorm:"pipeline_parent"` - Event WebhookEvent `json:"event" xorm:"pipeline_event"` - Status StatusValue `json:"status" xorm:"INDEX 'pipeline_status'"` - Errors []*errors.PipelineError `json:"errors" xorm:"json 'pipeline_errors'"` - Created int64 `json:"created_at" xorm:"pipeline_created"` - Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"` - Started int64 `json:"started_at" xorm:"pipeline_started"` - Finished int64 `json:"finished_at" xorm:"pipeline_finished"` - Deploy string `json:"deploy_to" xorm:"pipeline_deploy"` - Commit string `json:"commit" xorm:"pipeline_commit"` - Branch string `json:"branch" xorm:"pipeline_branch"` - Ref string `json:"ref" xorm:"pipeline_ref"` - Refspec string `json:"refspec" xorm:"pipeline_refspec"` - Title string `json:"title" xorm:"pipeline_title"` - Message string `json:"message" xorm:"TEXT 'pipeline_message'"` - Timestamp int64 `json:"timestamp" xorm:"pipeline_timestamp"` - Sender string `json:"sender" xorm:"pipeline_sender"` // uses reported user for webhooks and name of cron for cron pipelines - Avatar string `json:"author_avatar" xorm:"pipeline_avatar"` - Email string `json:"author_email" xorm:"pipeline_email"` - ForgeURL string `json:"forge_url" xorm:"pipeline_forge_url"` - Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"` - Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"` - Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` - ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` - AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` - PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` - IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` + ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` + RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'pipeline_repo_id'"` + Number int64 `json:"number" xorm:"UNIQUE(s) 'pipeline_number'"` + Author string `json:"author" xorm:"INDEX 'pipeline_author'"` + Parent int64 `json:"parent" xorm:"pipeline_parent"` + Event WebhookEvent `json:"event" xorm:"pipeline_event"` + Status StatusValue `json:"status" xorm:"INDEX 'pipeline_status'"` + Errors []*types.PipelineError `json:"errors" xorm:"json 'pipeline_errors'"` + Created int64 `json:"created_at" xorm:"pipeline_created"` + Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"` + Started int64 `json:"started_at" xorm:"pipeline_started"` + Finished int64 `json:"finished_at" xorm:"pipeline_finished"` + Deploy string `json:"deploy_to" xorm:"pipeline_deploy"` + Commit string `json:"commit" xorm:"pipeline_commit"` + Branch string `json:"branch" xorm:"pipeline_branch"` + Ref string `json:"ref" xorm:"pipeline_ref"` + Refspec string `json:"refspec" xorm:"pipeline_refspec"` + Title string `json:"title" xorm:"pipeline_title"` + Message string `json:"message" xorm:"TEXT 'pipeline_message'"` + Timestamp int64 `json:"timestamp" xorm:"pipeline_timestamp"` + Sender string `json:"sender" xorm:"pipeline_sender"` // uses reported user for webhooks and name of cron for cron pipelines + Avatar string `json:"author_avatar" xorm:"pipeline_avatar"` + Email string `json:"author_email" xorm:"pipeline_email"` + ForgeURL string `json:"forge_url" xorm:"pipeline_forge_url"` + Reviewer string `json:"reviewed_by" xorm:"pipeline_reviewer"` + Reviewed int64 `json:"reviewed_at" xorm:"pipeline_reviewed"` + Workflows []*Workflow `json:"workflows,omitempty" xorm:"-"` + ChangedFiles []string `json:"changed_files,omitempty" xorm:"LONGTEXT 'changed_files'"` + AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` + PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` + IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` } // @name Pipeline // TableName return database table name for xorm diff --git a/server/model/repo.go b/server/model/repo.go index 77ea96b4f..b9f26c5f9 100644 --- a/server/model/repo.go +++ b/server/model/repo.go @@ -22,8 +22,9 @@ import ( // Repo represents a repository. type Repo struct { - ID int64 `json:"id,omitempty" xorm:"pk autoincr 'repo_id'"` - UserID int64 `json:"-" xorm:"INDEX repo_user_id"` + ID int64 `json:"id,omitempty" xorm:"pk autoincr 'repo_id'"` + UserID int64 `json:"-" xorm:"INDEX repo_user_id"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` // ForgeRemoteID is the unique identifier for the repository on the forge. ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id"` OrgID int64 `json:"org_id" xorm:"INDEX repo_org_id"` @@ -44,6 +45,7 @@ type Repo struct { IsGated bool `json:"gated" xorm:"repo_gated"` IsActive bool `json:"active" xorm:"repo_active"` AllowPull bool `json:"allow_pr" xorm:"repo_allow_pr"` + AllowDeploy bool `json:"allow_deploy" xorm:"repo_allow_deploy"` Config string `json:"config_file" xorm:"varchar(500) 'repo_config_path'"` Hash string `json:"-" xorm:"varchar(500) 'repo_hash'"` Perm *Perm `json:"-" xorm:"-"` @@ -112,6 +114,7 @@ type RepoPatch struct { Timeout *int64 `json:"timeout,omitempty"` Visibility *string `json:"visibility,omitempty"` AllowPull *bool `json:"allow_pr,omitempty"` + AllowDeploy *bool `json:"allow_deploy,omitempty"` CancelPreviousPipelineEvents *[]WebhookEvent `json:"cancel_previous_pipeline_events"` NetrcOnlyTrusted *bool `json:"netrc_only_trusted"` } // @name RepoPatch diff --git a/server/model/user.go b/server/model/user.go index 152df68ac..578d6caf2 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -34,6 +34,8 @@ type User struct { // required: true ID int64 `json:"id" xorm:"pk autoincr 'user_id'"` + ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"` + ForgeRemoteID ForgeRemoteID `json:"-" xorm:"forge_remote_id"` // Login is the username for this user. diff --git a/server/pipeline/approve.go b/server/pipeline/approve.go index 31db7fdff..22d8a8fa4 100644 --- a/server/pipeline/approve.go +++ b/server/pipeline/approve.go @@ -20,6 +20,7 @@ import ( "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server" forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/store" @@ -32,6 +33,13 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe return nil, ErrBadRequest{Msg: fmt.Sprintf("cannot approve a pipeline with status %s", currentPipeline.Status)} } + forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) + } + // fetch the pipeline file from the database configs, err := store.ConfigsForPipeline(currentPipeline.ID) if err != nil { @@ -72,7 +80,7 @@ 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, forge, store, currentPipeline, user, repo, yamls, nil) if err != nil { msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName) log.Error().Err(err).Msg(msg) @@ -86,9 +94,9 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe return nil, err } - publishPipeline(ctx, currentPipeline, repo, user) + publishPipeline(ctx, forge, currentPipeline, repo, user) - currentPipeline, err = start(ctx, store, currentPipeline, user, repo, pipelineItems) + currentPipeline, err = start(ctx, forge, store, currentPipeline, user, repo, pipelineItems) if err != nil { msg := fmt.Sprintf("failure to start pipeline for %s: %v", repo.FullName, err) log.Error().Err(err).Msg(msg) diff --git a/server/pipeline/cancel.go b/server/pipeline/cancel.go index 46407436e..253e1d586 100644 --- a/server/pipeline/cancel.go +++ b/server/pipeline/cancel.go @@ -21,13 +21,14 @@ import ( "github.com/rs/zerolog/log" "go.woodpecker-ci.org/woodpecker/v2/server" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/queue" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // Cancel the pipeline and returns the status. -func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *model.User, pipeline *model.Pipeline) error { +func Cancel(ctx context.Context, _forge forge.Forge, store store.Store, repo *model.Repo, user *model.User, pipeline *model.Pipeline) error { if pipeline.Status != model.StatusRunning && pipeline.Status != model.StatusPending && pipeline.Status != model.StatusBlocked { return &ErrBadRequest{Msg: "Cannot cancel a non-running or non-pending or non-blocked pipeline"} } @@ -88,7 +89,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode return err } - updatePipelineStatus(ctx, killedPipeline, repo, user) + updatePipelineStatus(ctx, _forge, killedPipeline, repo, user) if killedPipeline.Workflows, err = store.WorkflowGetTree(killedPipeline); err != nil { return err @@ -100,6 +101,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode func cancelPreviousPipelines( ctx context.Context, + _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, @@ -150,7 +152,7 @@ func cancelPreviousPipelines( continue } - if err = Cancel(ctx, _store, repo, user, active); err != nil { + if err = Cancel(ctx, _forge, _store, repo, user, active); err != nil { log.Error(). Err(err). Str("ref", active.Ref). diff --git a/server/pipeline/create.go b/server/pipeline/create.go index c81881c0a..d2435959c 100644 --- a/server/pipeline/create.go +++ b/server/pipeline/create.go @@ -34,7 +34,6 @@ var skipPipelineRegex = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`) // Create a new pipeline and start it func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline *model.Pipeline) (*model.Pipeline, error) { - _forge := server.Config.Services.Forge repoUser, err := _store.GetUser(repo.UserID) if err != nil { msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID) @@ -42,14 +41,23 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return nil, fmt.Errorf(msg) } - skipMatch := skipPipelineRegex.FindString(pipeline.Message) - if len(skipMatch) > 0 { - ref := pipeline.Commit - if len(ref) == 0 { - ref = pipeline.Ref + if pipeline.Event == model.EventPush || pipeline.Event == model.EventPull || pipeline.Event == model.EventPullClosed { + skipMatch := skipPipelineRegex.FindString(pipeline.Message) + if len(skipMatch) > 0 { + ref := pipeline.Commit + if len(ref) == 0 { + ref = pipeline.Ref + } + log.Debug().Str("repo", repo.FullName).Msgf("ignoring pipeline as skip-ci was found in the commit (%s) message '%s'", ref, pipeline.Message) + return nil, ErrFiltered } - log.Debug().Str("repo", repo.FullName).Msgf("ignoring pipeline as skip-ci was found in the commit (%s) message '%s'", ref, pipeline.Message) - return nil, ErrFiltered + } + + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) } // If the forge has a refresh token, the current access token @@ -80,13 +88,13 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return nil, ErrFiltered } else if configFetchErr != nil { log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("error while fetching config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login) - return nil, updatePipelineWithErr(ctx, _store, pipeline, repo, repoUser, fmt.Errorf("pipeline definition not found in %s", repo.FullName)) + return nil, updatePipelineWithErr(ctx, _forge, _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(_forge, _store, pipeline, repoUser, repo, forgeYamlConfigs, nil) if pipeline_errors.HasBlockingErrors(parseErr) { log.Debug().Str("repo", repo.FullName).Err(parseErr).Msg("failed to parse yaml") - return nil, updatePipelineWithErr(ctx, _store, pipeline, repo, repoUser, parseErr) + return nil, updatePipelineWithErr(ctx, _forge, _store, pipeline, repo, repoUser, parseErr) } else if parseErr != nil { pipeline.Errors = pipeline_errors.GetPipelineErrors(parseErr) } @@ -120,7 +128,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return nil, fmt.Errorf(msg) } - if err := prepareStart(ctx, _store, pipeline, repoUser, repo); err != nil { + if err := prepareStart(ctx, _forge, _store, pipeline, repoUser, repo); err != nil { log.Error().Err(err).Str("repo", repo.FullName).Msgf("error preparing pipeline for %s#%d", repo.FullName, pipeline.Number) return nil, err } @@ -129,11 +137,11 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return pipeline, nil } - if err := updatePipelinePending(ctx, _store, pipeline, repo, repoUser); err != nil { + if err := updatePipelinePending(ctx, _forge, _store, pipeline, repo, repoUser); err != nil { return nil, err } - pipeline, err = start(ctx, _store, pipeline, repoUser, repo, pipelineItems) + pipeline, err = start(ctx, _forge, _store, pipeline, repoUser, repo, pipelineItems) if err != nil { msg := fmt.Sprintf("failed to start pipeline for %s", repo.FullName) log.Error().Err(err).Msg(msg) @@ -143,7 +151,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline return pipeline, nil } -func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error { +func updatePipelineWithErr(ctx context.Context, _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error { _pipeline, err := UpdateToStatusError(_store, *pipeline, err) if err != nil { return err @@ -151,12 +159,12 @@ func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *mo // update value in ref *pipeline = *_pipeline - publishPipeline(ctx, pipeline, repo, repoUser) + publishPipeline(ctx, _forge, pipeline, repo, repoUser) return nil } -func updatePipelinePending(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) error { +func updatePipelinePending(ctx context.Context, _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) error { _pipeline, err := UpdateToStatusPending(_store, *pipeline, "") if err != nil { return err @@ -164,7 +172,7 @@ func updatePipelinePending(ctx context.Context, _store store.Store, pipeline *mo // update value in ref *pipeline = *_pipeline - publishPipeline(ctx, pipeline, repo, repoUser) + publishPipeline(ctx, _forge, pipeline, repo, repoUser) return nil } diff --git a/server/pipeline/decline.go b/server/pipeline/decline.go index 9587bf4ba..cf8dd0e9a 100644 --- a/server/pipeline/decline.go +++ b/server/pipeline/decline.go @@ -20,17 +20,25 @@ import ( "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // Decline updates the status to declined for blocked pipelines because of a gated repo func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, user *model.User, repo *model.Repo) (*model.Pipeline, error) { + forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) + } + if pipeline.Status != model.StatusBlocked { return nil, fmt.Errorf("cannot decline a pipeline with status %s", pipeline.Status) } - pipeline, err := UpdateToStatusDeclined(store, *pipeline, user.Login) + pipeline, err = UpdateToStatusDeclined(store, *pipeline, user.Login) if err != nil { return nil, fmt.Errorf("error updating pipeline. %w", err) } @@ -53,7 +61,7 @@ func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, u } } - updatePipelineStatus(ctx, pipeline, repo, user) + updatePipelineStatus(ctx, forge, pipeline, repo, user) publishToTopic(pipeline, repo) diff --git a/server/pipeline/helper.go b/server/pipeline/helper.go index 497da291d..e8e3400a5 100644 --- a/server/pipeline/helper.go +++ b/server/pipeline/helper.go @@ -19,13 +19,13 @@ import ( "github.com/rs/zerolog/log" - "go.woodpecker-ci.org/woodpecker/v2/server" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" ) -func updatePipelineStatus(ctx context.Context, pipeline *model.Pipeline, repo *model.Repo, user *model.User) { +func updatePipelineStatus(ctx context.Context, forge forge.Forge, pipeline *model.Pipeline, repo *model.Repo, user *model.User) { for _, workflow := range pipeline.Workflows { - err := server.Config.Services.Forge.Status(ctx, user, repo, pipeline, workflow) + err := forge.Status(ctx, user, repo, pipeline, workflow) if err != nil { log.Error().Err(err).Msgf("error setting commit status for %s/%d", repo.FullName, pipeline.Number) return diff --git a/server/pipeline/items.go b/server/pipeline/items.go index b44a06d47..737f2fc43 100644 --- a/server/pipeline/items.go +++ b/server/pipeline/items.go @@ -24,14 +24,15 @@ import ( pipeline_errors "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler" "go.woodpecker-ci.org/woodpecker/v2/server" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pipeline/stepbuilder" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) -func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo, yamls []*forge_types.FileMeta, envs map[string]string) ([]*stepbuilder.Item, error) { - netrc, err := server.Config.Services.Forge.Netrc(user, repo) +func parsePipeline(forge forge.Forge, store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo, yamls []*forge_types.FileMeta, envs map[string]string) ([]*stepbuilder.Item, error) { + netrc, err := forge.Netrc(user, repo) if err != nil { log.Error().Err(err).Msg("failed to generate netrc file") } @@ -80,7 +81,7 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod Envs: envs, Host: server.Config.Server.Host, Yamls: yamls, - Forge: server.Config.Services.Forge, + Forge: forge, ProxyOpts: compiler.ProxyOptions{ NoProxy: server.Config.Pipeline.Proxy.No, HTTPProxy: server.Config.Pipeline.Proxy.HTTP, @@ -90,23 +91,23 @@ func parsePipeline(store store.Store, currentPipeline *model.Pipeline, user *mod return b.Build() } -func createPipelineItems(c context.Context, store store.Store, +func createPipelineItems(c context.Context, forge forge.Forge, store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo, yamls []*forge_types.FileMeta, envs map[string]string, ) (*model.Pipeline, []*stepbuilder.Item, error) { - pipelineItems, err := parsePipeline(store, currentPipeline, user, repo, yamls, envs) + pipelineItems, err := parsePipeline(forge, store, currentPipeline, user, repo, yamls, envs) if pipeline_errors.HasBlockingErrors(err) { currentPipeline, uerr := UpdateToStatusError(store, *currentPipeline, err) if uerr != nil { log.Error().Err(uerr).Msgf("error setting error status of pipeline for %s#%d", repo.FullName, currentPipeline.Number) } else { - updatePipelineStatus(c, currentPipeline, repo, user) + updatePipelineStatus(c, forge, currentPipeline, repo, user) } return currentPipeline, nil, err } else if err != nil { currentPipeline.Errors = pipeline_errors.GetPipelineErrors(err) - err = updatePipelinePending(c, store, currentPipeline, repo, user) + err = updatePipelinePending(c, forge, store, currentPipeline, repo, user) } currentPipeline = setPipelineStepsOnPipeline(currentPipeline, pipelineItems) diff --git a/server/pipeline/restart.go b/server/pipeline/restart.go index 11e3c68fc..fd0121974 100644 --- a/server/pipeline/restart.go +++ b/server/pipeline/restart.go @@ -29,7 +29,13 @@ import ( // Restart a pipeline by creating a new one out of the old and start it func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipeline, user *model.User, repo *model.Repo, envs map[string]string) (*model.Pipeline, error) { - forge := server.Config.Services.Forge + forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName) + log.Error().Err(err).Str("repo", repo.FullName).Msg(msg) + return nil, fmt.Errorf(msg) + } + switch lastPipeline.Status { case model.StatusDeclined, model.StatusBlocked: @@ -72,7 +78,7 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin if uerr != nil { log.Debug().Err(uerr).Msg("failure to update pipeline status") } else { - updatePipelineStatus(ctx, newPipeline, repo, user) + updatePipelineStatus(ctx, forge, newPipeline, repo, user) } return newPipeline, nil } @@ -82,20 +88,20 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin return nil, fmt.Errorf(msg) } - newPipeline, pipelineItems, err := createPipelineItems(ctx, store, newPipeline, user, repo, pipelineFiles, envs) + newPipeline, pipelineItems, err := createPipelineItems(ctx, forge, store, newPipeline, user, repo, pipelineFiles, envs) if err != nil { msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName) log.Error().Err(err).Msg(msg) return nil, fmt.Errorf(msg) } - if err := prepareStart(ctx, store, newPipeline, user, repo); err != nil { + if err := prepareStart(ctx, forge, store, newPipeline, user, repo); err != nil { msg := fmt.Sprintf("failure to prepare pipeline for %s", repo.FullName) log.Error().Err(err).Msg(msg) return nil, fmt.Errorf(msg) } - newPipeline, err = start(ctx, store, newPipeline, user, repo, pipelineItems) + newPipeline, err = start(ctx, forge, store, newPipeline, user, repo, pipelineItems) if err != nil { msg := fmt.Sprintf("failure to start pipeline for %s", repo.FullName) log.Error().Err(err).Msg(msg) diff --git a/server/pipeline/start.go b/server/pipeline/start.go index afad25723..f136fea29 100644 --- a/server/pipeline/start.go +++ b/server/pipeline/start.go @@ -19,20 +19,21 @@ import ( "github.com/rs/zerolog/log" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/pipeline/stepbuilder" "go.woodpecker-ci.org/woodpecker/v2/server/store" ) // start a pipeline, make sure it was stored persistent in the store before -func start(ctx context.Context, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo, pipelineItems []*stepbuilder.Item) (*model.Pipeline, error) { +func start(ctx context.Context, forge forge.Forge, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo, pipelineItems []*stepbuilder.Item) (*model.Pipeline, error) { // call to cancel previous pipelines if needed - if err := cancelPreviousPipelines(ctx, store, activePipeline, repo, user); err != nil { + if err := cancelPreviousPipelines(ctx, forge, store, activePipeline, repo, user); err != nil { // should be not breaking log.Error().Err(err).Msg("failed to cancel previous pipelines") } - publishPipeline(ctx, activePipeline, repo, user) + publishPipeline(ctx, forge, activePipeline, repo, user) if err := queuePipeline(ctx, repo, pipelineItems); err != nil { log.Error().Err(err).Msg("queuePipeline") @@ -42,17 +43,17 @@ func start(ctx context.Context, store store.Store, activePipeline *model.Pipelin return activePipeline, nil } -func prepareStart(ctx context.Context, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo) error { +func prepareStart(ctx context.Context, forge forge.Forge, store store.Store, activePipeline *model.Pipeline, user *model.User, repo *model.Repo) error { if err := store.WorkflowsCreate(activePipeline.Workflows); err != nil { log.Error().Err(err).Str("repo", repo.FullName).Msgf("error persisting steps for %s#%d", repo.FullName, activePipeline.Number) return err } - publishPipeline(ctx, activePipeline, repo, user) + publishPipeline(ctx, forge, activePipeline, repo, user) return nil } -func publishPipeline(ctx context.Context, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) { +func publishPipeline(ctx context.Context, forge forge.Forge, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) { publishToTopic(pipeline, repo) - updatePipelineStatus(ctx, pipeline, repo, repoUser) + updatePipelineStatus(ctx, forge, pipeline, repo, repoUser) } diff --git a/server/pipeline/stepbuilder/stepBuilder.go b/server/pipeline/stepbuilder/stepBuilder.go index a7075cccf..0bff2050c 100644 --- a/server/pipeline/stepbuilder/stepBuilder.go +++ b/server/pipeline/stepbuilder/stepBuilder.go @@ -26,6 +26,7 @@ import ( backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" pipeline_errors "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" + errorTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler" @@ -135,7 +136,7 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A // parse yaml pipeline parsed, err := yaml.ParseString(substituted) if err != nil { - return nil, &pipeline_errors.PipelineError{Message: err.Error(), Type: pipeline_errors.PipelineErrorTypeCompiler} + return nil, &errorTypes.PipelineError{Message: err.Error(), Type: errorTypes.PipelineErrorTypeCompiler} } // lint pipeline diff --git a/server/router/api.go b/server/router/api.go index 95eff6327..29926987a 100644 --- a/server/router/api.go +++ b/server/router/api.go @@ -104,6 +104,7 @@ func apiRoutes(e *gin.RouterGroup) { repo.POST("/pipelines/:number/decline", session.MustPush, api.PostDecline) repo.GET("/logs/:number/:stepId", api.GetStepLogs) + repo.DELETE("/logs/:number/:stepId", session.MustPush, api.DeleteStepLogs) // requires push permissions repo.DELETE("/logs/:number", session.MustPush, api.DeletePipelineLogs) diff --git a/server/router/middleware/session/repo.go b/server/router/middleware/session/repo.go index b89f163f8..93cfb2c7c 100644 --- a/server/router/middleware/session/repo.go +++ b/server/router/middleware/session/repo.go @@ -106,6 +106,13 @@ func SetPerm() gin.HandlerFunc { _store := store.FromContext(c) user := User(c) repo := Repo(c) + _forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from repo") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + perm := new(model.Perm) if user != nil { @@ -116,7 +123,7 @@ func SetPerm() gin.HandlerFunc { user.Login, repo.FullName) } if time.Unix(perm.Synced, 0).Add(time.Hour).Before(time.Now()) { - _repo, err := server.Config.Services.Forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) + _repo, err := _forge.Repo(c, user, repo.ForgeRemoteID, repo.Owner, repo.Name) if err == nil { log.Debug().Msgf("synced user permission for %s %s", user.Login, repo.FullName) perm = _repo.Perm diff --git a/server/router/middleware/session/user.go b/server/router/middleware/session/user.go index b0c1b7176..d1b1507ef 100644 --- a/server/router/middleware/session/user.go +++ b/server/router/middleware/session/user.go @@ -145,7 +145,14 @@ func MustOrgMember(admin bool) gin.HandlerFunc { return } - perm, err := server.Config.Services.Membership.Get(c, user, org.Name) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + log.Error().Err(err).Msg("Cannot get forge from user") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + perm, err := server.Config.Services.Membership.Get(c, _forge, user, org.Name) if err != nil { log.Error().Err(err).Msg("failed to check membership") c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) diff --git a/server/router/middleware/token/token.go b/server/router/middleware/token/token.go index 58415954d..8d45adda6 100644 --- a/server/router/middleware/token/token.go +++ b/server/router/middleware/token/token.go @@ -15,6 +15,8 @@ package token import ( + "net/http" + "github.com/gin-gonic/gin" "go.woodpecker-ci.org/woodpecker/v2/server" @@ -26,7 +28,13 @@ import ( func Refresh(c *gin.Context) { user := session.User(c) if user != nil { - forge.Refresh(c, server.Config.Services.Forge, store.FromContext(c), user) + _forge, err := server.Config.Services.Manager.ForgeFromUser(user) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + + forge.Refresh(c, _forge, store.FromContext(c), user) } c.Next() diff --git a/server/services/environment/extension.go b/server/services/environment/service.go similarity index 100% rename from server/services/environment/extension.go rename to server/services/environment/service.go diff --git a/server/services/manager.go b/server/services/manager.go index d5669df2b..ff281c40a 100644 --- a/server/services/manager.go +++ b/server/services/manager.go @@ -16,9 +16,12 @@ package services import ( "crypto" + "time" + "github.com/jellydator/ttlcache/v3" "github.com/urfave/cli/v2" + "go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/config" "go.woodpecker-ci.org/woodpecker/v2/server/services/environment" @@ -27,56 +30,119 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server/store" ) -type Manager struct { +//go:generate mockery --name Manager --output mocks --case underscore + +const forgeCacheTTL = 10 * time.Minute + +type SetupForge func(forge *model.Forge) (forge.Forge, error) + +type Manager interface { + SignaturePublicKey() crypto.PublicKey + SecretServiceFromRepo(repo *model.Repo) secret.Service + SecretService() secret.Service + RegistryServiceFromRepo(repo *model.Repo) registry.Service + RegistryService() registry.Service + ConfigServiceFromRepo(repo *model.Repo) config.Service + EnvironmentService() environment.Service + ForgeFromRepo(repo *model.Repo) (forge.Forge, error) + ForgeFromUser(user *model.User) (forge.Forge, error) + ForgeMain() (forge.Forge, error) +} + +type manager struct { + signaturePrivateKey crypto.PrivateKey + signaturePublicKey crypto.PublicKey + store store.Store secret secret.Service registry registry.Service config config.Service environment environment.Service - signaturePrivateKey crypto.PrivateKey - signaturePublicKey crypto.PublicKey + forgeCache *ttlcache.Cache[int64, forge.Forge] + setupForge SetupForge } -func NewManager(c *cli.Context, store store.Store) (*Manager, error) { +func NewManager(c *cli.Context, store store.Store, setupForge SetupForge) (Manager, error) { signaturePrivateKey, signaturePublicKey, err := setupSignatureKeys(store) if err != nil { return nil, err } - return &Manager{ + err = setupForgeService(c, store) + if err != nil { + return nil, err + } + + return &manager{ signaturePrivateKey: signaturePrivateKey, signaturePublicKey: signaturePublicKey, + store: store, secret: setupSecretService(store), registry: setupRegistryService(store, c.String("docker-config")), config: setupConfigService(c, signaturePrivateKey), environment: environment.Parse(c.StringSlice("environment")), + forgeCache: ttlcache.New(ttlcache.WithDisableTouchOnHit[int64, forge.Forge]()), + setupForge: setupForge, }, nil } -func (e *Manager) SignaturePublicKey() crypto.PublicKey { - return e.signaturePublicKey +func (m *manager) SignaturePublicKey() crypto.PublicKey { + return m.signaturePublicKey } -func (e *Manager) SecretServiceFromRepo(_ *model.Repo) secret.Service { - return e.SecretService() +func (m *manager) SecretServiceFromRepo(_ *model.Repo) secret.Service { + return m.SecretService() } -func (e *Manager) SecretService() secret.Service { - return e.secret +func (m *manager) SecretService() secret.Service { + return m.secret } -func (e *Manager) RegistryServiceFromRepo(_ *model.Repo) registry.Service { - return e.RegistryService() +func (m *manager) RegistryServiceFromRepo(_ *model.Repo) registry.Service { + return m.RegistryService() } -func (e *Manager) RegistryService() registry.Service { - return e.registry +func (m *manager) RegistryService() registry.Service { + return m.registry } -func (e *Manager) ConfigServiceFromRepo(_ *model.Repo) config.Service { +func (m *manager) ConfigServiceFromRepo(_ *model.Repo) config.Service { // TODO: decied based on repo property which config service to use - return e.config + return m.config } -func (e *Manager) EnvironmentService() environment.Service { - return e.environment +func (m *manager) EnvironmentService() environment.Service { + return m.environment +} + +func (m *manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { + return m.getForgeByID(repo.ForgeID) +} + +func (m *manager) ForgeFromUser(user *model.User) (forge.Forge, error) { + return m.getForgeByID(user.ForgeID) +} + +func (m *manager) ForgeMain() (forge.Forge, error) { + return m.getForgeByID(1) // main forge is always 1 and is configured via environment variables +} + +func (m *manager) getForgeByID(id int64) (forge.Forge, error) { + item := m.forgeCache.Get(id) + if item != nil && !item.IsExpired() { + return item.Value(), nil + } + + forgeModel, err := m.store.ForgeGet(id) + if err != nil { + return nil, err + } + + forge, err := m.setupForge(forgeModel) + if err != nil { + return nil, err + } + + m.forgeCache.Set(id, forge, forgeCacheTTL) + + return forge, nil } diff --git a/server/services/mocks/manager.go b/server/services/mocks/manager.go new file mode 100644 index 000000000..3c67c2c44 --- /dev/null +++ b/server/services/mocks/manager.go @@ -0,0 +1,270 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + crypto "crypto" + + config "go.woodpecker-ci.org/woodpecker/v2/server/services/config" + + environment "go.woodpecker-ci.org/woodpecker/v2/server/services/environment" + + forge "go.woodpecker-ci.org/woodpecker/v2/server/forge" + + mock "github.com/stretchr/testify/mock" + + model "go.woodpecker-ci.org/woodpecker/v2/server/model" + + registry "go.woodpecker-ci.org/woodpecker/v2/server/services/registry" + + secret "go.woodpecker-ci.org/woodpecker/v2/server/services/secret" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// ConfigServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) ConfigServiceFromRepo(repo *model.Repo) config.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ConfigServiceFromRepo") + } + + var r0 config.Service + if rf, ok := ret.Get(0).(func(*model.Repo) config.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.Service) + } + } + + return r0 +} + +// EnvironmentService provides a mock function with given fields: +func (_m *Manager) EnvironmentService() environment.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EnvironmentService") + } + + var r0 environment.Service + if rf, ok := ret.Get(0).(func() environment.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(environment.Service) + } + } + + return r0 +} + +// ForgeFromRepo provides a mock function with given fields: repo +func (_m *Manager) ForgeFromRepo(repo *model.Repo) (forge.Forge, error) { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for ForgeFromRepo") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo) (forge.Forge, error)); ok { + return rf(repo) + } + if rf, ok := ret.Get(0).(func(*model.Repo) forge.Forge); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo) error); ok { + r1 = rf(repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeFromUser provides a mock function with given fields: user +func (_m *Manager) ForgeFromUser(user *model.User) (forge.Forge, error) { + ret := _m.Called(user) + + if len(ret) == 0 { + panic("no return value specified for ForgeFromUser") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.User) (forge.Forge, error)); ok { + return rf(user) + } + if rf, ok := ret.Get(0).(func(*model.User) forge.Forge); ok { + r0 = rf(user) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeMain provides a mock function with given fields: +func (_m *Manager) ForgeMain() (forge.Forge, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ForgeMain") + } + + var r0 forge.Forge + var r1 error + if rf, ok := ret.Get(0).(func() (forge.Forge, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() forge.Forge); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(forge.Forge) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegistryService provides a mock function with given fields: +func (_m *Manager) RegistryService() registry.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RegistryService") + } + + var r0 registry.Service + if rf, ok := ret.Get(0).(func() registry.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Service) + } + } + + return r0 +} + +// RegistryServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) RegistryServiceFromRepo(repo *model.Repo) registry.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for RegistryServiceFromRepo") + } + + var r0 registry.Service + if rf, ok := ret.Get(0).(func(*model.Repo) registry.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Service) + } + } + + return r0 +} + +// SecretService provides a mock function with given fields: +func (_m *Manager) SecretService() secret.Service { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SecretService") + } + + var r0 secret.Service + if rf, ok := ret.Get(0).(func() secret.Service); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(secret.Service) + } + } + + return r0 +} + +// SecretServiceFromRepo provides a mock function with given fields: repo +func (_m *Manager) SecretServiceFromRepo(repo *model.Repo) secret.Service { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for SecretServiceFromRepo") + } + + var r0 secret.Service + if rf, ok := ret.Get(0).(func(*model.Repo) secret.Service); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(secret.Service) + } + } + + return r0 +} + +// SignaturePublicKey provides a mock function with given fields: +func (_m *Manager) SignaturePublicKey() crypto.PublicKey { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SignaturePublicKey") + } + + var r0 crypto.PublicKey + if rf, ok := ret.Get(0).(func() crypto.PublicKey); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(crypto.PublicKey) + } + } + + return r0 +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewManager(t interface { + mock.TestingT + Cleanup(func()) +}) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/services/setup.go b/server/services/setup.go index 1b25f82a3..c92ed9e3a 100644 --- a/server/services/setup.go +++ b/server/services/setup.go @@ -25,6 +25,7 @@ import ( "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" + "go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/services/config" "go.woodpecker-ci.org/woodpecker/v2/server/services/registry" "go.woodpecker-ci.org/woodpecker/v2/server/services/secret" @@ -93,3 +94,70 @@ func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey privateKey := ed25519.PrivateKey(privKeyStr) return privateKey, privateKey.Public(), nil } + +func setupForgeService(c *cli.Context, _store store.Store) error { + _forge, err := _store.ForgeGet(1) + if err != nil && !errors.Is(err, types.RecordNotExist) { + return err + } + forgeExists := err == nil + if _forge == nil { + _forge = &model.Forge{ + ID: 0, + } + } + if _forge.AdditionalOptions == nil { + _forge.AdditionalOptions = make(map[string]any) + } + + _forge.Client = c.String("forge-oauth-client") + _forge.ClientSecret = c.String("forge-oauth-secret") + _forge.URL = c.String("forge-url") + _forge.SkipVerify = c.Bool("forge-skip-verify") + + switch { + case c.String("addon-forge") != "": + _forge.Type = model.ForgeTypeAddon + _forge.AdditionalOptions["executable"] = c.String("addon-forge") + case c.Bool("github"): + _forge.Type = model.ForgeTypeGithub + _forge.AdditionalOptions["merge-ref"] = c.Bool("github-merge-ref") + _forge.AdditionalOptions["public-only"] = c.Bool("github-public-only") + if _forge.URL == "" { + _forge.URL = "https://github.com" + } + case c.Bool("gitlab"): + _forge.Type = model.ForgeTypeGitlab + if _forge.URL == "" { + _forge.URL = "https://gitlab.com" + } + case c.Bool("gitea"): + _forge.Type = model.ForgeTypeGitea + _forge.AdditionalOptions["oauth-server"] = c.String("gitea-oauth-server") + if _forge.URL == "" { + _forge.URL = "https://try.gitea.com" + } + case c.Bool("bitbucket"): + _forge.Type = model.ForgeTypeBitbucket + case c.Bool("bitbucket-dc"): + _forge.Type = model.ForgeTypeBitbucketDatacenter + _forge.AdditionalOptions["git-username"] = c.String("bitbucket-dc-git-username") + _forge.AdditionalOptions["git-password"] = c.String("bitbucket-dc-git-password") + default: + return errors.New("forge not configured") + } + + if forgeExists { + err := _store.ForgeUpdate(_forge) + if err != nil { + return err + } + } else { + err := _store.ForgeCreate(_forge) + if err != nil { + return err + } + } + + return nil +} diff --git a/server/store/datastore/forge.go b/server/store/datastore/forge.go new file mode 100644 index 000000000..5c1a29efc --- /dev/null +++ b/server/store/datastore/forge.go @@ -0,0 +1,54 @@ +// Copyright 2022 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datastore + +import ( + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +func (s storage) ForgeGet(id int64) (*model.Forge, error) { + forge := new(model.Forge) + return forge, wrapGet(s.engine.ID(id).Get(forge)) +} + +func (s storage) ForgeList(p *model.ListOptions) ([]*model.Forge, error) { + forges := make([]*model.Forge, 0, 10) + return forges, s.paginate(p).Find(&forges) +} + +func (s storage) ForgeCreate(forge *model.Forge) error { + // only Insert set auto created ID back to object + _, err := s.engine.Insert(forge) + return err +} + +func (s storage) ForgeUpdate(forge *model.Forge) error { + _, err := s.engine.ID(forge.ID).AllCols().Update(forge) + return err +} + +func (s storage) ForgeDelete(forge *model.Forge) error { + sess := s.engine.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if _, err := sess.ID(forge.ID).Delete(new(model.Forge)); err != nil { + return err + } + + return sess.Commit() +} diff --git a/server/store/datastore/forge_test.go b/server/store/datastore/forge_test.go new file mode 100644 index 000000000..a7583d8db --- /dev/null +++ b/server/store/datastore/forge_test.go @@ -0,0 +1,71 @@ +// 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. + +package datastore + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +func TestForgeCRUD(t *testing.T) { + store, closer := newTestStore(t, new(model.Forge), new(model.Repo), new(model.User)) + defer closer() + + forge1 := &model.Forge{ + Type: "github", + URL: "https://github.com", + Client: "client", + ClientSecret: "secret", + SkipVerify: false, + AdditionalOptions: map[string]any{ + "foo": "bar", + }, + } + + // create first forge to play with + assert.NoError(t, store.ForgeCreate(forge1)) + assert.EqualValues(t, "github", forge1.Type) + + // retrieve it + forgeOne, err := store.ForgeGet(forge1.ID) + assert.NoError(t, err) + assert.EqualValues(t, forge1, forgeOne) + + // change type + assert.NoError(t, store.ForgeUpdate(&model.Forge{ID: forge1.ID, Type: "gitlab"})) + + // find updated forge by id + forgeOne, err = store.ForgeGet(forge1.ID) + assert.NoError(t, err) + assert.EqualValues(t, "gitlab", forgeOne.Type) + + // create two more forges and repos + someUser := &model.Forge{Type: "bitbucket"} + assert.NoError(t, store.ForgeCreate(someUser)) + assert.NoError(t, store.ForgeCreate(&model.Forge{Type: "gitea"})) + + // get all repos for a specific forge + forges, err := store.ForgeList(&model.ListOptions{All: true}) + assert.NoError(t, err) + assert.Len(t, forges, 3) + + // delete an forge and check if it's gone + assert.NoError(t, store.ForgeDelete(forge1)) + _, err = store.ForgeGet(forge1.ID) + assert.Error(t, err) +} diff --git a/server/store/datastore/migration/027_convert_to_new_pipeline_errors_format.go b/server/store/datastore/migration/027_convert_to_new_pipeline_errors_format.go index de6bc229f..fa9994294 100644 --- a/server/store/datastore/migration/027_convert_to_new_pipeline_errors_format.go +++ b/server/store/datastore/migration/027_convert_to_new_pipeline_errors_format.go @@ -18,16 +18,16 @@ import ( "src.techknowlogick.com/xormigrate" "xorm.io/xorm" - "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors" + errorTypes "go.woodpecker-ci.org/woodpecker/v2/pipeline/errors/types" ) // perPage027 set the size of the slice to read per page var perPage027 = 100 type pipeline027 struct { - ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` - Error string `json:"error" xorm:"LONGTEXT 'pipeline_error'"` // old error format - Errors []*errors.PipelineError `json:"errors" xorm:"json 'pipeline_errors'"` // new error format + ID int64 `json:"id" xorm:"pk autoincr 'pipeline_id'"` + Error string `json:"error" xorm:"LONGTEXT 'pipeline_error'"` // old error format + Errors []*errorTypes.PipelineError `json:"errors" xorm:"json 'pipeline_errors'"` // new error format } func (pipeline027) TableName() string { @@ -64,7 +64,7 @@ var convertToNewPipelineErrorFormat = xormigrate.Migration{ for _, oldPipeline := range oldPipelines { var newPipeline pipeline027 newPipeline.ID = oldPipeline.ID - newPipeline.Errors = []*errors.PipelineError{{ + newPipeline.Errors = []*errorTypes.PipelineError{{ Type: "generic", Message: oldPipeline.Error, }} diff --git a/server/store/datastore/migration/030_set_default_forge_id.go b/server/store/datastore/migration/030_set_default_forge_id.go new file mode 100644 index 000000000..994ebee16 --- /dev/null +++ b/server/store/datastore/migration/030_set_default_forge_id.go @@ -0,0 +1,46 @@ +// 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. + +package migration + +import ( + "fmt" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" + + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +var setForgeID = xormigrate.Migration{ + ID: "set-forge-id", + MigrateSession: func(sess *xorm.Session) (err error) { + if err := sess.Sync(new(model.User), new(model.Repo), new(model.Forge), new(model.Org)); err != nil { + return fmt.Errorf("sync new models failed: %w", err) + } + + _, err = sess.Exec(fmt.Sprintf("UPDATE `%s` SET forge_id=1;", model.User{}.TableName())) + if err != nil { + return err + } + + _, err = sess.Exec(fmt.Sprintf("UPDATE `%s` SET forge_id=1;", model.Org{}.TableName())) + if err != nil { + return err + } + + _, err = sess.Exec(fmt.Sprintf("UPDATE `%s` SET forge_id=1;", model.Repo{}.TableName())) + return err + }, +} diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go index 6799c3e8e..f2af625a9 100644 --- a/server/store/datastore/migration/migration.go +++ b/server/store/datastore/migration/migration.go @@ -59,6 +59,7 @@ var migrationTasks = []*xormigrate.Migration{ &convertToNewPipelineErrorFormat, &renameLinkToURL, &cleanRegistryPipeline, + &setForgeID, } var allBeans = []any{ @@ -77,6 +78,7 @@ var allBeans = []any{ new(model.ServerConfig), new(model.Cron), new(model.Redirection), + new(model.Forge), new(model.Workflow), new(model.Org), } diff --git a/server/store/datastore/migration/testfiles/tmp_4282431288 b/server/store/datastore/migration/testfiles/tmp_4282431288 deleted file mode 100644 index d81b2c85d..000000000 Binary files a/server/store/datastore/migration/testfiles/tmp_4282431288 and /dev/null differ diff --git a/server/store/mocks/store.go b/server/store/mocks/store.go index 81522b9e9..3007b3ef4 100644 --- a/server/store/mocks/store.go +++ b/server/store/mocks/store.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery. DO NOT EDIT. package mocks @@ -539,6 +539,152 @@ func (_m *Store) DeleteUser(_a0 *model.User) error { return r0 } +// ForgeCreate provides a mock function with given fields: _a0 +func (_m *Store) ForgeCreate(_a0 *model.Forge) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Forge) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ForgeDelete provides a mock function with given fields: _a0 +func (_m *Store) ForgeDelete(_a0 *model.Forge) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Forge) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ForgeFindByRepo provides a mock function with given fields: _a0 +func (_m *Store) ForgeFindByRepo(_a0 *model.Repo) (*model.Forge, error) { + ret := _m.Called(_a0) + + var r0 *model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.Repo) (*model.Forge, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*model.Repo) *model.Forge); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.Repo) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeFindByUser provides a mock function with given fields: _a0 +func (_m *Store) ForgeFindByUser(_a0 *model.User) (*model.Forge, error) { + ret := _m.Called(_a0) + + var r0 *model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.User) (*model.Forge, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*model.User) *model.Forge); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeGet provides a mock function with given fields: _a0 +func (_m *Store) ForgeGet(_a0 int64) (*model.Forge, error) { + ret := _m.Called(_a0) + + var r0 *model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(int64) (*model.Forge, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(int64) *model.Forge); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeList provides a mock function with given fields: p +func (_m *Store) ForgeList(p *model.ListOptions) ([]*model.Forge, error) { + ret := _m.Called(p) + + var r0 []*model.Forge + var r1 error + if rf, ok := ret.Get(0).(func(*model.ListOptions) ([]*model.Forge, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func(*model.ListOptions) []*model.Forge); ok { + r0 = rf(p) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Forge) + } + } + + if rf, ok := ret.Get(1).(func(*model.ListOptions) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForgeUpdate provides a mock function with given fields: _a0 +func (_m *Store) ForgeUpdate(_a0 *model.Forge) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Forge) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetActivePipelineList provides a mock function with given fields: repo func (_m *Store) GetActivePipelineList(repo *model.Repo) ([]*model.Pipeline, error) { ret := _m.Called(repo) diff --git a/server/store/store.go b/server/store/store.go index 4dc47b9fc..6ba76573c 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -14,7 +14,6 @@ package store -//go:generate go install github.com/vektra/mockery/v2@latest //go:generate mockery --name Store --output mocks --case underscore import ( @@ -160,6 +159,13 @@ type Store interface { CronListNextExecute(int64, int64) ([]*model.Cron, error) CronGetLock(*model.Cron, int64) (bool, error) + // Forge + ForgeCreate(*model.Forge) error + ForgeGet(int64) (*model.Forge, error) + ForgeList(p *model.ListOptions) ([]*model.Forge, error) + ForgeUpdate(*model.Forge) error + ForgeDelete(*model.Forge) error + // Agent AgentCreate(*model.Agent) error AgentFind(int64) (*model.Agent, error) diff --git a/server/web/config.go b/server/web/config.go index 630ae9ee4..99e78461e 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -39,12 +39,20 @@ func Config(c *gin.Context) { ).Sign(user.Hash) } + // TODO: remove this and use the forge type from the corresponding repo + mainForge, err := server.Config.Services.Manager.ForgeMain() + if err != nil { + log.Error().Err(err).Msg("could not get main forge") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + configData := map[string]any{ "user": user, "csrf": csrf, "version": version.String(), "skip_version_check": server.Config.WebUI.SkipVersionCheck, - "forge": server.Config.Services.Forge.Name(), + "forge": mainForge.Name(), "root_path": server.Config.Server.RootPath, "enable_swagger": server.Config.WebUI.EnableSwagger, } diff --git a/shared/addon/addon.go b/shared/addon/addon.go deleted file mode 100644 index 51d668f6c..000000000 --- a/shared/addon/addon.go +++ /dev/null @@ -1,62 +0,0 @@ -package addon - -import ( - "errors" - "plugin" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "go.woodpecker-ci.org/woodpecker/v2/shared/addon/types" -) - -var pluginCache = map[string]*plugin.Plugin{} - -type Addon[T any] struct { - Type types.Type - Value T -} - -func Load[T any](files []string, t types.Type) (*Addon[T], error) { - for _, file := range files { - if _, has := pluginCache[file]; !has { - p, err := plugin.Open(file) - if err != nil { - return nil, err - } - pluginCache[file] = p - } - - typeLookup, err := pluginCache[file].Lookup("Type") - if err != nil { - return nil, err - } - if addonType, is := typeLookup.(*types.Type); !is { - return nil, errors.New("addon type is incorrect") - } else if *addonType != t { - continue - } - - mainLookup, err := pluginCache[file].Lookup("Addon") - if err != nil { - return nil, err - } - main, is := mainLookup.(func(zerolog.Logger) (T, error)) - if !is { - return nil, errors.New("addon main function has incorrect type") - } - - logger := log.Logger.With().Str("addon", file).Logger() - - mainOut, err := main(logger) - if err != nil { - return nil, err - } - return &Addon[T]{ - Type: t, - Value: mainOut, - }, nil - } - - return nil, nil -} diff --git a/shared/addon/types/types.go b/shared/addon/types/types.go deleted file mode 100644 index 96331d280..000000000 --- a/shared/addon/types/types.go +++ /dev/null @@ -1,7 +0,0 @@ -package types - -type Type string - -const ( - TypeForge Type = "forge" -) diff --git a/shared/token/token.go b/shared/token/token.go index fd0f574c8..d5c48aafd 100644 --- a/shared/token/token.go +++ b/shared/token/token.go @@ -139,19 +139,19 @@ func keyFunc(token *Token, fn SecretFunc) jwt.Keyfunc { // extract the token kind and cast to // the expected type. - kindv, ok := claims["type"] + kind, ok := claims["type"] if !ok { return nil, jwt.ErrInvalidType } - token.Kind, _ = kindv.(string) + token.Kind, _ = kind.(string) // extract the token value and cast to // expected type. - textv, ok := claims["text"] + text, ok := claims["text"] if !ok { return nil, jwt.ErrInvalidType } - token.Text, _ = textv.(string) + token.Text, _ = text.(string) // invoke the callback function to retrieve // the secret key used to verify diff --git a/shared/utils/strings.go b/shared/utils/strings.go index 6add8386e..bbbc33c08 100644 --- a/shared/utils/strings.go +++ b/shared/utils/strings.go @@ -14,8 +14,8 @@ package utils -// DedupStrings deduplicate string list, empty items are dropped -func DedupStrings(src []string) []string { +// DeduplicateStrings deduplicate string list, empty items are dropped +func DeduplicateStrings(src []string) []string { m := make(map[string]struct{}, len(src)) dst := make([]string, 0, len(src)) diff --git a/shared/utils/strings_test.go b/shared/utils/strings_test.go index 393e56cd8..98166c134 100644 --- a/shared/utils/strings_test.go +++ b/shared/utils/strings_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDedupStrings(t *testing.T) { +func TestDeduplicateStrings(t *testing.T) { tests := []struct { in []string out []string @@ -37,7 +37,7 @@ func TestDedupStrings(t *testing.T) { }} for _, tc := range tests { - result := DedupStrings(tc.in) + result := DeduplicateStrings(tc.in) sort.Strings(result) if len(tc.out) == 0 { assert.Len(t, result, 0) diff --git a/web/package.json b/web/package.json index 898bed6e3..a86646bda 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,7 @@ "test": "echo 'No tests configured' && exit 0" }, "dependencies": { - "@intlify/unplugin-vue-i18n": "^3.0.0", + "@intlify/unplugin-vue-i18n": "^4.0.0", "@kyvg/vue3-notification": "^3.1.3", "@vueuse/core": "^10.7.2", "ansi_up": "^6.0.2", @@ -57,7 +57,7 @@ "prettier": "^3.2.4", "replace-in-file": "^7.1.0", "tinycolor2": "^1.6.0", - "typescript": "5.4.2", + "typescript": "5.4.3", "unplugin-icons": "^0.18.2", "unplugin-vue-components": "^0.26.0", "vite": "^5.0.12", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index ae6b0eeac..b4a4f9ed3 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -9,8 +9,8 @@ overrides: dependencies: '@intlify/unplugin-vue-i18n': - specifier: ^3.0.0 - version: 3.0.1(vue-i18n@9.10.2) + specifier: ^4.0.0 + version: 4.0.0(vue-i18n@9.10.2) '@kyvg/vue3-notification': specifier: ^3.1.3 version: 3.2.1(vue@3.4.21) @@ -37,7 +37,7 @@ dependencies: version: 2.1.3 pinia: specifier: ^2.1.7 - version: 2.1.7(typescript@5.4.2)(vue@3.4.21) + version: 2.1.7(typescript@5.4.3)(vue@3.4.21) prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -46,7 +46,7 @@ dependencies: version: 7.6.0 vue: specifier: ^3.4.15 - version: 3.4.21(typescript@5.4.2) + version: 3.4.21(typescript@5.4.3) vue-i18n: specifier: ^9.9.0 version: 9.10.2(vue@3.4.21) @@ -57,13 +57,13 @@ dependencies: devDependencies: '@iconify/json': specifier: ^2.2.171 - version: 2.2.195 + version: 2.2.196 '@types/lodash': specifier: ^4.14.202 version: 4.17.0 '@types/node': specifier: ^20.11.5 - version: 20.11.30 + version: 20.12.2 '@types/node-emoji': specifier: ^2.0.0 version: 2.1.0 @@ -75,13 +75,13 @@ devDependencies: version: 7.5.8 '@typescript-eslint/eslint-plugin': specifier: ^7.0.0 - version: 7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.4.2) + version: 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3) '@typescript-eslint/parser': specifier: ^7.0.0 - version: 7.3.1(eslint@8.57.0)(typescript@5.4.2) + version: 7.4.0(eslint@8.57.0)(typescript@5.4.3) '@vitejs/plugin-vue': specifier: ^5.0.3 - version: 5.0.4(vite@5.2.6)(vue@3.4.21) + version: 5.0.4(vite@5.2.7)(vue@3.4.21) '@vue/compiler-sfc': specifier: ^3.4.15 version: 3.4.21 @@ -93,13 +93,13 @@ devDependencies: version: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-config-airbnb-typescript: specifier: ^18.0.0 - version: 18.0.0(@typescript-eslint/eslint-plugin@7.3.1)(@typescript-eslint/parser@7.3.1)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + version: 18.0.0(@typescript-eslint/eslint-plugin@7.4.0)(@typescript-eslint/parser@7.4.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0) + version: 2.29.1(@typescript-eslint/parser@7.4.0)(eslint@8.57.0) eslint-plugin-prettier: specifier: ^5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) @@ -111,7 +111,7 @@ devDependencies: version: 12.0.0(eslint@8.57.0) eslint-plugin-vue: specifier: ^9.20.1 - version: 9.23.0(eslint@8.57.0) + version: 9.24.0(eslint@8.57.0) eslint-plugin-vue-scoped-css: specifier: ^2.7.2 version: 2.8.0(eslint@8.57.0)(vue-eslint-parser@9.4.2) @@ -125,8 +125,8 @@ devDependencies: specifier: ^1.6.0 version: 1.6.0 typescript: - specifier: 5.4.2 - version: 5.4.2 + specifier: 5.4.3 + version: 5.4.3 unplugin-icons: specifier: ^0.18.2 version: 0.18.5(@vue/compiler-sfc@3.4.21) @@ -135,13 +135,13 @@ devDependencies: version: 0.26.0(vue@3.4.21) vite: specifier: ^5.0.12 - version: 5.2.6(@types/node@20.11.30) + version: 5.2.7(@types/node@20.12.2) vite-plugin-prismjs: specifier: ^0.0.11 version: 0.0.11(prismjs@1.29.0) vite-plugin-windicss: specifier: ^1.9.3 - version: 1.9.3(vite@5.2.6) + version: 1.9.3(vite@5.2.7) vite-svg-loader: specifier: ^5.1.0 version: 5.1.0(vue@3.4.21) @@ -150,7 +150,7 @@ devDependencies: version: 9.4.2(eslint@8.57.0) vue-tsc: specifier: ^2.0.0 - version: 2.0.7(typescript@5.4.2) + version: 2.0.7(typescript@5.4.3) windicss: specifier: ^3.5.6 version: 3.5.6 @@ -639,8 +639,8 @@ packages: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true - /@iconify/json@2.2.195: - resolution: {integrity: sha512-oDO79OGefkm/F4xeRDWSiigxsf7yMJAHRzyOoqmkrerUDJ/5JxRSsezUJhYlVarbRFdaBLGh7joJwtLO5mAmXw==} + /@iconify/json@2.2.196: + resolution: {integrity: sha512-hRZ0pq77N+mkAbZvFi/pfsKcspA8PyGSASc6zQoq6n/RSLxb8xAgORatVHyDl0ow7shcS+dvyiZI8xmr6yI2WA==} dependencies: '@iconify/types': 2.0.0 pathe: 1.1.2 @@ -664,8 +664,8 @@ packages: - supports-color dev: true - /@intlify/bundle-utils@7.5.1(vue-i18n@9.10.2): - resolution: {integrity: sha512-UovJl10oBIlmYEcWw+VIHdKY5Uv5sdPG0b/b6bOYxGLln3UwB75+2dlc0F3Fsa0RhoznQ5Rp589/BZpABpE4Xw==} + /@intlify/bundle-utils@8.0.0(vue-i18n@9.10.2): + resolution: {integrity: sha512-1B++zykRnMwQ+20SpsZI1JCnV/YJt9Oq7AGlEurzkWJOFtFAVqaGc/oV36PBRYeiKnTbY9VYfjBimr2Vt42wLQ==} engines: {node: '>= 14.16'} peerDependencies: petite-vue-i18n: '*' @@ -682,7 +682,6 @@ packages: escodegen: 2.1.0 estree-walker: 2.0.2 jsonc-eslint-parser: 2.4.0 - magic-string: 0.30.8 mlly: 1.6.1 source-map-js: 1.2.0 vue-i18n: 9.10.2(vue@3.4.21) @@ -710,8 +709,8 @@ packages: engines: {node: '>= 16'} dev: false - /@intlify/unplugin-vue-i18n@3.0.1(vue-i18n@9.10.2): - resolution: {integrity: sha512-q1zJhA/WpoLBzAAuKA5/AEp0e+bMOM10ll/HxT4g1VAw/9JhC4TTobP9KobKH90JMZ4U2daLFlYQfKNd29lpqw==} + /@intlify/unplugin-vue-i18n@4.0.0(vue-i18n@9.10.2): + resolution: {integrity: sha512-q2Mhqa/mLi0tulfLFO4fMXXvEbkSZpI5yGhNNsLTNJJ41icEGUuyDe+j5zRZIKSkOJRgX6YbCyibTDJdRsukmw==} engines: {node: '>= 14.16'} peerDependencies: petite-vue-i18n: '*' @@ -725,7 +724,7 @@ packages: vue-i18n-bridge: optional: true dependencies: - '@intlify/bundle-utils': 7.5.1(vue-i18n@9.10.2) + '@intlify/bundle-utils': 8.0.0(vue-i18n@9.10.2) '@intlify/shared': 9.10.2 '@rollup/pluginutils': 5.1.0 '@vue/compiler-sfc': 3.4.21 @@ -777,7 +776,7 @@ packages: peerDependencies: vue: ^3.0.0 dependencies: - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.21(typescript@5.4.3) dev: false /@nodelib/fs.scandir@2.1.5: @@ -816,104 +815,120 @@ packages: estree-walker: 2.0.2 picomatch: 2.3.1 - /@rollup/rollup-android-arm-eabi@4.13.0: - resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} + /@rollup/rollup-android-arm-eabi@4.13.2: + resolution: {integrity: sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.13.0: - resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==} + /@rollup/rollup-android-arm64@4.13.2: + resolution: {integrity: sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.13.0: - resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==} + /@rollup/rollup-darwin-arm64@4.13.2: + resolution: {integrity: sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.13.0: - resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==} + /@rollup/rollup-darwin-x64@4.13.2: + resolution: {integrity: sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.13.0: - resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==} + /@rollup/rollup-linux-arm-gnueabihf@4.13.2: + resolution: {integrity: sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.13.0: - resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==} + /@rollup/rollup-linux-arm64-gnu@4.13.2: + resolution: {integrity: sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.13.0: - resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==} + /@rollup/rollup-linux-arm64-musl@4.13.2: + resolution: {integrity: sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.13.0: - resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==} + /@rollup/rollup-linux-powerpc64le-gnu@4.13.2: + resolution: {integrity: sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==} + cpu: [ppc64le] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.13.2: + resolution: {integrity: sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.13.0: - resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} + /@rollup/rollup-linux-s390x-gnu@4.13.2: + resolution: {integrity: sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.13.2: + resolution: {integrity: sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.13.0: - resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==} + /@rollup/rollup-linux-x64-musl@4.13.2: + resolution: {integrity: sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.13.0: - resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==} + /@rollup/rollup-win32-arm64-msvc@4.13.2: + resolution: {integrity: sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.13.0: - resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==} + /@rollup/rollup-win32-ia32-msvc@4.13.2: + resolution: {integrity: sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.13.0: - resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==} + /@rollup/rollup-win32-x64-msvc@4.13.2: + resolution: {integrity: sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==} cpu: [x64] os: [win32] requiresBuild: true @@ -951,8 +966,8 @@ packages: node-emoji: 2.1.3 dev: true - /@types/node@20.11.30: - resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + /@types/node@20.12.2: + resolution: {integrity: sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==} dependencies: undici-types: 5.26.5 dev: true @@ -969,8 +984,8 @@ packages: resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} dev: false - /@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==} + /@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3): + resolution: {integrity: sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 @@ -981,25 +996,25 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/scope-manager': 7.3.1 - '@typescript-eslint/type-utils': 7.3.1(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/visitor-keys': 7.3.1 + '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/scope-manager': 7.4.0 + '@typescript-eslint/type-utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/visitor-keys': 7.4.0 debug: 4.3.4 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.2) - typescript: 5.4.2 + ts-api-utils: 1.3.0(typescript@5.4.3) + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==} + /@typescript-eslint/parser@7.4.0(eslint@8.57.0)(typescript@5.4.3): + resolution: {integrity: sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -1008,27 +1023,27 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 7.3.1 - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.4.2) - '@typescript-eslint/visitor-keys': 7.3.1 + '@typescript-eslint/scope-manager': 7.4.0 + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) + '@typescript-eslint/visitor-keys': 7.4.0 debug: 4.3.4 eslint: 8.57.0 - typescript: 5.4.2 + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@7.3.1: - resolution: {integrity: sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==} + /@typescript-eslint/scope-manager@7.4.0: + resolution: {integrity: sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==} engines: {node: ^18.18.0 || >=20.0.0} dependencies: - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/visitor-keys': 7.3.1 + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/visitor-keys': 7.4.0 dev: true - /@typescript-eslint/type-utils@7.3.1(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==} + /@typescript-eslint/type-utils@7.4.0(eslint@8.57.0)(typescript@5.4.3): + resolution: {integrity: sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -1037,23 +1052,23 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.4.2) - '@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) + '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) debug: 4.3.4 eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.2) - typescript: 5.4.2 + ts-api-utils: 1.3.0(typescript@5.4.3) + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@7.3.1: - resolution: {integrity: sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==} + /@typescript-eslint/types@7.4.0: + resolution: {integrity: sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==} engines: {node: ^18.18.0 || >=20.0.0} dev: true - /@typescript-eslint/typescript-estree@7.3.1(typescript@5.4.2): - resolution: {integrity: sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==} + /@typescript-eslint/typescript-estree@7.4.0(typescript@5.4.3): + resolution: {integrity: sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: typescript: '*' @@ -1061,21 +1076,21 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/visitor-keys': 7.3.1 + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/visitor-keys': 7.4.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.2) - typescript: 5.4.2 + ts-api-utils: 1.3.0(typescript@5.4.3) + typescript: 5.4.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@7.3.1(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==} + /@typescript-eslint/utils@7.4.0(eslint@8.57.0)(typescript@5.4.3): + resolution: {integrity: sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -1083,9 +1098,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.3.1 - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.4.2) + '@typescript-eslint/scope-manager': 7.4.0 + '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -1093,11 +1108,11 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@7.3.1: - resolution: {integrity: sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==} + /@typescript-eslint/visitor-keys@7.4.0: + resolution: {integrity: sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==} engines: {node: ^18.18.0 || >=20.0.0} dependencies: - '@typescript-eslint/types': 7.3.1 + '@typescript-eslint/types': 7.4.0 eslint-visitor-keys: 3.4.3 dev: true @@ -1105,15 +1120,15 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-vue@5.0.4(vite@5.2.6)(vue@3.4.21): + /@vitejs/plugin-vue@5.0.4(vite@5.2.7)(vue@3.4.21): resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 vue: ^3.2.25 dependencies: - vite: 5.2.6(@types/node@20.11.30) - vue: 3.4.21(typescript@5.4.2) + vite: 5.2.7(@types/node@20.12.2) + vue: 3.4.21(typescript@5.4.3) dev: true /@volar/language-core@2.1.5: @@ -1173,7 +1188,7 @@ packages: resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==} dev: false - /@vue/language-core@2.0.7(typescript@5.4.2): + /@vue/language-core@2.0.7(typescript@5.4.3): resolution: {integrity: sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==} peerDependencies: typescript: '*' @@ -1187,7 +1202,7 @@ packages: computeds: 0.0.1 minimatch: 9.0.3 path-browserify: 1.0.1 - typescript: 5.4.2 + typescript: 5.4.3 vue-template-compiler: 2.7.16 dev: true @@ -1216,7 +1231,7 @@ packages: dependencies: '@vue/compiler-ssr': 3.4.21 '@vue/shared': 3.4.21 - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.21(typescript@5.4.3) /@vue/shared@3.4.21: resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} @@ -1453,7 +1468,7 @@ packages: hasBin: true dependencies: caniuse-lite: 1.0.30001600 - electron-to-chromium: 1.4.715 + electron-to-chromium: 1.4.717 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) dev: true @@ -1760,8 +1775,8 @@ packages: domhandler: 5.0.3 dev: true - /electron-to-chromium@1.4.715: - resolution: {integrity: sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==} + /electron-to-chromium@1.4.717: + resolution: {integrity: sha512-6Fmg8QkkumNOwuZ/5mIbMU9WI3H2fmn5ajcVya64I5Yr5CcNmO7vcLt0Y7c96DCiMO5/9G+4sI2r6eEvdg1F7A==} dev: true /emoji-regex@8.0.0: @@ -1947,21 +1962,21 @@ packages: dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.4.0)(eslint@8.57.0) object.assign: 4.1.5 object.entries: 1.1.8 semver: 7.6.0 dev: true - /eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@7.3.1)(@typescript-eslint/parser@7.3.1)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + /eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@7.4.0)(@typescript-eslint/parser@7.4.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0): resolution: {integrity: sha512-oc+Lxzgzsu8FQyFVa4QFaVKiitTYiiW3frB9KYW5OWdPrqFc7FzxgB20hP4cHMlr+MBzGcLl3jnCOVOydL9mIg==} peerDependencies: '@typescript-eslint/eslint-plugin': ^7.0.0 '@typescript-eslint/parser': ^7.0.0 eslint: ^8.56.0 dependencies: - '@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/eslint-plugin': 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) eslint: 8.57.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: @@ -1987,7 +2002,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + /eslint-module-utils@2.8.1(@typescript-eslint/parser@7.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} engines: {node: '>=4'} peerDependencies: @@ -2008,7 +2023,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -2016,7 +2031,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.4.0)(eslint@8.57.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -2026,7 +2041,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -2035,7 +2050,7 @@ packages: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -2110,14 +2125,15 @@ packages: - supports-color dev: true - /eslint-plugin-vue@9.23.0(eslint@8.57.0): - resolution: {integrity: sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw==} + /eslint-plugin-vue@9.24.0(eslint@8.57.0): + resolution: {integrity: sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) eslint: 8.57.0 + globals: 13.24.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.0.16 @@ -3126,7 +3142,7 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - /pinia@2.1.7(typescript@5.4.2)(vue@3.4.21): + /pinia@2.1.7(typescript@5.4.3)(vue@3.4.21): resolution: {integrity: sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==} peerDependencies: '@vue/composition-api': ^1.4.0 @@ -3139,8 +3155,8 @@ packages: optional: true dependencies: '@vue/devtools-api': 6.6.1 - typescript: 5.4.2 - vue: 3.4.21(typescript@5.4.2) + typescript: 5.4.3 + vue: 3.4.21(typescript@5.4.3) vue-demi: 0.14.7(vue@3.4.21) dev: false @@ -3289,26 +3305,28 @@ packages: glob: 7.2.3 dev: true - /rollup@4.13.0: - resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} + /rollup@4.13.2: + resolution: {integrity: sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.13.0 - '@rollup/rollup-android-arm64': 4.13.0 - '@rollup/rollup-darwin-arm64': 4.13.0 - '@rollup/rollup-darwin-x64': 4.13.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.13.0 - '@rollup/rollup-linux-arm64-gnu': 4.13.0 - '@rollup/rollup-linux-arm64-musl': 4.13.0 - '@rollup/rollup-linux-riscv64-gnu': 4.13.0 - '@rollup/rollup-linux-x64-gnu': 4.13.0 - '@rollup/rollup-linux-x64-musl': 4.13.0 - '@rollup/rollup-win32-arm64-msvc': 4.13.0 - '@rollup/rollup-win32-ia32-msvc': 4.13.0 - '@rollup/rollup-win32-x64-msvc': 4.13.0 + '@rollup/rollup-android-arm-eabi': 4.13.2 + '@rollup/rollup-android-arm64': 4.13.2 + '@rollup/rollup-darwin-arm64': 4.13.2 + '@rollup/rollup-darwin-x64': 4.13.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.13.2 + '@rollup/rollup-linux-arm64-gnu': 4.13.2 + '@rollup/rollup-linux-arm64-musl': 4.13.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.13.2 + '@rollup/rollup-linux-riscv64-gnu': 4.13.2 + '@rollup/rollup-linux-s390x-gnu': 4.13.2 + '@rollup/rollup-linux-x64-gnu': 4.13.2 + '@rollup/rollup-linux-x64-musl': 4.13.2 + '@rollup/rollup-win32-arm64-msvc': 4.13.2 + '@rollup/rollup-win32-ia32-msvc': 4.13.2 + '@rollup/rollup-win32-x64-msvc': 4.13.2 fsevents: 2.3.3 dev: true @@ -3572,13 +3590,13 @@ packages: dependencies: is-number: 7.0.0 - /ts-api-utils@1.3.0(typescript@5.4.2): + /ts-api-utils@1.3.0(typescript@5.4.3): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.4.2 + typescript: 5.4.3 dev: true /tsconfig-paths@3.15.0: @@ -3650,8 +3668,8 @@ packages: possible-typed-array-names: 1.0.0 dev: true - /typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + /typescript@5.4.3: + resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} engines: {node: '>=14.17'} hasBin: true @@ -3730,7 +3748,7 @@ packages: minimatch: 9.0.3 resolve: 1.22.8 unplugin: 1.10.0 - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.21(typescript@5.4.3) transitivePeerDependencies: - rollup - supports-color @@ -3777,7 +3795,7 @@ packages: - supports-color dev: true - /vite-plugin-windicss@1.9.3(vite@5.2.6): + /vite-plugin-windicss@1.9.3(vite@5.2.7): resolution: {integrity: sha512-PqNiIsrEftCrgn0xIpj8ZMSdpz8NZn+OJ3gKXnOF+hFzbHFrKGJA49ViOUKCHDOquxoGBZMmTjepWr8GrftKcQ==} peerDependencies: vite: ^2.0.1 || ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -3785,7 +3803,7 @@ packages: '@windicss/plugin-utils': 1.9.3 debug: 4.3.4 kolorist: 1.8.0 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.7(@types/node@20.12.2) windicss: 3.5.6 transitivePeerDependencies: - supports-color @@ -3797,11 +3815,11 @@ packages: vue: '>=3.2.13' dependencies: svgo: 3.2.0 - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.21(typescript@5.4.3) dev: true - /vite@5.2.6(@types/node@20.11.30): - resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} + /vite@5.2.7(@types/node@20.12.2): + resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -3828,10 +3846,10 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.2 esbuild: 0.20.2 postcss: 8.4.38 - rollup: 4.13.0 + rollup: 4.13.2 optionalDependencies: fsevents: 2.3.3 dev: true @@ -3848,7 +3866,7 @@ packages: '@vue/composition-api': optional: true dependencies: - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.21(typescript@5.4.3) dev: false /vue-eslint-parser@9.4.2(eslint@8.57.0): @@ -3878,7 +3896,7 @@ packages: '@intlify/core-base': 9.10.2 '@intlify/shared': 9.10.2 '@vue/devtools-api': 6.6.1 - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.21(typescript@5.4.3) dev: false /vue-router@4.3.0(vue@3.4.21): @@ -3887,7 +3905,7 @@ packages: vue: ^3.2.0 dependencies: '@vue/devtools-api': 6.6.1 - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.21(typescript@5.4.3) dev: false /vue-template-compiler@2.7.16: @@ -3897,19 +3915,19 @@ packages: he: 1.2.0 dev: true - /vue-tsc@2.0.7(typescript@5.4.2): + /vue-tsc@2.0.7(typescript@5.4.3): resolution: {integrity: sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==} hasBin: true peerDependencies: typescript: '*' dependencies: '@volar/typescript': 2.1.5 - '@vue/language-core': 2.0.7(typescript@5.4.2) + '@vue/language-core': 2.0.7(typescript@5.4.3) semver: 7.6.0 - typescript: 5.4.2 + typescript: 5.4.3 dev: true - /vue@3.4.21(typescript@5.4.2): + /vue@3.4.21(typescript@5.4.3): resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==} peerDependencies: typescript: '*' @@ -3922,7 +3940,7 @@ packages: '@vue/runtime-dom': 3.4.21 '@vue/server-renderer': 3.4.21(vue@3.4.21) '@vue/shared': 3.4.21 - typescript: 5.4.2 + typescript: 5.4.3 /webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} diff --git a/web/src/assets/locales/de.json b/web/src/assets/locales/de.json index a9731e1aa..4e394674a 100644 --- a/web/src/assets/locales/de.json +++ b/web/src/assets/locales/de.json @@ -391,6 +391,10 @@ "public": "Öffentlich" }, "visibility": "Sichtbarkeit des Projekts" + }, + "allow_deploy": { + "allow": "Deployments erlauben", + "desc": "Deployments von erolgreichen Pipelines erlauben. Nur benutzen, wenn du allen Nutzern mit Push-Zugriff vertraust." } }, "not_allowed": "Zugriff auf die Einstellungen dieses Repositories nicht erlaubt", diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 159120e5c..83e98c2d8 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -90,6 +90,10 @@ "allow": "Allow Pull Requests", "desc": "Pipelines can run on pull requests." }, + "allow_deploy": { + "allow": "Allow deployments", + "desc": "Allow deployments from successful pipelines. Only use if you trust all users with push access." + }, "protected": { "protected": "Protected", "desc": "Every pipeline needs to be approved before being executed." @@ -241,6 +245,8 @@ "pipeline": "Pipeline #{pipelineId}", "log_title": "Step Logs", "log_download_error": "There was an error while downloading the log file", + "log_delete_confirm": "Do you really want to delete logs of this step?", + "log_delete_error": "There was an error while deleting the step logs", "actions": { "cancel": "Cancel", "restart": "Restart", @@ -249,11 +255,11 @@ "deploy": "Deploy", "restart_success": "Pipeline restarted", "log_download": "Download", + "log_delete": "Delete", "log_auto_scroll": "Automatically scroll down", "log_auto_scroll_off": "Turn off automatic scrolling" }, "protected": { - "review": "Review changes", "awaits": "This pipeline is awaiting approval by a maintainer!", "approve": "Approve", "decline": "Decline", diff --git a/web/src/assets/locales/fr.json b/web/src/assets/locales/fr.json index 014fc1ffe..63c50b788 100644 --- a/web/src/assets/locales/fr.json +++ b/web/src/assets/locales/fr.json @@ -390,6 +390,10 @@ "public": "Publique" }, "visibility": "Visibilité du projet" + }, + "allow_deploy": { + "allow": "Autoriser les déploiements", + "desc": "Autoriser les déploiements depuis les pipelines ayant réussis. À utiliser que si vous avez confiance dans les utilisateurs ayant un accès en écriture." } }, "not_allowed": "Vous n'êtes pas autorisé à accéder aux paramètres de ce dépôt", diff --git a/web/src/assets/locales/zh-Hans.json b/web/src/assets/locales/zh-Hans.json index 083479a79..6c53ac028 100644 --- a/web/src/assets/locales/zh-Hans.json +++ b/web/src/assets/locales/zh-Hans.json @@ -398,11 +398,11 @@ "placeholder": "Registry 地址(如 docker.io)" }, "created": "Registry 密码已创建", - "credentials": "注册表凭据", + "credentials": "Registry 凭据", "delete": "删除 registry", "deleted": "Registry 密码已删除", "desc": "可以添加 Registry 密码,以在流水线中使用私有镜像。", - "edit": "编辑注册表", + "edit": "编辑 Registry", "none": "现在没有 Registry 密码。", "registries": "注册表", "save": "保存 Registry", diff --git a/web/src/components/repo/pipeline/PipelineLog.vue b/web/src/components/repo/pipeline/PipelineLog.vue index 9fec6a2f0..8a505b304 100644 --- a/web/src/components/repo/pipeline/PipelineLog.vue +++ b/web/src/components/repo/pipeline/PipelineLog.vue @@ -20,6 +20,13 @@ icon="download" @click="download" /> + + ; } + deleteLogs(repoId: number, pipeline: number, step: number): Promise { + return this._delete(`/api/repos/${repoId}/logs/${pipeline}/${step}`); + } + getSecretList(repoId: number, page: number): Promise { return this._get(`/api/repos/${repoId}/secrets?page=${page}`) as Promise; } diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts index 1e1a75ee7..b40531b86 100644 --- a/web/src/lib/api/types/repo.ts +++ b/web/src/lib/api/types/repo.ts @@ -56,6 +56,8 @@ export type Repo = { // Whether pull requests should trigger a pipeline. allow_pr: boolean; + allow_deploy: boolean; + config_file: string; visibility: RepoVisibility; @@ -84,6 +86,7 @@ export type RepoSettings = Pick< | 'trusted' | 'gated' | 'allow_pr' + | 'allow_deploy' | 'cancel_previous_pipeline_events' | 'netrc_only_trusted' >; diff --git a/web/src/views/cli/Auth.vue b/web/src/views/cli/Auth.vue index fd5b1c793..bc4da4648 100644 --- a/web/src/views/cli/Auth.vue +++ b/web/src/views/cli/Auth.vue @@ -1,7 +1,7 @@