Merge remote-tracking branch 'upstream/main' into pr/6543/3539

This commit is contained in:
Anbraten 2024-04-17 13:00:56 +02:00
commit 5dc778e80d
145 changed files with 3396 additions and 1502 deletions

View file

@ -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"]

16
.github/renovate.json vendored
View file

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

1
.gitignore vendored
View file

@ -41,6 +41,7 @@ extras/
/build/
/dist/
/data/
datastore/migration/testfiles/
docs/venv

2
.mockery.yaml Normal file
View file

@ -0,0 +1,2 @@
---
disable-version-string: true

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
]
}
}
}`

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

@ -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.<br>
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).<br>
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.

View file

@ -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)).

View file

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

View file

@ -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).

View file

@ -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.
```

View file

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

View file

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

View file

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

View file

@ -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.
```

View file

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

View file

@ -1,6 +0,0 @@
label: 'Addons'
collapsible: true
collapsed: true
link:
type: 'doc'
id: 'overview'

View file

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

View file

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

View file

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

View file

@ -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.<br>
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).<br>
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.

23
go.mod
View file

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

74
go.sum
View file

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

View file

@ -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) {

View file

@ -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"`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <personal access token>)
// @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

View file

@ -45,9 +45,14 @@ import (
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @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
}

View file

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

View file

@ -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())

View file

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

View file

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

View file

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

View file

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

154
server/forge/addon/args.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.42.1. DO NOT EDIT.
// Code generated by mockery. DO NOT EDIT.
package mocks

134
server/forge/setup/setup.go Normal file
View file

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

View file

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

View file

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

36
server/model/forge.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more