Merge branch 'main' into service-use

This commit is contained in:
qwerty287 2024-05-13 07:55:49 +02:00
commit 9ed07b0e9d
No known key found for this signature in database
GPG key ID: 1218A32A886A5002
92 changed files with 3332 additions and 699 deletions

View file

@ -55,5 +55,5 @@ ci:
autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
autoupdate_schedule: quarterly
# NB: hadolint not included in pre-commit.ci
skip: [check-hooks-apply, check-useless-excludes, hadolint, prettier]
skip: [check-hooks-apply, check-useless-excludes, hadolint, prettier, golangci-lint]
submodules: false

View file

@ -4,8 +4,7 @@ when:
variables:
- &golang_image 'docker.io/golang:1.22.2'
- &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_image 'docker.io/techknowlogick/xgo:go-1.22.2'
- &xgo_version 'go-1.21.2'
steps:
@ -91,7 +90,7 @@ steps:
release:
depends_on:
- checksums
image: woodpeckerci/plugin-github-release:1.1.2
image: woodpeckerci/plugin-github-release:1.2.0
settings:
api_key:
from_secret: github_token

View file

@ -1,8 +1,7 @@
variables:
- &golang_image 'docker.io/golang:1.22.2'
- &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_image 'docker.io/techknowlogick/xgo:go-1.22.2'
- &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

@ -6,7 +6,7 @@ when:
- event: tag
steps:
mastodon-toot:
- name: mastodon-toot
image: docker.io/woodpeckerci/plugin-mastodon-post
settings:
server: https://floss.social
@ -16,10 +16,18 @@ steps:
ai_token:
from_secret: openai_token
ai_prompt: |
We want to present the next version of our app on Twitter.
We want to present the next version of our app on Mastodon.
Therefore we want to post a catching text, so users will know why they should
update to the newest version. If there is no special feature included
just summarize the changes in a few sentences. Use #WoodpeckerCI, #release and
additional fitting hashtags and emojis to make the post more appealing.
update to the newest version. Highlight the most special features. If there is no special feature
included just summarize the changes in a few sentences. The whole text should not be longer than 240
characters. Avoid naming contributors from. Use #WoodpeckerCI, #release and
additional fitting hashtags and emojis to make the post more appealing
The changelog entry: {{ changelog }}
- name: discord
image: docker.io/appleboy/drone-discord:1.3.1
settings:
webhook_id: 1236558396820295711
webhook_token:
from_secret: discord_token
message: '**{{ build.tag }}** was released: <https://github.com/woodpecker-ci/woodpecker/releases/tag/{{ build.tag }}>'

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

