Merge branch 'main' into woodpecker-fix-log-tail-cpu-lock

This commit is contained in:
Fernando Barbosa 2024-05-02 13:43:04 -03:00 committed by GitHub
commit 6cf032f03b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
166 changed files with 14446 additions and 6522 deletions

View file

@ -90,7 +90,11 @@
"binutils",
"nocolor",
"logfile",
"Keyfunc"
"Keyfunc",
"protoc",
"PROTOC",
"GOBIN",
"GOPATH"
],
"ignorePaths": [
"**/node_modules/**/*",

2
.gitignore vendored
View file

@ -13,7 +13,7 @@
*.so
*.dylib
vendor/
__debug_bin
__debug_bin*
# Test binary, built with `go test -c`
*.test

2
.mockery.yaml Normal file
View file

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

View file

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

@ -3,8 +3,9 @@ when:
variables:
- &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'
- &node_image 'docker.io/node:22-alpine'
# TODO: switch back to upstream image after https://github.com/techknowlogick/xgo/pull/224 got merged
- &xgo_image 'docker.io/pats22/xgo:go-1.22.1'
- &xgo_version 'go-1.21.2'
steps:
@ -90,11 +91,10 @@ steps:
release:
depends_on:
- checksums
image: docker.io/plugins/github-release
secrets:
- source: github_token
target: github_release_api_key
image: woodpeckerci/plugin-github-release:1.1.2
settings:
api_key:
from_secret: github_token
files:
- dist/*.tar.gz
- dist/*.deb

View file

@ -1,7 +1,8 @@
variables:
- &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'
- &node_image 'docker.io/node:22-alpine'
# TODO: switch back to upstream image after https://github.com/techknowlogick/xgo/pull/224 got merged
- &xgo_image 'docker.io/pats22/xgo:go-1.22.1'
- &xgo_version 'go-1.21.2'
- &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'

View file

@ -1,6 +1,6 @@
steps:
release-helper:
image: woodpeckerci/plugin-ready-release-go:1.1.0
image: woodpeckerci/plugin-ready-release-go:1.1.1
pull: true
settings:
release_branch: ${CI_REPO_DEFAULT_BRANCH}

View file

@ -13,7 +13,7 @@ steps:
branch: renovate/*
- name: spellcheck
image: docker.io/node:21-alpine
image: docker.io/node:22-alpine
depends_on: []
commands:
- corepack enable

View file

@ -6,7 +6,7 @@ when:
- renovate/*
variables:
- &node_image 'docker.io/node:21-alpine'
- &node_image 'docker.io/node:22-alpine'
- &when
path:
# related config files

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
@ -299,6 +306,10 @@ bundle-cli: bundle-prepare ## Create bundles for cli
.PHONY: bundle
bundle: bundle-agent bundle-server bundle-cli ## Create all bundles
.PHONY: spellcheck
spellcheck:
pnpx cspell lint --no-progress --gitignore '{**,.*}/{*,.*}'
##@ Docs
.PHONY: docs
docs: ## Generate docs (currently only for the cli)

View file

@ -7,8 +7,8 @@
</p>
<br/>
<p align="center">
<a href="https://ci.woodpecker-ci.org/repos/3780" title="Build Status">
<img src="https://ci.woodpecker-ci.org/api/badges/3780/status.svg" alt="Build Status">
<a href="https://ci.woodpecker-ci.org/repos/3780" title="Pipeline Status">
<img src="https://ci.woodpecker-ci.org/api/badges/3780/status.svg" alt="Pipeline Status">
</a>
<a href="https://codecov.io/gh/woodpecker-ci/woodpecker">
<img src="https://codecov.io/gh/woodpecker-ci/woodpecker/branch/main/graph/badge.svg" alt="Code coverage">

View file

@ -75,14 +75,29 @@ func FormatFlag(tmpl string, hidden ...bool) *cli.StringFlag {
}
}
// OutputFlags returns a slice of cli.Flag containing output format options.
func OutputFlags(def string) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "output",
Usage: "output format",
Value: def,
},
&cli.BoolFlag{
Name: "output-no-headers",
Usage: "don't print headers",
},
}
}
var RepoFlag = &cli.StringFlag{
Name: "repository",
Aliases: []string{"repo"},
Usage: "repository id or full-name (e.g. 134 or octocat/hello-world)",
Usage: "repository id or full name (e.g. 134 or octocat/hello-world)",
}
var OrgFlag = &cli.StringFlag{
Name: "organization",
Aliases: []string{"org"},
Usage: "organization id or full-name (e.g. 123 or octocat)",
Usage: "organization id or full name (e.g. 123 or octocat)",
}

View file

@ -57,7 +57,7 @@ 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
// 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

@ -26,7 +26,7 @@ import (
var logPurgeCmd = &cli.Command{
Name: "purge",
Usage: "purge a log",
ArgsUsage: "<repo-id|repo-full-name> <pipeline>",
ArgsUsage: "<repo-id|repo-full-name> <pipeline> [step]",
Action: logPurge,
}
@ -45,7 +45,21 @@ func logPurge(c *cli.Context) (err error) {
return err
}
err = client.LogsPurge(repoID, number)
stepArg := c.Args().Get(2) //nolint: gomnd
// TODO: Add lookup by name: stepID, err := internal.ParseStep(client, repoID, stepIDOrName)
var stepID int64
if len(stepArg) != 0 {
stepID, err = strconv.ParseInt(stepArg, 10, 64)
if err != nil {
return err
}
}
if stepID > 0 {
err = client.StepLogsPurge(repoID, number, stepID)
} else {
err = client.LogsPurge(repoID, number)
}
if err != nil {
return err
}

24
cli/output/output.go Normal file
View file

@ -0,0 +1,24 @@
package output
import (
"errors"
"strings"
)
var ErrOutputOptionRequired = errors.New("output option required")
func ParseOutputOptions(out string) (string, []string) {
out, opt, found := strings.Cut(out, "=")
if !found {
return out, nil
}
var optList []string
if opt != "" {
optList = strings.Split(opt, ",")
}
return out, optList
}

203
cli/output/table.go Normal file
View file

@ -0,0 +1,203 @@
package output
import (
"fmt"
"io"
"reflect"
"sort"
"strings"
"text/tabwriter"
"unicode"
"github.com/mitchellh/mapstructure"
)
// NewTable creates a new Table.
func NewTable(out io.Writer) *Table {
padding := 2
return &Table{
w: tabwriter.NewWriter(out, 0, 0, padding, ' ', 0),
columns: map[string]bool{},
fieldMapping: map[string]FieldFn{},
fieldAlias: map[string]string{},
allowedFields: map[string]bool{},
}
}
type FieldFn func(obj any) string
type writerFlusher interface {
io.Writer
Flush() error
}
// Table is a generic way to format object as a table.
type Table struct {
w writerFlusher
columns map[string]bool
fieldMapping map[string]FieldFn
fieldAlias map[string]string
allowedFields map[string]bool
}
// Columns returns a list of known output columns.
func (o *Table) Columns() (cols []string) {
for c := range o.columns {
cols = append(cols, c)
}
sort.Strings(cols)
return
}
// AddFieldAlias overrides the field name to allow custom column headers.
func (o *Table) AddFieldAlias(field, alias string) *Table {
o.fieldAlias[field] = alias
return o
}
// AddFieldFn adds a function which handles the output of the specified field.
func (o *Table) AddFieldFn(field string, fn FieldFn) *Table {
o.fieldMapping[field] = fn
o.allowedFields[field] = true
o.columns[field] = true
return o
}
// AddAllowedFields reads all first level fieldnames of the struct and allows them to be used.
func (o *Table) AddAllowedFields(obj any) (*Table, error) {
v := reflect.ValueOf(obj)
if v.Kind() != reflect.Struct {
return o, fmt.Errorf("AddAllowedFields input must be a struct")
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
k := t.Field(i).Type.Kind()
if k != reflect.Bool &&
k != reflect.Float32 &&
k != reflect.Float64 &&
k != reflect.String &&
k != reflect.Int &&
k != reflect.Int64 {
// only allow simple values
// complex values need to be mapped via a FieldFn
continue
}
o.allowedFields[strings.ToLower(t.Field(i).Name)] = true
o.allowedFields[fieldName(t.Field(i).Name)] = true
o.columns[fieldName(t.Field(i).Name)] = true
}
return o, nil
}
// RemoveAllowedField removes fields from the allowed list.
func (o *Table) RemoveAllowedField(fields ...string) *Table {
for _, field := range fields {
delete(o.allowedFields, field)
delete(o.columns, field)
}
return o
}
// ValidateColumns returns an error if invalid columns are specified.
func (o *Table) ValidateColumns(cols []string) error {
var invalidCols []string
for _, col := range cols {
if _, ok := o.allowedFields[strings.ToLower(col)]; !ok {
invalidCols = append(invalidCols, col)
}
}
if len(invalidCols) > 0 {
return fmt.Errorf("invalid table columns: %s", strings.Join(invalidCols, ","))
}
return nil
}
// WriteHeader writes the table header.
func (o *Table) WriteHeader(columns []string) {
var header []string
for _, col := range columns {
if alias, ok := o.fieldAlias[col]; ok {
col = alias
}
header = append(header, strings.ReplaceAll(strings.ToUpper(col), "_", " "))
}
_, _ = fmt.Fprintln(o.w, strings.Join(header, "\t"))
}
func (o *Table) Flush() error {
return o.w.Flush()
}
// Write writes a table line.
func (o *Table) Write(columns []string, obj any) error {
var data map[string]any
if err := mapstructure.Decode(obj, &data); err != nil {
return fmt.Errorf("failed to decode object: %w", err)
}
dataL := map[string]any{}
for key, value := range data {
dataL[strings.ToLower(key)] = value
}
var out []string
for _, col := range columns {
colName := strings.ToLower(col)
if alias, ok := o.fieldAlias[colName]; ok {
if fn, ok := o.fieldMapping[alias]; ok {
out = append(out, fn(obj))
continue
}
}
if fn, ok := o.fieldMapping[colName]; ok {
out = append(out, fn(obj))
continue
}
if value, ok := dataL[strings.ReplaceAll(colName, "_", "")]; ok {
if value == nil {
out = append(out, NA(""))
continue
}
if b, ok := value.(bool); ok {
out = append(out, YesNo(b))
continue
}
if s, ok := value.(string); ok {
out = append(out, NA(s))
continue
}
out = append(out, fmt.Sprintf("%v", value))
}
}
_, _ = fmt.Fprintln(o.w, strings.Join(out, "\t"))
return nil
}
func NA(s string) string {
if s == "" {
return "-"
}
return s
}
func YesNo(b bool) string {
if b {
return "yes"
}
return "no"
}
func fieldName(name string) string {
r := []rune(name)
var out []rune
for i := range r {
if i > 0 && (unicode.IsUpper(r[i])) && (i+1 < len(r) && unicode.IsLower(r[i+1]) || unicode.IsLower(r[i-1])) {
out = append(out, '_')
}
out = append(out, unicode.ToLower(r[i]))
}
return string(out)
}

75
cli/output/table_test.go Normal file
View file

@ -0,0 +1,75 @@
package output
import (
"bytes"
"os"
"strings"
"testing"
)
type writerFlusherStub struct {
bytes.Buffer
}
func (s writerFlusherStub) Flush() error {
return nil
}
type testFieldsStruct struct {
Name string
Number int
}
func TestTableOutput(t *testing.T) {
var wfs writerFlusherStub
to := NewTable(os.Stdout)
to.w = &wfs
t.Run("AddAllowedFields", func(t *testing.T) {
_, _ = to.AddAllowedFields(testFieldsStruct{})
if _, ok := to.allowedFields["name"]; !ok {
t.Error("name should be a allowed field")
}
})
t.Run("AddFieldAlias", func(t *testing.T) {
to.AddFieldAlias("woodpecker_ci", "woodpecker ci")
if alias, ok := to.fieldAlias["woodpecker_ci"]; !ok || alias != "woodpecker ci" {
t.Errorf("woodpecker_ci alias should be 'woodpecker ci', is: %v", alias)
}
})
t.Run("AddFieldOutputFn", func(t *testing.T) {
to.AddFieldFn("woodpecker ci", FieldFn(func(_ any) string {
return "WOODPECKER CI!!!"
}))
if _, ok := to.fieldMapping["woodpecker ci"]; !ok {
t.Errorf("'woodpecker ci' field output fn should be set")
}
})
t.Run("ValidateColumns", func(t *testing.T) {
err := to.ValidateColumns([]string{"non-existent", "NAME"})
if err == nil ||
strings.Contains(err.Error(), "name") ||
!strings.Contains(err.Error(), "non-existent") {
t.Errorf("error should contain 'non-existent' but not 'name': %v", err)
}
})
t.Run("WriteHeader", func(t *testing.T) {
to.WriteHeader([]string{"woodpecker_ci", "name"})
if wfs.String() != "WOODPECKER CI\tNAME\n" {
t.Errorf("written header should be 'WOODPECKER CI\\tNAME\\n', is: %q", wfs.String())
}
wfs.Reset()
})
t.Run("WriteLine", func(t *testing.T) {
_ = to.Write([]string{"woodpecker_ci", "name", "number"}, &testFieldsStruct{"test123", 1000000000})
if wfs.String() != "WOODPECKER CI!!!\ttest123\t1000000000\n" {
t.Errorf("written line should be 'WOODPECKER CI!!!\\ttest123\\t1000000000\\n', is: %q", wfs.String())
}
wfs.Reset()
})
t.Run("Columns", func(t *testing.T) {
if len(to.Columns()) != 3 {
t.Errorf("unexpected number of columns: %v", to.Columns())
}
})
}

View file

@ -15,9 +15,7 @@
package pipeline
import (
"os"
"strings"
"text/template"
"github.com/urfave/cli/v2"
@ -31,8 +29,7 @@ var pipelineCreateCmd = &cli.Command{
Usage: "create new pipeline",
ArgsUsage: "<repo-id|repo-full-name>",
Action: pipelineCreate,
Flags: []cli.Flag{
common.FormatFlag(tmplPipelineList),
Flags: append(common.OutputFlags("table"), []cli.Flag{
&cli.StringFlag{
Name: "branch",
Usage: "branch to create pipeline from",
@ -42,7 +39,7 @@ var pipelineCreateCmd = &cli.Command{
Name: "var",
Usage: "key=value",
},
},
}...),
}
func pipelineCreate(c *cli.Context) error {
@ -76,10 +73,5 @@ func pipelineCreate(c *cli.Context) error {
return err
}
tmpl, err := template.New("_").Parse(c.String("format") + "\n")
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, pipeline)
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline})
}

View file

@ -15,14 +15,13 @@
package pipeline
import (
"os"
"strconv"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var pipelineInfoCmd = &cli.Command{
@ -30,7 +29,7 @@ var pipelineInfoCmd = &cli.Command{
Usage: "show pipeline details",
ArgsUsage: "<repo-id|repo-full-name> [pipeline]",
Action: pipelineInfo,
Flags: []cli.Flag{common.FormatFlag(tmplPipelineInfo)},
Flags: common.OutputFlags("table"),
}
func pipelineInfo(c *cli.Context) error {
@ -65,20 +64,5 @@ func pipelineInfo(c *cli.Context) error {
return err
}
tmpl, err := template.New("_").Parse(c.String("format"))
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, pipeline)
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline})
}
// template for pipeline information
var tmplPipelineInfo = `Number: {{ .Number }}
Status: {{ .Status }}
Event: {{ .Event }}
Commit: {{ .Commit }}
Branch: {{ .Branch }}
Ref: {{ .Ref }}
Message: {{ .Message }}
Author: {{ .Author }}
`

View file

@ -15,13 +15,11 @@
package pipeline
import (
"os"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var pipelineLastCmd = &cli.Command{
@ -29,14 +27,13 @@ var pipelineLastCmd = &cli.Command{
Usage: "show latest pipeline details",
ArgsUsage: "<repo-id|repo-full-name>",
Action: pipelineLast,
Flags: []cli.Flag{
common.FormatFlag(tmplPipelineInfo),
Flags: append(common.OutputFlags("table"), []cli.Flag{
&cli.StringFlag{
Name: "branch",
Usage: "branch name",
Value: "main",
},
},
}...),
}
func pipelineLast(c *cli.Context) error {
@ -55,9 +52,5 @@ func pipelineLast(c *cli.Context) error {
return err
}
tmpl, err := template.New("_").Parse(c.String("format"))
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, pipeline)
return pipelineOutput(c, []woodpecker.Pipeline{*pipeline})
}

View file

@ -15,13 +15,11 @@
package pipeline
import (
"os"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
//nolint:gomnd
@ -29,9 +27,8 @@ var pipelineListCmd = &cli.Command{
Name: "ls",
Usage: "show pipeline history",
ArgsUsage: "<repo-id|repo-full-name>",
Action: pipelineList,
Flags: []cli.Flag{
common.FormatFlag(tmplPipelineList),
Action: List,
Flags: append(common.OutputFlags("table"), []cli.Flag{
&cli.StringFlag{
Name: "branch",
Usage: "branch filter",
@ -49,28 +46,33 @@ var pipelineListCmd = &cli.Command{
Usage: "limit the list size",
Value: 25,
},
},
}...),
}
func pipelineList(c *cli.Context) error {
repoIDOrFullName := c.Args().First()
func List(c *cli.Context) error {
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
resources, err := pipelineList(c, client)
if err != nil {
return err
}
return pipelineOutput(c, resources)
}
func pipelineList(c *cli.Context, client woodpecker.Client) ([]woodpecker.Pipeline, error) {
resources := make([]woodpecker.Pipeline, 0)
repoIDOrFullName := c.Args().First()
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil {
return resources, err
}
pipelines, err := client.PipelineList(repoID)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(c.String("format") + "\n")
if err != nil {
return err
return resources, err
}
branch := c.String("branch")
@ -92,21 +94,9 @@ func pipelineList(c *cli.Context) error {
if status != "" && pipeline.Status != status {
continue
}
if err := tmpl.Execute(os.Stdout, pipeline); err != nil {
return err
}
resources = append(resources, *pipeline)
count++
}
return nil
}
// template for pipeline list information
var tmplPipelineList = "\x1b[33mPipeline #{{ .Number }} \x1b[0m" + `
Status: {{ .Status }}
Event: {{ .Event }}
Commit: {{ .Commit }}
Branch: {{ .Branch }}
Ref: {{ .Ref }}
Author: {{ .Author }} {{ if .Email }}<{{.Email}}>{{ end }}
Message: {{ .Message }}
`
return resources, nil
}

132
cli/pipeline/list_test.go Normal file
View file

@ -0,0 +1,132 @@
package pipeline
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker/mocks"
)
func TestPipelineList(t *testing.T) {
testtases := []struct {
name string
repoID int64
repoErr error
pipelines []*woodpecker.Pipeline
pipelineErr error
args []string
expected []woodpecker.Pipeline
wantErr error
}{
{
name: "success",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
},
{
name: "filter by branch",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--branch", "main", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
},
{
name: "filter by event",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--event", "push", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
},
{
name: "filter by status",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--status", "success", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
},
},
{
name: "limit results",
repoID: 1,
pipelines: []*woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
{ID: 3, Branch: "main", Event: "push", Status: "failure"},
},
args: []string{"ls", "--limit", "2", "repo/name"},
expected: []woodpecker.Pipeline{
{ID: 1, Branch: "main", Event: "push", Status: "success"},
{ID: 2, Branch: "develop", Event: "pull_request", Status: "running"},
},
},
{
name: "pipeline list error",
repoID: 1,
pipelineErr: errors.New("pipeline error"),
args: []string{"ls", "repo/name"},
wantErr: errors.New("pipeline error"),
},
}
for _, tt := range testtases {
t.Run(tt.name, func(t *testing.T) {
mockClient := mocks.NewClient(t)
mockClient.On("PipelineList", mock.Anything).Return(tt.pipelines, tt.pipelineErr)
mockClient.On("RepoLookup", mock.Anything).Return(&woodpecker.Repo{ID: tt.repoID}, nil)
app := &cli.App{Writer: io.Discard}
c := cli.NewContext(app, nil, nil)
command := pipelineListCmd
command.Action = func(c *cli.Context) error {
pipelines, err := pipelineList(c, mockClient)
if tt.wantErr != nil {
assert.EqualError(t, err, tt.wantErr.Error())
return nil
}
assert.NoError(t, err)
assert.EqualValues(t, tt.expected, pipelines)
return nil
}
_ = command.Run(c, tt.args...)
})
}
}

View file

@ -15,7 +15,15 @@
package pipeline
import (
"fmt"
"io"
"os"
"text/template"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/output"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
// Command exports the pipeline command set.
@ -37,3 +45,53 @@ var Command = &cli.Command{
pipelineCreateCmd,
},
}
func pipelineOutput(c *cli.Context, resources []woodpecker.Pipeline, fd ...io.Writer) error {
outfmt, outopt := output.ParseOutputOptions(c.String("output"))
noHeader := c.Bool("output-no-headers")
var out io.Writer
switch len(fd) {
case 0:
out = os.Stdout
case 1:
out = fd[0]
default:
out = os.Stdout
}
switch outfmt {
case "go-template":
if len(outopt) < 1 {
return fmt.Errorf("%w: missing template", output.ErrOutputOptionRequired)
}
tmpl, err := template.New("_").Parse(outopt[0] + "\n")
if err != nil {
return err
}
if err := tmpl.Execute(out, resources); err != nil {
return err
}
case "table":
fallthrough
default:
table := output.NewTable(out)
cols := []string{"Number", "Status", "Event", "Branch", "Commit", "Author"}
if len(outopt) > 0 {
cols = outopt
}
if !noHeader {
table.WriteHeader(cols)
}
for _, resource := range resources {
if err := table.Write(cols, resource); err != nil {
return err
}
}
table.Flush()
}
return nil
}

View file

@ -0,0 +1,86 @@
package pipeline
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
func TestPipelineOutput(t *testing.T) {
tests := []struct {
name string
args []string
expected string
wantErr bool
}{
{
name: "table output with default columns",
args: []string{},
expected: "NUMBER STATUS EVENT BRANCH COMMIT AUTHOR\n1 success push main abcdef John Doe\n",
},
{
name: "table output with custom columns",
args: []string{"output", "--output", "table=Number,Status,Branch"},
expected: "NUMBER STATUS BRANCH\n1 success main\n",
},
{
name: "table output with no header",
args: []string{"output", "--output-no-headers"},
expected: "1 success push main abcdef John Doe\n",
},
{
name: "go-template output",
args: []string{"output", "--output", "go-template={{range . }}{{.Number}} {{.Status}} {{.Branch}}{{end}}"},
expected: "1 success main\n",
},
{
name: "invalid go-template",
args: []string{"output", "--output", "go-template={{.InvalidField}}"},
wantErr: true,
},
}
pipelines := []woodpecker.Pipeline{
{
Number: 1,
Status: "success",
Event: "push",
Branch: "main",
Commit: "abcdef",
Author: "John Doe",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &cli.App{Writer: io.Discard}
c := cli.NewContext(app, nil, nil)
command := &cli.Command{}
command.Name = "output"
command.Flags = common.OutputFlags("table")
command.Action = func(c *cli.Context) error {
var buf bytes.Buffer
err := pipelineOutput(c, pipelines, &buf)
if tt.wantErr {
assert.Error(t, err)
return nil
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, buf.String())
return nil
}
_ = command.Run(c, tt.args...)
})
}
}

View file

@ -26,7 +26,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get agent list",
"summary": "List agents",
"parameters": [
{
"type": "string",
@ -64,13 +64,14 @@ const docTemplate = `{
}
},
"post": {
"description": "Creates a new agent with a random token",
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Create a new agent with a random token so a new agent can connect to the server",
"summary": "Create a new agent",
"parameters": [
{
"type": "string",
@ -108,7 +109,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get agent information",
"summary": "Get an agent",
"parameters": [
{
"type": "string",
@ -173,7 +174,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Update agent information",
"summary": "Update an agent",
"parameters": [
{
"type": "string",
@ -218,7 +219,7 @@ const docTemplate = `{
"tags": [
"Agents"
],
"summary": "Get agent tasks",
"summary": "List agent tasks",
"parameters": [
{
"type": "string",
@ -283,7 +284,7 @@ const docTemplate = `{
"tags": [
"Badges"
],
"summary": "Get status badge, SVG format",
"summary": "Get status of pipeline as SVG badge",
"parameters": [
{
"type": "integer",
@ -744,7 +745,7 @@ const docTemplate = `{
"tags": [
"Organizations"
],
"summary": "Lookup organization by full-name",
"summary": "Lookup an organization by full name",
"parameters": [
{
"type": "string",
@ -756,7 +757,7 @@ const docTemplate = `{
},
{
"type": "string",
"description": "the organizations full-name / slug",
"description": "the organizations full name / slug",
"name": "org_full_name",
"in": "path",
"required": true
@ -781,7 +782,7 @@ const docTemplate = `{
"tags": [
"Orgs"
],
"summary": "Get all orgs",
"summary": "List organizations",
"parameters": [
{
"type": "string",
@ -828,7 +829,7 @@ const docTemplate = `{
"tags": [
"Orgs"
],
"summary": "Delete an org",
"summary": "Delete an organization",
"parameters": [
{
"type": "string",
@ -861,7 +862,7 @@ const docTemplate = `{
"tags": [
"Organization"
],
"summary": "Get organization by id",
"summary": "Get an organization",
"parameters": [
{
"type": "string",
@ -900,7 +901,7 @@ const docTemplate = `{
"tags": [
"Organization permissions"
],
"summary": "Get the permissions of the current user in the given organization",
"summary": "Get the permissions of the currently authenticated user for the given organization",
"parameters": [
{
"type": "string",
@ -939,7 +940,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Get the organization secret list",
"summary": "List organization secrets",
"parameters": [
{
"type": "string",
@ -990,7 +991,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Persist/create an organization secret",
"summary": "Create an organization secret",
"parameters": [
{
"type": "string",
@ -1035,7 +1036,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Get the named organization secret",
"summary": "Get a organization secret by name",
"parameters": [
{
"type": "string",
@ -1076,7 +1077,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Delete the named secret from an organization",
"summary": "Delete an organization secret by name",
"parameters": [
{
"type": "string",
@ -1114,7 +1115,7 @@ const docTemplate = `{
"tags": [
"Organization secrets"
],
"summary": "Update an organization secret",
"summary": "Update an organization secret by name",
"parameters": [
{
"type": "string",
@ -1166,7 +1167,7 @@ const docTemplate = `{
"tags": [
"Pipeline queues"
],
"summary": "List pipeline queues",
"summary": "List pipelines in queue",
"parameters": [
{
"type": "string",
@ -1257,7 +1258,7 @@ const docTemplate = `{
"tags": [
"Pipeline queues"
],
"summary": "Pause a pipeline queue",
"summary": "Pause the pipeline queue",
"parameters": [
{
"type": "string",
@ -1283,7 +1284,7 @@ const docTemplate = `{
"tags": [
"Pipeline queues"
],
"summary": "Resume a pipeline queue",
"summary": "Resume the pipeline queue",
"parameters": [
{
"type": "string",
@ -1303,13 +1304,14 @@ const docTemplate = `{
},
"/repos": {
"get": {
"description": "Returns a list of all repositories. Requires admin rights.",
"produces": [
"application/json"
],
"tags": [
"Repositories"
],
"summary": "List all repositories on the server. Requires admin rights.",
"summary": "List all repositories on the server",
"parameters": [
{
"type": "string",
@ -1395,7 +1397,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Get repository by full-name",
"summary": "Lookup a repository by full name",
"parameters": [
{
"type": "string",
@ -1407,7 +1409,7 @@ const docTemplate = `{
},
{
"type": "string",
"description": "the repository full-name / slug",
"description": "the repository full name / slug",
"name": "repo_full_name",
"in": "path",
"required": true
@ -1425,13 +1427,14 @@ const docTemplate = `{
},
"/repos/repair": {
"post": {
"description": "Executes a repair process on all repositories. Requires admin rights.",
"produces": [
"text/plain"
],
"tags": [
"Repositories"
],
"summary": "Repair all repositories on the server. Requires admin rights.",
"summary": "Repair all repositories on the server",
"parameters": [
{
"type": "string",
@ -1457,7 +1460,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Get repository information",
"summary": "Get a repository",
"parameters": [
{
"type": "string",
@ -1525,7 +1528,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Change a repository",
"summary": "Update a repository",
"parameters": [
{
"type": "string",
@ -1570,7 +1573,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Get repository branches",
"summary": "Get branches of a repository",
"parameters": [
{
"type": "string",
@ -1623,7 +1626,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Change a repository's owner, to the one holding the access token",
"summary": "Change a repository's owner to the currently authenticated user",
"parameters": [
{
"type": "string",
@ -1659,7 +1662,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Get the cron job list",
"summary": "List cron jobs",
"parameters": [
{
"type": "string",
@ -1710,7 +1713,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Persist/creat a cron job",
"summary": "Create a cron job",
"parameters": [
{
"type": "string",
@ -1755,7 +1758,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Get a cron job by id",
"summary": "Get a cron job",
"parameters": [
{
"type": "string",
@ -1837,7 +1840,7 @@ const docTemplate = `{
"tags": [
"Repository cron jobs"
],
"summary": "Delete a cron job by id",
"summary": "Delete a cron job",
"parameters": [
{
"type": "string",
@ -1927,7 +1930,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Deletes log",
"summary": "Deletes all logs of a pipeline",
"parameters": [
{
"type": "string",
@ -1967,7 +1970,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Log information",
"summary": "Get logs for a pipeline step",
"parameters": [
{
"type": "string",
@ -2020,7 +2023,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Deletes step log",
"summary": "Delete step logs of a pipeline",
"parameters": [
{
"type": "string",
@ -2108,7 +2111,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "Repository permission information",
"summary": "Check current authenticated users access to the repository",
"parameters": [
{
"type": "string",
@ -2138,13 +2141,14 @@ const docTemplate = `{
},
"/repos/{repo_id}/pipelines": {
"get": {
"description": "Get a list of pipelines for a repository.",
"produces": [
"application/json"
],
"tags": [
"Pipelines"
],
"summary": "Get pipelines, current running and past ones",
"summary": "List repository pipelines",
"parameters": [
{
"type": "string",
@ -2174,6 +2178,18 @@ const docTemplate = `{
"description": "for response pagination, max items per page",
"name": "perPage",
"in": "query"
},
{
"type": "string",
"description": "only return pipelines before this RFC3339 date",
"name": "before",
"in": "query"
},
{
"type": "string",
"description": "only return pipelines after this RFC3339 date",
"name": "after",
"in": "query"
}
],
"responses": {
@ -2195,7 +2211,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Run/trigger a pipelines",
"summary": "Trigger a manual pipeline",
"parameters": [
{
"type": "string",
@ -2240,7 +2256,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Pipeline information by number",
"summary": "Get a repositories pipeline",
"parameters": [
{
"type": "string",
@ -2327,6 +2343,44 @@ const docTemplate = `{
}
}
}
},
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Pipelines"
],
"summary": "Delete a pipeline",
"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
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/repos/{repo_id}/pipelines/{number}/approve": {
@ -2337,7 +2391,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Start pipelines in gated repos",
"summary": "Approve and start a pipeline",
"parameters": [
{
"type": "string",
@ -2380,7 +2434,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Cancels a pipeline",
"summary": "Cancel a pipeline",
"parameters": [
{
"type": "string",
@ -2420,7 +2474,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Pipeline configuration",
"summary": "Get configuration files for a pipeline",
"parameters": [
{
"type": "string",
@ -2466,7 +2520,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Decline pipelines in gated repos",
"summary": "Decline a pipeline",
"parameters": [
{
"type": "string",
@ -2509,7 +2563,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "List active pull requests",
"summary": "List active pull requests of a repository",
"parameters": [
{
"type": "string",
@ -2562,7 +2616,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Get the registry list",
"summary": "List registries",
"parameters": [
{
"type": "string",
@ -2613,7 +2667,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Persist/create a registry",
"summary": "Create a registry",
"parameters": [
{
"type": "string",
@ -2658,7 +2712,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Get a named registry",
"summary": "Get a registry by name",
"parameters": [
{
"type": "string",
@ -2699,7 +2753,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Delete a named registry",
"summary": "Delete a registry by name",
"parameters": [
{
"type": "string",
@ -2737,7 +2791,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Update a named registry",
"summary": "Update a registry by name",
"parameters": [
{
"type": "string",
@ -2822,7 +2876,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Get the secret list",
"summary": "List repository secrets",
"parameters": [
{
"type": "string",
@ -2873,7 +2927,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Persist/create a secret",
"summary": "Create a repository secret",
"parameters": [
{
"type": "string",
@ -2918,7 +2972,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Get a named secret",
"summary": "Get a repository secret by name",
"parameters": [
{
"type": "string",
@ -2959,7 +3013,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Delete a named secret",
"summary": "Delete a repository secret by name",
"parameters": [
{
"type": "string",
@ -2997,7 +3051,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Update a named secret",
"summary": "Update a repository secret by name",
"parameters": [
{
"type": "string",
@ -3049,7 +3103,7 @@ const docTemplate = `{
"tags": [
"Secrets"
],
"summary": "Get the global secret list",
"summary": "List global secrets",
"parameters": [
{
"type": "string",
@ -3093,7 +3147,7 @@ const docTemplate = `{
"tags": [
"Secrets"
],
"summary": "Persist/create a global secret",
"summary": "Create a global secret",
"parameters": [
{
"type": "string",
@ -3261,14 +3315,14 @@ const docTemplate = `{
},
"/stream/events": {
"get": {
"description": "event source streaming for compatibility with quic and http2",
"description": "With quic and http2 support",
"produces": [
"text/plain"
],
"tags": [
"Events"
],
"summary": "Event stream",
"summary": "Stream events like pipeline updates",
"responses": {
"200": {
"description": "OK"
@ -3284,7 +3338,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Log stream",
"summary": "Stream logs of a pipeline step",
"parameters": [
{
"type": "integer",
@ -3323,7 +3377,7 @@ const docTemplate = `{
"tags": [
"User"
],
"summary": "Returns the currently authenticated user.",
"summary": "Get the currently authenticated user",
"parameters": [
{
"type": "string",
@ -3346,14 +3400,14 @@ const docTemplate = `{
},
"/user/feed": {
"get": {
"description": "Feed entries can be used to display information on the latest builds.",
"description": "The feed lists the most recent pipeline for the currently authenticated user.",
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "A feed entry for a build.",
"summary": "Get the currently authenticaed users pipeline feed",
"parameters": [
{
"type": "string",
@ -3368,7 +3422,10 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Feed"
"type": "array",
"items": {
"$ref": "#/definitions/Feed"
}
}
}
}
@ -3383,7 +3440,7 @@ const docTemplate = `{
"tags": [
"User"
],
"summary": "Get user's repos",
"summary": "Get user's repositories",
"parameters": [
{
"type": "string",
@ -3473,7 +3530,7 @@ const docTemplate = `{
"tags": [
"Users"
],
"summary": "Get all users",
"summary": "List users",
"parameters": [
{
"type": "string",
@ -3627,7 +3684,7 @@ const docTemplate = `{
"tags": [
"Users"
],
"summary": "Change a user",
"summary": "Update a user",
"parameters": [
{
"type": "string",
@ -3891,6 +3948,9 @@ const docTemplate = `{
"Org": {
"type": "object",
"properties": {
"forge_id": {
"type": "integer"
},
"id": {
"type": "integer"
},
@ -4122,6 +4182,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"
@ -4418,6 +4481,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"

View file

@ -246,11 +246,6 @@ var flags = append([]cli.Flag{
Usage: "Disable version check in admin web ui.",
Name: "skip-version-check",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ADDON_FORGE"},
Name: "addon-forge",
Usage: "forge addon",
},
//
// 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",
@ -346,11 +352,6 @@ var flags = append([]cli.Flag{
Usage: "github tokens should only get access to public repos",
Value: false,
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_GITHUB_SKIP_VERIFY"},
Name: "github-skip-verify",
Usage: "github skip ssl verification",
},
//
// Gitea
//
@ -359,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",
@ -395,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
//
@ -415,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)
//
@ -446,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,13 +29,6 @@ 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/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/queue"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
@ -100,103 +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) {
switch {
case c.String("addon-forge") != "":
return addon.Load(c.String("addon-forge"))
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"),
OnlyPublic: c.Bool("github-public-only"),
}
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,28 +1,34 @@
# 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:22-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.
COPY --from=golang_image /usr/local/go /usr/local/go
COPY Makefile /
ENV PATH=$PATH:/usr/local/go/bin
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
# 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

@ -161,6 +161,9 @@ Only build steps can define commands. You cannot use commands with plugins or se
Allows you to specify the entrypoint for containers. Note that this must be a list of the command and its arguments (e.g. `["/bin/sh", "-c"]`).
If you define [`commands`](#commands), the default entrypoint will be `["/bin/sh", "-c", "echo $CI_SCRIPT | base64 -d | /bin/sh -e"]`.
You can also use a custom shell with `CI_SCRIPT` (Base64-encoded) if you set `commands`.
### `environment`
Woodpecker provides the ability to pass environment variables to individual steps.
@ -359,20 +362,6 @@ when:
- platform: [linux/*, windows/amd64]
```
<!-- markdownlint-disable no-duplicate-heading -->
#### `environment`
<!-- markdownlint-enable no-duplicate-heading -->
Execute a step for deployment events matching the target deployment environment:
```yaml
when:
- environment: production
- event: deployment
```
#### `matrix`
Execute a step for a single matrix permutation:
@ -758,7 +747,7 @@ Workflows that should run even on failure should set the `runs_on` tag. See [her
Woodpecker gives the ability to configure privileged mode in the YAML. You can use this parameter to launch containers with escalated capabilities.
:::info
Privileged mode is only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./71-project-settings.md#trusted) to enable trusted mode.
Privileged mode is only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./75-project-settings.md#trusted) to enable trusted mode.
:::
```diff

View file

@ -6,7 +6,7 @@ In case there is a single configuration in `.woodpecker.yaml` Woodpecker will cr
By placing the configurations in a folder which is by default named `.woodpecker/` Woodpecker will create a pipeline with multiple workflows each named by the file they are defined in. Only `.yml` and `.yaml` files will be used and files in any subfolders like `.woodpecker/sub-folder/test.yaml` will be ignored.
You can also set some custom path like `.my-ci/pipelines/` instead of `.woodpecker/` in the [project settings](./71-project-settings.md).
You can also set some custom path like `.my-ci/pipelines/` instead of `.woodpecker/` in the [project settings](./75-project-settings.md).
## Benefits of using workflows

View file

@ -3,7 +3,7 @@
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.
:::note
Volumes are only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./71-project-settings.md#trusted) to enable trusted mode.
Volumes are only available to trusted repositories and for security reasons should only be used in private environments. See [project settings](./75-project-settings.md#trusted) to enable trusted mode.
:::
```diff

View file

@ -0,0 +1,62 @@
# Linter
Woodpecker automatically lints your workflow files for errors, deprecations and bad habits. Errors and warnings are shown in the UI for any pipelines.
![errors and warnings in UI](./linter-warnings-errors.png)
## Running the linter from CLI
You can run the linter also manually from the CLI:
```shell
woodpecker-cli lint <workflow files>
```
## Bad habit warnings
Woodpecker warns you if your configuration contains some bad habits.
### Event filter for all steps
All your items in `when` blocks should have an `event` filter, so no step runs on all events. This is recommended because if new events are added, your steps probably shouldn't run on those as well.
Examples of an **incorrect** config for this rule:
```yaml
when:
- branch: main
- event: tag
```
This will trigger the warning because the first item (`branch: main`) does not filter with an event.
```yaml
steps:
- name: test
when:
branch: main
- name: deploy
when:
event: tag
```
Examples of a **correct** config for this rule:
```yaml
when:
- branch: main
event: push
- event: tag
```
```yaml
steps:
- name: test
when:
event: [tag, push]
- name: deploy
when:
- event: tag
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -1,6 +1,6 @@
# 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.
If the forge you're using does not comply with [Woodpecker's requirements](../../92-development/02-core-ideas.md#forges) 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.

View file

@ -11,6 +11,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Deprecated uppercasing all secret env vars, instead, the value of the `secrets` property is used. [Read more](./20-usage/40-secrets.md#use-secrets-in-commands)
- Deprecated alternative names for secrets, use `environment` with `from_secret`
- Deprecated slice definition for env vars
- Deprecated `environment` filter, use `when.evaluate`
## 2.0.0
@ -66,7 +67,7 @@ Some versions need some changes to the server configuration or the pipeline conf
Only projects created after updating will have an empty value by default. Existing projects will stick to the current pipeline path which is `.drone.yml` in most cases.
Read more about it at the [Project Settings](./20-usage/71-project-settings.md#pipeline-path)
Read more about it at the [Project Settings](./20-usage/75-project-settings.md#pipeline-path)
- From version `0.15.0` ongoing there will be three types of docker images: `latest`, `next` and `x.x.x` with an alpine variant for each type like `latest-alpine`.
If you used `latest` before to try pre-release features you should switch to `next` after this release.

View file

@ -10,6 +10,7 @@ const config: Config = {
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'throw',
onBrokenAnchors: 'throw',
onDuplicateRoutes: 'throw',
organizationName: 'woodpecker-ci',
projectName: 'woodpecker-ci.github.io',

View file

@ -43,7 +43,7 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.1.0",
"@docusaurus/tsconfig": "3.1.1",
"@docusaurus/tsconfig": "3.2.1",
"@docusaurus/types": "^3.1.0",
"@types/node": "^20.11.30",
"@types/react": "^18.2.67",
@ -53,8 +53,8 @@
},
"pnpm": {
"overrides": {
"trim": "^0.0.3",
"got": "^11.8.5"
"trim": "^1.0.0",
"got": "^14.0.0"
}
}
}

View file

@ -190,10 +190,20 @@
"docs": "https://codeberg.org/woodpecker-plugins/mastodon-post/raw/branch/main/docs.md",
"verified": true
},
{
"name": "Discord",
"docs": "https://raw.githubusercontent.com/appleboy/drone-discord/master/DOCS.md",
"verified": false
},
{
"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
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -640,7 +640,7 @@ You can manually configure the clone step in your workflow for customization:
```diff
+clone:
+ git:
+ - name: git
+ image: woodpeckerci/plugin-git
steps:
@ -666,7 +666,7 @@ Example configuration to use a custom clone plugin:
```diff
clone:
git:
- name: git
+ image: octocat/custom-git-plugin
```

8
go.mod
View file

@ -57,11 +57,11 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0
github.com/zalando/go-keyring v0.2.4
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.21.0
golang.org/x/net v0.22.0
golang.org/x/crypto v0.22.0
golang.org/x/net v0.24.0
golang.org/x/oauth2 v0.18.0
golang.org/x/sync v0.6.0
golang.org/x/term v0.18.0
golang.org/x/term v0.19.0
golang.org/x/text v0.14.0
google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0
@ -170,7 +170,7 @@ require (
go.uber.org/zap v1.26.0 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/appengine v1.6.8 // indirect

16
go.sum
View file

@ -540,8 +540,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
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=
@ -573,8 +573,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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=
@ -619,16 +619,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View file

@ -18,21 +18,19 @@ import (
"encoding/base64"
)
func GenerateContainerConf(commands []string, goos string) (env map[string]string, entry []string, cmd string) {
func GenerateContainerConf(commands []string, goos string) (env map[string]string, entry []string) {
env = make(map[string]string)
if goos == "windows" {
env["CI_SCRIPT"] = base64.StdEncoding.EncodeToString([]byte(generateScriptWindows(commands)))
env["HOME"] = "c:\\root"
env["SHELL"] = "powershell.exe"
entry = []string{"powershell", "-noprofile", "-noninteractive", "-command"}
cmd = "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex"
entry = []string{"powershell", "-noprofile", "-noninteractive", "-command", "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex"}
} else {
env["CI_SCRIPT"] = base64.StdEncoding.EncodeToString([]byte(generateScriptPosix(commands)))
env["HOME"] = "/root"
env["SHELL"] = "/bin/sh"
entry = []string{"/bin/sh", "-c"}
cmd = "echo $CI_SCRIPT | base64 -d | /bin/sh -e"
entry = []string{"/bin/sh", "-c", "echo $CI_SCRIPT | base64 -d | /bin/sh -e"}
}
return env, entry, cmd
return env, entry
}

View file

@ -12,16 +12,14 @@ const (
)
func TestGenerateContainerConf(t *testing.T) {
gotEnv, gotEntry, gotCmd := GenerateContainerConf([]string{"echo hello world"}, "windows")
gotEnv, gotEntry := GenerateContainerConf([]string{"echo hello world"}, "windows")
assert.Equal(t, windowsScriptBase64, gotEnv["CI_SCRIPT"])
assert.Equal(t, "c:\\root", gotEnv["HOME"])
assert.Equal(t, "powershell.exe", gotEnv["SHELL"])
assert.Equal(t, []string{"powershell", "-noprofile", "-noninteractive", "-command"}, gotEntry)
assert.Equal(t, "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex", gotCmd)
gotEnv, gotEntry, gotCmd = GenerateContainerConf([]string{"echo hello world"}, "linux")
assert.Equal(t, []string{"powershell", "-noprofile", "-noninteractive", "-command", "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex"}, gotEntry)
gotEnv, gotEntry = GenerateContainerConf([]string{"echo hello world"}, "linux")
assert.Equal(t, posixScriptBase64, gotEnv["CI_SCRIPT"])
assert.Equal(t, "/root", gotEnv["HOME"])
assert.Equal(t, "/bin/sh", gotEnv["SHELL"])
assert.Equal(t, []string{"/bin/sh", "-c"}, gotEntry)
assert.Equal(t, "echo $CI_SCRIPT | base64 -d | /bin/sh -e", gotCmd)
assert.Equal(t, []string{"/bin/sh", "-c", "echo $CI_SCRIPT | base64 -d | /bin/sh -e"}, gotEntry)
}

View file

@ -45,16 +45,15 @@ func (e *docker) toConfig(step *types.Step) *container.Config {
configEnv := make(map[string]string)
maps.Copy(configEnv, step.Environment)
if len(step.Commands) != 0 {
env, entry, cmd := common.GenerateContainerConf(step.Commands, e.info.OSType)
if len(step.Commands) > 0 {
env, entry := common.GenerateContainerConf(step.Commands, e.info.OSType)
for k, v := range env {
configEnv[k] = v
}
if len(step.Entrypoint) > 0 {
entry = step.Entrypoint
}
config.Entrypoint = entry
config.Cmd = []string{cmd}
}
if len(step.Entrypoint) > 0 {
config.Entrypoint = step.Entrypoint
}
if len(configEnv) != 0 {

View file

@ -105,8 +105,7 @@ func TestToConfigSmall(t *testing.T) {
assert.EqualValues(t, &container.Config{
AttachStdout: true,
AttachStderr: true,
Cmd: []string{"echo $CI_SCRIPT | base64 -d | /bin/sh -e"},
Entrypoint: []string{"/bin/sh", "-c"},
Entrypoint: []string{"/bin/sh", "-c", "echo $CI_SCRIPT | base64 -d | /bin/sh -e"},
Labels: map[string]string{
"wp_step": "test",
"wp_uuid": "09238932",
@ -163,8 +162,7 @@ func TestToConfigFull(t *testing.T) {
WorkingDir: "/src/abc",
AttachStdout: true,
AttachStderr: true,
Cmd: []string{"echo $CI_SCRIPT | base64 -d | /bin/sh -e"},
Entrypoint: []string{"/bin/sh", "-c"},
Entrypoint: []string{"/bin/sh", "-c", "echo $CI_SCRIPT | base64 -d | /bin/sh -e"},
Labels: map[string]string{
"wp_step": "test",
"wp_uuid": "09238932",

View file

@ -147,15 +147,14 @@ func podContainer(step *types.Step, podName, goos string, options BackendOptions
container.ImagePullPolicy = v1.PullAlways
}
if len(step.Commands) != 0 {
scriptEnv, command, args := common.GenerateContainerConf(step.Commands, goos)
if len(step.Entrypoint) > 0 {
command = step.Entrypoint
}
if len(step.Commands) > 0 {
scriptEnv, command := common.GenerateContainerConf(step.Commands, goos)
container.Command = command
container.Args = []string{args}
maps.Copy(step.Environment, scriptEnv)
}
if len(step.Entrypoint) > 0 {
container.Command = step.Entrypoint
}
container.Env = mapToEnvVars(step.Environment)

View file

@ -90,9 +90,7 @@ func TestTinyPod(t *testing.T) {
"image": "gradle:8.4.0-jdk21",
"command": [
"/bin/sh",
"-c"
],
"args": [
"-c",
"echo $CI_SCRIPT | base64 -d | /bin/sh -e"
],
"workingDir": "/woodpecker/src",
@ -183,9 +181,6 @@ func TestFullPod(t *testing.T) {
"/bin/sh",
"-c"
],
"args": [
"echo $CI_SCRIPT | base64 -d | /bin/sh -e"
],
"workingDir": "/woodpecker/src",
"ports": [
{
@ -415,3 +410,49 @@ func TestPodPrivilege(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, true, *pod.Spec.SecurityContext.RunAsNonRoot)
}
func TestScratchPod(t *testing.T) {
expected := `
{
"metadata": {
"name": "wp-01he8bebctabr3kgk0qj36d2me-0",
"namespace": "woodpecker",
"creationTimestamp": null,
"labels": {
"step": "curl-google"
}
},
"spec": {
"containers": [
{
"name": "wp-01he8bebctabr3kgk0qj36d2me-0",
"image": "quay.io/curl/curl",
"command": [
"/usr/bin/curl",
"-v",
"google.com"
],
"resources": {}
}
],
"restartPolicy": "Never"
},
"status": {}
}`
pod, err := mkPod(&types.Step{
Name: "curl-google",
Image: "quay.io/curl/curl",
Entrypoint: []string{"/usr/bin/curl", "-v", "google.com"},
}, &config{
Namespace: "woodpecker",
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64", BackendOptions{})
assert.NoError(t, err)
podJSON, err := json.Marshal(pod)
assert.NoError(t, err)
ja := jsonassert.New(t)
t.Log(string(podJSON))
ja.Assertf(string(podJSON), expected)
}

View file

@ -19,6 +19,12 @@ type DeprecationErrorData struct {
Docs string `json:"docs"`
}
type BadHabitErrorData struct {
File string `json:"file"`
Field string `json:"field"`
Docs string `json:"docs"`
}
func GetLinterData(e *types.PipelineError) *LinterErrorData {
if e.Type != types.PipelineErrorTypeLinter {
return nil

View file

@ -305,7 +305,39 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) {
Data: errors.DeprecationErrorData{
File: config.File,
Field: fmt.Sprintf("steps.%s.secrets[%d]", step.Name, i),
Docs: "https://woodpecker-ci.org/docs/usage/workflow-syntax#event",
Docs: "https://woodpecker-ci.org/docs/usage/secrets#use-secrets-in-settings-and-environment",
},
IsWarning: true,
})
}
}
}
for i, c := range parsed.When.Constraints {
if !c.Environment.IsEmpty() {
err = multierr.Append(err, &errorTypes.PipelineError{
Type: errorTypes.PipelineErrorTypeDeprecation,
Message: "environment filters are deprecated, use evaluate with CI_PIPELINE_DEPLOY_TARGET",
Data: errors.DeprecationErrorData{
File: config.File,
Field: fmt.Sprintf("when[%d].environment", i),
Docs: "https://woodpecker-ci.org/docs/usage/workflow-syntax#evaluate",
},
IsWarning: true,
})
}
}
for _, step := range parsed.Steps.ContainerList {
for i, c := range step.When.Constraints {
if !c.Environment.IsEmpty() {
err = multierr.Append(err, &errorTypes.PipelineError{
Type: errorTypes.PipelineErrorTypeDeprecation,
Message: "environment filters are deprecated, use evaluate with CI_PIPELINE_DEPLOY_TARGET",
Data: errors.DeprecationErrorData{
File: config.File,
Field: fmt.Sprintf("steps.%s.when[%d].environment", step.Name, i),
Docs: "https://woodpecker-ci.org/docs/usage/workflow-syntax#evaluate",
},
IsWarning: true,
})
@ -351,10 +383,11 @@ func (l *Linter) lintBadHabits(config *WorkflowConfig) (err error) {
if field != "" {
err = multierr.Append(err, &errorTypes.PipelineError{
Type: errorTypes.PipelineErrorTypeBadHabit,
Message: "Please set an event filter on all when branches",
Data: errors.LinterErrorData{
Message: "Please set an event filter for all steps or the whole workflow on all items of the when block",
Data: errors.BadHabitErrorData{
File: config.File,
Field: field,
Docs: "https://woodpecker-ci.org/docs/usage/linter#event-filter-for-all-steps",
},
IsWarning: true,
})

View file

@ -189,11 +189,11 @@ func TestBadHabits(t *testing.T) {
}{
{
from: "steps: { build: { image: golang } }",
want: "Please set an event filter on all when branches",
want: "Please set an event filter for all steps or the whole workflow on all items of the when block",
},
{
from: "when: [{branch: xyz}, {event: push}]\nsteps: { build: { image: golang } }",
want: "Please set an event filter on all when branches",
want: "Please set an event filter for all steps or the whole workflow on all items of the when block",
},
}

View file

@ -14,6 +14,14 @@ steps:
image: golang
commands: go test
entrypoint:
image: alpine
entrypoint: ['some_entry', '--some-flag']
singla-entrypoint:
image: alpine
entrypoint: some_entry
commands:
privileged: true
image: golang

View file

@ -371,6 +371,21 @@
},
"backend_options": {
"$ref": "#/definitions/step_backend_options"
},
"entrypoint": {
"description": "Defines container entrypoint.",
"oneOf": [
{
"type": "array",
"minLength": 1,
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
}
}
},

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

@ -30,7 +30,7 @@ import (
// GetAgents
//
// @Summary Get agent list
// @Summary List agents
// @Router /agents [get]
// @Produce json
// @Success 200 {array} Agent
@ -49,7 +49,7 @@ func GetAgents(c *gin.Context) {
// GetAgent
//
// @Summary Get agent information
// @Summary Get an agent
// @Router /agents/{agent} [get]
// @Produce json
// @Success 200 {object} Agent
@ -73,7 +73,7 @@ func GetAgent(c *gin.Context) {
// GetAgentTasks
//
// @Summary Get agent tasks
// @Summary List agent tasks
// @Router /agents/{agent}/tasks [get]
// @Produce json
// @Success 200 {array} Task
@ -106,7 +106,7 @@ func GetAgentTasks(c *gin.Context) {
// PatchAgent
//
// @Summary Update agent information
// @Summary Update an agent
// @Router /agents/{agent} [patch]
// @Produce json
// @Success 200 {object} Agent
@ -152,7 +152,8 @@ func PatchAgent(c *gin.Context) {
// PostAgent
//
// @Summary Create a new agent with a random token so a new agent can connect to the server
// @Summary Create a new agent
// @Description Creates a new agent with a random token
// @Router /agents [post]
// @Produce json
// @Success 200 {object} Agent

View file

@ -37,7 +37,7 @@ import (
// GetBadge
//
// @Summary Get status badge, SVG format
// @Summary Get status of pipeline as SVG badge
// @Router /badges/{repo_id}/status.svg [get]
// @Produce image/svg+xml
// @Success 200
@ -120,7 +120,7 @@ func GetCC(c *gin.Context) {
return
}
pipelines, err := _store.GetPipelineList(repo, &model.ListOptions{Page: 1, PerPage: 1})
pipelines, err := _store.GetPipelineList(repo, &model.ListOptions{Page: 1, PerPage: 1}, nil)
if err != nil && !errors.Is(err, types.RecordNotExist) {
log.Warn().Err(err).Msg("could not get pipeline list")
c.AbortWithStatus(http.StatusInternalServerError)

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"
@ -31,7 +32,7 @@ import (
// GetCron
//
// @Summary Get a cron job by id
// @Summary Get a cron job
// @Router /repos/{repo_id}/cron/{cron} [get]
// @Produce json
// @Success 200 {object} 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
@ -97,7 +98,7 @@ func RunCron(c *gin.Context) {
// PostCron
//
// @Summary Persist/creat a cron job
// @Summary Create a cron job
// @Router /repos/{repo_id}/cron [post]
// @Produce json
// @Success 200 {object} Cron
@ -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
@ -222,7 +233,7 @@ func PatchCron(c *gin.Context) {
// GetCronList
//
// @Summary Get the cron job list
// @Summary List cron jobs
// @Router /repos/{repo_id}/cron [get]
// @Produce json
// @Success 200 {array} Cron
@ -243,7 +254,7 @@ func GetCronList(c *gin.Context) {
// DeleteCron
//
// @Summary Delete a cron job by id
// @Summary Delete a cron job
// @Router /repos/{repo_id}/cron/{cron} [delete]
// @Produce plain
// @Success 204

View file

@ -26,7 +26,7 @@ import (
// GetGlobalSecretList
//
// @Summary Get the global secret list
// @Summary List global secrets
// @Router /secrets [get]
// @Produce json
// @Success 200 {array} Secret
@ -71,7 +71,7 @@ func GetGlobalSecret(c *gin.Context) {
// PostGlobalSecret
//
// @Summary Persist/create a global secret
// @Summary Create a global secret
// @Router /secrets [post]
// @Produce json
// @Success 200 {object} Secret

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,23 @@ 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)
}
// pipelineDeleteAllowed checks if the given pipeline can be deleted based on its status.
// It returns a bool indicating if delete is allowed, and the pipeline's status.
func pipelineDeleteAllowed(pl *model.Pipeline) bool {
switch pl.Status {
case model.StatusRunning, model.StatusPending, model.StatusBlocked:
return false
}
return true
}

View file

@ -52,7 +52,7 @@ func GetQueueInfo(c *gin.Context) {
// PauseQueue
//
// @Summary Pause a pipeline queue
// @Summary Pause the pipeline queue
// @Router /queue/pause [post]
// @Produce plain
// @Success 204
@ -65,7 +65,7 @@ func PauseQueue(c *gin.Context) {
// ResumeQueue
//
// @Summary Resume a pipeline queue
// @Summary Resume the pipeline queue
// @Router /queue/resume [post]
// @Produce plain
// @Success 204
@ -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

@ -30,7 +30,7 @@ import (
// GetOrg
//
// @Summary Get organization by id
// @Summary Get an organization
// @Router /orgs/{org_id} [get]
// @Produce json
// @Success 200 {array} Org
@ -57,7 +57,7 @@ func GetOrg(c *gin.Context) {
// GetOrgPermissions
//
// @Summary Get the permissions of the current user in the given organization
// @Summary Get the permissions of the currently authenticated user for the given organization
// @Router /orgs/{org_id}/permissions [get]
// @Produce json
// @Success 200 {array} OrgPerm
@ -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
@ -107,15 +114,22 @@ func GetOrgPermissions(c *gin.Context) {
// LookupOrg
//
// @Summary Lookup organization by full-name
// @Summary Lookup an organization by full name
// @Router /org/lookup/{org_full_name} [get]
// @Produce json
// @Success 200 {object} Org
// @Tags Organizations
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_full_name path string true "the organizations full-name / slug"
// @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,7 +27,7 @@ import (
// GetOrgSecret
//
// @Summary Get the named organization secret
// @Summary Get a organization secret by name
// @Router /orgs/{org_id}/secrets/{secret} [get]
// @Produce json
// @Success 200 {object} Secret
@ -55,7 +55,7 @@ func GetOrgSecret(c *gin.Context) {
// GetOrgSecretList
//
// @Summary Get the organization secret list
// @Summary List organization secrets
// @Router /orgs/{org_id}/secrets [get]
// @Produce json
// @Success 200 {array} Secret
@ -87,7 +87,7 @@ func GetOrgSecretList(c *gin.Context) {
// PostOrgSecret
//
// @Summary Persist/create an organization secret
// @Summary Create an organization secret
// @Router /orgs/{org_id}/secrets [post]
// @Produce json
// @Success 200 {object} Secret
@ -129,7 +129,7 @@ func PostOrgSecret(c *gin.Context) {
// PatchOrgSecret
//
// @Summary Update an organization secret
// @Summary Update an organization secret by name
// @Router /orgs/{org_id}/secrets/{secret} [patch]
// @Produce json
// @Success 200 {object} Secret
@ -183,7 +183,7 @@ func PatchOrgSecret(c *gin.Context) {
// DeleteOrgSecret
//
// @Summary Delete the named secret from an organization
// @Summary Delete an organization secret by name
// @Router /orgs/{org_id}/secrets/{secret} [delete]
// @Produce plain
// @Success 204

View file

@ -26,7 +26,7 @@ import (
// GetOrgs
//
// @Summary Get all orgs
// @Summary List organizations
// @Description Returns all registered orgs in the system. Requires admin rights.
// @Router /orgs [get]
// @Produce json
@ -46,7 +46,7 @@ func GetOrgs(c *gin.Context) {
// DeleteOrg
//
// @Summary Delete an org
// @Summary Delete an organization
// @Description Deletes the given org. Requires admin rights.
// @Router /orgs/{id} [delete]
// @Produce plain

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"
@ -37,7 +38,7 @@ import (
// CreatePipeline
//
// @Summary Run/trigger a pipelines
// @Summary Trigger a manual pipeline
// @Router /repos/{repo_id}/pipelines [post]
// @Produce json
// @Success 200 {object} Pipeline
@ -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)
@ -93,7 +100,8 @@ func createTmpPipeline(event model.WebhookEvent, commit *model.Commit, user *mod
// GetPipelines
//
// @Summary Get pipelines, current running and past ones
// @Summary List repository pipelines
// @Description Get a list of pipelines for a repository.
// @Router /repos/{repo_id}/pipelines [get]
// @Produce json
// @Success 200 {array} Pipeline
@ -102,10 +110,34 @@ func createTmpPipeline(event model.WebhookEvent, commit *model.Commit, user *mod
// @Param repo_id path int true "the repository id"
// @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50)
// @Param before query string false "only return pipelines before this RFC3339 date"
// @Param after query string false "only return pipelines after this RFC3339 date"
func GetPipelines(c *gin.Context) {
repo := session.Repo(c)
before := c.Query("before")
after := c.Query("after")
pipelines, err := store.FromContext(c).GetPipelineList(repo, session.Pagination(c))
filter := new(model.PipelineFilter)
if before != "" {
beforeDt, err := time.Parse(time.RFC3339, before)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
filter.Before = beforeDt.Unix()
}
if after != "" {
afterDt, err := time.Parse(time.RFC3339, after)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
filter.After = afterDt.Unix()
}
pipelines, err := store.FromContext(c).GetPipelineList(repo, session.Pagination(c), filter)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@ -113,9 +145,49 @@ func GetPipelines(c *gin.Context) {
c.JSON(http.StatusOK, pipelines)
}
// DeletePipeline
//
// @Summary Delete a pipeline
// @Router /repos/{repo_id}/pipelines/{number} [delete]
// @Produce plain
// @Success 204
// @Tags Pipelines
// @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"
func DeletePipeline(c *gin.Context) {
_store := store.FromContext(c)
repo := session.Repo(c)
num, err := strconv.ParseInt(c.Param("number"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
pl, err := _store.GetPipelineNumber(repo, num)
if err != nil {
handleDBError(c, err)
return
}
if ok := pipelineDeleteAllowed(pl); !ok {
c.String(http.StatusUnprocessableEntity, "Cannot delete pipeline with status %s", pl.Status)
return
}
err = store.FromContext(c).DeletePipeline(pl)
if err != nil {
c.String(http.StatusInternalServerError, "Error deleting pipeline. %s", err)
return
}
c.Status(http.StatusNoContent)
}
// GetPipeline
//
// @Summary Pipeline information by number
// @Summary Get a repositories pipeline
// @Router /repos/{repo_id}/pipelines/{number} [get]
// @Produce json
// @Success 200 {object} Pipeline
@ -170,7 +242,7 @@ func GetPipelineLast(c *gin.Context) {
// GetStepLogs
//
// @Summary Log information
// @Summary Get logs for a pipeline step
// @Router /repos/{repo_id}/logs/{number}/{stepID} [get]
// @Produce json
// @Success 200 {array} LogEntry
@ -226,7 +298,7 @@ func GetStepLogs(c *gin.Context) {
// DeleteStepLogs
//
// @Summary Deletes step log
// @Summary Delete step logs of a pipeline
// @Router /repos/{repo_id}/logs/{number}/{stepId} [delete]
// @Produce plain
// @Success 204
@ -286,7 +358,7 @@ func DeleteStepLogs(c *gin.Context) {
// GetPipelineConfig
//
// @Summary Pipeline configuration
// @Summary Get configuration files for a pipeline
// @Router /repos/{repo_id}/pipelines/{number}/config [get]
// @Produce json
// @Success 200 {array} Config
@ -320,7 +392,7 @@ func GetPipelineConfig(c *gin.Context) {
// CancelPipeline
//
// @Summary Cancels a pipeline
// @Summary Cancel a pipeline
// @Router /repos/{repo_id}/pipelines/{number}/cancel [post]
// @Produce plain
// @Success 200
@ -332,6 +404,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)
@ -340,7 +419,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)
@ -349,7 +428,7 @@ func CancelPipeline(c *gin.Context) {
// PostApproval
//
// @Summary Start pipelines in gated repos
// @Summary Approve and start a pipeline
// @Router /repos/{repo_id}/pipelines/{number}/approve [post]
// @Produce json
// @Success 200 {object} Pipeline
@ -381,7 +460,7 @@ func PostApproval(c *gin.Context) {
// PostDecline
//
// @Summary Decline pipelines in gated repos
// @Summary Decline a pipeline
// @Router /repos/{repo_id}/pipelines/{number}/decline [post]
// @Produce json
// @Success 200 {object} Pipeline
@ -413,7 +492,7 @@ func PostDecline(c *gin.Context) {
// GetPipelineQueue
//
// @Summary List pipeline queues
// @Summary List pipelines in queue
// @Router /pipelines [get]
// @Produce json
// @Success 200 {array} Feed
@ -510,7 +589,7 @@ func PostPipeline(c *gin.Context) {
// DeletePipelineLogs
//
// @Summary Deletes log
// @Summary Deletes all logs of a pipeline
// @Router /repos/{repo_id}/logs/{number} [delete]
// @Produce plain
// @Success 204
@ -536,9 +615,8 @@ func DeletePipelineLogs(c *gin.Context) {
return
}
switch pl.Status {
case model.StatusRunning, model.StatusPending:
c.String(http.StatusUnprocessableEntity, "Cannot delete logs for a pending or running pipeline")
if ok := pipelineDeleteAllowed(pl); !ok {
c.String(http.StatusUnprocessableEntity, "Cannot delete logs for pipeline with status %s", pl.Status)
return
}
@ -548,7 +626,7 @@ func DeletePipelineLogs(c *gin.Context) {
}
}
if err != nil {
c.String(http.StatusInternalServerError, "There was a problem deleting your logs. %s", err)
c.String(http.StatusInternalServerError, "Error deleting pipeline logs. %s", err)
return
}

129
server/api/pipeline_test.go Normal file
View file

@ -0,0 +1,129 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/franela/goblin"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
)
var fakePipeline = &model.Pipeline{
Status: model.StatusSuccess,
}
func TestGetPipelines(t *testing.T) {
gin.SetMode(gin.TestMode)
g := goblin.Goblin(t)
g.Describe("Pipeline", func() {
g.It("should get pipelines", func() {
pipelines := []*model.Pipeline{fakePipeline}
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("store", mockStore)
GetPipelines(c)
mockStore.AssertCalled(t, "GetPipelineList", mock.Anything, mock.Anything, mock.Anything)
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
g.It("should not parse pipeline filter", func() {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("DELETE", "/?before=2023-01-16&after=2023-01-15", nil)
GetPipelines(c)
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
})
g.It("should parse pipeline filter", func() {
pipelines := []*model.Pipeline{fakePipeline}
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Request, _ = http.NewRequest("DELETE", "/?2023-01-16T15:00:00Z&after=2023-01-15T15:00:00Z", nil)
GetPipelines(c)
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
g.It("should parse pipeline filter with tz offset", func() {
pipelines := []*model.Pipeline{fakePipeline}
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Request, _ = http.NewRequest("DELETE", "/?before=2023-01-16T15:00:00%2B01:00&after=2023-01-15T15:00:00%2B01:00", nil)
GetPipelines(c)
assert.Equal(t, http.StatusOK, c.Writer.Status())
})
})
}
func TestDeletePipeline(t *testing.T) {
gin.SetMode(gin.TestMode)
g := goblin.Goblin(t)
g.Describe("Pipeline", func() {
g.It("should delete pipeline", func() {
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
mockStore.On("DeletePipeline", mock.Anything).Return(nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "1"}}
DeletePipeline(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusNoContent, c.Writer.Status())
})
g.It("should not delete without pipeline number", func() {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
DeletePipeline(c)
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
})
g.It("should not delete pending", func() {
fakePipeline.Status = model.StatusPending
mockStore := mocks.NewStore(t)
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set("store", mockStore)
c.Params = gin.Params{{Key: "number", Value: "1"}}
DeletePipeline(c)
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
mockStore.AssertNotCalled(t, "DeletePipeline", mock.Anything)
assert.Equal(t, http.StatusUnprocessableEntity, c.Writer.Status())
})
})
}

View file

@ -26,7 +26,7 @@ import (
// GetRegistry
//
// @Summary Get a named registry
// @Summary Get a registry by name
// @Router /repos/{repo_id}/registry/{registry} [get]
// @Produce json
// @Success 200 {object} Registry
@ -49,7 +49,7 @@ func GetRegistry(c *gin.Context) {
// PostRegistry
//
// @Summary Persist/create a registry
// @Summary Create a registry
// @Router /repos/{repo_id}/registry [post]
// @Produce json
// @Success 200 {object} Registry
@ -86,7 +86,7 @@ func PostRegistry(c *gin.Context) {
// PatchRegistry
//
// @Summary Update a named registry
// @Summary Update a registry by name
// @Router /repos/{repo_id}/registry/{registry} [patch]
// @Produce json
// @Success 200 {object} Registry
@ -134,7 +134,7 @@ func PatchRegistry(c *gin.Context) {
// GetRegistryList
//
// @Summary Get the registry list
// @Summary List registries
// @Router /repos/{repo_id}/registry [get]
// @Produce json
// @Success 200 {array} Registry
@ -161,7 +161,7 @@ func GetRegistryList(c *gin.Context) {
// DeleteRegistry
//
// @Summary Delete a named registry
// @Summary Delete a registry by name
// @Router /repos/{repo_id}/registry/{registry} [delete]
// @Produce plain
// @Success 204

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
@ -138,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)
@ -146,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."
@ -157,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)
@ -168,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 {
@ -192,7 +199,7 @@ func PostRepo(c *gin.Context) {
// PatchRepo
//
// @Summary Change a repository
// @Summary Update a repository
// @Router /repos/{repo_id} [patch]
// @Produce json
// @Success 200 {object} Repo
@ -266,7 +273,7 @@ func PatchRepo(c *gin.Context) {
// ChownRepo
//
// @Summary Change a repository's owner, to the one holding the access token
// @Summary Change a repository's owner to the currently authenticated user
// @Router /repos/{repo_id}/chown [post]
// @Produce json
// @Success 200 {object} Repo
@ -289,20 +296,20 @@ func ChownRepo(c *gin.Context) {
// LookupRepo
//
// @Summary Get repository by full-name
// @Summary Lookup a repository by full name
// @Router /repos/lookup/{repo_full_name} [get]
// @Produce json
// @Success 200 {object} Repo
// @Tags Repositories
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_full_name path string true "the repository full-name / slug"
// @Param repo_full_name path string true "the repository full name / slug"
func LookupRepo(c *gin.Context) {
c.JSON(http.StatusOK, session.Repo(c))
}
// GetRepo
//
// @Summary Get repository information
// @Summary Get a repository
// @Router /repos/{repo_id} [get]
// @Produce json
// @Success 200 {object} Repo
@ -315,7 +322,7 @@ func GetRepo(c *gin.Context) {
// GetRepoPermissions
//
// @Summary Repository permission information
// @Summary Check current authenticated users access to the repository
// @Description The repository permission, according to the used access token.
// @Router /repos/{repo_id}/permissions [get]
// @Produce json
@ -330,7 +337,7 @@ func GetRepoPermissions(c *gin.Context) {
// GetRepoBranches
//
// @Summary Get repository branches
// @Summary Get branches of a repository
// @Router /repos/{repo_id}/branches [get]
// @Produce json
// @Success 200 {array} string
@ -342,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
@ -355,7 +367,7 @@ func GetRepoBranches(c *gin.Context) {
// GetRepoPullRequests
//
// @Summary List active pull requests
// @Summary List active pull requests of a repository
// @Router /repos/{repo_id}/pull_requests [get]
// @Produce json
// @Success 200 {array} PullRequest
@ -367,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
@ -390,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
@ -409,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
}
@ -445,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 {
@ -463,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
@ -508,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
}
@ -520,7 +547,8 @@ func MoveRepo(c *gin.Context) {
// GetAllRepos
//
// @Summary List all repositories on the server. Requires admin rights.
// @Summary List all repositories on the server
// @Description Returns a list of all repositories. Requires admin rights.
// @Router /repos [get]
// @Produce json
// @Success 200 {array} Repo
@ -545,7 +573,8 @@ func GetAllRepos(c *gin.Context) {
// RepairAllRepos
//
// @Summary Repair all repositories on the server. Requires admin rights.
// @Summary Repair all repositories on the server
// @Description Executes a repair process on all repositories. Requires admin rights.
// @Router /repos/repair [post]
// @Produce plain
// @Success 204
@ -571,8 +600,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 {
@ -603,7 +637,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 {
@ -636,10 +670,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

@ -26,7 +26,7 @@ import (
// GetSecret
//
// @Summary Get a named secret
// @Summary Get a repository secret by name
// @Router /repos/{repo_id}/secrets/{secretName} [get]
// @Produce json
// @Success 200 {object} Secret
@ -49,7 +49,7 @@ func GetSecret(c *gin.Context) {
// PostSecret
//
// @Summary Persist/create a secret
// @Summary Create a repository secret
// @Router /repos/{repo_id}/secrets [post]
// @Produce json
// @Success 200 {object} Secret
@ -87,7 +87,7 @@ func PostSecret(c *gin.Context) {
// PatchSecret
//
// @Summary Update a named secret
// @Summary Update a repository secret by name
// @Router /repos/{repo_id}/secrets/{secretName} [patch]
// @Produce json
// @Success 200 {object} Secret
@ -138,7 +138,7 @@ func PatchSecret(c *gin.Context) {
// GetSecretList
//
// @Summary Get the secret list
// @Summary List repository secrets
// @Router /repos/{repo_id}/secrets [get]
// @Produce json
// @Success 200 {array} Secret
@ -165,7 +165,7 @@ func GetSecretList(c *gin.Context) {
// DeleteSecret
//
// @Summary Delete a named secret
// @Summary Delete a repository secret by name
// @Router /repos/{repo_id}/secrets/{secretName} [delete]
// @Produce plain
// @Success 204

View file

@ -36,8 +36,8 @@ import (
// EventStreamSSE
//
// @Summary Event stream
// @Description event source streaming for compatibility with quic and http2
// @Summary Stream events like pipeline updates
// @Description With quic and http2 support
// @Router /stream/events [get]
// @Produce plain
// @Success 200
@ -124,7 +124,7 @@ func EventStreamSSE(c *gin.Context) {
// LogStreamSSE
//
// @Summary Log stream
// @Summary Stream logs of a pipeline step
// @Router /stream/logs/{repo_id}/{pipeline}/{stepID} [get]
// @Produce plain
// @Success 200

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"
@ -31,7 +32,7 @@ import (
// GetSelf
//
// @Summary Returns the currently authenticated user.
// @Summary Get the currently authenticated user
// @Router /user [get]
// @Produce json
// @Success 200 {object} User
@ -43,11 +44,11 @@ func GetSelf(c *gin.Context) {
// GetFeed
//
// @Summary A feed entry for a build.
// @Description Feed entries can be used to display information on the latest builds.
// @Summary Get the currently authenticaed users pipeline feed
// @Description The feed lists the most recent pipeline for the currently authenticated user.
// @Router /user/feed [get]
// @Produce json
// @Success 200 {object} Feed
// @Success 200 {array} Feed
// @Tags User
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
func GetFeed(c *gin.Context) {
@ -76,7 +77,7 @@ func GetFeed(c *gin.Context) {
// GetRepos
//
// @Summary Get user's repos
// @Summary Get user's repositories
// @Description Retrieve the currently authenticated User's Repository list
// @Router /user/repos [get]
// @Produce json
@ -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

@ -28,7 +28,7 @@ import (
// GetUsers
//
// @Summary Get all users
// @Summary List users
// @Description Returns all registered, active users in the system. Requires admin rights.
// @Router /users [get]
// @Produce json
@ -67,7 +67,7 @@ func GetUser(c *gin.Context) {
// PatchUser
//
// @Summary Change a user
// @Summary Update a user
// @Description Changes the data of an existing user. Requires admin rights.
// @Router /users/{login} [patch]
// @Produce json
@ -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(ctx, 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,7 +78,7 @@ func CalcNewNext(schedule string, now time.Time) (time.Time, error) {
return c.Next(now), nil
}
func runCron(ctx context.Context, 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)
newNext, err := CalcNewNext(cron.Schedule, now)
@ -95,7 +96,7 @@ func runCron(ctx context.Context, store store.Store, forge forge.Forge, cron *mo
return nil
}
repo, newPipeline, err := CreatePipeline(ctx, store, forge, cron)
repo, newPipeline, err := CreatePipeline(ctx, store, cron)
if err != nil {
return err
}
@ -104,12 +105,17 @@ func runCron(ctx context.Context, store store.Store, forge forge.Forge, cron *mo
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
@ -123,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)

View file

@ -217,14 +217,14 @@ func Test_bitbucket(t *testing.T) {
g.Describe("When requesting repo directory contents", func() {
g.It("Should return the details", func() {
files, err := c.Dir(ctx, fakeUser, fakeRepo, fakePipeline, "/dir")
files, err := c.Dir(ctx, fakeUser, fakeRepo, fakePipeline, "dir")
g.Assert(err).IsNil()
g.Assert(len(files)).Equal(3)
g.Assert(files[0].Name).Equal("README.md")
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

@ -114,7 +114,7 @@ func getRepoFile(c *gin.Context) {
switch c.Param("file") {
case "dir":
c.String(http.StatusOK, repoDirPayload)
case "dir_not_found/":
case "dir_not_found":
c.String(http.StatusNotFound, "")
case "file_not_found":
c.String(http.StatusNotFound, "")

View file

@ -52,7 +52,7 @@ const (
pathOrgPerms = "%s/2.0/workspaces/%s/permissions?%s"
pathPullRequests = "%s/2.0/repositories/%s/%s/pullrequests?%s"
pathBranchCommits = "%s/2.0/repositories/%s/%s/commits/%s"
pathDir = "%s/2.0/repositories/%s/%s/src/%s%s"
pathDir = "%s/2.0/repositories/%s/%s/src/%s/%s"
pageSize = 100
)

View file

@ -262,8 +262,14 @@ func (c *client) File(ctx context.Context, u *model.User, r *model.Repo, p *mode
return nil, fmt.Errorf("unable to create bitbucket client: %w", err)
}
b, _, err := bc.Projects.GetTextFileContent(ctx, r.Owner, r.Name, f, p.Commit)
b, resp, err := bc.Projects.GetTextFileContent(ctx, r.Owner, r.Name, f, p.Commit)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
// requested directory might not exist
return nil, &forge_types.ErrConfigNotFound{
Configs: []string{f},
}
}
return nil, err
}
return b, nil
@ -281,7 +287,10 @@ func (c *client) Dir(ctx context.Context, u *model.User, r *model.Repo, p *model
list, resp, err := bc.Projects.ListFiles(ctx, r.Owner, r.Name, path, opts)
if err != nil {
if resp.StatusCode == http.StatusNotFound {
break // requested directory might not exist
// requested directory might not exist
return nil, &forge_types.ErrConfigNotFound{
Configs: []string{path},
}
}
return nil, err
}

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

@ -127,13 +127,14 @@ func pipelineFromTag(hook *pushHook) *model.Pipeline {
hook.Repo.HTMLURL,
fixMalformedAvatar(hook.Sender.AvatarURL),
)
ref := strings.TrimPrefix(hook.Ref, "refs/tags/")
return &model.Pipeline{
Event: model.EventTag,
Commit: hook.Sha,
Ref: fmt.Sprintf("refs/tags/%s", hook.Ref),
ForgeURL: fmt.Sprintf("%s/src/tag/%s", hook.Repo.HTMLURL, hook.Ref),
Message: fmt.Sprintf("created tag %s", hook.Ref),
Ref: fmt.Sprintf("refs/tags/%s", ref),
ForgeURL: fmt.Sprintf("%s/src/tag/%s", hook.Repo.HTMLURL, ref),
Message: fmt.Sprintf("created tag %s", ref),
Avatar: avatar,
Author: hook.Sender.UserName,
Sender: hook.Sender.UserName,

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
@ -418,11 +418,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

@ -53,6 +53,11 @@ type Pipeline struct {
IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"`
} // @name Pipeline
type PipelineFilter struct {
Before int64
After int64
}
// TableName return database table name for xorm
func (Pipeline) TableName() string {
return "pipelines"

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:"repo_user_id"`
ID int64 `json:"id,omitempty" xorm:"pk autoincr 'repo_id'"`
UserID int64 `json:"-" xorm:"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:"repo_org_id"`

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.

View file

@ -20,6 +20,7 @@ import (
"github.com/rs/zerolog/log"
"go.woodpecker-ci.org/woodpecker/v2/server"
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
@ -32,6 +33,13 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe
return nil, ErrBadRequest{Msg: fmt.Sprintf("cannot approve a pipeline with status %s", currentPipeline.Status)}
}
forge, err := server.Config.Services.Manager.ForgeFromRepo(repo)
if err != nil {
msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName)
log.Error().Err(err).Str("repo", repo.FullName).Msg(msg)
return nil, fmt.Errorf(msg)
}
// fetch the pipeline file from the database
configs, err := store.ConfigsForPipeline(currentPipeline.ID)
if err != nil {
@ -72,7 +80,7 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe
}
}
currentPipeline, pipelineItems, err := createPipelineItems(ctx, store, currentPipeline, user, repo, yamls, nil)
currentPipeline, pipelineItems, err := createPipelineItems(ctx, forge, store, currentPipeline, user, repo, yamls, nil)
if err != nil {
msg := fmt.Sprintf("failure to createPipelineItems for %s", repo.FullName)
log.Error().Err(err).Msg(msg)
@ -86,9 +94,9 @@ func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipe
return nil, err
}
publishPipeline(ctx, currentPipeline, repo, user)
publishPipeline(ctx, forge, currentPipeline, repo, user)
currentPipeline, err = start(ctx, store, currentPipeline, user, repo, pipelineItems)
currentPipeline, err = start(ctx, forge, store, currentPipeline, user, repo, pipelineItems)
if err != nil {
msg := fmt.Sprintf("failure to start pipeline for %s: %v", repo.FullName, err)
log.Error().Err(err).Msg(msg)

View file

@ -21,13 +21,14 @@ import (
"github.com/rs/zerolog/log"
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/queue"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
)
// Cancel the pipeline and returns the status.
func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *model.User, pipeline *model.Pipeline) error {
func Cancel(ctx context.Context, _forge forge.Forge, store store.Store, repo *model.Repo, user *model.User, pipeline *model.Pipeline) error {
if pipeline.Status != model.StatusRunning && pipeline.Status != model.StatusPending && pipeline.Status != model.StatusBlocked {
return &ErrBadRequest{Msg: "Cannot cancel a non-running or non-pending or non-blocked pipeline"}
}
@ -88,7 +89,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode
return err
}
updatePipelineStatus(ctx, killedPipeline, repo, user)
updatePipelineStatus(ctx, _forge, killedPipeline, repo, user)
if killedPipeline.Workflows, err = store.WorkflowGetTree(killedPipeline); err != nil {
return err
@ -100,6 +101,7 @@ func Cancel(ctx context.Context, store store.Store, repo *model.Repo, user *mode
func cancelPreviousPipelines(
ctx context.Context,
_forge forge.Forge,
_store store.Store,
pipeline *model.Pipeline,
repo *model.Repo,
@ -150,7 +152,7 @@ func cancelPreviousPipelines(
continue
}
if err = Cancel(ctx, _store, repo, user, active); err != nil {
if err = Cancel(ctx, _forge, _store, repo, user, active); err != nil {
log.Error().
Err(err).
Str("ref", active.Ref).

View file

@ -34,7 +34,6 @@ var skipPipelineRegex = regexp.MustCompile(`\[(?i:ci *skip|skip *ci)\]`)
// Create a new pipeline and start it
func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline *model.Pipeline) (*model.Pipeline, error) {
_forge := server.Config.Services.Forge
repoUser, err := _store.GetUser(repo.UserID)
if err != nil {
msg := fmt.Sprintf("failure to find repo owner via id '%d'", repo.UserID)
@ -54,6 +53,13 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
}
}
_forge, err := server.Config.Services.Manager.ForgeFromRepo(repo)
if err != nil {
msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName)
log.Error().Err(err).Str("repo", repo.FullName).Msg(msg)
return nil, fmt.Errorf(msg)
}
// If the forge has a refresh token, the current access token
// may be stale. Therefore, we should refresh prior to dispatching
// the pipeline.
@ -82,13 +88,13 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
return nil, ErrFiltered
} else if configFetchErr != nil {
log.Debug().Str("repo", repo.FullName).Err(configFetchErr).Msgf("error while fetching config '%s' in '%s' with user: '%s'", repo.Config, pipeline.Ref, repoUser.Login)
return nil, updatePipelineWithErr(ctx, _store, pipeline, repo, repoUser, fmt.Errorf("pipeline definition not found in %s", repo.FullName))
return nil, updatePipelineWithErr(ctx, _forge, _store, pipeline, repo, repoUser, fmt.Errorf("pipeline definition not found in %s", repo.FullName))
}
pipelineItems, parseErr := parsePipeline(_store, pipeline, repoUser, repo, forgeYamlConfigs, nil)
pipelineItems, parseErr := parsePipeline(_forge, _store, pipeline, repoUser, repo, forgeYamlConfigs, nil)
if pipeline_errors.HasBlockingErrors(parseErr) {
log.Debug().Str("repo", repo.FullName).Err(parseErr).Msg("failed to parse yaml")
return nil, updatePipelineWithErr(ctx, _store, pipeline, repo, repoUser, parseErr)
return nil, updatePipelineWithErr(ctx, _forge, _store, pipeline, repo, repoUser, parseErr)
} else if parseErr != nil {
pipeline.Errors = pipeline_errors.GetPipelineErrors(parseErr)
}
@ -122,7 +128,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
return nil, fmt.Errorf(msg)
}
if err := prepareStart(ctx, _store, pipeline, repoUser, repo); err != nil {
if err := prepareStart(ctx, _forge, _store, pipeline, repoUser, repo); err != nil {
log.Error().Err(err).Str("repo", repo.FullName).Msgf("error preparing pipeline for %s#%d", repo.FullName, pipeline.Number)
return nil, err
}
@ -131,11 +137,11 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
return pipeline, nil
}
if err := updatePipelinePending(ctx, _store, pipeline, repo, repoUser); err != nil {
if err := updatePipelinePending(ctx, _forge, _store, pipeline, repo, repoUser); err != nil {
return nil, err
}
pipeline, err = start(ctx, _store, pipeline, repoUser, repo, pipelineItems)
pipeline, err = start(ctx, _forge, _store, pipeline, repoUser, repo, pipelineItems)
if err != nil {
msg := fmt.Sprintf("failed to start pipeline for %s", repo.FullName)
log.Error().Err(err).Msg(msg)
@ -145,7 +151,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
return pipeline, nil
}
func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error {
func updatePipelineWithErr(ctx context.Context, _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error {
_pipeline, err := UpdateToStatusError(_store, *pipeline, err)
if err != nil {
return err
@ -153,12 +159,12 @@ func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *mo
// update value in ref
*pipeline = *_pipeline
publishPipeline(ctx, pipeline, repo, repoUser)
publishPipeline(ctx, _forge, pipeline, repo, repoUser)
return nil
}
func updatePipelinePending(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) error {
func updatePipelinePending(ctx context.Context, _forge forge.Forge, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) error {
_pipeline, err := UpdateToStatusPending(_store, *pipeline, "")
if err != nil {
return err
@ -166,7 +172,7 @@ func updatePipelinePending(ctx context.Context, _store store.Store, pipeline *mo
// update value in ref
*pipeline = *_pipeline
publishPipeline(ctx, pipeline, repo, repoUser)
publishPipeline(ctx, _forge, pipeline, repo, repoUser)
return nil
}

View file

@ -20,17 +20,25 @@ import (
"github.com/rs/zerolog/log"
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/store"
)
// Decline updates the status to declined for blocked pipelines because of a gated repo
func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, user *model.User, repo *model.Repo) (*model.Pipeline, error) {
forge, err := server.Config.Services.Manager.ForgeFromRepo(repo)
if err != nil {
msg := fmt.Sprintf("failure to load forge for repo '%s'", repo.FullName)
log.Error().Err(err).Str("repo", repo.FullName).Msg(msg)
return nil, fmt.Errorf(msg)
}
if pipeline.Status != model.StatusBlocked {
return nil, fmt.Errorf("cannot decline a pipeline with status %s", pipeline.Status)
}
pipeline, err := UpdateToStatusDeclined(store, *pipeline, user.Login)
pipeline, err = UpdateToStatusDeclined(store, *pipeline, user.Login)
if err != nil {
return nil, fmt.Errorf("error updating pipeline. %w", err)
}
@ -53,7 +61,7 @@ func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, u
}
}
updatePipelineStatus(ctx, pipeline, repo, user)
updatePipelineStatus(ctx, forge, pipeline, repo, user)
publishToTopic(pipeline, repo)

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