@ -204,6 +204,10 @@ var flags = []cli.Flag{
EnvVars: []string{"CI_PIPELINE_TARGET"},
Name: "pipeline-target",
},
&cli.StringFlag{
EnvVars: []string{"CI_PIPELINE_TASK"},
Name: "pipeline-task",
},
&cli.StringFlag{
EnvVars: []string{"CI_COMMIT_SHA"},
Name: "commit-sha",

View file

@ -61,6 +61,7 @@ func metadataFromContext(c *cli.Context, axis matrix.Axis) metadata.Metadata {
Event: c.String("pipeline-event"),
ForgeURL: c.String("pipeline-url"),
Target: c.String("pipeline-target"),
Task: c.String("pipeline-task"),
Commit: metadata.Commit{
Sha: c.String("commit-sha"),
Ref: c.String("commit-ref"),

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",
@ -2207,7 +2211,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Run/trigger a pipelines",
"summary": "Trigger a manual pipeline",
"parameters": [
{
"type": "string",
@ -2252,7 +2256,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Pipeline information by number",
"summary": "Get a repositories pipeline",
"parameters": [
{
"type": "string",
@ -2347,7 +2351,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Delete pipeline",
"summary": "Delete a pipeline",
"parameters": [
{
"type": "string",
@ -2387,7 +2391,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Start pipelines in gated repos",
"summary": "Approve and start a pipeline",
"parameters": [
{
"type": "string",
@ -2430,7 +2434,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Cancels a pipeline",
"summary": "Cancel a pipeline",
"parameters": [
{
"type": "string",
@ -2470,7 +2474,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Pipeline configuration",
"summary": "Get configuration files for a pipeline",
"parameters": [
{
"type": "string",
@ -2516,7 +2520,7 @@ const docTemplate = `{
"tags": [
"Pipelines"
],
"summary": "Decline pipelines in gated repos",
"summary": "Decline a pipeline",
"parameters": [
{
"type": "string",
@ -2559,7 +2563,7 @@ const docTemplate = `{
"tags": [
"Repositories"
],
"summary": "List active pull requests",
"summary": "List active pull requests of a repository",
"parameters": [
{
"type": "string",
@ -2612,7 +2616,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Get the registry list",
"summary": "List registries",
"parameters": [
{
"type": "string",
@ -2663,7 +2667,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Persist/create a registry",
"summary": "Create a registry",
"parameters": [
{
"type": "string",
@ -2708,7 +2712,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Get a named registry",
"summary": "Get a registry by name",
"parameters": [
{
"type": "string",
@ -2749,7 +2753,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Delete a named registry",
"summary": "Delete a registry by name",
"parameters": [
{
"type": "string",
@ -2787,7 +2791,7 @@ const docTemplate = `{
"tags": [
"Repository registries"
],
"summary": "Update a named registry",
"summary": "Update a registry by name",
"parameters": [
{
"type": "string",
@ -2872,7 +2876,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Get the secret list",
"summary": "List repository secrets",
"parameters": [
{
"type": "string",
@ -2923,7 +2927,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Persist/create a secret",
"summary": "Create a repository secret",
"parameters": [
{
"type": "string",
@ -2968,7 +2972,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Get a named secret",
"summary": "Get a repository secret by name",
"parameters": [
{
"type": "string",
@ -3009,7 +3013,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Delete a named secret",
"summary": "Delete a repository secret by name",
"parameters": [
{
"type": "string",
@ -3047,7 +3051,7 @@ const docTemplate = `{
"tags": [
"Repository secrets"
],
"summary": "Update a named secret",
"summary": "Update a repository secret by name",
"parameters": [
{
"type": "string",
@ -3099,7 +3103,7 @@ const docTemplate = `{
"tags": [
"Secrets"
],
"summary": "Get the global secret list",
"summary": "List global secrets",
"parameters": [
{
"type": "string",
@ -3143,7 +3147,7 @@ const docTemplate = `{
"tags": [
"Secrets"
],
"summary": "Persist/create a global secret",
"summary": "Create a global secret",
"parameters": [
{
"type": "string",
@ -3311,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"
@ -3334,7 +3338,7 @@ const docTemplate = `{
"tags": [
"Pipeline logs"
],
"summary": "Log stream",
"summary": "Stream logs of a pipeline step",
"parameters": [
{
"type": "integer",
@ -3373,7 +3377,7 @@ const docTemplate = `{
"tags": [
"User"
],
"summary": "Returns the currently authenticated user.",
"summary": "Get the currently authenticated user",
"parameters": [
{
"type": "string",
@ -3396,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",
@ -3418,7 +3422,10 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Feed"
"type": "array",
"items": {
"$ref": "#/definitions/Feed"
}
}
}
}
@ -3433,7 +3440,7 @@ const docTemplate = `{
"tags": [
"User"
],
"summary": "Get user's repos",
"summary": "Get user's repositories",
"parameters": [
{
"type": "string",
@ -3523,7 +3530,7 @@ const docTemplate = `{
"tags": [
"Users"
],
"summary": "Get all users",
"summary": "List users",
"parameters": [
{
"type": "string",
@ -3677,7 +3684,7 @@ const docTemplate = `{
"tags": [
"Users"
],
"summary": "Change a user",
"summary": "Update a user",
"parameters": [
{
"type": "string",
@ -4016,6 +4023,9 @@ const docTemplate = `{
"created_at": {
"type": "integer"
},
"deploy_task": {
"type": "string"
},
"deploy_to": {
"type": "string"
},

View file

@ -1,6 +1,10 @@
# Workflow syntax
The workflow section defines a list of steps to build, test and deploy your code. Steps are executed serially, in the order in which they are defined. If a step returns a non-zero exit code, the workflow and therefore all other workflows and the pipeline immediately aborts and returns a failure status.
The Workflow section defines a list of steps to build, test and deploy your code. The steps are executed serially in the order in which they are defined. If a step returns a non-zero exit code, the workflow and therefore the entire pipeline terminates immediately and returns an error status.
:::note
An exception to this rule are steps with a [`status: [failure]`](#status) condition, which ensures that they are executed in the case of a failed run.
:::
Example steps:
@ -161,6 +165,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.

View file

@ -85,6 +85,7 @@ This is the reference list of all environment variables available to your pipeli
| `CI_PIPELINE_URL` | link to the web UI for the pipeline |
| `CI_PIPELINE_FORGE_URL` | link to the forge's web UI for the commit(s) or tag that triggered the pipeline |
| `CI_PIPELINE_DEPLOY_TARGET` | pipeline deploy target for `deployment` events (i.e. production) |
| `CI_PIPELINE_DEPLOY_TASK` | pipeline deploy task for `deployment` events (i.e. migration) |
| `CI_PIPELINE_STATUS` | pipeline status (success, failure) |
| `CI_PIPELINE_CREATED` | pipeline created UNIX timestamp |
| `CI_PIPELINE_STARTED` | pipeline started UNIX timestamp |
@ -118,6 +119,7 @@ This is the reference list of all environment variables available to your pipeli
| `CI_PREV_PIPELINE_URL` | previous pipeline link in CI |
| `CI_PREV_PIPELINE_FORGE_URL` | previous pipeline link to event in forge |
| `CI_PREV_PIPELINE_DEPLOY_TARGET` | previous pipeline deploy target for `deployment` events (ie production) |
| `CI_PREV_PIPELINE_DEPLOY_TASK` | previous pipeline deploy task for `deployment` events (ie migration) |
| `CI_PREV_PIPELINE_STATUS` | previous pipeline status (success, failure) |
| `CI_PREV_PIPELINE_CREATED` | previous pipeline created UNIX timestamp |
| `CI_PREV_PIPELINE_STARTED` | previous pipeline started UNIX timestamp |

View file

@ -52,8 +52,9 @@ If you have some missing resources, please feel free to [open a pull-request](ht
- [Quest For CICD - WoodpeckerCI](https://omaramin.me/posts/woodpecker/)
- [Getting started with Woodpecker CI](https://systeemkabouter.eu/getting-started-with-woodpecker-ci.html)
- [Installing gitea and woodpecker using binary packages](https://neelex.com/2023/03/26/Installing-gitea-using-binary-packages/)
- [Deploying mdbook to codeberg pages using woodpecker CI](https://www.markpitblado.me/blog/ci-deployment-of-mdbook)
- [Deploying mdbook to codeberg pages using woodpecker CI](https://www.markpitblado.me/blog/deploying-mdbook-to-codeberg-pages-using-woodpecker-ci/)
- [Deploy a Fly app with Woodpecker CI](https://joeroe.io/2024/01/09/deploy-fly-woodpecker-ci.html)
- [Ansible - using Woodpecker as an alternative to Semaphore](https://pat-s.me/ansible-using-woodpecker-as-an-alternative-to-semaphore/)
## Videos

View file

@ -3,7 +3,6 @@
## ORM
Woodpecker uses [Xorm](https://xorm.io/) as ORM for the database connection.
You can find its documentation at [gobook.io/read/gitea.com/xorm](https://gobook.io/read/gitea.com/xorm/manual-en-US/).
## Add a new migration

View file

@ -60,6 +60,11 @@
"docs": "https://raw.githubusercontent.com/markopolo123/gitea-comment-plugin/main/docs.md",
"verified": false
},
{
"name": "Gitea publisher-golang",
"docs": "https://raw.githubusercontent.com/woodpecker-kit/woodpecker-gitea-publisher-golang/main/doc/docs.md",
"verified": false
},
{
"name": "Git Push",
"docs": "https://raw.githubusercontent.com/appleboy/drone-git-push/master/DOCS.md",

8
go.sum
View file

@ -540,8 +540,6 @@ 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=
@ -575,8 +573,6 @@ 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=
@ -623,8 +619,6 @@ 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=
@ -633,8 +627,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
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=

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",
@ -162,8 +161,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

@ -11,6 +11,8 @@ type BackendOptions struct {
Resources Resources `mapstructure:"resources"`
RuntimeClassName *string `mapstructure:"runtimeClassName"`
ServiceAccountName string `mapstructure:"serviceAccountName"`
Labels map[string]string `mapstructure:"labels"`
Annotations map[string]string `mapstructure:"annotations"`
NodeSelector map[string]string `mapstructure:"nodeSelector"`
Tolerations []Toleration `mapstructure:"tolerations"`
SecurityContext *SecurityContext `mapstructure:"securityContext"`

View file

@ -20,6 +20,8 @@ func Test_parseBackendOptions(t *testing.T) {
"kubernetes": map[string]any{
"nodeSelector": map[string]string{"storage": "ssd"},
"serviceAccountName": "wp-svc-acc",
"labels": map[string]string{"app": "test"},
"annotations": map[string]string{"apps.kubernetes.io/pod-index": "0"},
"tolerations": []map[string]any{
{"key": "net-port", "value": "100Mbit", "effect": TaintEffectNoSchedule},
},
@ -49,6 +51,8 @@ func Test_parseBackendOptions(t *testing.T) {
assert.Equal(t, BackendOptions{
NodeSelector: map[string]string{"storage": "ssd"},
ServiceAccountName: "wp-svc-acc",
Labels: map[string]string{"app": "test"},
Annotations: map[string]string{"apps.kubernetes.io/pod-index": "0"},
Tolerations: []Toleration{{Key: "net-port", Value: "100Mbit", Effect: TaintEffectNoSchedule}},
Resources: Resources{
Requests: map[string]string{"memory": "128Mi", "cpu": "1000m"},

View file

@ -46,15 +46,27 @@ var Flags = []cli.Flag{
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BACKEND_K8S_POD_LABELS"},
Name: "backend-k8s-pod-labels",
Usage: "backend k8s additional worker pod labels",
Usage: "backend k8s additional Agent-wide worker pod labels",
Value: "",
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_BACKEND_K8S_POD_LABELS_ALLOW_FROM_STEP"},
Name: "backend-k8s-pod-labels-allow-from-step",
Usage: "whether to allow using labels from step's backend options",
Value: false,
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_BACKEND_K8S_POD_ANNOTATIONS"},
Name: "backend-k8s-pod-annotations",
Usage: "backend k8s additional worker pod annotations",
Usage: "backend k8s additional Agent-wide worker pod annotations",
Value: "",
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_BACKEND_K8S_POD_ANNOTATIONS_ALLOW_FROM_STEP"},
Name: "backend-k8s-pod-annotations-allow-from-step",
Usage: "whether to allow using annotations from step's backend options",
Value: false,
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_BACKEND_K8S_SECCTX_NONROOT"},
Name: "backend-k8s-secctx-nonroot",

View file

@ -55,14 +55,16 @@ type kube struct {
}
type config struct {
Namespace string
StorageClass string
VolumeSize string
StorageRwx bool
PodLabels map[string]string
PodAnnotations map[string]string
ImagePullSecretNames []string
SecurityContext SecurityContextConfig
Namespace string
StorageClass string
VolumeSize string
StorageRwx bool
PodLabels map[string]string
PodLabelsAllowFromStep bool
PodAnnotations map[string]string
PodAnnotationsAllowFromStep bool
ImagePullSecretNames []string
SecurityContext SecurityContextConfig
}
type SecurityContextConfig struct {
RunAsNonRoot bool
@ -82,13 +84,15 @@ func configFromCliContext(ctx context.Context) (*config, error) {
if ctx != nil {
if c, ok := ctx.Value(types.CliContext).(*cli.Context); ok {
config := config{
Namespace: c.String("backend-k8s-namespace"),
StorageClass: c.String("backend-k8s-storage-class"),
VolumeSize: c.String("backend-k8s-volume-size"),
StorageRwx: c.Bool("backend-k8s-storage-rwx"),
PodLabels: make(map[string]string), // just init empty map to prevent nil panic
PodAnnotations: make(map[string]string), // just init empty map to prevent nil panic
ImagePullSecretNames: c.StringSlice("backend-k8s-pod-image-pull-secret-names"),
Namespace: c.String("backend-k8s-namespace"),
StorageClass: c.String("backend-k8s-storage-class"),
VolumeSize: c.String("backend-k8s-volume-size"),
StorageRwx: c.Bool("backend-k8s-storage-rwx"),
PodLabels: make(map[string]string), // just init empty map to prevent nil panic
PodLabelsAllowFromStep: c.Bool("backend-k8s-pod-labels-allow-from-step"),
PodAnnotations: make(map[string]string), // just init empty map to prevent nil panic
PodAnnotationsAllowFromStep: c.Bool("backend-k8s-pod-annotations-allow-from-step"),
ImagePullSecretNames: c.StringSlice("backend-k8s-pod-image-pull-secret-names"),
SecurityContext: SecurityContextConfig{
RunAsNonRoot: c.Bool("backend-k8s-secctx-nonroot"),
},

View file

@ -76,43 +76,76 @@ func podName(step *types.Step) (string, error) {
func podMeta(step *types.Step, config *config, options BackendOptions, podName string) (metav1.ObjectMeta, error) {
var err error
meta := metav1.ObjectMeta{
Name: podName,
Namespace: config.Namespace,
Name: podName,
Namespace: config.Namespace,
Annotations: podAnnotations(config, options, podName),
}
meta.Labels = config.PodLabels
if meta.Labels == nil {
meta.Labels = make(map[string]string, 1)
}
meta.Labels[StepLabel], err = stepLabel(step)
meta.Labels, err = podLabels(step, config, options)
if err != nil {
return meta, err
}
if step.Type == types.StepTypeService {
meta.Labels[ServiceLabel], _ = serviceName(step)
}
meta.Annotations = config.PodAnnotations
if meta.Annotations == nil {
meta.Annotations = make(map[string]string)
}
securityContext := options.SecurityContext
if securityContext != nil {
key, value := apparmorAnnotation(podName, securityContext.ApparmorProfile)
if key != nil && value != nil {
meta.Annotations[*key] = *value
}
}
return meta, nil
}
func podLabels(step *types.Step, config *config, options BackendOptions) (map[string]string, error) {
var err error
labels := make(map[string]string)
if len(options.Labels) > 0 {
if config.PodLabelsAllowFromStep {
log.Trace().Msgf("using labels from the backend options: %v", options.Labels)
maps.Copy(labels, options.Labels)
} else {
log.Debug().Msg("Pod labels were defined in backend options, but its using disallowed by instance configuration")
}
}
if len(config.PodLabels) > 0 {
log.Trace().Msgf("using labels from the configuration: %v", config.PodLabels)
maps.Copy(labels, config.PodLabels)
}
if step.Type == types.StepTypeService {
labels[ServiceLabel], _ = serviceName(step)
}
labels[StepLabel], err = stepLabel(step)
if err != nil {
return labels, err
}
return labels, nil
}
func stepLabel(step *types.Step) (string, error) {
return toDNSName(step.Name)
}
func podAnnotations(config *config, options BackendOptions, podName string) map[string]string {
annotations := make(map[string]string)
if len(options.Annotations) > 0 {
if config.PodAnnotationsAllowFromStep {
log.Trace().Msgf("using annotations from the backend options: %v", options.Annotations)
maps.Copy(annotations, options.Annotations)
} else {
log.Debug().Msg("Pod annotations were defined in backend options, but its using disallowed by instance configuration ")
}
}
if len(config.PodAnnotations) > 0 {
log.Trace().Msgf("using annotations from the configuration: %v", config.PodAnnotations)
maps.Copy(annotations, config.PodAnnotations)
}
securityContext := options.SecurityContext
if securityContext != nil {
key, value := apparmorAnnotation(podName, securityContext.ApparmorProfile)
if key != nil && value != nil {
annotations[*key] = *value
}
}
return annotations
}
func podSpec(step *types.Step, config *config, options BackendOptions) (v1.PodSpec, error) {
var err error
spec := v1.PodSpec{
@ -147,15 +180,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",
@ -159,11 +157,13 @@ func TestFullPod(t *testing.T) {
"creationTimestamp": null,
"labels": {
"app": "test",
"part-of": "woodpecker-ci",
"step": "go-test"
},
"annotations": {
"apps.kubernetes.io/pod-index": "0",
"container.apparmor.security.beta.kubernetes.io/wp-01he8bebctabr3kgk0qj36d2me-0": "localhost/k8s-apparmor-example-deny-write"
"container.apparmor.security.beta.kubernetes.io/wp-01he8bebctabr3kgk0qj36d2me-0": "localhost/k8s-apparmor-example-deny-write",
"kubernetes.io/limit-ranger": "LimitRanger plugin set: cpu, memory request and limit for container"
}
},
"spec": {
@ -183,9 +183,6 @@ func TestFullPod(t *testing.T) {
"/bin/sh",
"-c"
],
"args": [
"echo $CI_SCRIPT | base64 -d | /bin/sh -e"
],
"workingDir": "/woodpecker/src",
"ports": [
{
@ -328,12 +325,16 @@ func TestFullPod(t *testing.T) {
ExtraHosts: hostAliases,
Ports: ports,
}, &config{
Namespace: "woodpecker",
ImagePullSecretNames: []string{"regcred", "another-pull-secret"},
PodLabels: map[string]string{"app": "test"},
PodAnnotations: map[string]string{"apps.kubernetes.io/pod-index": "0"},
SecurityContext: SecurityContextConfig{RunAsNonRoot: false},
Namespace: "woodpecker",
ImagePullSecretNames: []string{"regcred", "another-pull-secret"},
PodLabels: map[string]string{"app": "test"},
PodLabelsAllowFromStep: true,
PodAnnotations: map[string]string{"apps.kubernetes.io/pod-index": "0"},
PodAnnotationsAllowFromStep: true,
SecurityContext: SecurityContextConfig{RunAsNonRoot: false},
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64", BackendOptions{
Labels: map[string]string{"part-of": "woodpecker-ci"},
Annotations: map[string]string{"kubernetes.io/limit-ranger": "LimitRanger plugin set: cpu, memory request and limit for container"},
NodeSelector: map[string]string{"storage": "ssd"},
RuntimeClassName: &runtimeClass,
ServiceAccountName: "wp-svc-acc",
@ -415,3 +416,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

@ -77,6 +77,7 @@ func (m *Metadata) Environ() map[string]string {
"CI_PIPELINE_URL": m.getPipelineWebURL(m.Curr, 0),
"CI_PIPELINE_FORGE_URL": m.Curr.ForgeURL,
"CI_PIPELINE_DEPLOY_TARGET": m.Curr.Target,
"CI_PIPELINE_DEPLOY_TASK": m.Curr.Task,
"CI_PIPELINE_STATUS": m.Curr.Status,
"CI_PIPELINE_CREATED": strconv.FormatInt(m.Curr.Created, 10),
"CI_PIPELINE_STARTED": strconv.FormatInt(m.Curr.Started, 10),
@ -108,6 +109,7 @@ func (m *Metadata) Environ() map[string]string {
"CI_PREV_PIPELINE_URL": m.getPipelineWebURL(m.Prev, 0),
"CI_PREV_PIPELINE_FORGE_URL": m.Prev.ForgeURL,
"CI_PREV_PIPELINE_DEPLOY_TARGET": m.Prev.Target,
"CI_PREV_PIPELINE_DEPLOY_TASK": m.Prev.Task,
"CI_PREV_PIPELINE_STATUS": m.Prev.Status,
"CI_PREV_PIPELINE_CREATED": strconv.FormatInt(m.Prev.Created, 10),
"CI_PREV_PIPELINE_STARTED": strconv.FormatInt(m.Prev.Started, 10),

View file

@ -53,6 +53,7 @@ type (
Event string `json:"event,omitempty"`
ForgeURL string `json:"forge_url,omitempty"`
Target string `json:"target,omitempty"`
Task string `json:"task,omitempty"`
Trusted bool `json:"trusted,omitempty"`
Commit Commit `json:"commit,omitempty"`
Parent int64 `json:"parent,omitempty"`

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"
}
]
}
}
},
@ -691,8 +706,17 @@
"description": "Advanced options for the kubernetes agent backends",
"type": "object",
"properties": {
"resources": {
"$ref": "#/definitions/step_backend_kubernetes_resources"
"labels": {
"type": "object",
"additionalProperties": {
"type": ["boolean", "string", "number"]
}
},
"annotations": {
"type": "object",
"additionalProperties": {
"type": ["boolean", "string", "number"]
}
},
"securityContext": {
"$ref": "#/definitions/step_backend_kubernetes_security_context"

View file

@ -141,7 +141,7 @@ func TestUnmarshalContainer(t *testing.T) {
assert.EqualValues(t, want, got, "problem parsing container")
}
// TestUnmarshalContainersErr unmarshals a map of containers. The order is
// TestUnmarshalContainers unmarshals a map of containers. The order is
// retained and the container key may be used as the container name if a
// name is not explicitly provided.
func TestUnmarshalContainers(t *testing.T) {

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

View file

@ -32,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
@ -98,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
@ -233,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
@ -254,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

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

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
@ -114,13 +114,13 @@ 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)

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

@ -38,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
@ -100,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
@ -146,7 +147,7 @@ func GetPipelines(c *gin.Context) {
// DeletePipeline
//
// @Summary Delete pipeline
// @Summary Delete a pipeline
// @Router /repos/{repo_id}/pipelines/{number} [delete]
// @Produce plain
// @Success 204
@ -186,7 +187,7 @@ func DeletePipeline(c *gin.Context) {
// GetPipeline
//
// @Summary Pipeline information by number
// @Summary Get a repositories pipeline
// @Router /repos/{repo_id}/pipelines/{number} [get]
// @Produce json
// @Success 200 {object} Pipeline
@ -241,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
@ -297,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
@ -357,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
@ -391,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
@ -427,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
@ -459,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
@ -491,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
@ -546,6 +547,9 @@ func PostPipeline(c *gin.Context) {
// make Deploy overridable
// make Deploy task overridable
pl.DeployTask = c.DefaultQuery("deploy_task", pl.DeployTask)
// make Event overridable to deploy
// TODO refactor to use own proper API for deploy
if event, ok := c.GetQuery("event"); ok {
@ -588,7 +592,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

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

@ -199,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
@ -273,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
@ -296,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
@ -322,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
@ -337,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
@ -367,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
@ -547,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
@ -572,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

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

@ -32,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
@ -44,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) {
@ -77,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

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

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

@ -141,7 +141,7 @@ func convertUser(from *internal.Account, token *oauth2.Token) *model.User {
}
}
// convertTeamList is a helper function used to convert a Bitbucket team list
// convertWorkspaceList is a helper function used to convert a Bitbucket team list
// structure to the Woodpecker Team structure.
func convertWorkspaceList(from []*internal.Workspace) []*model.Team {
var teams []*model.Team
@ -151,7 +151,7 @@ func convertWorkspaceList(from []*internal.Workspace) []*model.Team {
return teams
}
// convertTeam is a helper function used to convert a Bitbucket team account
// convertWorkspace is a helper function used to convert a Bitbucket team account
// structure to the Woodpecker Team structure.
func convertWorkspace(from *internal.Workspace) *model.Team {
return &model.Team{

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

@ -161,7 +161,7 @@ func convertRepoHook(eventRepo *github.PushEventRepository) *model.Repo {
return repo
}
// covertLabels is a helper function used to convert a GitHub label list to
// convertLabels is a helper function used to convert a GitHub label list to
// the common Woodpecker label structure.
func convertLabels(from []*github.Label) []string {
labels := make([]string, len(from))

View file

@ -224,6 +224,7 @@ func Test_helper(t *testing.T) {
from := &github.DeploymentEvent{Deployment: &github.Deployment{}, Sender: &github.User{}}
from.Deployment.Description = github.String(":shipit:")
from.Deployment.Environment = github.String("production")
from.Deployment.Task = github.String("deploy")
from.Deployment.ID = github.Int64(42)
from.Deployment.Ref = github.String("main")
from.Deployment.SHA = github.String("f72fc19")

View file

@ -120,16 +120,17 @@ func parsePushHook(hook *github.PushEvent) (*model.Repo, *model.Pipeline) {
// If the commit type is unsupported nil values are returned.
func parseDeployHook(hook *github.DeploymentEvent) (*model.Repo, *model.Pipeline) {
pipeline := &model.Pipeline{
Event: model.EventDeploy,
Commit: hook.GetDeployment().GetSHA(),
ForgeURL: hook.GetDeployment().GetURL(),
Message: hook.GetDeployment().GetDescription(),
Ref: hook.GetDeployment().GetRef(),
Branch: hook.GetDeployment().GetRef(),
Deploy: hook.GetDeployment().GetEnvironment(),
Avatar: hook.GetSender().GetAvatarURL(),
Author: hook.GetSender().GetLogin(),
Sender: hook.GetSender().GetLogin(),
Event: model.EventDeploy,
Commit: hook.GetDeployment().GetSHA(),
ForgeURL: hook.GetDeployment().GetURL(),
Message: hook.GetDeployment().GetDescription(),
Ref: hook.GetDeployment().GetRef(),
Branch: hook.GetDeployment().GetRef(),
Deploy: hook.GetDeployment().GetEnvironment(),
Avatar: hook.GetSender().GetAvatarURL(),
Author: hook.GetSender().GetLogin(),
Sender: hook.GetSender().GetLogin(),
DeployTask: hook.GetDeployment().GetTask(),
}
// if the ref is a sha or short sha we need to manually construct the ref.
if strings.HasPrefix(pipeline.Commit, pipeline.Ref) || pipeline.Commit == pipeline.Ref {

View file

@ -119,6 +119,8 @@ func Test_parser(t *testing.T) {
g.Assert(b).IsNotNil()
g.Assert(p).IsNil()
g.Assert(b.Event).Equal(model.EventDeploy)
g.Assert(b.Deploy).Equal("production")
g.Assert(b.DeployTask).Equal("deploy")
})
})

View file

@ -33,6 +33,7 @@ type Pipeline struct {
Started int64 `json:"started_at" xorm:"pipeline_started"`
Finished int64 `json:"finished_at" xorm:"pipeline_finished"`
Deploy string `json:"deploy_to" xorm:"pipeline_deploy"`
DeployTask string `json:"deploy_task" xorm:"pipeline_deploy_task"`
Commit string `json:"commit" xorm:"pipeline_commit"`
Branch string `json:"branch" xorm:"pipeline_branch"`
Ref string `json:"ref" xorm:"pipeline_ref"`

View file

@ -64,7 +64,7 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin
}
newPipeline := createNewOutOfOld(lastPipeline)
newPipeline.Parent = lastPipeline.ID
newPipeline.Parent = lastPipeline.Number
err = store.CreatePipeline(newPipeline)
if err != nil {

View file

@ -47,11 +47,11 @@ func TestMetadataFromStruct(t *testing.T) {
"CI_COMMIT_AUTHOR": "", "CI_COMMIT_AUTHOR_AVATAR": "", "CI_COMMIT_AUTHOR_EMAIL": "", "CI_COMMIT_BRANCH": "",
"CI_COMMIT_MESSAGE": "", "CI_COMMIT_PULL_REQUEST": "", "CI_COMMIT_PULL_REQUEST_LABELS": "", "CI_COMMIT_REF": "", "CI_COMMIT_REFSPEC": "", "CI_COMMIT_SHA": "", "CI_COMMIT_SOURCE_BRANCH": "",
"CI_COMMIT_TAG": "", "CI_COMMIT_TARGET_BRANCH": "", "CI_COMMIT_URL": "", "CI_FORGE_TYPE": "", "CI_FORGE_URL": "",
"CI_PIPELINE_CREATED": "0", "CI_PIPELINE_DEPLOY_TARGET": "", "CI_PIPELINE_EVENT": "", "CI_PIPELINE_FINISHED": "0", "CI_PIPELINE_FILES": "[]", "CI_PIPELINE_NUMBER": "0",
"CI_PIPELINE_CREATED": "0", "CI_PIPELINE_DEPLOY_TARGET": "", "CI_PIPELINE_DEPLOY_TASK": "", "CI_PIPELINE_EVENT": "", "CI_PIPELINE_FINISHED": "0", "CI_PIPELINE_FILES": "[]", "CI_PIPELINE_NUMBER": "0",
"CI_PIPELINE_PARENT": "0", "CI_PIPELINE_STARTED": "0", "CI_PIPELINE_STATUS": "", "CI_PIPELINE_URL": "/repos/0/pipeline/0", "CI_PIPELINE_FORGE_URL": "",
"CI_PREV_COMMIT_AUTHOR": "", "CI_PREV_COMMIT_AUTHOR_AVATAR": "", "CI_PREV_COMMIT_AUTHOR_EMAIL": "", "CI_PREV_COMMIT_BRANCH": "",
"CI_PREV_COMMIT_MESSAGE": "", "CI_PREV_COMMIT_REF": "", "CI_PREV_COMMIT_REFSPEC": "", "CI_PREV_COMMIT_SHA": "", "CI_PREV_COMMIT_URL": "", "CI_PREV_PIPELINE_CREATED": "0",
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_NUMBER": "0", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_DEPLOY_TASK": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_NUMBER": "0", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_STARTED": "0", "CI_PREV_PIPELINE_STATUS": "", "CI_PREV_PIPELINE_URL": "/repos/0/pipeline/0", "CI_PREV_PIPELINE_FORGE_URL": "", "CI_REPO": "", "CI_REPO_CLONE_URL": "", "CI_REPO_CLONE_SSH_URL": "", "CI_REPO_DEFAULT_BRANCH": "", "CI_REPO_REMOTE_ID": "",
"CI_REPO_NAME": "", "CI_REPO_OWNER": "", "CI_REPO_PRIVATE": "false", "CI_REPO_SCM": "git", "CI_REPO_TRUSTED": "false", "CI_REPO_URL": "", "CI_STEP_FINISHED": "",
"CI_STEP_NAME": "", "CI_STEP_NUMBER": "0", "CI_STEP_STARTED": "", "CI_STEP_STATUS": "", "CI_STEP_URL": "/repos/0/pipeline/0", "CI_SYSTEM_HOST": "", "CI_SYSTEM_NAME": "woodpecker",
@ -82,11 +82,11 @@ func TestMetadataFromStruct(t *testing.T) {
"CI_COMMIT_AUTHOR": "", "CI_COMMIT_AUTHOR_AVATAR": "", "CI_COMMIT_AUTHOR_EMAIL": "", "CI_COMMIT_BRANCH": "",
"CI_COMMIT_MESSAGE": "", "CI_COMMIT_PULL_REQUEST": "", "CI_COMMIT_PULL_REQUEST_LABELS": "", "CI_COMMIT_REF": "", "CI_COMMIT_REFSPEC": "", "CI_COMMIT_SHA": "", "CI_COMMIT_SOURCE_BRANCH": "",
"CI_COMMIT_TAG": "", "CI_COMMIT_TARGET_BRANCH": "", "CI_COMMIT_URL": "", "CI_FORGE_TYPE": "gitea", "CI_FORGE_URL": "https://gitea.com",
"CI_PIPELINE_CREATED": "0", "CI_PIPELINE_DEPLOY_TARGET": "", "CI_PIPELINE_EVENT": "", "CI_PIPELINE_FINISHED": "0", "CI_PIPELINE_FILES": `["test.go","markdown file.md"]`,
"CI_PIPELINE_CREATED": "0", "CI_PIPELINE_DEPLOY_TARGET": "", "CI_PIPELINE_DEPLOY_TASK": "", "CI_PIPELINE_EVENT": "", "CI_PIPELINE_FINISHED": "0", "CI_PIPELINE_FILES": `["test.go","markdown file.md"]`,
"CI_PIPELINE_NUMBER": "3", "CI_PIPELINE_PARENT": "0", "CI_PIPELINE_STARTED": "0", "CI_PIPELINE_STATUS": "", "CI_PIPELINE_URL": "https://example.com/repos/0/pipeline/3", "CI_PIPELINE_FORGE_URL": "",
"CI_PREV_COMMIT_AUTHOR": "", "CI_PREV_COMMIT_AUTHOR_AVATAR": "", "CI_PREV_COMMIT_AUTHOR_EMAIL": "", "CI_PREV_COMMIT_BRANCH": "",
"CI_PREV_COMMIT_MESSAGE": "", "CI_PREV_COMMIT_REF": "", "CI_PREV_COMMIT_REFSPEC": "", "CI_PREV_COMMIT_SHA": "", "CI_PREV_COMMIT_URL": "", "CI_PREV_PIPELINE_CREATED": "0",
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_NUMBER": "2", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_DEPLOY_TASK": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_NUMBER": "2", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_STARTED": "0", "CI_PREV_PIPELINE_STATUS": "", "CI_PREV_PIPELINE_URL": "https://example.com/repos/0/pipeline/2", "CI_PREV_PIPELINE_FORGE_URL": "", "CI_REPO": "testUser/testRepo", "CI_REPO_CLONE_URL": "https://gitea.com/testUser/testRepo.git", "CI_REPO_CLONE_SSH_URL": "git@gitea.com:testUser/testRepo.git",
"CI_REPO_DEFAULT_BRANCH": "main", "CI_REPO_NAME": "testRepo", "CI_REPO_OWNER": "testUser", "CI_REPO_PRIVATE": "true", "CI_REPO_REMOTE_ID": "",
"CI_REPO_SCM": "git", "CI_REPO_TRUSTED": "false", "CI_REPO_URL": "https://gitea.com/testUser/testRepo", "CI_STEP_FINISHED": "",

View file

@ -66,7 +66,7 @@ func SetRepo() gin.HandlerFunc {
repo, err = _store.GetRepoName(fullName)
}
if repo != nil {
if repo != nil && err == nil {
c.Set("repo", repo)
c.Next()
return

View file

@ -92,7 +92,7 @@ func (f *forgeFetcherContext) fetch(c context.Context, config string) ([]*types.
fileMetas, err := f.getFirstAvailableConfig(ctx, configs)
if err == nil {
return fileMetas, err
return fileMetas, nil
}
return nil, fmt.Errorf("user defined config '%s' not found: %w", config, err)
@ -102,7 +102,7 @@ func (f *forgeFetcherContext) fetch(c context.Context, config string) ([]*types.
// for the order see shared/constants/constants.go
fileMetas, err := f.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder[:])
if err == nil {
return fileMetas, err
return fileMetas, nil
}
select {

View file

@ -197,7 +197,8 @@
"title": "Zusätzliche Pipeline-Variablen",
"value": "Variablenwert",
"delete": "Variable löschen"
}
},
"enter_task": "Aufgabe des Deployments"
},
"enable": {
"disabled": "Deaktiviert",
@ -228,8 +229,8 @@
"cancel_success": "Pipeline abgebrochen",
"canceled": "Dieser Schritt wurde abgebrochen.",
"deploy": "Deploy",
"log_auto_scroll": "Automatisches folgen",
"log_auto_scroll_off": "Schalte automatisches folgen aus",
"log_auto_scroll": "Automatisches Folgen",
"log_auto_scroll_off": "Schalte automatisches Folgen aus",
"log_download": "Herunterladen",
"restart": "Neustarten",
"restart_success": "Pipeline neu gestartet",
@ -397,7 +398,7 @@
},
"allow_deploy": {
"allow": "Deployments erlauben",
"desc": "Deployments von erolgreichen Pipelines erlauben. Nur benutzen, wenn du allen Nutzern mit Push-Zugriff vertraust."
"desc": "Deployments von erfolgreichen Pipelines erlauben. Nur benutzen, wenn du allen Nutzern mit Push-Zugriff vertraust."
}
},
"not_allowed": "Zugriff auf die Einstellungen dieses Repositorys nicht erlaubt",

View file

@ -48,6 +48,7 @@
"deploy_pipeline": {
"title": "Trigger deployment event for current pipeline #{pipelineId}",
"enter_target": "Target deployment environment",
"enter_task": "Deployment task",
"trigger": "Deploy",
"variables": {
"delete": "Delete variable",

View file

@ -1,309 +1,337 @@
{
"admin": {
"settings": {
"not_allowed": "Ви не маєте права доступу до налаштувань сервера",
"secrets": {
"add": "Додати секрет",
"created": "Створено глобальний секрет",
"deleted": "Глобальну таємницю видалено",
"desc": "Глобальні секрети можуть бути передані всім сховищам, окремим крокам конвеєра під час виконання як змінні середовища.",
"events": {
"events": "Доступно на наступних заходах",
"pr_warning": "Будь ласка, будьте обережні з цією опцією, оскільки зловмисник може надіслати зловмисний запит, який розкриє ваші секрети."
},
"images": {
"desc": "Через кому список зображень, для яких доступний цей секрет, залишити порожнім, щоб дозволити всі зображення",
"images": "Доступно для наступних зображень"
},
"name": "Найменування",
"none": "Глобальних секретів поки що не існує.",
"save": "Зберегти секрет",
"saved": "Глобальний секрет збережено",
"secrets": "Секрети",
"show": "Показати секрети",
"value": "Значення",
"warning": "Ці секрети будуть доступні для всіх користувачів сервера."
},
"settings": "Налаштування"
}
},
"back": "Назад",
"cancel": "Скасувати",
"docs": "Документи",
"documentation_for": "Документація для \"{topic}\"",
"errors": {
"not_found": "Серверу не вдалося знайти запитуваний об'єкт"
},
"login": "Логін",
"logout": "Вихід",
"not_found": {
"back_home": "Повертаємося додому",
"not_found": "Ого 404, або ми щось зламали, або у вас помилка при наборі тексту :-/"
},
"org": {
"settings": {
"not_allowed": "Ви не маєте права доступу до налаштувань цієї організації",
"secrets": {
"add": "Додати секрет",
"created": "Секрет організації збережено",
"deleted": "Організаційну таємницю видалено",
"desc": "Секрети організації можуть бути передані всім окремим крокам конвеєра сховища організації під час виконання як змінні середовища.",
"events": {
"events": "Доступно на наступних заходах",
"pr_warning": "Будь ласка, будьте обережні з цією опцією, оскільки зловмисник може надіслати зловмисний запит, який розкриє ваші секрети."
},
"images": {
"desc": "Через кому список зображень, для яких доступний цей секрет, залишити порожнім, щоб дозволити всі зображення",
"images": "Доступно для наступних зображень"
},
"name": "Найменування",
"none": "Секретів організації поки що немає.",
"save": "Зберегти секрет",
"saved": "Секрет організації збережено",
"secrets": "Секрети",
"show": "Показати секрети",
"value": "Значення"
},
"settings": "Налаштування"
}
},
"password": "Пароль",
"pipeline_feed": "Трубопровідна подача",
"repo": {
"activity": "Активність",
"add": "Додати репозиторій",
"branches": "Відділення",
"deploy_pipeline": {
"enter_target": "Цільове середовище розгортання",
"variables": {
"add": "Додати змінну",
"desc": "Вкажіть додаткові змінні для використання у конвеєрі. Змінні з однаковими іменами буде перезаписано.",
"title": "Додаткові змінні конвеєра"
}
},
"enable": {
"enable": "Увімкнути",
"enabled": "Вже увімкнено",
"list_reloaded": "Оновлений список репозиторіїв",
"reload": "Перезавантажити репозиторії",
"success": "Репозиторій увімкнено"
},
"manual_pipeline": {
"select_branch": "Оберіть відділення",
"title": "Запустити ручний прогін трубопроводу",
"trigger": "Запустити трубопровід",
"variables": {
"add": "Додати змінну",
"desc": "Вкажіть додаткові змінні для використання в конвеєрі. Змінні з однаковими іменами будуть перезаписані.",
"name": "Ім'я змінної",
"title": "Додаткові змінні трубопроводу",
"value": "Значення змінної"
}
},
"not_allowed": "Ви не маєте права доступу до цього сховища",
"open_in_forge": "Відкрити репозиторій у системі керування версіями",
"pipeline": {
"actions": {
"cancel": "Скасувати",
"cancel_success": "Трубопровід скасовано",
"canceled": "Цей крок було скасовано.",
"log_auto_scroll": "Автоматична прокрутка вниз",
"log_auto_scroll_off": "Вимкнути автоматичну прокрутку",
"log_download": "Завантажити",
"restart": "Перезапуск",
"restart_success": "Трубопровід перезапущено"
},
"config": "Конфіг",
"event": {
"cron": "Крон",
"deploy": "Розгорнути",
"manual": "Посібник",
"pr": "Запит на вилучення",
"push": "Натисни",
"tag": "Тег"
},
"exit_code": "код виходу {exitCode}",
"files": "Змінені файли ({files})",
"loading": "Загрузка…",
"log_download_error": "Виникла помилка при завантаженні лог-файлу",
"no_files": "Жодні файли не були змінені.",
"no_pipeline_steps": "Сходинки трубопроводу відсутні!",
"no_pipelines": "Жоден трубопровід ще не був запущений.",
"pipeline": "Трубопровід #{pipelineId}",
"pipelines_for": "Трубопроводи для відгалуження \"{branch}\"",
"protected": {
"approve": "Затвердити",
"approve_success": "Трубопровід схвалено",
"awaits": "Цей трубопровід чекає на погодження якогось експлуатаційника!",
"decline": "Спад",
"decline_success": "Трубопровід відхилено",
"declined": "Від цього газопроводу відмовилися!"
},
"step_not_started": "Цей крок ще не розпочався.",
"tasks": "Задачі"
},
"settings": {
"actions": {
"actions": "Дії",
"delete": {
"confirm": "Всі дані будуть втрачені після цієї дії!!!\n\nВи дійсно хочете продовжити?",
"delete": "Видалити сховище",
"success": "Репозиторій видалено"
},
"disable": {
"disable": "Відключити репозиторій",
"success": "Репозиторій відключено"
},
"repair": {
"repair": "Ремонтний репозиторій",
"success": "Сховище відремонтовано"
"admin": {
"settings": {
"not_allowed": "Ви не маєте права доступу до налаштувань сервера",
"secrets": {
"add": "Додати секрет",
"created": "Створено глобальний секрет",
"deleted": "Глобальну таємницю видалено",
"desc": "Глобальні секрети можуть бути передані всім сховищам, окремим крокам конвеєра під час виконання як змінні середовища.",
"events": {
"events": "Доступно на наступних заходах",
"pr_warning": "Будь ласка, будьте обережні з цією опцією, оскільки зловмисник може надіслати зловмисний запит, який розкриє ваші секрети."
},
"images": {
"desc": "Через кому список зображень, для яких доступний цей секрет, залишити порожнім, щоб дозволити всі зображення",
"images": "Доступно для наступних зображень"
},
"name": "Найменування",
"none": "Глобальних секретів поки що не існує.",
"save": "Зберегти секрет",
"saved": "Глобальний секрет збережено",
"secrets": "Секрети",
"show": "Показати секрети",
"value": "Значення",
"warning": "Ці секрети будуть доступні для всіх користувачів сервера."
},
"settings": "Налаштування"
}
},
"badge": {
"badge": "Бейдж",
"branch": "Філія",
"type": "Синтаксис",
"type_html": "HTML",
"type_markdown": "Уцінка",
"type_url": "URL"
},
"crons": {
"add": "Додати cron",
"branch": {
"placeholder": "Гілка (використовує гілку за замовчуванням, якщо порожня)",
"title": "Філія"
},
"created": "Створено Cron",
"crons": "Крони",
"delete": "Видалити cron",
"deleted": "Cron видалено",
"desc": "Завдання Cron можна використовувати для регулярного запуску трубопроводів.",
"edit": "Редагувати cron",
"name": {
"name": "Назва",
"placeholder": "Назва cron завдання"
},
"next_exec": "Наступне виконання",
"none": "Крон поки що немає.",
"not_executed_yet": "Ще не виконано",
"save": "Зберегти cron",
"saved": "Cron збережено",
"schedule": {
"placeholder": "Розклад",
"title": "Розклад (на основі UTC)"
},
"show": "Показати крони"
},
"general": {
"allow_pr": {
"allow": "Дозволити запити на витяг",
"desc": "Конвеєри можуть працювати на основі запитів."
},
"cancel_prev": {
"cancel": "Скасувати попередні трубопроводи",
"desc": "Дозволяє скасовувати відкладені та запущені конвеєри однієї і тієї ж події та контексту перед запуском нового конвеєра."
},
"general": "Генерал",
"pipeline_path": {
"default": "За замовчуванням: .woodpecker/*.yml -> .woodpecker.yml",
"path": "Траса трубопроводу"
},
"project": "Налаштування проекту",
"protected": {
"desc": "Кожен трубопровід повинен бути схвалений перед початком будівництва.",
"protected": "Захищений"
},
"save": "Зберегти настройки",
"success": "Оновлено налаштування репозиторію",
"timeout": {
"minutes": "хвилини",
"timeout": "Таймаут"
},
"trusted": {
"desc": "Кожен трубопровід повинен бути схвалений перед початком будівництва.",
"trusted": "Довірені"
},
"visibility": {
"internal": {
"desc": "Цей проект можуть бачити лише авторизовані користувачі інстанції Woodpecker.",
"internal": "Внутрішній"
},
"private": {
"desc": "Цей проект можете бачити тільки ви та інші власники сховища.",
"private": "Приватний"
},
"public": {
"desc": "Кожен користувач може побачити ваш проект, не входячи в систему.",
"public": "Публічні"
},
"visibility": "Прозорість проекту"
}
},
"not_allowed": "Ви не маєте права доступу до налаштувань цього сховища",
"registries": {
"add": "Додати реєстр",
"address": {
"address": "Адреса",
"placeholder": "Адреса реєстру (наприклад, docker.io)"
},
"created": "Створено облікові дані реєстру",
"credentials": "Реквізити реєстру",
"delete": "Видалення реєстру",
"deleted": "Видалено облікові дані реєстру",
"desc": "Облікові дані реєстрів можуть бути додані для використання приватних зображень для вашого конвеєра.",
"edit": "Редагування реєстру",
"none": "Повноважень реєстру поки що немає.",
"registries": "Реєстри",
"save": "Зберегти реєстр",
"saved": "Облікові дані реєстру збережено",
"show": "Показати реєстри"
},
"secrets": {
"add": "Додати секрет",
"created": "Секрет створено",
"delete": "Видалити секрет",
"deleted": "Секрет видалено",
"desc": "Секрети можуть бути передані окремим етапам конвеєра під час виконання як змінні середовища.",
"edit": "Секрет редагування",
"events": {
"events": "Доступно на наступних заходах",
"pr_warning": "Будь ласка, будьте обережні з цією опцією, оскільки зловмисник може надіслати зловмисний запит, який розкриє ваші секрети."
},
"images": {
"desc": "Через кому список зображень, для яких доступний цей секрет, залишити порожнім, щоб дозволити всі зображення",
"images": "Доступно для наступних зображень"
},
"name": "Назва",
"none": "Секретів поки що немає.",
"save": "Зберегти секрет",
"saved": "Секрет збережено",
"secrets": "Секрети",
"show": "Показати секрети",
"value": "Значення"
},
"settings": "Налаштування"
},
"user_none": "Ця організація/користувач ще не має проектів."
},
"repos": "Репо",
"repositories": "Сховища",
"search": "Обшук…",
"time": {
"days_short": "д",
"hours_short": "г",
"min_short": "хв",
"not_started": "ще не розпочато",
"sec_short": "сек",
"template": "MMM D, РРРР, ГГ:п z",
"weeks_short": "т"
},
"unknown_error": "Виникла невідома помилка",
"url": "URL",
"user": {
"access_denied": "Ви не авторизовані для входу",
"internal_error": "Виникла внутрішня помилка",
"oauth_error": "Помилка під час автентифікації у провайдера OAuth"
},
"username": "Ім'я користувача",
"welcome": "Ласкаво просимо до Woodpcker"
"back": "Назад",
"cancel": "Скасувати",
"docs": "Документи",
"documentation_for": "Документація для \"{topic}\"",
"errors": {
"not_found": "Серверу не вдалося знайти запитуваний об'єкт"
},
"login": "Логін",
"logout": "Вихід",
"not_found": {
"back_home": "Повертаємося додому",
"not_found": "Ого 404, або ми щось зламали, або у вас помилка при наборі тексту :-/"
},
"org": {
"settings": {
"not_allowed": "Ви не маєте права доступу до налаштувань цієї організації",
"secrets": {
"add": "Додати секрет",
"created": "Секрет організації збережено",
"deleted": "Організаційну таємницю видалено",
"desc": "Секрети організації можуть бути передані всім окремим крокам конвеєра сховища організації під час виконання як змінні середовища.",
"events": {
"events": "Доступно на наступних заходах",
"pr_warning": "Будь ласка, будьте обережні з цією опцією, оскільки зловмисник може надіслати зловмисний запит, який розкриє ваші секрети."
},
"images": {
"desc": "Через кому список зображень, для яких доступний цей секрет, залишити порожнім, щоб дозволити всі зображення",
"images": "Доступно для наступних зображень"
},
"name": "Найменування",
"none": "Секретів організації поки що немає.",
"save": "Зберегти секрет",
"saved": "Секрет організації збережено",
"secrets": "Секрети",
"show": "Показати секрети",
"value": "Значення"
},
"settings": "Налаштування"
}
},
"password": "Пароль",
"pipeline_feed": "Трубопровідна подача",
"repo": {
"activity": "Активність",
"add": "Додати репозиторій",
"branches": "Відділення",
"deploy_pipeline": {
"enter_target": "Цільове середовище розгортання",
"variables": {
"add": "Додати змінну",
"desc": "Вкажіть додаткові змінні для використання у конвеєрі. Змінні з однаковими іменами буде перезаписано.",
"title": "Додаткові змінні конвеєра",
"delete": "Видалити змінну",
"name": "Ім'я змінної",
"value": "Змінне значення"
},
"enter_task": "Завдання на розгортання",
"title": "Ініціювати подію розгортання для поточного конвеєра #{pipelineId}",
"trigger": "Розгорнути"
},
"enable": {
"enable": "Увімкнути",
"enabled": "Вже увімкнено",
"list_reloaded": "Оновлений список репозиторіїв",
"reload": "Перезавантажити репозиторії",
"success": "Репозиторій увімкнено",
"disabled": "Вимкнено"
},
"manual_pipeline": {
"select_branch": "Оберіть відділення",
"title": "Запустити ручний прогін трубопроводу",
"trigger": "Запуск конвеєра",
"variables": {
"add": "Додати змінну",
"desc": "Вкажіть додаткові змінні для використання в конвеєрі. Змінні з однаковими іменами будуть перезаписані.",
"name": "Ім'я змінної",
"title": "Додаткові змінні трубопроводу",
"value": "Значення змінної",
"delete": "Вилучити змінну"
}
},
"not_allowed": "Ви не маєте права доступу до цього сховища",
"open_in_forge": "Відкрити репозиторій у системі керування версіями",
"pipeline": {
"actions": {
"cancel": "Скасувати",
"cancel_success": "Трубопровід скасовано",
"canceled": "Цей крок було скасовано.",
"log_auto_scroll": "Автоматична прокрутка вниз",
"log_auto_scroll_off": "Вимкнути автоматичну прокрутку",
"log_download": "Завантажити",
"restart": "Перезапуск",
"restart_success": "Трубопровід перезапущено"
},
"config": "Конфіг",
"event": {
"cron": "Крон",
"deploy": "Розгорнути",
"manual": "Посібник",
"pr": "Запит на вилучення",
"push": "Натисни",
"tag": "Тег"
},
"exit_code": "код виходу {exitCode}",
"files": "Змінені файли ({files})",
"loading": "Загрузка…",
"log_download_error": "Виникла помилка при завантаженні лог-файлу",
"no_files": "Жодні файли не були змінені.",
"no_pipeline_steps": "Сходинки трубопроводу відсутні!",
"no_pipelines": "Жоден трубопровід ще не був запущений.",
"pipeline": "Трубопровід #{pipelineId}",
"pipelines_for": "Трубопроводи для відгалуження \"{branch}\"",
"protected": {
"approve": "Затвердити",
"approve_success": "Трубопровід схвалено",
"awaits": "Цей трубопровід чекає на погодження якогось експлуатаційника!",
"decline": "Спад",
"decline_success": "Трубопровід відхилено",
"declined": "Від цього газопроводу відмовилися!"
},
"step_not_started": "Цей крок ще не розпочався.",
"tasks": "Задачі",
"pipelines_for_pr": "Конвеєри для запиту на пул #{index}"
},
"settings": {
"actions": {
"actions": "Дії",
"delete": {
"confirm": "Всі дані будуть втрачені після цієї дії!!!\n\nВи дійсно хочете продовжити?",
"delete": "Видалити сховище",
"success": "Репозиторій видалено"
},
"disable": {
"disable": "Відключити репозиторій",
"success": "Репозиторій відключено"
},
"repair": {
"repair": "Ремонтний репозиторій",
"success": "Сховище відремонтовано"
},
"enable": {
"enable": "Увімкнути репозиторій",
"success": "Репозиторій увімкнено"
}
},
"badge": {
"badge": "Бейдж",
"branch": "Філія",
"type": "Синтаксис",
"type_html": "HTML",
"type_markdown": "Уцінка",
"type_url": "URL"
},
"crons": {
"add": "Додати cron",
"branch": {
"placeholder": "Гілка (використовує гілку за замовчуванням, якщо порожня)",
"title": "Філія"
},
"created": "Створено Cron",
"crons": "Крони",
"delete": "Видалити cron",
"deleted": "Cron видалено",
"desc": "Завдання Cron можна використовувати для регулярного запуску трубопроводів.",
"edit": "Редагувати cron",
"name": {
"name": "Назва",
"placeholder": "Назва cron завдання"
},
"next_exec": "Наступне виконання",
"none": "Крон поки що немає.",
"not_executed_yet": "Ще не виконано",
"save": "Зберегти cron",
"saved": "Cron збережено",
"schedule": {
"placeholder": "Розклад",
"title": "Розклад (на основі UTC)"
},
"show": "Показати крони",
"run": "Виконати зараз"
},
"general": {
"allow_pr": {
"allow": "Дозволити запити на витяг",
"desc": "Конвеєри можуть працювати на основі запитів."
},
"cancel_prev": {
"cancel": "Скасувати попередні трубопроводи",
"desc": "Дозволяє скасовувати відкладені та запущені конвеєри однієї і тієї ж події та контексту перед запуском нового конвеєра."
},
"general": "Генерал",
"pipeline_path": {
"default": "За замовчуванням: .woodpecker/*.yml -> .woodpecker.yml",
"path": "Траса трубопроводу",
"desc_path_example": "мій/шлях/"
},
"project": "Налаштування проекту",
"protected": {
"desc": "Кожен трубопровід повинен бути схвалений перед початком будівництва.",
"protected": "Захищений"
},
"save": "Зберегти настройки",
"success": "Оновлено налаштування репозиторію",
"timeout": {
"minutes": "хвилини",
"timeout": "Таймаут"
},
"trusted": {
"desc": "Кожен трубопровід повинен бути схвалений перед початком будівництва.",
"trusted": "Довірені"
},
"visibility": {
"internal": {
"desc": "Цей проект можуть бачити лише авторизовані користувачі інстанції Woodpecker.",
"internal": "Внутрішній"
},
"private": {
"desc": "Цей проект можете бачити тільки ви та інші власники сховища.",
"private": "Приватний"
},
"public": {
"desc": "Кожен користувач може побачити ваш проект, не входячи в систему.",
"public": "Публічні"
},
"visibility": "Прозорість проекту"
},
"netrc_only_trusted": {
"netrc_only_trusted": "Вставляйте облікові дані netrc лише в надійні контейнери",
"desc": "Вставляйте облікові дані netrc лише в надійні контейнери (рекомендовано)."
},
"allow_deploy": {
"allow": "Дозволити розгортання",
"desc": "Дозволити розгортання з успішних конвеєрів. Використовуйте лише в тому випадку, якщо ви довіряєте всім користувачам із доступом push."
}
},
"not_allowed": "Ви не маєте права доступу до налаштувань цього сховища",
"registries": {
"add": "Додати реєстр",
"address": {
"address": "Адреса",
"placeholder": "Адреса реєстру (наприклад, docker.io)"
},
"created": "Створено облікові дані реєстру",
"credentials": "Реквізити реєстру",
"delete": "Видалення реєстру",
"deleted": "Видалено облікові дані реєстру",
"desc": "Облікові дані реєстрів можуть бути додані для використання приватних зображень для вашого конвеєра.",
"edit": "Редагування реєстру",
"none": "Повноважень реєстру поки що немає.",
"registries": "Реєстри",
"save": "Зберегти реєстр",
"saved": "Облікові дані реєстру збережено",
"show": "Показати реєстри"
},
"secrets": {
"add": "Додати секрет",
"created": "Секрет створено",
"delete": "Видалити секрет",
"deleted": "Секрет видалено",
"desc": "Секрети можуть бути передані окремим етапам конвеєра під час виконання як змінні середовища.",
"edit": "Секрет редагування",
"events": {
"events": "Доступно на наступних заходах",
"pr_warning": "Будь ласка, будьте обережні з цією опцією, оскільки зловмисник може надіслати зловмисний запит, який розкриє ваші секрети."
},
"images": {
"desc": "Через кому список зображень, для яких доступний цей секрет, залишити порожнім, щоб дозволити всі зображення",
"images": "Доступно для наступних зображень"
},
"name": "Назва",
"none": "Секретів поки що немає.",
"save": "Зберегти секрет",
"saved": "Секрет збережено",
"secrets": "Секрети",
"show": "Показати секрети",
"value": "Значення",
"delete_confirm": "Ви справді хочете видалити цей секрет?",
"plugins_only": "Доступно лише для плагінів"
},
"settings": "Налаштування"
},
"user_none": "Ця організація/користувач ще не має проектів.",
"pull_requests": "Запит на пул"
},
"repos": "Репо",
"repositories": "Сховища",
"search": "Обшук…",
"time": {
"days_short": "д",
"hours_short": "г",
"min_short": "хв",
"not_started": "ще не розпочато",
"sec_short": "сек",
"template": "MMM D, РРРР, ГГ:п z",
"weeks_short": "т"
},
"unknown_error": "Виникла невідома помилка",
"url": "URL",
"user": {
"access_denied": "Ви не авторизовані для входу",
"internal_error": "Виникла внутрішня помилка",
"oauth_error": "Помилка під час автентифікації у провайдера OAuth"
},
"username": "Ім'я користувача",
"welcome": "Ласкаво просимо до Woodpcker",
"api": "API",
"empty_list": "{entity} не знайдено!"
}

View file

@ -158,7 +158,7 @@ const selectedAgent = ref<Partial<Agent>>();
const isEditingAgent = computed(() => !!selectedAgent.value?.id);
async function loadAgents(page: number): Promise<Agent[] | null> {
return apiClient.getAgents(page);
return apiClient.getAgents({ page });
}
const { resetPage, data: agents } = usePagination(loadAgents, () => !selectedAgent.value);

View file

@ -50,7 +50,7 @@ const notifications = useNotifications();
const { t } = useI18n();
async function loadOrgs(page: number): Promise<Org[] | null> {
return apiClient.getOrgs(page);
return apiClient.getOrgs({ page });
}
const { resetPage, data: orgs } = usePagination(loadOrgs);

View file

@ -56,7 +56,7 @@ const notifications = useNotifications();
const i18n = useI18n();
async function loadRepos(page: number): Promise<Repo[] | null> {
return apiClient.getAllRepos(page);
return apiClient.getAllRepos({ page });
}
const { data: repos } = usePagination(loadRepos);

View file

@ -65,7 +65,7 @@ const selectedSecret = ref<Partial<Secret>>();
const isEditingSecret = computed(() => !!selectedSecret.value?.id);
async function loadSecrets(page: number): Promise<Secret[] | null> {
return apiClient.getGlobalSecretList(page);
return apiClient.getGlobalSecretList({ page });
}
const { resetPage, data: secrets } = usePagination(loadSecrets, () => !selectedSecret.value);

View file

@ -107,7 +107,7 @@ const selectedUser = ref<Partial<User>>();
const isEditingUser = computed(() => !!selectedUser.value?.id);
async function loadUsers(page: number): Promise<User[] | null> {
return apiClient.getUsers(page);
return apiClient.getUsers({ page });
}
const { resetPage, data: users } = usePagination(loadUsers, () => !selectedUser.value);

View file

@ -8,6 +8,9 @@
<InputField v-slot="{ id }" :label="$t('repo.deploy_pipeline.enter_target')">
<TextField :id="id" v-model="payload.environment" required />
</InputField>
<InputField v-slot="{ id }" :label="$t('repo.deploy_pipeline.enter_task')">
<TextField :id="id" v-model="payload.task" />
</InputField>
<InputField v-slot="{ id }" :label="$t('repo.deploy_pipeline.variables.title')">
<span class="text-sm text-wp-text-alt-100 mb-2">{{ $t('repo.deploy_pipeline.variables.desc') }}</span>
<div class="flex flex-col gap-2">
@ -69,9 +72,10 @@ const repo = inject('repo');
const router = useRouter();
const payload = ref<{ id: string; environment: string; variables: { name: string; value: string }[] }>({
const payload = ref<{ id: string; environment: string; task: string; variables: { name: string; value: string }[] }>({
id: '',
environment: '',
task: '',
variables: [
{
name: '',

View file

@ -90,7 +90,7 @@ const pipelineOptions = computed(() => {
const loading = ref(true);
onMounted(async () => {
const data = await usePaginate((page) => apiClient.getRepoBranches(repo.value.id, page));
const data = await usePaginate((page) => apiClient.getRepoBranches(repo.value.id, { page }));
branches.value = data.map((e) => ({
text: e,
value: e,

View file

@ -69,7 +69,7 @@ async function loadSecrets(page: number): Promise<Secret[] | null> {
throw new Error("Unexpected: Can't load org");
}
return apiClient.getOrgSecretList(org.value.id, page);
return apiClient.getOrgSecretList(org.value.id, { page });
}
const { resetPage, data: secrets } = usePagination(loadSecrets, () => !selectedSecret.value);

View file

@ -70,7 +70,7 @@ async function loadBranches() {
throw new Error('Unexpected: "repo" should be provided at this place');
}
branches.value = (await usePaginate((page) => apiClient.getRepoBranches(repo.value.id, page)))
branches.value = (await usePaginate((page) => apiClient.getRepoBranches(repo.value.id, { page })))
.map((b) => ({
value: b,
text: b,

View file

@ -121,7 +121,7 @@ async function loadCrons(page: number): Promise<Cron[] | null> {
throw new Error("Unexpected: Can't load repo");
}
return apiClient.getCronList(repo.value.id, page);
return apiClient.getCronList(repo.value.id, { page });
}
const { resetPage, data: crons } = usePagination(loadCrons, () => !selectedCron.value);

View file

@ -104,7 +104,7 @@ async function loadRegistries(page: number): Promise<Registry[] | null> {
throw new Error("Unexpected: Can't load repo");
}
return apiClient.getRegistryList(repo.value.id, page);
return apiClient.getRegistryList(repo.value.id, { page });
}
const { resetPage, data: registries } = usePagination(loadRegistries, () => !selectedRegistry.value);

View file

@ -71,11 +71,11 @@ async function loadSecrets(page: number, level: 'repo' | 'org' | 'global'): Prom
switch (level) {
case 'repo':
return apiClient.getSecretList(repo.value.id, page);
return apiClient.getSecretList(repo.value.id, { page });
case 'org':
return apiClient.getOrgSecretList(repo.value.org_id, page);
return apiClient.getOrgSecretList(repo.value.org_id, { page });
case 'global':
return apiClient.getGlobalSecretList(page);
return apiClient.getGlobalSecretList({ page });
default:
throw new Error(`Unexpected level: ${level}`);
}

View file

@ -78,7 +78,7 @@ async function loadSecrets(page: number): Promise<Secret[] | null> {
throw new Error('Unexpected: Unauthenticated');
}
return apiClient.getOrgSecretList(user.org_id, page);
return apiClient.getOrgSecretList(user.org_id, { page });
}
const { resetPage, data: secrets } = usePagination(loadSecrets, () => !selectedSecret.value);

View file

@ -31,9 +31,15 @@ type PipelineOptions = {
type DeploymentOptions = {
id: string;
environment: string;
task: string;
variables: Record<string, string>;
};
type PaginationOptions = {
page?: number;
perPage?: number;
};
export default class WoodpeckerClient extends ApiClient {
getRepoList(opts?: RepoListOptions): Promise<Repo[]> {
const query = encodeQueryString(opts);
@ -52,12 +58,14 @@ export default class WoodpeckerClient extends ApiClient {
return this._get(`/api/repos/${repoId}/permissions`) as Promise<RepoPermissions>;
}
getRepoBranches(repoId: number, page: number): Promise<string[]> {
return this._get(`/api/repos/${repoId}/branches?page=${page}`) as Promise<string[]>;
getRepoBranches(repoId: number, opts?: PaginationOptions): Promise<string[]> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/branches?${query}`) as Promise<string[]>;
}
getRepoPullRequests(repoId: number, page: number): Promise<PullRequest[]> {
return this._get(`/api/repos/${repoId}/pull_requests?page=${page}`) as Promise<PullRequest[]>;
getRepoPullRequests(repoId: number, opts?: PaginationOptions): Promise<PullRequest[]> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/pull_requests?${query}`) as Promise<PullRequest[]>;
}
activateRepo(forgeRemoteId: string): Promise<Repo> {
@ -82,18 +90,19 @@ export default class WoodpeckerClient extends ApiClient {
}
// Deploy triggers a deployment for an existing pipeline using the
// specified target environment.
// specified target environment and task.
deployPipeline(repoId: number, pipelineNumber: string, options: DeploymentOptions): Promise<Pipeline> {
const vars = {
...options.variables,
event: 'deployment',
deploy_to: options.environment,
deploy_task: options.task,
};
const query = encodeQueryString(vars);
return this._post(`/api/repos/${repoId}/pipelines/${pipelineNumber}?${query}`) as Promise<Pipeline>;
}
getPipelineList(repoId: number, opts?: Record<string, string | number | boolean>): Promise<Pipeline[]> {
getPipelineList(repoId: number, opts?: PaginationOptions & { before?: string; after?: string }): Promise<Pipeline[]> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/pipelines?${query}`) as Promise<Pipeline[]>;
}
@ -106,9 +115,8 @@ export default class WoodpeckerClient extends ApiClient {
return this._get(`/api/repos/${repoId}/pipelines/${pipelineNumber}/config`) as Promise<PipelineConfig[]>;
}
getPipelineFeed(opts?: Record<string, string | number | boolean>): Promise<PipelineFeed[]> {
const query = encodeQueryString(opts);
return this._get(`/api/user/feed?${query}`) as Promise<PipelineFeed[]>;
getPipelineFeed(): Promise<PipelineFeed[]> {
return this._get(`/api/user/feed`) as Promise<PipelineFeed[]>;
}
cancelPipeline(repoId: number, pipelineNumber: number): Promise<unknown> {
@ -126,7 +134,7 @@ export default class WoodpeckerClient extends ApiClient {
restartPipeline(
repoId: number,
pipeline: string,
opts?: Record<string, string | number | boolean>,
opts?: { event?: string; deploy_to?: string; fork?: boolean },
): Promise<Pipeline> {
const query = encodeQueryString(opts);
return this._post(`/api/repos/${repoId}/pipelines/${pipeline}?${query}`) as Promise<Pipeline>;
@ -140,8 +148,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._delete(`/api/repos/${repoId}/logs/${pipeline}/${step}`);
}
getSecretList(repoId: number, page: number): Promise<Secret[] | null> {
return this._get(`/api/repos/${repoId}/secrets?page=${page}`) as Promise<Secret[] | null>;
getSecretList(repoId: number, opts?: PaginationOptions): Promise<Secret[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/secrets?${query}`) as Promise<Secret[] | null>;
}
createSecret(repoId: number, secret: Partial<Secret>): Promise<unknown> {
@ -158,8 +167,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._delete(`/api/repos/${repoId}/secrets/${name}`);
}
getRegistryList(repoId: number, page: number): Promise<Registry[] | null> {
return this._get(`/api/repos/${repoId}/registry?page=${page}`) as Promise<Registry[] | null>;
getRegistryList(repoId: number, opts?: PaginationOptions): Promise<Registry[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/registry?${query}`) as Promise<Registry[] | null>;
}
createRegistry(repoId: number, registry: Partial<Registry>): Promise<unknown> {
@ -174,8 +184,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._delete(`/api/repos/${repoId}/registry/${registryAddress}`);
}
getCronList(repoId: number, page: number): Promise<Cron[] | null> {
return this._get(`/api/repos/${repoId}/cron?page=${page}`) as Promise<Cron[] | null>;
getCronList(repoId: number, opts?: PaginationOptions): Promise<Cron[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/cron?${query}`) as Promise<Cron[] | null>;
}
createCron(repoId: number, cron: Partial<Cron>): Promise<unknown> {
@ -206,8 +217,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._get(`/api/orgs/${orgId}/permissions`) as Promise<OrgPermissions>;
}
getOrgSecretList(orgId: number, page: number): Promise<Secret[] | null> {
return this._get(`/api/orgs/${orgId}/secrets?page=${page}`) as Promise<Secret[] | null>;
getOrgSecretList(orgId: number, opts?: PaginationOptions): Promise<Secret[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/orgs/${orgId}/secrets?${query}`) as Promise<Secret[] | null>;
}
createOrgSecret(orgId: number, secret: Partial<Secret>): Promise<unknown> {
@ -224,8 +236,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._delete(`/api/orgs/${orgId}/secrets/${name}`);
}
getGlobalSecretList(page: number): Promise<Secret[] | null> {
return this._get(`/api/secrets?page=${page}`) as Promise<Secret[] | null>;
getGlobalSecretList(opts?: PaginationOptions): Promise<Secret[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/secrets?${query}`) as Promise<Secret[] | null>;
}
createGlobalSecret(secret: Partial<Secret>): Promise<unknown> {
@ -250,8 +263,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._post('/api/user/token') as Promise<string>;
}
getAgents(page: number): Promise<Agent[] | null> {
return this._get(`/api/agents?page=${page}`) as Promise<Agent[] | null>;
getAgents(opts?: PaginationOptions): Promise<Agent[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/agents?${query}`) as Promise<Agent[] | null>;
}
getAgent(agentId: Agent['id']): Promise<Agent> {
@ -282,8 +296,9 @@ export default class WoodpeckerClient extends ApiClient {
return this._post('/api/queue/resume');
}
getUsers(page: number): Promise<User[] | null> {
return this._get(`/api/users?page=${page}`) as Promise<User[] | null>;
getUsers(opts?: PaginationOptions): Promise<User[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/users?${query}`) as Promise<User[] | null>;
}
getUser(username: string): Promise<User> {
@ -306,16 +321,18 @@ export default class WoodpeckerClient extends ApiClient {
return this._delete('/api/user/token') as Promise<string>;
}
getOrgs(page: number): Promise<Org[] | null> {
return this._get(`/api/orgs?page=${page}`) as Promise<Org[] | null>;
getOrgs(opts?: PaginationOptions): Promise<Org[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/orgs?${query}`) as Promise<Org[] | null>;
}
deleteOrg(org: Org): Promise<unknown> {
return this._delete(`/api/orgs/${org.id}`);
}
getAllRepos(page: number): Promise<Repo[] | null> {
return this._get(`/api/repos?page=${page}`) as Promise<Repo[] | null>;
getAllRepos(opts?: PaginationOptions): Promise<Repo[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/repos?${query}`) as Promise<Repo[] | null>;
}
repairAllRepos(): Promise<unknown> {

View file

@ -46,8 +46,8 @@ export const usePipelineStore = defineStore('pipelines', () => {
setPipeline(repoId, pipeline);
}
async function loadRepoPipelines(repoId: number) {
const _pipelines = await apiClient.getPipelineList(repoId);
async function loadRepoPipelines(repoId: number, page?: number) {
const _pipelines = await apiClient.getPipelineList(repoId, { page });
_pipelines.forEach((pipeline) => {
setPipeline(repoId, pipeline);
});

View file

@ -41,7 +41,7 @@ async function loadBranches(page: number): Promise<string[]> {
throw new Error('Unexpected: "repo" should be provided at this place');
}
return apiClient.getRepoBranches(repo.value.id, page);
return apiClient.getRepoBranches(repo.value.id, { page });
}
const { resetPage, data: branches, loading } = usePagination(loadBranches);

View file

@ -48,7 +48,7 @@ async function loadPullRequests(page: number): Promise<PullRequest[]> {
throw new Error('Unexpected: "repo" should be provided at this place');
}
return apiClient.getRepoPullRequests(repo.value.id, page);
return apiClient.getRepoPullRequests(repo.value.id, { page });
}
const { resetPage, data: pullRequests, loading } = usePagination(loadPullRequests);

View file

@ -17,7 +17,7 @@
<a v-if="badgeUrl" :href="badgeUrl" target="_blank">
<img :src="badgeUrl" />
</a>
<IconButton :href="repo.forge_url" :title="$t('repo.open_in_forge')" :icon="forge ?? 'repo'" />
<IconButton :href="repo.forge_url" :title="$t('repo.open_in_forge')" :icon="forge ?? 'repo'" class="forge" />
<IconButton
v-if="repoPermissions.admin"
:to="{ name: 'repo-settings' }"

View file

@ -7,7 +7,7 @@ const (
pathGlobalSecret = "%s/api/secrets/%s"
)
// GlobalOrgSecret returns an global secret by name.
// GlobalSecret returns an global secret by name.
func (c *client) GlobalSecret(secret string) (*Secret, error) {
out := new(Secret)
uri := fmt.Sprintf(pathGlobalSecret, c.addr, secret)

View file

@ -18,6 +18,8 @@ import (
"net/http"
)
//go:generate mockery --name Client --output mocks --case underscore
// Client is used to communicate with a Woodpecker server.
type Client interface {
// SetClient sets the http.Client.

File diff suppressed because it is too large Load diff