Merge branch 'main' into backend/docker/non-conf-windows-container-support

This commit is contained in:
6543 2024-11-19 21:08:13 +01:00 committed by GitHub
commit 35c7fe9597
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 3562 additions and 3197 deletions

View file

@ -2,7 +2,6 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>woodpecker-ci/renovate-config"], "extends": ["github>woodpecker-ci/renovate-config"],
"automergeType": "pr", "automergeType": "pr",
"enabledManagers": ["woodpecker"],
"customManagers": [ "customManagers": [
{ {
"customType": "regex", "customType": "regex",

View file

@ -10,7 +10,7 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v1.61.0 rev: v1.62.0
hooks: hooks:
- id: golangci-lint - id: golangci-lint
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/igorshubovych/markdownlint-cli

View file

@ -41,9 +41,6 @@ variables:
when: when:
- event: [pull_request, tag] - event: [pull_request, tag]
- event: push
branch:
- renovate/*
- event: push - event: push
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
path: *when_path path: *when_path

View file

@ -31,7 +31,6 @@ when:
- <<: *docker_path - <<: *docker_path
branch: branch:
- ${CI_REPO_DEFAULT_BRANCH} - ${CI_REPO_DEFAULT_BRANCH}
- renovate/*
- event: pull_request_closed - event: pull_request_closed
path: *when_path path: *when_path
- event: manual - event: manual

View file

@ -3,7 +3,6 @@ when:
- event: push - event: push
branch: branch:
- ${CI_REPO_DEFAULT_BRANCH} - ${CI_REPO_DEFAULT_BRANCH}
- renovate/*
variables: variables:
- &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.2.0 - &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.2.0

View file

@ -9,8 +9,6 @@ steps:
depends_on: [] depends_on: []
when: when:
- event: pull_request - event: pull_request
- event: push
branch: renovate/*
- name: spellcheck - name: spellcheck
image: docker.io/node:23-alpine image: docker.io/node:23-alpine

View file

@ -16,8 +16,6 @@ variables:
when: when:
- event: pull_request - event: pull_request
- event: push
branch: renovate/*
- event: push - event: push
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
path: *when_path path: *when_path

View file

@ -3,7 +3,6 @@ when:
- event: push - event: push
branch: branch:
- release/* - release/*
- renovate/*
variables: variables:
- &node_image 'docker.io/node:23-alpine' - &node_image 'docker.io/node:23-alpine'

View file

@ -40,7 +40,7 @@ CGO_ENABLED ?= 1 # only used to compile server
HAS_GO = $(shell hash go > /dev/null 2>&1 && echo "GO" || echo "NOGO" ) HAS_GO = $(shell hash go > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
ifeq ($(HAS_GO),GO) ifeq ($(HAS_GO),GO)
# renovate: datasource=docker depName=docker.io/techknowlogick/xgo # renovate: datasource=docker depName=docker.io/techknowlogick/xgo
XGO_VERSION ?= go-1.22.x XGO_VERSION ?= go-1.23.x
CGO_CFLAGS ?= $(shell go env CGO_CFLAGS) CGO_CFLAGS ?= $(shell go env CGO_CFLAGS)
endif endif
CGO_CFLAGS ?= CGO_CFLAGS ?=

View file

@ -17,9 +17,9 @@ var (
cancelWaitForUpdate context.CancelCauseFunc cancelWaitForUpdate context.CancelCauseFunc
) )
func Before(ctx context.Context, c *cli.Command) error { func Before(ctx context.Context, c *cli.Command) (context.Context, error) {
if err := setupGlobalLogger(ctx, c); err != nil { if err := setupGlobalLogger(ctx, c); err != nil {
return err return ctx, err
} }
go func(context.Context) { go func(context.Context) {
@ -50,7 +50,7 @@ func Before(ctx context.Context, c *cli.Command) error {
} }
}(ctx) }(ctx)
return config.Load(ctx, c) return ctx, config.Load(ctx, c)
} }
func After(_ context.Context, _ *cli.Command) error { func After(_ context.Context, _ *cli.Command) error {

View file

@ -24,31 +24,33 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" "go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
) )
//nolint:mnd func buildPipelineListCmd() *cli.Command {
var pipelineListCmd = &cli.Command{ return &cli.Command{
Name: "ls", Name: "ls",
Usage: "show pipeline history", Usage: "show pipeline history",
ArgsUsage: "<repo-id|repo-full-name>", ArgsUsage: "<repo-id|repo-full-name>",
Action: List, Action: List,
Flags: append(common.OutputFlags("table"), []cli.Flag{ Flags: append(common.OutputFlags("table"), []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "branch", Name: "branch",
Usage: "branch filter", Usage: "branch filter",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "event", Name: "event",
Usage: "event filter", Usage: "event filter",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "status", Name: "status",
Usage: "status filter", Usage: "status filter",
}, },
&cli.IntFlag{ &cli.IntFlag{
Name: "limit", Name: "limit",
Usage: "limit the list size", Usage: "limit the list size",
Value: 25, //nolint:mnd
}, Value: 25,
}...), },
}...),
}
} }
func List(ctx context.Context, c *cli.Command) error { func List(ctx context.Context, c *cli.Command) error {

View file

@ -110,7 +110,7 @@ func TestPipelineList(t *testing.T) {
mockClient.On("PipelineList", mock.Anything).Return(tt.pipelines, tt.pipelineErr) mockClient.On("PipelineList", mock.Anything).Return(tt.pipelines, tt.pipelineErr)
mockClient.On("RepoLookup", mock.Anything).Return(&woodpecker.Repo{ID: tt.repoID}, nil) mockClient.On("RepoLookup", mock.Anything).Return(&woodpecker.Repo{ID: tt.repoID}, nil)
command := pipelineListCmd command := buildPipelineListCmd()
command.Writer = io.Discard command.Writer = io.Discard
command.Action = func(ctx context.Context, c *cli.Command) error { command.Action = func(ctx context.Context, c *cli.Command) error {
pipelines, err := pipelineList(ctx, c, mockClient) pipelines, err := pipelineList(ctx, c, mockClient)

View file

@ -31,7 +31,7 @@ var Command = &cli.Command{
Name: "pipeline", Name: "pipeline",
Usage: "manage pipelines", Usage: "manage pipelines",
Commands: []*cli.Command{ Commands: []*cli.Command{
pipelineListCmd, buildPipelineListCmd(),
pipelineLastCmd, pipelineLastCmd,
pipelineLogsCmd, pipelineLogsCmd,
pipelineInfoCmd, pipelineInfoCmd,

View file

@ -65,6 +65,7 @@ Visibility: {{ .Visibility }}
Private: {{ .IsSCMPrivate }} Private: {{ .IsSCMPrivate }}
Trusted: {{ .IsTrusted }} Trusted: {{ .IsTrusted }}
Gated: {{ .IsGated }} Gated: {{ .IsGated }}
Require approval for: {{ .RequireApproval }}
Clone url: {{ .Clone }} Clone url: {{ .Clone }}
Allow pull-requests: {{ .AllowPullRequests }} Allow pull-requests: {{ .AllowPullRequests }}
` `

View file

@ -39,6 +39,10 @@ var repoUpdateCmd = &cli.Command{
Name: "gated", Name: "gated",
Usage: "repository is gated", Usage: "repository is gated",
}, },
&cli.StringFlag{
Name: "require-approval",
Usage: "repository requires approval for",
},
&cli.DurationFlag{ &cli.DurationFlag{
Name: "timeout", Name: "timeout",
Usage: "repository timeout", Usage: "repository timeout",
@ -79,6 +83,7 @@ func repoUpdate(ctx context.Context, c *cli.Command) error {
timeout = c.Duration("timeout") timeout = c.Duration("timeout")
trusted = c.Bool("trusted") trusted = c.Bool("trusted")
gated = c.Bool("gated") gated = c.Bool("gated")
requireApproval = c.String("require-approval")
pipelineCounter = int(c.Int("pipeline-counter")) pipelineCounter = int(c.Int("pipeline-counter"))
unsafe = c.Bool("unsafe") unsafe = c.Bool("unsafe")
) )
@ -87,8 +92,29 @@ func repoUpdate(ctx context.Context, c *cli.Command) error {
if c.IsSet("trusted") { if c.IsSet("trusted") {
patch.IsTrusted = &trusted patch.IsTrusted = &trusted
} }
// TODO: remove isGated in next major release
if c.IsSet("gated") { if c.IsSet("gated") {
patch.IsGated = &gated if gated {
patch.RequireApproval = &woodpecker.RequireApprovalAllEvents
} else {
patch.RequireApproval = &woodpecker.RequireApprovalNone
}
}
if c.IsSet("require-approval") {
if mode := woodpecker.ApprovalMode(requireApproval); mode.Valid() {
patch.RequireApproval = &mode
} else {
return fmt.Errorf("update approval mode failed: '%s' is no valid mode", mode)
}
// TODO: remove isGated in next major release
if requireApproval == string(woodpecker.RequireApprovalAllEvents) {
trueBool := true
patch.IsGated = &trueBool
} else if requireApproval == string(woodpecker.RequireApprovalNone) {
falseBool := false
patch.IsGated = &falseBool
}
} }
if c.IsSet("timeout") { if c.IsSet("timeout") {
v := int64(timeout / time.Minute) v := int64(timeout / time.Minute)

View file

@ -4905,6 +4905,9 @@ const docTemplate = `{
"forge_url": { "forge_url": {
"type": "string" "type": "string"
}, },
"from_fork": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -5068,9 +5071,6 @@ const docTemplate = `{
"full_name": { "full_name": {
"type": "string" "type": "string"
}, },
"gated": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
@ -5092,6 +5092,9 @@ const docTemplate = `{
"private": { "private": {
"type": "boolean" "type": "boolean"
}, },
"require_approval": {
"$ref": "#/definitions/model.ApprovalMode"
},
"scm": { "scm": {
"$ref": "#/definitions/SCMKind" "$ref": "#/definitions/SCMKind"
}, },
@ -5125,11 +5128,15 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"gated": { "gated": {
"description": "TODO: deprecated in favor of RequireApproval =\u003e Remove in next major release",
"type": "boolean" "type": "boolean"
}, },
"netrc_only_trusted": { "netrc_only_trusted": {
"type": "boolean" "type": "boolean"
}, },
"require_approval": {
"type": "string"
},
"timeout": { "timeout": {
"type": "integer" "type": "integer"
}, },
@ -5621,6 +5628,27 @@ const docTemplate = `{
} }
} }
}, },
"model.ApprovalMode": {
"type": "string",
"enum": [
"none",
"forks",
"pull_requests",
"all_events"
],
"x-enum-comments": {
"RequireApprovalAllEvents": "require approval for all external events",
"RequireApprovalForks": "require approval for PRs from forks (default)",
"RequireApprovalNone": "require approval for no events",
"RequireApprovalPullRequests": "require approval for all PRs"
},
"x-enum-varnames": [
"RequireApprovalNone",
"RequireApprovalForks",
"RequireApprovalPullRequests",
"RequireApprovalAllEvents"
]
},
"model.ForgeType": { "model.ForgeType": {
"type": "string", "type": "string",
"enum": [ "enum": [

View file

@ -3,7 +3,7 @@ version: '3'
services: services:
gitea-database: gitea-database:
image: postgres:17.0-alpine image: postgres:17.1-alpine
environment: environment:
POSTGRES_USER: gitea POSTGRES_USER: gitea
POSTGRES_PASSWORD: 123456 POSTGRES_PASSWORD: 123456

View file

@ -25,10 +25,9 @@ Only activate this option if you trust all users who have push access to your re
Otherwise, these users will be able to steal secrets that are only available for `deploy` events. Otherwise, these users will be able to steal secrets that are only available for `deploy` events.
::: :::
## Protected ## Require approval for
Every pipeline initiated by an webhook event needs to be approved by a project members with push permissions before being executed. To prevent malicious pipelines from extracting secrets or running harmful commands or to prevent accidental pipeline runs, you can require approval for an additional review process. Depending on the enabled option, a pipeline will be put on hold after creation and will only continue after approval. The default restrictive setting is `Approvals for forked repositories`.
The protected option can be used as an additional review process before running potentially harmful pipelines. Especially if pipelines can be executed by third-parties through pull-requests.
## Trusted ## Trusted

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 353 KiB

View file

@ -34,7 +34,7 @@ Install make on:
### Install Node.js & `pnpm` ### Install Node.js & `pnpm`
Install [Node.js (>=14)](https://nodejs.org/en/download/) if you want to build Woodpecker's UI or documentation. Install [Node.js (>=20)](https://nodejs.org/en/download/package-manager) if you want to build Woodpecker's UI or documentation.
For dependency installation (`node_modules`) of UI and documentation of Woodpecker the package manager pnpm is used. For dependency installation (`node_modules`) of UI and documentation of Woodpecker the package manager pnpm is used.
[This guide](https://pnpm.io/installation) describes the installation of `pnpm`. [This guide](https://pnpm.io/installation) describes the installation of `pnpm`.

View file

@ -14,9 +14,9 @@
"write-heading-ids": "docusaurus write-heading-ids" "write-heading-ids": "docusaurus write-heading-ids"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "^3.5.2", "@docusaurus/core": "^3.6.1",
"@docusaurus/plugin-content-blog": "^3.5.2", "@docusaurus/plugin-content-blog": "^3.6.1",
"@docusaurus/preset-classic": "^3.5.2", "@docusaurus/preset-classic": "^3.6.1",
"@easyops-cn/docusaurus-search-local": "^0.45.0", "@easyops-cn/docusaurus-search-local": "^0.45.0",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
@ -26,7 +26,7 @@
"prism-react-renderer": "^2.4.0", "prism-react-renderer": "^2.4.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"redocusaurus": "^2.1.2", "redocusaurus": "^2.2.0",
"url-loader": "^4.1.1" "url-loader": "^4.1.1"
}, },
"browserslist": { "browserslist": {
@ -42,10 +42,10 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.2", "@docusaurus/module-type-aliases": "^3.6.1",
"@docusaurus/tsconfig": "3.5.2", "@docusaurus/tsconfig": "3.6.1",
"@docusaurus/types": "^3.5.2", "@docusaurus/types": "^3.6.1",
"@types/node": "^20.17.1", "@types/node": "^22.9.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",

View file

@ -10,17 +10,17 @@
"style": "mkdir -p dist/theme/ && cp src/theme/style.css dist/theme/style.css" "style": "mkdir -p dist/theme/ && cp src/theme/style.css dist/theme/style.css"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^3.3.2", "@docusaurus/module-type-aliases": "^3.6.1",
"@docusaurus/theme-classic": "^3.3.2", "@docusaurus/theme-classic": "^3.6.1",
"@docusaurus/types": "^3.3.2", "@docusaurus/types": "^3.6.1",
"@tsconfig/docusaurus": "^2.0.3", "@tsconfig/docusaurus": "^2.0.3",
"@types/node": "^20.12.13", "@types/node": "^22.9.0",
"axios": "^1.7.2", "axios": "^1.7.7",
"concurrently": "^9.0.0", "concurrently": "^9.1.0",
"isomorphic-dompurify": "^2.11.0", "isomorphic-dompurify": "^2.16.0",
"marked": "^14.0.0", "marked": "^15.0.0",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"typescript": "^5.4.5" "typescript": "^5.6.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^17.0.2 || ^18.0.0", "react": "^17.0.2 || ^18.0.0",
@ -28,6 +28,6 @@
}, },
"dependencies": { "dependencies": {
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"yaml": "^2.4.2" "yaml": "^2.6.0"
} }
} }

View file

@ -224,6 +224,11 @@
"name": "EditorConfig Checker", "name": "EditorConfig Checker",
"docs": "https://codeberg.org/woodpecker-plugins/editorconfig-checker/raw/branch/main/docs.md", "docs": "https://codeberg.org/woodpecker-plugins/editorconfig-checker/raw/branch/main/docs.md",
"verified": true "verified": true
},
{
"name": "Microsoft Teams Notify",
"docs": "https://raw.githubusercontent.com/GECO-IT/woodpecker-plugin-teams-notify/refs/heads/main/docs.md",
"verified": false
} }
] ]
} }

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@
addlicense addlicense
protoc-gen-go protoc-gen-go
protoc-gen-go-grpc protoc-gen-go-grpc
gcc
]; ];
CFLAGS = "-I${pkgs.glibc.dev}/include"; CFLAGS = "-I${pkgs.glibc.dev}/include";
LDFLAGS = "-L${pkgs.glibc}/lib"; LDFLAGS = "-L${pkgs.glibc}/lib";

34
go.mod
View file

@ -1,6 +1,6 @@
module go.woodpecker-ci.org/woodpecker/v2 module go.woodpecker-ci.org/woodpecker/v2
go 1.22.0 go 1.22.7
toolchain go1.23.2 toolchain go1.23.2
@ -11,7 +11,7 @@ require (
codeberg.org/6543/xyaml v1.1.0 codeberg.org/6543/xyaml v1.1.0
codeberg.org/mvdkleijn/forgejo-sdk/forgejo v1.2.0 codeberg.org/mvdkleijn/forgejo-sdk/forgejo v1.2.0
github.com/6543/logfile-open v1.2.1 github.com/6543/logfile-open v1.2.1
github.com/adrg/xdg v0.5.2 github.com/adrg/xdg v0.5.3
github.com/bmatcuk/doublestar/v4 v4.7.1 github.com/bmatcuk/doublestar/v4 v4.7.1
github.com/caddyserver/certmagic v0.21.4 github.com/caddyserver/certmagic v0.21.4
github.com/cenkalti/backoff/v4 v4.3.0 github.com/cenkalti/backoff/v4 v4.3.0
@ -25,7 +25,7 @@ require (
github.com/drone/envsubst v1.0.3 github.com/drone/envsubst v1.0.3
github.com/expr-lang/expr v1.16.9 github.com/expr-lang/expr v1.16.9
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.8.0
github.com/gdgvda/cron v0.3.0 github.com/gdgvda/cron v0.3.0
github.com/getkin/kin-openapi v0.127.0 github.com/getkin/kin-openapi v0.127.0
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
@ -55,21 +55,21 @@ require (
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
github.com/urfave/cli-docs/v3 v3.0.0-alpha5 github.com/urfave/cli-docs/v3 v3.0.0-alpha6
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20241004184838-20ef97b2155a github.com/urfave/cli/v3 v3.0.0-alpha9.3
github.com/xanzy/go-gitlab v0.112.0 github.com/xanzy/go-gitlab v0.113.0
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
github.com/yaronf/httpsign v0.3.1 github.com/yaronf/httpsign v0.3.1
github.com/zalando/go-keyring v0.2.6 github.com/zalando/go-keyring v0.2.6
go.uber.org/multierr v1.11.0 go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.29.0
golang.org/x/net v0.30.0 golang.org/x/net v0.31.0
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.8.0 golang.org/x/sync v0.9.0
golang.org/x/term v0.25.0 golang.org/x/term v0.26.0
golang.org/x/text v0.19.0 golang.org/x/text v0.20.0
google.golang.org/grpc v1.67.1 google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.1 google.golang.org/protobuf v1.35.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.31.2 k8s.io/api v0.31.2
k8s.io/apimachinery v0.31.2 k8s.io/apimachinery v0.31.2
@ -204,11 +204,11 @@ require (
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/mod v0.18.0 // indirect golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.4.0 // indirect gotest.tools/v3 v3.4.0 // indirect

64
go.sum
View file

@ -30,8 +30,8 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/adrg/xdg v0.5.2 h1:HNeVffMIG56GLMaoKTcTcyFhD2xS/dhyuBlKSNCM6Ug= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.2/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@ -138,8 +138,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc= github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc=
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
@ -521,14 +521,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5 h1:H1oWnR2/GN0dNm2PVylws+GxSOD6YOwW/jI5l78YfPk= github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5/go.mod h1:AIqom6Q60U4tiqHp41i7+/AB2XHgi1WvQ7jOFlccmZ4= github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU=
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20241004184838-20ef97b2155a h1:ipFw/N7kumxX+CA9UoKXX86MNfYsfsom8YOdUC+Rsfw= github.com/urfave/cli/v3 v3.0.0-alpha9.3 h1:RfQlgUHMRxDMwEEmGsrHd+mXYJpWpXlcJM8w86cpjGs=
github.com/urfave/cli/v3 v3.0.0-alpha9.0.20241004184838-20ef97b2155a/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= github.com/urfave/cli/v3 v3.0.0-alpha9.3/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+DKw= github.com/xanzy/go-gitlab v0.113.0 h1:v5O4R+YZbJGxKqa9iIZxjMyeKkMKBN8P6sZsNl+YckM=
github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/xanzy/go-gitlab v0.113.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -601,8 +601,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@ -624,17 +624,17 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -664,14 +664,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -679,8 +679,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -705,14 +705,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View file

@ -91,6 +91,7 @@ func PostRepo(c *gin.Context) {
repo.Update(from) repo.Update(from)
} else { } else {
repo = from repo = from
repo.RequireApproval = model.RequireApprovalForks
repo.AllowPull = true repo.AllowPull = true
repo.AllowDeploy = false repo.AllowDeploy = false
repo.NetrcOnlyTrusted = true repo.NetrcOnlyTrusted = true
@ -250,8 +251,20 @@ func PatchRepo(c *gin.Context) {
if in.AllowDeploy != nil { if in.AllowDeploy != nil {
repo.AllowDeploy = *in.AllowDeploy repo.AllowDeploy = *in.AllowDeploy
} }
if in.IsGated != nil {
repo.IsGated = *in.IsGated if in.RequireApproval != nil {
if mode := model.ApprovalMode(*in.RequireApproval); mode.Valid() {
repo.RequireApproval = mode
} else {
c.String(http.StatusBadRequest, "Invalid require-approval setting")
return
}
} else if in.IsGated != nil { // TODO: remove isGated in next major release
if *in.IsGated {
repo.RequireApproval = model.RequireApprovalAllEvents
} else {
repo.RequireApproval = model.RequireApprovalForks
}
} }
if in.Timeout != nil { if in.Timeout != nil {
repo.Timeout = *in.Timeout repo.Timeout = *in.Timeout

View file

@ -183,6 +183,7 @@ func convertPullHook(from *internal.PullRequestHook) *model.Pipeline {
Author: from.Actor.Login, Author: from.Actor.Login,
Sender: from.Actor.Login, Sender: from.Actor.Login,
Timestamp: from.PullRequest.Updated.UTC().Unix(), Timestamp: from.PullRequest.Updated.UTC().Unix(),
FromFork: from.PullRequest.Source.Repo.UUID != from.PullRequest.Dest.Repo.UUID,
} }
if from.PullRequest.State == stateClosed { if from.PullRequest.State == stateClosed {

View file

@ -123,6 +123,7 @@ func convertPullRequestEvent(ev *bb.PullRequestEvent, baseURL string) *model.Pip
Ref: fmt.Sprintf("refs/pull-requests/%d/from", ev.PullRequest.ID), Ref: fmt.Sprintf("refs/pull-requests/%d/from", ev.PullRequest.ID),
ForgeURL: fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s", baseURL, ev.PullRequest.Source.Repository.Project.Key, ev.PullRequest.Source.Repository.Slug, ev.PullRequest.Source.Latest), ForgeURL: fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s", baseURL, ev.PullRequest.Source.Repository.Project.Key, ev.PullRequest.Source.Repository.Slug, ev.PullRequest.Source.Latest),
Refspec: fmt.Sprintf("%s:%s", ev.PullRequest.Source.DisplayID, ev.PullRequest.Target.DisplayID), Refspec: fmt.Sprintf("%s:%s", ev.PullRequest.Source.DisplayID, ev.PullRequest.Target.DisplayID),
FromFork: ev.PullRequest.Source.Repository.ID != ev.PullRequest.Target.Repository.ID,
} }
if ev.EventKey == bb.EventKeyPullRequestMerged || ev.EventKey == bb.EventKeyPullRequestDeclined || ev.EventKey == bb.EventKeyPullRequestDeleted { if ev.EventKey == bb.EventKeyPullRequestMerged || ev.EventKey == bb.EventKeyPullRequestDeclined || ev.EventKey == bb.EventKeyPullRequestDeleted {

View file

@ -171,6 +171,7 @@ func pipelineFromPullRequest(hook *pullRequestHook) *model.Pipeline {
hook.PullRequest.Base.Ref, hook.PullRequest.Base.Ref,
), ),
PullRequestLabels: convertLabels(hook.PullRequest.Labels), PullRequestLabels: convertLabels(hook.PullRequest.Labels),
FromFork: hook.PullRequest.Head.RepoID != hook.PullRequest.Base.RepoID,
} }
return pipeline return pipeline

View file

@ -172,6 +172,7 @@ func pipelineFromPullRequest(hook *pullRequestHook) *model.Pipeline {
hook.PullRequest.Base.Ref, hook.PullRequest.Base.Ref,
), ),
PullRequestLabels: convertLabels(hook.PullRequest.Labels), PullRequestLabels: convertLabels(hook.PullRequest.Labels),
FromFork: hook.PullRequest.Head.RepoID != hook.PullRequest.Base.RepoID,
} }
return pipeline return pipeline

View file

@ -157,6 +157,8 @@ func parsePullHook(hook *github.PullRequestEvent, merge bool) (*github.PullReque
event = model.EventPullClosed event = model.EventPullClosed
} }
fromFork := hook.GetPullRequest().GetHead().GetRepo().GetID() != hook.GetPullRequest().GetBase().GetRepo().GetID()
pipeline := &model.Pipeline{ pipeline := &model.Pipeline{
Event: event, Event: event,
Commit: hook.GetPullRequest().GetHead().GetSHA(), Commit: hook.GetPullRequest().GetHead().GetSHA(),
@ -173,6 +175,7 @@ func parsePullHook(hook *github.PullRequestEvent, merge bool) (*github.PullReque
hook.GetPullRequest().GetBase().GetRef(), hook.GetPullRequest().GetBase().GetRef(),
), ),
PullRequestLabels: convertLabels(hook.GetPullRequest().Labels), PullRequestLabels: convertLabels(hook.GetPullRequest().Labels),
FromFork: fromFork,
} }
if merge { if merge {
pipeline.Ref = fmt.Sprintf(mergeRefs, hook.GetPullRequest().GetNumber()) pipeline.Ref = fmt.Sprintf(mergeRefs, hook.GetPullRequest().GetNumber())

View file

@ -138,6 +138,7 @@ func convertMergeRequestHook(hook *gitlab.MergeEvent, req *http.Request) (int, *
pipeline.Title = obj.Title pipeline.Title = obj.Title
pipeline.ForgeURL = obj.URL pipeline.ForgeURL = obj.URL
pipeline.PullRequestLabels = convertLabels(hook.Labels) pipeline.PullRequestLabels = convertLabels(hook.Labels)
pipeline.FromFork = target.PathWithNamespace != source.PathWithNamespace
return obj.IID, repo, pipeline, nil return obj.IID, repo, pipeline, nil
} }

View file

@ -52,6 +52,7 @@ type Pipeline struct {
AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"` AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"`
PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"` PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"`
IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"` IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"`
FromFork bool `json:"from_fork,omitempty" xorm:"from_fork"`
} // @name Pipeline } // @name Pipeline
// TableName return database table name for xorm. // TableName return database table name for xorm.

View file

@ -20,6 +20,27 @@ import (
"strings" "strings"
) )
type ApprovalMode string
const (
RequireApprovalNone ApprovalMode = "none" // require approval for no events
RequireApprovalForks ApprovalMode = "forks" // require approval for PRs from forks (default)
RequireApprovalPullRequests ApprovalMode = "pull_requests" // require approval for all PRs
RequireApprovalAllEvents ApprovalMode = "all_events" // require approval for all external events
)
func (mode ApprovalMode) Valid() bool {
switch mode {
case RequireApprovalNone,
RequireApprovalForks,
RequireApprovalPullRequests,
RequireApprovalAllEvents:
return true
default:
return false
}
}
// Repo represents a repository. // Repo represents a repository.
type Repo struct { type Repo struct {
ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"` ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"`
@ -42,7 +63,7 @@ type Repo struct {
Visibility RepoVisibility `json:"visibility" xorm:"varchar(10) 'visibility'"` Visibility RepoVisibility `json:"visibility" xorm:"varchar(10) 'visibility'"`
IsSCMPrivate bool `json:"private" xorm:"private"` IsSCMPrivate bool `json:"private" xorm:"private"`
Trusted TrustedConfiguration `json:"trusted" xorm:"json 'trusted'"` Trusted TrustedConfiguration `json:"trusted" xorm:"json 'trusted'"`
IsGated bool `json:"gated" xorm:"gated"` RequireApproval ApprovalMode `json:"require_approval" xorm:"varchar(50) require_approval"`
IsActive bool `json:"active" xorm:"active"` IsActive bool `json:"active" xorm:"active"`
AllowPull bool `json:"allow_pr" xorm:"allow_pr"` AllowPull bool `json:"allow_pr" xorm:"allow_pr"`
AllowDeploy bool `json:"allow_deploy" xorm:"allow_deploy"` AllowDeploy bool `json:"allow_deploy" xorm:"allow_deploy"`
@ -109,7 +130,8 @@ func (r *Repo) Update(from *Repo) {
// RepoPatch represents a repository patch object. // RepoPatch represents a repository patch object.
type RepoPatch struct { type RepoPatch struct {
Config *string `json:"config_file,omitempty"` Config *string `json:"config_file,omitempty"`
IsGated *bool `json:"gated,omitempty"` IsGated *bool `json:"gated,omitempty"` // TODO: deprecated in favor of RequireApproval => Remove in next major release
RequireApproval *string `json:"require_approval,omitempty"`
Timeout *int64 `json:"timeout,omitempty"` Timeout *int64 `json:"timeout,omitempty"`
Visibility *string `json:"visibility,omitempty"` Visibility *string `json:"visibility,omitempty"`
AllowPull *bool `json:"allow_pr,omitempty"` AllowPull *bool `json:"allow_pr,omitempty"`

View file

@ -27,8 +27,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
) )
// Approve update the status to pending for a blocked pipeline because of a gated repo // Approve update the status to pending for a blocked pipeline so it can be executed.
// and start them afterward.
func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo) (*model.Pipeline, error) { func Approve(ctx context.Context, store store.Store, currentPipeline *model.Pipeline, user *model.User, repo *model.Repo) (*model.Pipeline, error) {
if currentPipeline.Status != model.StatusBlocked { if currentPipeline.Status != model.StatusBlocked {
return nil, ErrBadRequest{Msg: fmt.Sprintf("cannot approve a pipeline with status %s", currentPipeline.Status)} return nil, ErrBadRequest{Msg: fmt.Sprintf("cannot approve a pipeline with status %s", currentPipeline.Status)}

View file

@ -68,7 +68,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
// update some pipeline fields // update some pipeline fields
pipeline.RepoID = repo.ID pipeline.RepoID = repo.ID
pipeline.Status = model.StatusCreated pipeline.Status = model.StatusCreated
setGatedState(repo, pipeline) setApprovalState(repo, pipeline)
err = _store.CreatePipeline(pipeline) err = _store.CreatePipeline(pipeline)
if err != nil { if err != nil {
msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName) msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName)

View file

@ -26,7 +26,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/store" "go.woodpecker-ci.org/woodpecker/v2/server/store"
) )
// Decline updates the status to declined for blocked pipelines because of a gated repo. // Decline updates the status to declined for blocked pipelines.
func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, user *model.User, repo *model.Repo) (*model.Pipeline, error) { func Decline(ctx context.Context, store store.Store, pipeline *model.Pipeline, user *model.User, repo *model.Repo) (*model.Pipeline, error) {
forge, err := server.Config.Services.Manager.ForgeFromRepo(repo) forge, err := server.Config.Services.Manager.ForgeFromRepo(repo)
if err != nil { if err != nil {

View file

@ -16,11 +16,40 @@ package pipeline
import "go.woodpecker-ci.org/woodpecker/v2/server/model" import "go.woodpecker-ci.org/woodpecker/v2/server/model"
func setGatedState(repo *model.Repo, pipeline *model.Pipeline) { func setApprovalState(repo *model.Repo, pipeline *model.Pipeline) {
// TODO(336): extend gated feature with an allow/block List if !needsApproval(repo, pipeline) {
if repo.IsGated && return
// events created by woodpecker itself should run right away
pipeline.Event != model.EventCron && pipeline.Event != model.EventManual {
pipeline.Status = model.StatusBlocked
} }
// set pipeline status to blocked and require approval
pipeline.Status = model.StatusBlocked
}
func needsApproval(repo *model.Repo, pipeline *model.Pipeline) bool {
// skip events created by woodpecker itself
if pipeline.Event == model.EventCron || pipeline.Event == model.EventManual {
return false
}
// repository allows all events without approval
if repo.RequireApproval == model.RequireApprovalNone {
return false
}
// repository requires approval for pull requests from forks
if pipeline.Event == model.EventPull && pipeline.FromFork {
return true
}
// repository requires approval for pull requests
if pipeline.Event == model.EventPull && repo.RequireApproval == model.RequireApprovalPullRequests {
return true
}
// repository requires approval for all events
if repo.RequireApproval == model.RequireApprovalAllEvents {
return true
}
return false
} }

View file

@ -0,0 +1,78 @@
package pipeline
import (
"testing"
"github.com/stretchr/testify/assert"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)
func TestSetGatedState(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
repo *model.Repo
pipeline *model.Pipeline
expectBlocked bool
}{
{
name: "by-pass for cron",
repo: &model.Repo{
RequireApproval: model.RequireApprovalAllEvents,
},
pipeline: &model.Pipeline{
Event: model.EventCron,
},
expectBlocked: false,
},
{
name: "by-pass for manual pipeline",
repo: &model.Repo{
RequireApproval: model.RequireApprovalAllEvents,
},
pipeline: &model.Pipeline{
Event: model.EventManual,
},
expectBlocked: false,
},
{
name: "require approval for fork PRs",
repo: &model.Repo{
RequireApproval: model.RequireApprovalForks,
},
pipeline: &model.Pipeline{
Event: model.EventPull,
FromFork: true,
},
expectBlocked: true,
},
{
name: "require approval for PRs",
repo: &model.Repo{
RequireApproval: model.RequireApprovalPullRequests,
},
pipeline: &model.Pipeline{
Event: model.EventPull,
FromFork: false,
},
expectBlocked: true,
},
{
name: "require approval for everything",
repo: &model.Repo{
RequireApproval: model.RequireApprovalAllEvents,
},
pipeline: &model.Pipeline{
Event: model.EventPush,
},
expectBlocked: true,
},
}
for _, tc := range testCases {
setApprovalState(tc.repo, tc.pipeline)
assert.Equal(t, tc.expectBlocked, tc.pipeline.Status == model.StatusBlocked)
}
}

View file

@ -16,6 +16,7 @@ package queue
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sync" "sync"
"testing" "testing"
@ -26,64 +27,85 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/model" "go.woodpecker-ci.org/woodpecker/v2/server/model"
) )
var filterFnTrue = func(*model.Task) (bool, int) { return true, 1 } var (
filterFnTrue = func(*model.Task) (bool, int) { return true, 1 }
genDummyTask = func() *model.Task {
return &model.Task{
ID: "1",
Data: []byte("{}"),
}
}
waitForProcess = func() { time.Sleep(processTimeInterval + 10*time.Millisecond) }
)
func TestFifo(t *testing.T) { func TestFifo(t *testing.T) {
want := &model.Task{ID: "1"} ctx, cancel := context.WithCancelCause(context.Background())
ctx := context.Background() t.Cleanup(func() { cancel(nil) })
q := NewMemoryQueue(ctx) q := NewMemoryQueue(ctx)
assert.NoError(t, q.Push(ctx, want)) dummyTask := genDummyTask()
assert.NoError(t, q.Push(ctx, dummyTask))
waitForProcess()
info := q.Info(ctx) info := q.Info(ctx)
assert.Len(t, info.Pending, 1, "expect task in pending queue") assert.Len(t, info.Pending, 1, "expect task in pending queue")
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, want, got) assert.Equal(t, dummyTask, got)
waitForProcess()
info = q.Info(ctx) info = q.Info(ctx)
assert.Len(t, info.Pending, 0, "expect task removed from pending queue") assert.Len(t, info.Pending, 0, "expect task removed from pending queue")
assert.Len(t, info.Running, 1, "expect task in running queue") assert.Len(t, info.Running, 1, "expect task in running queue")
assert.NoError(t, q.Done(ctx, got.ID, model.StatusSuccess)) assert.NoError(t, q.Done(ctx, got.ID, model.StatusSuccess))
waitForProcess()
info = q.Info(ctx) info = q.Info(ctx)
assert.Len(t, info.Pending, 0, "expect task removed from pending queue") assert.Len(t, info.Pending, 0, "expect task removed from pending queue")
assert.Len(t, info.Running, 0, "expect task removed from running queue") assert.Len(t, info.Running, 0, "expect task removed from running queue")
} }
func TestFifoExpire(t *testing.T) { func TestFifoExpire(t *testing.T) {
want := &model.Task{ID: "1"}
ctx, cancel := context.WithCancelCause(context.Background()) ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(nil) })
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
dummyTask := genDummyTask()
q.extension = 0 q.extension = 0
assert.NoError(t, q.Push(ctx, want)) assert.NoError(t, q.Push(ctx, dummyTask))
waitForProcess()
info := q.Info(ctx) info := q.Info(ctx)
assert.Len(t, info.Pending, 1, "expect task in pending queue") assert.Len(t, info.Pending, 1, "expect task in pending queue")
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
waitForProcess()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, want, got) assert.Equal(t, dummyTask, got)
// cancel the context to let the process func end info = q.Info(ctx)
go func() {
time.Sleep(time.Millisecond)
cancel(nil)
}()
q.process()
assert.Len(t, info.Pending, 1, "expect task re-added to pending queue") assert.Len(t, info.Pending, 1, "expect task re-added to pending queue")
} }
func TestFifoWait(t *testing.T) { func TestFifoWait(t *testing.T) {
want := &model.Task{ID: "1"} ctx, cancel := context.WithCancelCause(context.Background())
ctx := context.Background() t.Cleanup(func() { cancel(nil) })
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NoError(t, q.Push(ctx, want)) assert.NotNil(t, q)
dummyTask := genDummyTask()
assert.NoError(t, q.Push(ctx, dummyTask))
waitForProcess()
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, want, got) assert.Equal(t, dummyTask, got)
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
@ -98,27 +120,34 @@ func TestFifoWait(t *testing.T) {
} }
func TestFifoEvict(t *testing.T) { func TestFifoEvict(t *testing.T) {
t1 := &model.Task{ID: "1"} ctx, cancel := context.WithCancelCause(context.Background())
ctx := context.Background() t.Cleanup(func() { cancel(nil) })
q := NewMemoryQueue(ctx) q := NewMemoryQueue(ctx)
assert.NoError(t, q.Push(ctx, t1)) dummyTask := genDummyTask()
assert.NoError(t, q.Push(ctx, dummyTask))
waitForProcess()
info := q.Info(ctx) info := q.Info(ctx)
assert.Len(t, info.Pending, 1, "expect task in pending queue") assert.Len(t, info.Pending, 1, "expect task in pending queue")
err := q.Evict(ctx, t1.ID)
err := q.Evict(ctx, dummyTask.ID)
assert.NoError(t, err) assert.NoError(t, err)
waitForProcess()
info = q.Info(ctx) info = q.Info(ctx)
assert.Len(t, info.Pending, 0) assert.Len(t, info.Pending, 0)
err = q.Evict(ctx, t1.ID)
err = q.Evict(ctx, dummyTask.ID)
assert.ErrorIs(t, err, ErrNotFound) assert.ErrorIs(t, err, ErrNotFound)
} }
func TestFifoDependencies(t *testing.T) { func TestFifoDependencies(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
task1 := genDummyTask()
task2 := &model.Task{ task2 := &model.Task{
ID: "2", ID: "2",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
@ -126,31 +155,34 @@ func TestFifoDependencies(t *testing.T) {
} }
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task1})) assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task1}))
waitForProcess()
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task1, got) assert.Equal(t, task1, got)
waitForProcess()
assert.NoError(t, q.Done(ctx, got.ID, model.StatusSuccess)) assert.NoError(t, q.Done(ctx, got.ID, model.StatusSuccess))
waitForProcess()
got, err = q.Poll(ctx, 1, filterFnTrue) got, err = q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task2, got) assert.Equal(t, task2, got)
} }
func TestFifoErrors(t *testing.T) { func TestFifoErrors(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
task1 := genDummyTask()
task2 := &model.Task{ task2 := &model.Task{
ID: "2", ID: "2",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
DepStatus: make(map[string]model.StatusValue), DepStatus: make(map[string]model.StatusValue),
} }
task3 := &model.Task{ task3 := &model.Task{
ID: "3", ID: "3",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
@ -159,19 +191,24 @@ func TestFifoErrors(t *testing.T) {
} }
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1})) assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1}))
waitForProcess()
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task1, got) assert.Equal(t, task1, got)
assert.NoError(t, q.Error(ctx, got.ID, fmt.Errorf("exit code 1, there was an error"))) assert.NoError(t, q.Error(ctx, got.ID, fmt.Errorf("exit code 1, there was an error")))
waitForProcess()
got, err = q.Poll(ctx, 1, filterFnTrue) got, err = q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task2, got) assert.Equal(t, task2, got)
assert.False(t, got.ShouldRun()) assert.False(t, got.ShouldRun())
waitForProcess()
got, err = q.Poll(ctx, 1, filterFnTrue) got, err = q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task3, got) assert.Equal(t, task3, got)
@ -179,15 +216,13 @@ func TestFifoErrors(t *testing.T) {
} }
func TestFifoErrors2(t *testing.T) { func TestFifoErrors2(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
task1 := genDummyTask()
task2 := &model.Task{ task2 := &model.Task{
ID: "2", ID: "2",
} }
task3 := &model.Task{ task3 := &model.Task{
ID: "3", ID: "3",
Dependencies: []string{"1", "2"}, Dependencies: []string{"1", "2"},
@ -195,9 +230,12 @@ func TestFifoErrors2(t *testing.T) {
} }
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1})) assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1}))
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
waitForProcess()
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, got != task1 && got != task2, "expect task1 or task2 returned from queue as task3 depends on them") assert.False(t, got != task1 && got != task2, "expect task1 or task2 returned from queue as task3 depends on them")
@ -210,6 +248,7 @@ func TestFifoErrors2(t *testing.T) {
} }
} }
waitForProcess()
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task3, got) assert.Equal(t, task3, got)
@ -217,17 +256,15 @@ func TestFifoErrors2(t *testing.T) {
} }
func TestFifoErrorsMultiThread(t *testing.T) { func TestFifoErrorsMultiThread(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
task1 := genDummyTask()
task2 := &model.Task{ task2 := &model.Task{
ID: "2", ID: "2",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
DepStatus: make(map[string]model.StatusValue), DepStatus: make(map[string]model.StatusValue),
} }
task3 := &model.Task{ task3 := &model.Task{
ID: "3", ID: "3",
Dependencies: []string{"1", "2"}, Dependencies: []string{"1", "2"},
@ -235,15 +272,21 @@ func TestFifoErrorsMultiThread(t *testing.T) {
} }
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1})) assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1}))
obtainedWorkCh := make(chan *model.Task) obtainedWorkCh := make(chan *model.Task)
defer func() { close(obtainedWorkCh) }()
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
go func(i int) { go func(i int) {
for { for {
fmt.Printf("Worker %d started\n", i) fmt.Printf("Worker %d started\n", i)
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
if err != nil && errors.Is(err, context.Canceled) {
return
}
assert.NoError(t, err) assert.NoError(t, err)
obtainedWorkCh <- got obtainedWorkCh <- got
} }
@ -266,7 +309,11 @@ func TestFifoErrorsMultiThread(t *testing.T) {
go func() { go func() {
for { for {
fmt.Printf("Worker spawned\n") fmt.Printf("Worker spawned\n")
got, _ := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
if err != nil && errors.Is(err, context.Canceled) {
return
}
assert.NoError(t, err)
obtainedWorkCh <- got obtainedWorkCh <- got
} }
}() }()
@ -277,7 +324,11 @@ func TestFifoErrorsMultiThread(t *testing.T) {
go func() { go func() {
for { for {
fmt.Printf("Worker spawned\n") fmt.Printf("Worker spawned\n")
got, _ := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
if err != nil && errors.Is(err, context.Canceled) {
return
}
assert.NoError(t, err)
obtainedWorkCh <- got obtainedWorkCh <- got
} }
}() }()
@ -297,17 +348,15 @@ func TestFifoErrorsMultiThread(t *testing.T) {
} }
func TestFifoTransitiveErrors(t *testing.T) { func TestFifoTransitiveErrors(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
task1 := genDummyTask()
task2 := &model.Task{ task2 := &model.Task{
ID: "2", ID: "2",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
DepStatus: make(map[string]model.StatusValue), DepStatus: make(map[string]model.StatusValue),
} }
task3 := &model.Task{ task3 := &model.Task{
ID: "3", ID: "3",
Dependencies: []string{"2"}, Dependencies: []string{"2"},
@ -315,19 +364,24 @@ func TestFifoTransitiveErrors(t *testing.T) {
} }
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1})) assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1}))
waitForProcess()
got, err := q.Poll(ctx, 1, filterFnTrue) got, err := q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task1, got) assert.Equal(t, task1, got)
assert.NoError(t, q.Error(ctx, got.ID, fmt.Errorf("exit code 1, there was an error"))) assert.NoError(t, q.Error(ctx, got.ID, fmt.Errorf("exit code 1, there was an error")))
waitForProcess()
got, err = q.Poll(ctx, 1, filterFnTrue) got, err = q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task2, got) assert.Equal(t, task2, got)
assert.False(t, got.ShouldRun(), "expect task2 should not run, since task1 failed") assert.False(t, got.ShouldRun(), "expect task2 should not run, since task1 failed")
assert.NoError(t, q.Done(ctx, got.ID, model.StatusSkipped)) assert.NoError(t, q.Done(ctx, got.ID, model.StatusSkipped))
waitForProcess()
got, err = q.Poll(ctx, 1, filterFnTrue) got, err = q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, task3, got) assert.Equal(t, task3, got)
@ -335,17 +389,15 @@ func TestFifoTransitiveErrors(t *testing.T) {
} }
func TestFifoCancel(t *testing.T) { func TestFifoCancel(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
task1 := genDummyTask()
task2 := &model.Task{ task2 := &model.Task{
ID: "2", ID: "2",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
DepStatus: make(map[string]model.StatusValue), DepStatus: make(map[string]model.StatusValue),
} }
task3 := &model.Task{ task3 := &model.Task{
ID: "3", ID: "3",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
@ -354,24 +406,33 @@ func TestFifoCancel(t *testing.T) {
} }
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1})) assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1}))
_, _ = q.Poll(ctx, 1, filterFnTrue) _, _ = q.Poll(ctx, 1, filterFnTrue)
assert.NoError(t, q.Error(ctx, task1.ID, fmt.Errorf("canceled"))) assert.NoError(t, q.Error(ctx, task1.ID, fmt.Errorf("canceled")))
assert.NoError(t, q.Error(ctx, task2.ID, fmt.Errorf("canceled"))) assert.NoError(t, q.Error(ctx, task2.ID, fmt.Errorf("canceled")))
assert.NoError(t, q.Error(ctx, task3.ID, fmt.Errorf("canceled"))) assert.NoError(t, q.Error(ctx, task3.ID, fmt.Errorf("canceled")))
info := q.Info(ctx) info := q.Info(ctx)
assert.Len(t, info.Pending, 0, "all pipelines should be canceled") assert.Len(t, info.Pending, 0, "all pipelines should be canceled")
time.Sleep(processTimeInterval * 2)
info = q.Info(ctx)
assert.Len(t, info.Pending, 2, "canceled are rescheduled")
assert.Len(t, info.Running, 0, "canceled are rescheduled")
assert.Len(t, info.WaitingOnDeps, 0, "canceled are rescheduled")
} }
func TestFifoPause(t *testing.T) { func TestFifoPause(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
dummyTask := genDummyTask()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
@ -381,8 +442,8 @@ func TestFifoPause(t *testing.T) {
q.Pause() q.Pause()
t0 := time.Now() t0 := time.Now()
assert.NoError(t, q.Push(ctx, task1)) assert.NoError(t, q.Push(ctx, dummyTask))
time.Sleep(20 * time.Millisecond) waitForProcess()
q.Resume() q.Resume()
wg.Wait() wg.Wait()
@ -391,37 +452,37 @@ func TestFifoPause(t *testing.T) {
assert.Greater(t, t1.Sub(t0), 20*time.Millisecond, "should have waited til resume") assert.Greater(t, t1.Sub(t0), 20*time.Millisecond, "should have waited til resume")
q.Pause() q.Pause()
assert.NoError(t, q.Push(ctx, task1)) assert.NoError(t, q.Push(ctx, dummyTask))
q.Resume() q.Resume()
_, _ = q.Poll(ctx, 1, filterFnTrue) _, _ = q.Poll(ctx, 1, filterFnTrue)
} }
func TestFifoPauseResume(t *testing.T) { func TestFifoPauseResume(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
dummyTask := genDummyTask()
q.Pause() q.Pause()
assert.NoError(t, q.Push(ctx, task1)) assert.NoError(t, q.Push(ctx, dummyTask))
q.Resume() q.Resume()
_, _ = q.Poll(ctx, 1, filterFnTrue) _, _ = q.Poll(ctx, 1, filterFnTrue)
} }
func TestWaitingVsPending(t *testing.T) { func TestWaitingVsPending(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
task1 := &model.Task{ t.Cleanup(func() { cancel(nil) })
ID: "1",
}
task1 := genDummyTask()
task2 := &model.Task{ task2 := &model.Task{
ID: "2", ID: "2",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
DepStatus: make(map[string]model.StatusValue), DepStatus: make(map[string]model.StatusValue),
} }
task3 := &model.Task{ task3 := &model.Task{
ID: "3", ID: "3",
Dependencies: []string{"1"}, Dependencies: []string{"1"},
@ -430,10 +491,13 @@ func TestWaitingVsPending(t *testing.T) {
} }
q, _ := NewMemoryQueue(ctx).(*fifo) q, _ := NewMemoryQueue(ctx).(*fifo)
assert.NotNil(t, q)
assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1})) assert.NoError(t, q.PushAtOnce(ctx, []*model.Task{task2, task3, task1}))
got, _ := q.Poll(ctx, 1, filterFnTrue) got, _ := q.Poll(ctx, 1, filterFnTrue)
waitForProcess()
info := q.Info(ctx) info := q.Info(ctx)
assert.Equal(t, 2, info.Stats.WaitingOnDeps) assert.Equal(t, 2, info.Stats.WaitingOnDeps)
@ -442,6 +506,7 @@ func TestWaitingVsPending(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, task2, got) assert.EqualValues(t, task2, got)
waitForProcess()
info = q.Info(ctx) info = q.Info(ctx)
assert.Equal(t, 0, info.Stats.WaitingOnDeps) assert.Equal(t, 0, info.Stats.WaitingOnDeps)
assert.Equal(t, 1, info.Stats.Pending) assert.Equal(t, 1, info.Stats.Pending)
@ -518,7 +583,9 @@ func TestShouldRun(t *testing.T) {
} }
func TestFifoWithScoring(t *testing.T) { func TestFifoWithScoring(t *testing.T) {
ctx := context.Background() ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(nil) })
q := NewMemoryQueue(ctx) q := NewMemoryQueue(ctx)
// Create tasks with different labels // Create tasks with different labels
@ -530,9 +597,7 @@ func TestFifoWithScoring(t *testing.T) {
{ID: "5", Labels: map[string]string{"org-id": "*", "platform": "linux"}}, {ID: "5", Labels: map[string]string{"org-id": "*", "platform": "linux"}},
} }
for _, task := range tasks { assert.NoError(t, q.PushAtOnce(ctx, tasks))
assert.NoError(t, q.Push(ctx, task))
}
// Create filter functions for different workers // Create filter functions for different workers
filters := map[int]FilterFn{ filters := map[int]FilterFn{

View file

@ -0,0 +1,72 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package migration
import (
"fmt"
"src.techknowlogick.com/xormigrate"
"xorm.io/builder"
"xorm.io/xorm"
)
var gatedToRequireApproval = xormigrate.Migration{
ID: "gated-to-require-approval",
MigrateSession: func(sess *xorm.Session) (err error) {
const (
RequireApprovalNone string = "none"
RequireApprovalForks string = "forks"
RequireApprovalPullRequests string = "pull_requests"
RequireApprovalAllEvents string = "all_events"
)
type repos struct {
ID int64 `xorm:"pk autoincr 'id'"`
IsGated bool `xorm:"gated"`
RequireApproval string `xorm:"require_approval"`
Visibility string `xorm:"varchar(10) 'visibility'"`
}
if err := sess.Sync(new(repos)); err != nil {
return fmt.Errorf("sync new models failed: %w", err)
}
// migrate gated repos
if _, err := sess.Exec(
builder.Update(builder.Eq{"require_approval": RequireApprovalAllEvents}).
From("repos").
Where(builder.Eq{"gated": true})); err != nil {
return err
}
// migrate public repos to new default require approval
if _, err := sess.Exec(
builder.Update(builder.Eq{"require_approval": RequireApprovalForks}).
From("repos").
Where(builder.Eq{"gated": false, "visibility": "public"})); err != nil {
return err
}
// migrate private repos to new default require approval
if _, err := sess.Exec(
builder.Update(builder.Eq{"require_approval": RequireApprovalNone}).
From("repos").
Where(builder.Eq{"gated": false}.And(builder.Neq{"visibility": "public"}))); err != nil {
return err
}
return dropTableColumns(sess, "repos", "gated")
},
}

View file

@ -47,6 +47,7 @@ var migrationTasks = []*xormigrate.Migration{
&addCustomLabelsToAgent, &addCustomLabelsToAgent,
&splitTrusted, &splitTrusted,
&correctPotentialCorruptOrgsUsersRelation, &correctPotentialCorruptOrgsUsersRelation,
&gatedToRequireApproval,
} }
var allBeans = []any{ var allBeans = []any{

View file

@ -5,7 +5,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=14" "node": ">=20"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -18,7 +18,7 @@
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"@intlify/unplugin-vue-i18n": "^5.0.0", "@intlify/unplugin-vue-i18n": "^6.0.0",
"@kyvg/vue3-notification": "^3.2.1", "@kyvg/vue3-notification": "^3.2.1",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@vueuse/core": "^11.0.0", "@vueuse/core": "^11.0.0",
@ -45,7 +45,7 @@
"@intlify/eslint-plugin-vue-i18n": "3.0.0", "@intlify/eslint-plugin-vue-i18n": "3.0.0",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@types/node": "^20.14.15", "@types/node": "^22.0.0",
"@types/prismjs": "^1.26.4", "@types/prismjs": "^1.26.4",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@types/tinycolor2": "^1.4.6", "@types/tinycolor2": "^1.4.6",

File diff suppressed because it is too large Load diff

View file

@ -88,10 +88,6 @@
"allow": "Allow deployments", "allow": "Allow deployments",
"desc": "Allow deployments from successful pipelines. Only use if you trust all users with push access." "desc": "Allow deployments from successful pipelines. Only use if you trust all users with push access."
}, },
"protected": {
"protected": "Protected",
"desc": "Every pipeline needs to be approved before being executed."
},
"netrc_only_trusted": { "netrc_only_trusted": {
"netrc_only_trusted": "Only inject netrc credentials into trusted clone plugins", "netrc_only_trusted": "Only inject netrc credentials into trusted clone plugins",
"desc": "If enabled, git netrc credentials are only available for trusted clone plugins set in `WOODPECKER_PLUGINS_TRUSTED_CLONE`. Otherwise, all clone plugins can use the netrc credentials. This option has no effect on non-clone steps." "desc": "If enabled, git netrc credentials are only available for trusted clone plugins set in `WOODPECKER_PLUGINS_TRUSTED_CLONE`. Otherwise, all clone plugins can use the netrc credentials. This option has no effect on non-clone steps."
@ -504,5 +500,14 @@
"internal_error": "Some internal error occurred", "internal_error": "Some internal error occurred",
"registration_closed": "The registration is closed", "registration_closed": "The registration is closed",
"access_denied": "You are not allowed to access this instance", "access_denied": "You are not allowed to access this instance",
"invalid_state": "The OAuth state is invalid" "invalid_state": "The OAuth state is invalid",
"require_approval": {
"require_approval_for": "Require approval for",
"none": "No approval required",
"none_desc": "This setting can be dangerous and should only be used on private forges where all users are trusted.",
"forks": "Pull request from forked repositories",
"pull_requests": "All pull requests",
"all_events": "All events from forge",
"desc": "Prevent malicious pipelines from exposing secrets or running harmful tasks by approving them before execution."
}
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('admin.settings.agents.agents')" :desc="desc"> <Settings :title="$t('admin.settings.agents.agents')" :description>
<template #titleActions> <template #headerActions>
<Button <Button
v-if="selectedAgent" v-if="selectedAgent"
:text="$t('admin.settings.agents.show')" :text="$t('admin.settings.agents.show')"
@ -46,7 +46,7 @@ import AgentForm from './AgentForm.vue';
import AgentList from './AgentList.vue'; import AgentList from './AgentList.vue';
const props = defineProps<{ const props = defineProps<{
desc: string; description: string;
loadAgents: (page: number) => Promise<Agent[] | null>; loadAgents: (page: number) => Promise<Agent[] | null>;
createAgent: (agent: Partial<Agent>) => Promise<Agent>; createAgent: (agent: Partial<Agent>) => Promise<Agent>;
updateAgent: (agent: Agent) => Promise<Agent | void>; updateAgent: (agent: Agent) => Promise<Agent | void>;

View file

@ -2,7 +2,7 @@
<component <component
:is="to === undefined ? 'button' : httpLink ? 'a' : 'router-link'" :is="to === undefined ? 'button' : httpLink ? 'a' : 'router-link'"
v-bind="btnAttrs" v-bind="btnAttrs"
class="relative flex items-center py-1 px-2 rounded-md border shadow-sm cursor-pointer transition-all duration-150 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed" class="relative flex flex-shrink-0 whitespace-nowrap items-center py-1 px-2 rounded-md border shadow-sm cursor-pointer transition-all duration-150 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
:class="{ :class="{
'bg-wp-control-neutral-100 hover:bg-wp-control-neutral-200 border-wp-control-neutral-300 text-wp-text-100': 'bg-wp-control-neutral-100 hover:bg-wp-control-neutral-200 border-wp-control-neutral-300 text-wp-text-100':
color === 'gray', color === 'gray',
@ -16,7 +16,7 @@
> >
<slot> <slot>
<Icon v-if="startIcon" :name="startIcon" class="!w-6 !h-6" :class="{ invisible: isLoading, 'mr-1': text }" /> <Icon v-if="startIcon" :name="startIcon" class="!w-6 !h-6" :class="{ invisible: isLoading, 'mr-1': text }" />
<span :class="{ invisible: isLoading }" class="flex-shrink-0">{{ text }}</span> <span :class="{ invisible: isLoading }">{{ text }}</span>
<Icon v-if="endIcon" :name="endIcon" class="ml-2 w-6 h-6" :class="{ invisible: isLoading }" /> <Icon v-if="endIcon" :name="endIcon" class="ml-2 w-6 h-6" :class="{ invisible: isLoading }" />
<div <div
v-if="isLoading" v-if="isLoading"

View file

@ -1,10 +1,10 @@
<template> <template>
<div <div
class="flex gap-2 items-center text-gray-700 font-bold rounded-md p-2 border border-solid border-l-6 border-wp-hint-warn-200 bg-wp-hint-warn-100" class="flex gap-4 items-center text-gray-700 font-bold rounded-md p-4 border border-solid border-l-6 border-wp-hint-warn-200 bg-wp-hint-warn-100"
> >
<Icon v-if="!textOnly" name="warning" /> <Icon v-if="!textOnly" name="warning" class="flex-shrink-0" />
<slot> <slot>
<span class="whitespace-pre">{{ text }}</span> <span class="whitespace-pre-wrap">{{ text }}</span>
</slot> </slot>
</div> </div>
</template> </template>

View file

@ -5,7 +5,7 @@
type="radio" type="radio"
class="radio relative flex-shrink-0 border bg-wp-control-neutral-100 border-wp-control-neutral-200 cursor-pointer rounded-full w-5 h-5 checked:bg-wp-control-ok-200 checked:border-wp-control-ok-200 focus-visible:border-wp-control-neutral-300 checked:focus-visible:border-wp-control-ok-300" class="radio relative flex-shrink-0 border bg-wp-control-neutral-100 border-wp-control-neutral-200 cursor-pointer rounded-full w-5 h-5 checked:bg-wp-control-ok-200 checked:border-wp-control-ok-200 focus-visible:border-wp-control-neutral-300 checked:focus-visible:border-wp-control-ok-300"
:value="option.value" :value="option.value"
:checked="innerValue.includes(option.value)" :checked="innerValue?.includes(option.value)"
@click="innerValue = option.value" @click="innerValue = option.value"
/> />
<div class="flex flex-col ml-4"> <div class="flex flex-col ml-4">

View file

@ -1,18 +1,18 @@
<template> <template>
<Panel> <Panel>
<div class="flex flex-row border-b mb-4 pb-4 items-center dark:border-wp-background-100"> <div class="flex flex-col border-b mb-4 pb-4 justify-center dark:border-wp-background-100">
<div class="ml-2"> <h1 class="text-xl text-wp-text-100 flex items-center gap-1">
<h1 class="text-xl text-wp-text-100">{{ title }}</h1> {{ title }}
<p v-if="desc" class="text-sm text-wp-text-alt-100"> <DocsLink v-if="docsUrl" :topic="title" :url="docsUrl" />
{{ desc }} </h1>
<DocsLink v-if="docsUrl" :topic="title" :url="docsUrl" />
</p>
<Warning v-if="warning" class="text-sm mt-1" :text="warning" />
</div>
<div class="ml-auto"> <div class="flex flex-wrap gap-x-4 gap-y-2 items-center justify-between">
<slot v-if="$slots.titleActions" name="titleActions" /> <p v-if="description" class="text-sm text-wp-text-alt-100">{{ description }}</p>
<div v-if="$slots.headerActions">
<slot name="headerActions" />
</div>
</div> </div>
<slot name="headerEnd" />
</div> </div>
<slot /> <slot />
@ -21,13 +21,11 @@
<script setup lang="ts"> <script setup lang="ts">
import DocsLink from '~/components/atomic/DocsLink.vue'; import DocsLink from '~/components/atomic/DocsLink.vue';
import Warning from '~/components/atomic/Warning.vue';
import Panel from '~/components/layout/Panel.vue'; import Panel from '~/components/layout/Panel.vue';
defineProps<{ defineProps<{
title: string; title: string;
desc?: string; description?: string;
docsUrl?: string; docsUrl?: string;
warning?: string;
}>(); }>();
</script> </script>

View file

@ -6,7 +6,7 @@
> >
<div v-if="pipelineCount > 0" class="spinner" /> <div v-if="pipelineCount > 0" class="spinner" />
<div <div
class="z-1 flex items-center justify-center h-full w-full font-bold bg-white bg-opacity-15 dark:bg-black dark:bg-opacity-10 rounded-md" class="z-0 flex items-center justify-center h-full w-full font-bold bg-white bg-opacity-15 dark:bg-black dark:bg-opacity-10 rounded-md"
> >
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
{{ pipelineCount > 9 ? '9+' : pipelineCount }} {{ pipelineCount > 9 ? '9+' : pipelineCount }}
@ -31,17 +31,17 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
@keyframes spinner-rotate { @keyframes rotate {
100% { 100% {
transform: rotate(1turn); transform: rotate(1turn);
} }
} }
.spinner { .spinner {
@apply absolute z-0 inset-1.5 rounded-md; @apply absolute inset-1.5 rounded-md;
overflow: hidden; overflow: hidden;
} }
.spinner::before { .spinner::before {
@apply absolute -z-2 bg-wp-primary-200 dark:bg-wp-primary-300; @apply absolute bg-wp-primary-200 dark:bg-wp-primary-300;
content: ''; content: '';
left: -50%; left: -50%;
top: -50%; top: -50%;
@ -51,11 +51,16 @@ onMounted(async () => {
background-size: background-size:
50% 50%, 50% 50%,
50% 50%; 50% 50%;
background-image: linear-gradient(#fff, transparent); background-image: linear-gradient(#fff, #fff);
animation: spinner-rotate 1.5s linear infinite; animation: rotate 1.5s linear infinite;
} }
.spinner::after { .spinner::after {
@apply absolute inset-0.5 rounded-md bg-blend-darken bg-wp-primary-200 dark:bg-wp-primary-300; @apply absolute inset-0.5 bg-wp-primary-200 dark:bg-wp-primary-300;
/*
The nested border radius needs to be calculated correctly to look right:
https://www.30secondsofcode.org/css/s/nested-border-radius/
*/
border-radius: calc(0.375rem - 0.125rem);
content: ''; content: '';
} }
</style> </style>

View file

@ -13,45 +13,28 @@
</InputField> </InputField>
<InputField v-slot="{ id }" :label="$t('repo.deploy_pipeline.variables.title')"> <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> <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"> <KeyValueEditor
<div v-for="(_, i) in payload.variables" :key="i" class="flex gap-4"> :id="id"
<TextField v-model="payload.variables"
:id="id" :key-placeholder="$t('repo.deploy_pipeline.variables.name')"
v-model="payload.variables[i].name" :value-placeholder="$t('repo.deploy_pipeline.variables.value')"
:placeholder="$t('repo.deploy_pipeline.variables.name')" :delete-title="$t('repo.deploy_pipeline.variables.delete')"
/> @update:is-valid="isVariablesValid = $event"
<TextField />
:id="id"
v-model="payload.variables[i].value"
:placeholder="$t('repo.deploy_pipeline.variables.value')"
/>
<div class="w-10 flex-shrink-0">
<Button
v-if="i !== payload.variables.length - 1"
color="red"
class="ml-auto"
:title="$t('repo.deploy_pipeline.variables.delete')"
@click="deleteVar(i)"
>
<Icon name="remove" />
</Button>
</div>
</div>
</div>
</InputField> </InputField>
<Button type="submit" :text="$t('repo.deploy_pipeline.trigger')" /> <Button type="submit" :text="$t('repo.deploy_pipeline.trigger')" :disabled="!isFormValid" />
</form> </form>
</Panel> </Panel>
</Popup> </Popup>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, toRef, watch } from 'vue'; import { computed, onMounted, ref, toRef } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue';
import InputField from '~/components/form/InputField.vue'; import InputField from '~/components/form/InputField.vue';
import KeyValueEditor from '~/components/form/KeyValueEditor.vue';
import TextField from '~/components/form/TextField.vue'; import TextField from '~/components/form/TextField.vue';
import Panel from '~/components/layout/Panel.vue'; import Panel from '~/components/layout/Panel.vue';
import Popup from '~/components/layout/Popup.vue'; import Popup from '~/components/layout/Popup.vue';
@ -68,55 +51,37 @@ const emit = defineEmits<{
}>(); }>();
const apiClient = useApiClient(); const apiClient = useApiClient();
const repo = inject('repo'); const repo = inject('repo');
const router = useRouter(); const router = useRouter();
const payload = ref<{ id: string; environment: string; task: string; variables: { name: string; value: string }[] }>({ const payload = ref<{
id: string;
environment: string;
task: string;
variables: Record<string, string>;
}>({
id: '', id: '',
environment: '', environment: '',
task: '', task: '',
variables: [ variables: {},
{
name: '',
value: '',
},
],
}); });
const pipelineOptions = computed(() => { const isVariablesValid = ref(true);
const variables = Object.fromEntries(
payload.value.variables.filter((e) => e.name !== '').map((item) => [item.name, item.value]), const isFormValid = computed(() => {
); return payload.value.environment !== '' && isVariablesValid.value;
return {
...payload.value,
variables,
};
}); });
const pipelineOptions = computed(() => ({
...payload.value,
variables: payload.value.variables,
}));
const loading = ref(true); const loading = ref(true);
onMounted(async () => { onMounted(async () => {
loading.value = false; loading.value = false;
}); });
watch(
payload,
() => {
if (payload.value.variables[payload.value.variables.length - 1].name !== '') {
payload.value.variables.push({
name: '',
value: '',
});
}
},
{ deep: true },
);
function deleteVar(index: number) {
payload.value.variables.splice(index, 1);
}
const pipelineNumber = toRef(props, 'pipelineNumber'); const pipelineNumber = toRef(props, 'pipelineNumber');
async function triggerDeployPipeline() { async function triggerDeployPipeline() {
loading.value = true; loading.value = true;

View file

@ -24,26 +24,26 @@
</div> </div>
<TextField <TextField
v-if="searchBoxPresent" v-if="searchBoxPresent"
class="w-auto <md:w-full <md:order-3" class="w-auto <md:w-full flex-grow <md:order-3"
:aria-label="$t('search')" :aria-label="$t('search')"
:placeholder="$t('search')" :placeholder="$t('search')"
:model-value="search" :model-value="search"
@update:model-value="(value: string) => $emit('update:search', value)" @update:model-value="(value: string) => $emit('update:search', value)"
/> />
<div <div
v-if="$slots.titleActions" v-if="$slots.headerActions"
class="flex items-center md:justify-end gap-x-2 min-w-0" class="flex items-center md:justify-end gap-x-2 min-w-0"
:class="{ :class="{
'md:flex-1': searchBoxPresent, 'md:flex-1': searchBoxPresent,
}" }"
> >
<slot name="titleActions" /> <slot name="headerActions" />
</div> </div>
</div> </div>
<div v-if="enableTabs" class="flex md:items-center flex-col py-2 md:flex-row md:justify-between md:py-0"> <div v-if="enableTabs" class="flex md:items-center flex-col py-2 md:flex-row md:justify-between md:py-0">
<Tabs class="<md:order-2" /> <Tabs class="<md:order-2" />
<div v-if="$slots.titleActions" class="flex content-start md:justify-end"> <div v-if="$slots.headerActions" class="flex content-start md:justify-end">
<slot name="tabActions" /> <slot name="tabActions" />
</div> </div>
</div> </div>
@ -52,6 +52,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import IconButton from '~/components/atomic/IconButton.vue'; import IconButton from '~/components/atomic/IconButton.vue';
import TextField from '~/components/form/TextField.vue'; import TextField from '~/components/form/TextField.vue';
import Container from '~/components/layout/Container.vue'; import Container from '~/components/layout/Container.vue';
@ -69,5 +71,5 @@ defineEmits<{
(event: 'update:search', query: string): void; (event: 'update:search', query: string): void;
}>(); }>();
const searchBoxPresent = props.search !== undefined; const searchBoxPresent = computed(() => props.search !== undefined);
</script> </script>

View file

@ -7,7 +7,7 @@
@update:search="(value) => $emit('update:search', value)" @update:search="(value) => $emit('update:search', value)"
> >
<template #title><slot name="title" /></template> <template #title><slot name="title" /></template>
<template v-if="$slots.titleActions" #titleActions><slot name="titleActions" /></template> <template v-if="$slots.headerActions" #headerActions><slot name="headerActions" /></template>
<template v-if="$slots.tabActions" #tabActions><slot name="tabActions" /></template> <template v-if="$slots.tabActions" #tabActions><slot name="tabActions" /></template>
</Header> </Header>
@ -18,8 +18,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue';
import Container from '~/components/layout/Container.vue'; import Container from '~/components/layout/Container.vue';
import { useTabsProvider } from '~/compositions/useTabs'; import { useTabsProvider } from '~/compositions/useTabs';
@ -33,37 +31,16 @@ const props = defineProps<{
// Tabs // Tabs
enableTabs?: boolean; enableTabs?: boolean;
disableTabUrlHashMode?: boolean;
activeTab?: string;
// Content // Content
fluidContent?: boolean; fluidContent?: boolean;
}>(); }>();
const emit = defineEmits<{ defineEmits<{
(event: 'update:activeTab', value: string | undefined): void;
(event: 'update:search', value: string): void; (event: 'update:search', value: string): void;
}>(); }>();
if (props.enableTabs) { if (props.enableTabs) {
const internalActiveTab = ref(props.activeTab); useTabsProvider();
watch(
() => props.activeTab,
(activeTab) => {
internalActiveTab.value = activeTab;
},
);
useTabsProvider({
activeTab: computed({
get: () => internalActiveTab.value,
set: (value) => {
internalActiveTab.value = value;
emit('update:activeTab', value);
},
}),
disableUrlHashMode: computed(() => props.disableTabUrlHashMode || false),
});
} }
</script> </script>

View file

@ -1,38 +1,40 @@
<template> <template><span /></template>
<div v-if="$slots.default" v-show="isActive" :aria-hidden="!isActive">
<slot />
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { onMounted } from 'vue';
import type { RouteLocationRaw } from 'vue-router';
import type { IconNames } from '~/components/atomic/Icon.vue'; import type { IconNames } from '~/components/atomic/Icon.vue';
import { useTabsClient, type Tab } from '~/compositions/useTabs'; import { useTabsClient } from '~/compositions/useTabs';
const props = defineProps<{ const props = defineProps<{
id?: string; to: RouteLocationRaw;
title: string; title: string;
icon?: IconNames; icon?: IconNames;
iconClass?: string; iconClass?: string;
matchChildren?: boolean;
}>(); }>();
const { tabs, activeTab } = useTabsClient(); const { tabs } = useTabsClient();
const tab = ref<Tab>();
// TODO: find a better way to compare routes like
// https://github.com/vuejs/router/blob/0eaaeb9697acd40ad524d913d0348748e9797acb/packages/router/src/utils/index.ts#L17
function isSameRoute(a: RouteLocationRaw, b: RouteLocationRaw): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
onMounted(() => { onMounted(() => {
tab.value = { // don't add tab if tab id is already present
id: props.id || props.title.toLocaleLowerCase().replace(' ', '-') || tabs.value.length.toString(), if (tabs.value.find(({ to }) => isSameRoute(to, props.to))) {
return;
}
tabs.value.push({
to: props.to,
title: props.title, title: props.title,
icon: props.icon, icon: props.icon,
iconClass: props.iconClass, iconClass: props.iconClass,
}; matchChildren: props.matchChildren,
});
// don't add tab if tab id is already present
if (!tabs.value.find(({ id }) => id === props.id)) {
tabs.value.push(tab.value);
}
}); });
const isActive = computed(() => tab.value && tab.value.id === activeTab.value);
</script> </script>

View file

@ -1,46 +1,27 @@
<template> <template>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<button <router-link
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.id" :key="tab.title"
class="w-full py-1 md:py-2 md:w-auto md:px-6 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center" v-slot="{ isActive, isExactActive }"
:class="{ :to="tab.to"
'border-wp-text-100': activeTab === tab.id, class="border-transparent w-full py-1 md:py-2 md:w-auto md:px-6 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center"
'border-transparent': activeTab !== tab.id, :active-class="tab.matchChildren ? '!border-wp-text-100' : ''"
}" :exact-active-class="tab.matchChildren ? '' : '!border-wp-text-100'"
type="button"
@click="selectTab(tab)"
> >
<Icon v-if="activeTab === tab.id" name="chevron-right" class="md:hidden" /> <Icon v-if="isExactActive || (isActive && tab.matchChildren)" name="chevron-right" class="md:hidden" />
<Icon v-else name="blank" class="md:hidden" /> <Icon v-else name="blank" class="md:hidden" />
<span class="flex gap-2 items-center flex-row-reverse md:flex-row"> <span class="flex gap-2 items-center flex-row-reverse md:flex-row">
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" /> <Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" />
<span>{{ tab.title }}</span> <span>{{ tab.title }}</span>
</span> </span>
</button> </router-link>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import Icon from '~/components/atomic/Icon.vue'; import Icon from '~/components/atomic/Icon.vue';
import { useTabsClient, type Tab } from '~/compositions/useTabs'; import { useTabsClient } from '~/compositions/useTabs';
const router = useRouter(); const { tabs } = useTabsClient();
const route = useRoute();
const { activeTab, tabs, disableUrlHashMode } = useTabsClient();
async function selectTab(tab: Tab) {
if (tab.id === undefined) {
return;
}
activeTab.value = tab.id;
if (!disableUrlHashMode.value) {
await router.replace({ params: route.params, hash: `#${tab.id}` });
}
}
</script> </script>

View file

@ -1,12 +1,17 @@
import type { InjectionKey, Ref } from 'vue'; import type { InjectionKey, Ref } from 'vue';
import { inject as vueInject, provide as vueProvide } from 'vue'; import { inject as vueInject, provide as vueProvide } from 'vue';
import type { Org, OrgPermissions, Repo } from '~/lib/api/types'; import type { Org, OrgPermissions, Pipeline, PipelineConfig, Repo } from '~/lib/api/types';
import type { Tab } from './useTabs';
export interface InjectKeys { export interface InjectKeys {
repo: Ref<Repo>; repo: Ref<Repo>;
org: Ref<Org | undefined>; org: Ref<Org | undefined>;
'org-permissions': Ref<OrgPermissions | undefined>; 'org-permissions': Ref<OrgPermissions | undefined>;
pipeline: Ref<Pipeline | undefined>;
'pipeline-configs': Ref<PipelineConfig[] | undefined>;
tabs: Ref<Tab[]>;
} }
export function inject<T extends keyof InjectKeys>(key: T): InjectKeys[T] { export function inject<T extends keyof InjectKeys>(key: T): InjectKeys[T] {

View file

@ -1,49 +1,24 @@
import { inject, onMounted, provide, ref, type Ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import type { RouteLocationRaw } from 'vue-router';
import type { IconNames } from '~/components/atomic/Icon.vue'; import type { IconNames } from '~/components/atomic/Icon.vue';
import { inject, provide } from './useInjectProvide';
export interface Tab { export interface Tab {
id: string; to: RouteLocationRaw;
title: string; title: string;
icon?: IconNames; icon?: IconNames;
iconClass?: string; iconClass?: string;
matchChildren?: boolean;
} }
export function useTabsProvider({ export function useTabsProvider() {
activeTab,
disableUrlHashMode,
}: {
activeTab: Ref<string | undefined>;
disableUrlHashMode: Ref<boolean>;
}) {
const route = useRoute();
const tabs = ref<Tab[]>([]); const tabs = ref<Tab[]>([]);
provide('tabs', tabs); provide('tabs', tabs);
provide('disable-url-hash-mode', disableUrlHashMode);
provide('active-tab', activeTab);
onMounted(() => {
if (activeTab.value !== undefined) {
return;
}
const hashTab = route.hash.replace(/^#/, '');
activeTab.value = hashTab || tabs.value[0].id;
});
} }
export function useTabsClient() { export function useTabsClient() {
const tabs = inject<Ref<Tab[]>>('tabs'); const tabs = inject('tabs');
const disableUrlHashMode = inject<Ref<boolean>>('disable-url-hash-mode'); return { tabs };
const activeTab = inject<Ref<string>>('active-tab');
if (activeTab === undefined || tabs === undefined || disableUrlHashMode === undefined) {
throw new Error('Please use this "useTabsClient" composition inside a component running "useTabsProvider".');
}
return { activeTab, tabs, disableUrlHashMode };
} }

View file

@ -67,7 +67,7 @@ export interface Repo {
last_pipeline: number; last_pipeline: number;
gated: boolean; require_approval: RepoRequireApproval;
// Events that will cancel running pipelines before starting a new one // Events that will cancel running pipelines before starting a new one
cancel_previous_pipeline_events: string[]; cancel_previous_pipeline_events: string[];
@ -81,6 +81,13 @@ export enum RepoVisibility {
Private = 'private', Private = 'private',
Internal = 'internal', Internal = 'internal',
} }
export enum RepoRequireApproval {
None = 'none',
Forks = 'forks',
PullRequests = 'pull_requests',
AllEvents = 'all_events',
}
/* eslint-enable */ /* eslint-enable */
export type RepoSettings = Pick< export type RepoSettings = Pick<
@ -89,7 +96,7 @@ export type RepoSettings = Pick<
| 'timeout' | 'timeout'
| 'visibility' | 'visibility'
| 'trusted' | 'trusted'
| 'gated' | 'require_approval'
| 'allow_pr' | 'allow_pr'
| 'allow_deploy' | 'allow_deploy'
| 'cancel_previous_pipeline_events' | 'cancel_previous_pipeline_events'

View file

@ -42,29 +42,38 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
path: 'branches', path: 'branches',
name: 'repo-branches',
component: (): Component => import('~/views/repo/RepoBranches.vue'),
meta: { repoHeader: true }, meta: { repoHeader: true },
children: [
{
path: '',
name: 'repo-branches',
component: (): Component => import('~/views/repo/RepoBranches.vue'),
},
{
path: ':branch',
name: 'repo-branch',
component: (): Component => import('~/views/repo/RepoBranch.vue'),
props: (route) => ({ branch: route.params.branch }),
},
],
}, },
{
path: 'branches/:branch',
name: 'repo-branch',
component: (): Component => import('~/views/repo/RepoBranch.vue'),
meta: { repoHeader: true },
props: (route) => ({ branch: route.params.branch }),
},
{ {
path: 'pull-requests', path: 'pull-requests',
name: 'repo-pull-requests',
component: (): Component => import('~/views/repo/RepoPullRequests.vue'),
meta: { repoHeader: true }, meta: { repoHeader: true },
}, children: [
{ {
path: 'pull-requests/:pullRequest', path: '',
name: 'repo-pull-request', name: 'repo-pull-requests',
component: (): Component => import('~/views/repo/RepoPullRequest.vue'), component: (): Component => import('~/views/repo/RepoPullRequests.vue'),
meta: { repoHeader: true }, },
props: (route) => ({ pullRequest: route.params.pullRequest }), {
path: ':pullRequest',
name: 'repo-pull-request',
component: (): Component => import('~/views/repo/RepoPullRequest.vue'),
props: (route) => ({ pullRequest: route.params.pullRequest }),
},
],
}, },
{ {
path: 'pipeline/:pipelineId', path: 'pipeline/:pipelineId',
@ -98,15 +107,53 @@ const routes: RouteRecordRaw[] = [
path: 'debug', path: 'debug',
name: 'repo-pipeline-debug', name: 'repo-pipeline-debug',
component: (): Component => import('~/views/repo/pipeline/PipelineDebug.vue'), component: (): Component => import('~/views/repo/pipeline/PipelineDebug.vue'),
props: true,
}, },
], ],
}, },
{ {
path: 'settings', path: 'settings',
name: 'repo-settings', component: (): Component => import('~/views/repo/settings/RepoSettings.vue'),
component: (): Component => import('~/views/repo/RepoSettings.vue'),
meta: { authentication: 'required' }, meta: { authentication: 'required' },
props: true, props: true,
children: [
{
path: '',
name: 'repo-settings',
component: (): Component => import('~/views/repo/settings/General.vue'),
props: true,
},
{
path: 'secrets',
name: 'repo-settings-secrets',
component: (): Component => import('~/views/repo/settings/Secrets.vue'),
props: true,
},
{
path: 'registries',
name: 'repo-settings-registries',
component: (): Component => import('~/views/repo/settings/Registries.vue'),
props: true,
},
{
path: 'crons',
name: 'repo-settings-crons',
component: (): Component => import('~/views/repo/settings/Crons.vue'),
props: true,
},
{
path: 'badge',
name: 'repo-settings-badge',
component: (): Component => import('~/views/repo/settings/Badge.vue'),
props: true,
},
{
path: 'actions',
name: 'repo-settings-actions',
component: (): Component => import('~/views/repo/settings/Actions.vue'),
props: true,
},
],
}, },
{ {
path: 'manual', path: 'manual',
@ -137,9 +184,29 @@ const routes: RouteRecordRaw[] = [
{ {
path: 'settings', path: 'settings',
name: 'org-settings', name: 'org-settings',
component: (): Component => import('~/views/org/OrgSettings.vue'), component: (): Component => import('~/views/org/settings/OrgSettingsWrapper.vue'),
meta: { authentication: 'required' }, meta: { authentication: 'required' },
props: true, props: true,
children: [
{
path: 'secrets',
name: 'org-settings-secrets',
component: (): Component => import('~/views/org/settings/OrgSecrets.vue'),
props: true,
},
{
path: 'registries',
name: 'org-settings-registries',
component: (): Component => import('~/views/org/settings/OrgRegistries.vue'),
props: true,
},
{
path: 'agents',
name: 'org-settings-agents',
component: (): Component => import('~/views/org/settings/OrgAgents.vue'),
props: true,
},
],
}, },
], ],
}, },
@ -150,18 +217,98 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
path: `${rootPath}/admin`, path: `${rootPath}/admin`,
name: 'admin-settings', component: (): Component => import('~/views/admin/AdminSettingsWrapper.vue'),
component: (): Component => import('~/views/admin/AdminSettings.vue'),
props: true, props: true,
meta: { authentication: 'required' }, meta: { authentication: 'required' },
children: [
{
path: '',
name: 'admin-settings',
component: (): Component => import('~/views/admin/AdminInfo.vue'),
props: true,
},
{
path: 'secrets',
name: 'admin-settings-secrets',
component: (): Component => import('~/views/admin/AdminSecrets.vue'),
props: true,
},
{
path: 'registries',
name: 'admin-settings-registries',
component: (): Component => import('~/views/admin/AdminRegistries.vue'),
props: true,
},
{
path: 'repos',
name: 'admin-settings-repos',
component: (): Component => import('~/views/admin/AdminRepos.vue'),
props: true,
},
{
path: 'users',
name: 'admin-settings-users',
component: (): Component => import('~/views/admin/AdminUsers.vue'),
props: true,
},
{
path: 'orgs',
name: 'admin-settings-orgs',
component: (): Component => import('~/views/admin/AdminOrgs.vue'),
props: true,
},
{
path: 'agents',
name: 'admin-settings-agents',
component: (): Component => import('~/views/admin/AdminAgents.vue'),
props: true,
},
{
path: 'queue',
name: 'admin-settings-queue',
component: (): Component => import('~/views/admin/AdminQueue.vue'),
props: true,
},
],
}, },
{ {
path: `${rootPath}/user`, path: `${rootPath}/user`,
name: 'user', component: (): Component => import('~/views/user/UserWrapper.vue'),
component: (): Component => import('~/views/User.vue'),
meta: { authentication: 'required' }, meta: { authentication: 'required' },
props: true, props: true,
children: [
{
path: '',
name: 'user',
component: (): Component => import('~/views/user/UserGeneral.vue'),
props: true,
},
{
path: 'secrets',
name: 'user-secrets',
component: (): Component => import('~/views/user/UserSecrets.vue'),
props: true,
},
{
path: 'registries',
name: 'user-registries',
component: (): Component => import('~/views/user/UserRegistries.vue'),
props: true,
},
{
path: 'cli-and-api',
name: 'user-cli-and-api',
component: (): Component => import('~/views/user/UserCLIAndAPI.vue'),
props: true,
},
{
path: 'agents',
name: 'user-agents',
component: (): Component => import('~/views/user/UserAgents.vue'),
props: true,
},
],
}, },
{ {
path: `${rootPath}/login`, path: `${rootPath}/login`,

View file

@ -4,7 +4,7 @@
{{ $t('repositories') }} {{ $t('repositories') }}
</template> </template>
<template #titleActions> <template #headerActions>
<Button :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" /> <Button :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" />
</template> </template>

View file

@ -1,35 +0,0 @@
<template>
<Scaffold enable-tabs>
<template #title>{{ $t('user.settings.settings') }}</template>
<template #titleActions><Button :text="$t('logout')" :to="`${address}/logout`" /></template>
<Tab id="general" :title="$t('user.settings.general.general')">
<UserGeneralTab />
</Tab>
<Tab id="secrets" :title="$t('secrets.secrets')">
<UserSecretsTab />
</Tab>
<Tab id="registries" :title="$t('registries.registries')">
<UserRegistriesTab />
</Tab>
<Tab id="cli-and-api" :title="$t('user.settings.cli_and_api.cli_and_api')">
<UserCLIAndAPITab />
</Tab>
<Tab v-if="useConfig().userRegisteredAgents" id="agents" :title="$t('admin.settings.agents.agents')">
<UserAgentsTab />
</Tab>
</Scaffold>
</template>
<script lang="ts" setup>
import Button from '~/components/atomic/Button.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import UserAgentsTab from '~/components/user/UserAgentsTab.vue';
import UserCLIAndAPITab from '~/components/user/UserCLIAndAPITab.vue';
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
import UserRegistriesTab from '~/components/user/UserRegistriesTab.vue';
import UserSecretsTab from '~/components/user/UserSecretsTab.vue';
import useConfig from '~/compositions/useConfig';
const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host
</script>

View file

@ -1,6 +1,6 @@
<template> <template>
<AgentManager <AgentManager
:desc="$t('admin.settings.agents.desc')" :description="$t('admin.settings.agents.desc')"
:load-agents="loadAgents" :load-agents="loadAgents"
:create-agent="createAgent" :create-agent="createAgent"
:update-agent="updateAgent" :update-agent="updateAgent"

View file

@ -3,7 +3,7 @@
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<WoodpeckerLogo class="w-32 h-32 fill-wp-text-200" /> <WoodpeckerLogo class="w-32 h-32 fill-wp-text-200" />
<i18n-t keypath="running_version" tag="p" class="text-xl"> <i18n-t keypath="running_version" tag="p" class="text-xl text-center">
<span class="font-bold">{{ version?.current }}</span> <span class="font-bold">{{ version?.current }}</span>
</i18n-t> </i18n-t>

View file

@ -1,5 +1,5 @@
<template> <template>
<Settings :title="$t('admin.settings.orgs.orgs')" :desc="$t('admin.settings.orgs.desc')"> <Settings :title="$t('admin.settings.orgs.orgs')" :description="$t('admin.settings.orgs.desc')">
<div class="space-y-4 text-wp-text-100"> <div class="space-y-4 text-wp-text-100">
<ListItem <ListItem
v-for="org in orgs" v-for="org in orgs"

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('admin.settings.queue.queue')" :desc="$t('admin.settings.queue.desc')"> <Settings :title="$t('admin.settings.queue.queue')" :description="$t('admin.settings.queue.desc')">
<template #titleActions> <template #headerActions>
<div v-if="queueInfo"> <div v-if="queueInfo">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button <Button
@ -78,6 +78,7 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import AdminQueueStats from '~/components/admin/settings/queue/AdminQueueStats.vue';
import Badge from '~/components/atomic/Badge.vue'; import Badge from '~/components/atomic/Badge.vue';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue'; import Icon from '~/components/atomic/Icon.vue';
@ -87,8 +88,6 @@ import useApiClient from '~/compositions/useApiClient';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import type { QueueInfo } from '~/lib/api/types'; import type { QueueInfo } from '~/lib/api/types';
import AdminQueueStats from './queue/AdminQueueStats.vue';
const apiClient = useApiClient(); const apiClient = useApiClient();
const notifications = useNotifications(); const notifications = useNotifications();
const { t } = useI18n(); const { t } = useI18n();

View file

@ -1,11 +1,10 @@
<template> <template>
<Settings <Settings
:title="$t('registries.registries')" :title="$t('registries.registries')"
:desc="$t('admin.settings.registries.desc')" :description="$t('admin.settings.registries.desc')"
docs-url="docs/usage/registries" docs-url="docs/usage/registries"
:warning="$t('admin.settings.registries.warning')"
> >
<template #titleActions> <template #headerActions>
<Button <Button
v-if="selectedRegistry" v-if="selectedRegistry"
:text="$t('registries.show')" :text="$t('registries.show')"
@ -15,6 +14,10 @@
<Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" /> <Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</template> </template>
<template #headerEnd>
<Warning class="text-sm mt-4" :text="$t('admin.settings.registries.warning')" />
</template>
<RegistryList <RegistryList
v-if="!selectedRegistry" v-if="!selectedRegistry"
v-model="registries" v-model="registries"
@ -39,6 +42,7 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import Warning from '~/components/atomic/Warning.vue';
import Settings from '~/components/layout/Settings.vue'; import Settings from '~/components/layout/Settings.vue';
import RegistryEdit from '~/components/registry/RegistryEdit.vue'; import RegistryEdit from '~/components/registry/RegistryEdit.vue';
import RegistryList from '~/components/registry/RegistryList.vue'; import RegistryList from '~/components/registry/RegistryList.vue';

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('admin.settings.repos.repos')" :desc="$t('admin.settings.repos.desc')"> <Settings :title="$t('admin.settings.repos.repos')" :description="$t('admin.settings.repos.desc')">
<template #titleActions> <template #headerActions>
<Button <Button
start-icon="heal" start-icon="heal"
:is-loading="isRepairingRepos" :is-loading="isRepairingRepos"

View file

@ -1,15 +1,18 @@
<template> <template>
<Settings <Settings
:title="$t('secrets.secrets')" :title="$t('secrets.secrets')"
:desc="$t('admin.settings.secrets.desc')" :description="$t('admin.settings.secrets.desc')"
docs-url="docs/usage/secrets" docs-url="docs/usage/secrets"
:warning="$t('admin.settings.secrets.warning')"
> >
<template #titleActions> <template #headerActions>
<Button v-if="selectedSecret" :text="$t('secrets.show')" start-icon="back" @click="selectedSecret = undefined" /> <Button v-if="selectedSecret" :text="$t('secrets.show')" start-icon="back" @click="selectedSecret = undefined" />
<Button v-else :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" /> <Button v-else :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" />
</template> </template>
<template #headerEnd>
<Warning class="text-sm mt-4" :text="$t('admin.settings.secrets.warning')" />
</template>
<SecretList <SecretList
v-if="!selectedSecret" v-if="!selectedSecret"
v-model="secrets" v-model="secrets"
@ -34,6 +37,7 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import Warning from '~/components/atomic/Warning.vue';
import Settings from '~/components/layout/Settings.vue'; import Settings from '~/components/layout/Settings.vue';
import SecretEdit from '~/components/secrets/SecretEdit.vue'; import SecretEdit from '~/components/secrets/SecretEdit.vue';
import SecretList from '~/components/secrets/SecretList.vue'; import SecretList from '~/components/secrets/SecretList.vue';

View file

@ -1,67 +0,0 @@
<template>
<Scaffold enable-tabs>
<template #title>
{{ $t('settings') }}
</template>
<Tab id="info" :title="$t('info')">
<AdminInfoTab />
</Tab>
<Tab id="secrets" :title="$t('secrets.secrets')">
<AdminSecretsTab />
</Tab>
<Tab id="registries" :title="$t('registries.registries')">
<AdminRegistriesTab />
</Tab>
<Tab id="repos" :title="$t('admin.settings.repos.repos')">
<AdminReposTab />
</Tab>
<Tab id="users" :title="$t('admin.settings.users.users')">
<AdminUsersTab />
</Tab>
<Tab id="orgs" :title="$t('admin.settings.orgs.orgs')">
<AdminOrgsTab />
</Tab>
<Tab id="agents" :title="$t('admin.settings.agents.agents')">
<AdminAgentsTab />
</Tab>
<Tab id="queue" :title="$t('admin.settings.queue.queue')">
<AdminQueueTab />
</Tab>
</Scaffold>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import AdminAgentsTab from '~/components/admin/settings/AdminAgentsTab.vue';
import AdminInfoTab from '~/components/admin/settings/AdminInfoTab.vue';
import AdminOrgsTab from '~/components/admin/settings/AdminOrgsTab.vue';
import AdminQueueTab from '~/components/admin/settings/AdminQueueTab.vue';
import AdminRegistriesTab from '~/components/admin/settings/AdminRegistriesTab.vue';
import AdminReposTab from '~/components/admin/settings/AdminReposTab.vue';
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
const notifications = useNotifications();
const router = useRouter();
const i18n = useI18n();
const { user } = useAuthentication();
onMounted(async () => {
if (!user?.admin) {
notifications.notify({ type: 'error', title: i18n.t('admin.settings.not_allowed') });
await router.replace({ name: 'home' });
}
if (!user?.admin) {
notifications.notify({ type: 'error', title: i18n.t('admin.settings.not_allowed') });
await router.replace({ name: 'home' });
}
});
</script>

View file

@ -0,0 +1,40 @@
<template>
<Scaffold enable-tabs>
<template #title>
{{ $t('settings') }}
</template>
<Tab :to="{ name: 'admin-settings' }" :title="$t('info')" />
<Tab :to="{ name: 'admin-settings-secrets' }" :title="$t('secrets.secrets')" />
<Tab :to="{ name: 'admin-settings-registries' }" :title="$t('registries.registries')" />
<Tab :to="{ name: 'admin-settings-repos' }" :title="$t('admin.settings.repos.repos')" />
<Tab :to="{ name: 'admin-settings-users' }" :title="$t('admin.settings.users.users')" />
<Tab :to="{ name: 'admin-settings-orgs' }" :title="$t('admin.settings.orgs.orgs')" />
<Tab :to="{ name: 'admin-settings-agents' }" :title="$t('admin.settings.agents.agents')" />
<Tab :to="{ name: 'admin-settings-queue' }" :title="$t('admin.settings.queue.queue')" />
<router-view />
</Scaffold>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
const notifications = useNotifications();
const router = useRouter();
const i18n = useI18n();
const { user } = useAuthentication();
onMounted(async () => {
if (!user?.admin) {
notifications.notify({ type: 'error', title: i18n.t('admin.settings.not_allowed') });
await router.replace({ name: 'home' });
}
});
</script>

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('admin.settings.users.users')" :desc="$t('admin.settings.users.desc')"> <Settings :title="$t('admin.settings.users.users')" :description="$t('admin.settings.users.desc')">
<template #titleActions> <template #headerActions>
<Button <Button
v-if="selectedUser" v-if="selectedUser"
:text="$t('admin.settings.users.show')" :text="$t('admin.settings.users.show')"

View file

@ -4,11 +4,11 @@
{{ org.name }} {{ org.name }}
</template> </template>
<template #titleActions> <template #headerActions>
<IconButton <IconButton
v-if="orgPermissions.admin" v-if="orgPermissions.admin"
icon="settings" icon="settings"
:to="{ name: org.is_user ? 'user' : 'org-settings' }" :to="{ name: org.is_user ? 'user' : 'org-settings-secrets' }"
:title="$t('settings')" :title="$t('settings')"
/> />
</template> </template>

View file

@ -4,10 +4,10 @@
{{ org.name }} {{ org.name }}
</template> </template>
<template #titleActions> <template #headerActions>
<IconButton <IconButton
v-if="orgPermissions.admin" v-if="orgPermissions.admin"
:to="{ name: org.is_user ? 'user' : 'repo-settings' }" :to="{ name: org.is_user ? 'user' : 'org-settings-secrets' }"
:title="$t('settings')" :title="$t('settings')"
icon="settings" icon="settings"
/> />

View file

@ -1,6 +1,6 @@
<template> <template>
<AgentManager <AgentManager
:desc="$t('org.settings.agents.desc')" :description="$t('org.settings.agents.desc')"
:load-agents="loadAgents" :load-agents="loadAgents"
:create-agent="createAgent" :create-agent="createAgent"
:update-agent="updateAgent" :update-agent="updateAgent"

View file

@ -1,10 +1,10 @@
<template> <template>
<Settings <Settings
:title="$t('registries.registries')" :title="$t('registries.registries')"
:desc="$t('org.settings.registries.desc')" :description="$t('org.settings.registries.desc')"
docs-url="docs/usage/registries" docs-url="docs/usage/registries"
> >
<template #titleActions> <template #headerActions>
<Button <Button
v-if="selectedRegistry" v-if="selectedRegistry"
:text="$t('registries.show')" :text="$t('registries.show')"

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('secrets.secrets')" :desc="$t('org.settings.secrets.desc')" docs-url="docs/usage/secrets"> <Settings :title="$t('secrets.secrets')" :description="$t('org.settings.secrets.desc')" docs-url="docs/usage/secrets">
<template #titleActions> <template #headerActions>
<Button v-if="selectedSecret" :text="$t('secrets.show')" start-icon="back" @click="selectedSecret = undefined" /> <Button v-if="selectedSecret" :text="$t('secrets.show')" start-icon="back" @click="selectedSecret = undefined" />
<Button v-else :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" /> <Button v-else :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" />
</template> </template>

View file

@ -11,17 +11,15 @@
</span> </span>
</template> </template>
<Tab id="secrets" :title="$t('secrets.secrets')"> <Tab :to="{ name: 'org-settings-secrets' }" :title="$t('secrets.secrets')" />
<OrgSecretsTab /> <Tab :to="{ name: 'org-settings-registries' }" :title="$t('registries.registries')" />
</Tab> <Tab
v-if="useConfig().userRegisteredAgents"
:to="{ name: 'org-settings-agents' }"
:title="$t('admin.settings.agents.agents')"
/>
<Tab id="registries" :title="$t('registries.registries')"> <router-view />
<OrgRegistriesTab />
</Tab>
<Tab v-if="useConfig().userRegisteredAgents" id="agents" :title="$t('admin.settings.agents.agents')">
<OrgAgentsTab />
</Tab>
</Scaffold> </Scaffold>
</template> </template>
@ -32,9 +30,6 @@ import { useRouter } from 'vue-router';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue'; import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue'; import Tab from '~/components/layout/scaffold/Tab.vue';
import OrgAgentsTab from '~/components/org/settings/OrgAgentsTab.vue';
import OrgRegistriesTab from '~/components/org/settings/OrgRegistriesTab.vue';
import OrgSecretsTab from '~/components/org/settings/OrgSecretsTab.vue';
import useConfig from '~/compositions/useConfig'; import useConfig from '~/compositions/useConfig';
import { inject } from '~/compositions/useInjectProvide'; import { inject } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';

View file

@ -1,10 +1,5 @@
<template> <template>
<Scaffold <Scaffold v-if="repo && repoPermissions && route.meta.repoHeader" enable-tabs>
v-if="repo && repoPermissions && route.meta.repoHeader"
v-model:active-tab="activeTab"
enable-tabs
disable-tab-url-hash-mode
>
<template #title> <template #title>
<span class="flex"> <span class="flex">
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{ <router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{
@ -15,7 +10,7 @@
{{ repo.name }} {{ repo.name }}
</span> </span>
</template> </template>
<template #titleActions> <template #headerActions>
<a v-if="badgeUrl" :href="badgeUrl" target="_blank"> <a v-if="badgeUrl" :href="badgeUrl" target="_blank">
<img :src="badgeUrl" /> <img :src="badgeUrl" />
</a> </a>
@ -43,9 +38,14 @@
/> />
</template> </template>
<Tab id="activity" :title="$t('repo.activity')" /> <Tab :to="{ name: 'repo' }" :title="$t('repo.activity')" />
<Tab id="branches" :title="$t('repo.branches')" /> <Tab :to="{ name: 'repo-branches' }" match-children :title="$t('repo.branches')" />
<Tab v-if="repo.pr_enabled && repo.allow_pr" id="pull_requests" :title="$t('repo.pull_requests')" /> <Tab
v-if="repo.pr_enabled && repo.allow_pr"
:to="{ name: 'repo-pull-requests' }"
match-children
:title="$t('repo.pull_requests')"
/>
<router-view /> <router-view />
</Scaffold> </Scaffold>
@ -132,25 +132,4 @@ watch([repositoryId], () => {
}); });
const badgeUrl = computed(() => repo.value && `${config.rootPath}/api/badges/${repo.value.id}/status.svg`); const badgeUrl = computed(() => repo.value && `${config.rootPath}/api/badges/${repo.value.id}/status.svg`);
const activeTab = computed({
get() {
if (route.name === 'repo-branches' || route.name === 'repo-branch') {
return 'branches';
}
if (route.name === 'repo-pull-requests' || route.name === 'repo-pull-request') {
return 'pull_requests';
}
return 'activity';
},
set(tab: string) {
if (tab === 'branches') {
router.push({ name: 'repo-branches' });
} else if (tab === 'pull_requests') {
router.push({ name: 'repo-pull-requests' });
} else {
router.push({ name: 'repo' });
}
},
});
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col gap-y-6"> <div class="flex flex-col gap-y-6">
<Panel <Panel
v-for="pipelineConfig in pipelineConfigsDecoded || []" v-for="pipelineConfig in pipelineConfigsDecoded"
:key="pipelineConfig.hash" :key="pipelineConfig.hash"
:collapsable="pipelineConfigsDecoded && pipelineConfigsDecoded.length > 1" :collapsable="pipelineConfigsDecoded && pipelineConfigsDecoded.length > 1"
collapsed-by-default collapsed-by-default
@ -14,21 +14,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import { decode } from 'js-base64'; import { decode } from 'js-base64';
import { computed, inject, type Ref } from 'vue'; import { computed } from 'vue';
import SyntaxHighlight from '~/components/atomic/SyntaxHighlight'; import SyntaxHighlight from '~/components/atomic/SyntaxHighlight';
import Panel from '~/components/layout/Panel.vue'; import Panel from '~/components/layout/Panel.vue';
import type { PipelineConfig } from '~/lib/api/types'; import { inject } from '~/compositions/useInjectProvide';
const pipelineConfigs = inject<Ref<PipelineConfig[]>>('pipeline-configs'); const pipelineConfigs = inject('pipeline-configs');
if (!pipelineConfigs) { if (!pipelineConfigs) {
throw new Error('Unexpected: "pipelineConfigs" should be provided at this place'); throw new Error('Unexpected: "pipelineConfigs" should be provided at this place');
} }
const pipelineConfigsDecoded = computed(() => const pipelineConfigsDecoded = computed(
pipelineConfigs.value.map((i) => ({ () =>
...i, pipelineConfigs.value?.map((i) => ({
data: decode(i.data), ...i,
})), data: decode(i.data),
})) ?? [],
); );
</script> </script>

View file

@ -1,11 +1,9 @@
<template> <template>
<Scaffold <Scaffold
v-if="pipeline && repo" v-if="pipeline && repo"
v-model:active-tab="activeTab"
enable-tabs enable-tabs
disable-tab-url-hash-mode
:go-back="goBack" :go-back="goBack"
:fluid-content="activeTab === 'tasks'" :fluid-content="route.name === 'repo-pipeline'"
full-width-header full-width-header
> >
<template #title> <template #title>
@ -19,7 +17,7 @@
</span> </span>
</template> </template>
<template #titleActions> <template #headerActions>
<div class="flex md:items-center flex-col gap-2 md:flex-row md:justify-between min-w-0"> <div class="flex md:items-center flex-col gap-2 md:flex-row md:justify-between min-w-0">
<div class="flex content-start gap-2 min-w-0"> <div class="flex content-start gap-2 min-w-0">
<PipelineStatusIcon :status="pipeline.status" class="flex flex-shrink-0" /> <PipelineStatusIcon :status="pipeline.status" class="flex flex-shrink-0" />
@ -75,10 +73,10 @@
</div> </div>
</template> </template>
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" /> <Tab :to="{ name: 'repo-pipeline' }" :title="$t('repo.pipeline.tasks')" />
<Tab <Tab
v-if="pipeline.errors && pipeline.errors.length > 0" v-if="pipeline.errors && pipeline.errors.length > 0"
id="errors" :to="{ name: 'repo-pipeline-errors' }"
icon="attention" icon="attention"
:title=" :title="
pipeline.errors.some((e) => !e.is_warning) pipeline.errors.some((e) => !e.is_warning)
@ -87,20 +85,24 @@
" "
:icon-class="pipeline.errors.some((e) => !e.is_warning) ? 'text-wp-state-error-100' : 'text-wp-state-warn-100'" :icon-class="pipeline.errors.some((e) => !e.is_warning) ? 'text-wp-state-error-100' : 'text-wp-state-warn-100'"
/> />
<Tab id="config" :title="$t('repo.pipeline.config')" /> <Tab :to="{ name: 'repo-pipeline-config' }" :title="$t('repo.pipeline.config')" />
<Tab <Tab
v-if="pipeline.changed_files && pipeline.changed_files.length > 0" v-if="pipeline.changed_files && pipeline.changed_files.length > 0"
id="changed-files" :to="{ name: 'repo-pipeline-changed-files' }"
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })" :title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })"
/> />
<Tab v-if="repoPermissions && repoPermissions.push" id="debug" :title="$t('repo.pipeline.debug.title')" /> <Tab
v-if="repoPermissions && repoPermissions.push"
:to="{ name: 'repo-pipeline-debug' }"
:title="$t('repo.pipeline.debug.title')"
/>
<router-view /> <router-view />
</Scaffold> </Scaffold>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onBeforeUnmount, onMounted, provide, ref, toRef, watch, type Ref } from 'vue'; import { computed, inject, onBeforeUnmount, onMounted, ref, toRef, watch, type Ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
@ -113,6 +115,7 @@ import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vu
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction'; import { useAsyncAction } from '~/compositions/useAsyncAction';
import { useFavicon } from '~/compositions/useFavicon'; import { useFavicon } from '~/compositions/useFavicon';
import { provide } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import usePipeline from '~/compositions/usePipeline'; import usePipeline from '~/compositions/usePipeline';
import { useRouteBack } from '~/compositions/useRouteBack'; import { useRouteBack } from '~/compositions/useRouteBack';
@ -206,48 +209,5 @@ onBeforeUnmount(() => {
favicon.updateStatus('default'); favicon.updateStatus('default');
}); });
const activeTab = computed({
get() {
if (route.name === 'repo-pipeline-changed-files') {
return 'changed-files';
}
if (route.name === 'repo-pipeline-config') {
return 'config';
}
if (route.name === 'repo-pipeline-errors') {
return 'errors';
}
if (route.name === 'repo-pipeline-debug' && repoPermissions.value?.push) {
return 'debug';
}
return 'tasks';
},
set(tab: string) {
if (tab === 'tasks') {
router.replace({ name: 'repo-pipeline' });
}
if (tab === 'changed-files') {
router.replace({ name: 'repo-pipeline-changed-files' });
}
if (tab === 'config') {
router.replace({ name: 'repo-pipeline-config' });
}
if (tab === 'errors') {
router.replace({ name: 'repo-pipeline-errors' });
}
if (tab === 'debug' && repoPermissions.value?.push) {
router.replace({ name: 'repo-pipeline-debug' });
}
},
});
const goBack = useRouteBack({ name: 'repo' }); const goBack = useRouteBack({ name: 'repo' });
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('repo.settings.badge.badge')"> <Settings :title="$t('repo.settings.badge.badge')">
<template #titleActions> <template #headerActions>
<a v-if="badgeUrl" :href="badgeUrl" target="_blank"> <a v-if="badgeUrl" :href="badgeUrl" target="_blank">
<img :src="badgeUrl" /> <img :src="badgeUrl" />
</a> </a>

View file

@ -1,6 +1,10 @@
<template> <template>
<Settings :title="$t('repo.settings.crons.crons')" :desc="$t('repo.settings.crons.desc')" docs-url="docs/usage/cron"> <Settings
<template #titleActions> :title="$t('repo.settings.crons.crons')"
:description="$t('repo.settings.crons.desc')"
docs-url="docs/usage/cron"
>
<template #headerActions>
<Button <Button
v-if="selectedCron" v-if="selectedCron"
start-icon="back" start-icon="back"

View file

@ -1,26 +1,6 @@
<template> <template>
<Settings :title="$t('repo.settings.general.general')"> <Settings :title="$t('repo.settings.general.general')">
<form v-if="repoSettings" class="flex flex-col" @submit.prevent="saveRepoSettings"> <form v-if="repoSettings" class="flex flex-col" @submit.prevent="saveRepoSettings">
<InputField
docs-url="docs/usage/project-settings#pipeline-path"
:label="$t('repo.settings.general.pipeline_path.path')"
>
<template #default="{ id }">
<TextField
:id="id"
v-model="repoSettings.config_file"
:placeholder="$t('repo.settings.general.pipeline_path.default')"
/>
</template>
<template #description>
<i18n-t keypath="repo.settings.general.pipeline_path.desc" tag="p" class="text-sm text-wp-text-alt-100">
<span class="code-box-inline">{{ $t('repo.settings.general.pipeline_path.desc_path_example') }}</span>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
<span class="code-box-inline">/</span>
</i18n-t>
</template>
</InputField>
<InputField <InputField
docs-url="docs/usage/project-settings#project-settings-1" docs-url="docs/usage/project-settings#project-settings-1"
:label="$t('repo.settings.general.project')" :label="$t('repo.settings.general.project')"
@ -35,11 +15,6 @@
:label="$t('repo.settings.general.allow_deploy.allow')" :label="$t('repo.settings.general.allow_deploy.allow')"
:description="$t('repo.settings.general.allow_deploy.desc')" :description="$t('repo.settings.general.allow_deploy.desc')"
/> />
<Checkbox
v-model="repoSettings.gated"
:label="$t('repo.settings.general.protected.protected')"
:description="$t('repo.settings.general.protected.desc')"
/>
<Checkbox <Checkbox
v-model="repoSettings.netrc_only_trusted" v-model="repoSettings.netrc_only_trusted"
:label="$t('repo.settings.general.netrc_only_trusted.netrc_only_trusted')" :label="$t('repo.settings.general.netrc_only_trusted.netrc_only_trusted')"
@ -69,6 +44,36 @@
/> />
</InputField> </InputField>
<InputField :label="$t('require_approval.require_approval_for')">
<RadioField
v-model="repoSettings.require_approval"
:options="[
{
value: RepoRequireApproval.None,
text: $t('require_approval.none'),
description: $t('require_approval.none_desc'),
},
{
value: RepoRequireApproval.Forks,
text: $t('require_approval.forks'),
},
{
value: RepoRequireApproval.PullRequests,
text: $t('require_approval.pull_requests'),
},
{
value: RepoRequireApproval.AllEvents,
text: $t('require_approval.all_events'),
},
]"
/>
<template #description>
<p class="text-sm">
{{ $t('require_approval.desc') }}
</p>
</template>
</InputField>
<InputField <InputField
docs-url="docs/usage/project-settings#project-visibility" docs-url="docs/usage/project-settings#project-visibility"
:label="$t('repo.settings.general.visibility.visibility')" :label="$t('repo.settings.general.visibility.visibility')"
@ -87,6 +92,26 @@
</div> </div>
</InputField> </InputField>
<InputField
docs-url="docs/usage/project-settings#pipeline-path"
:label="$t('repo.settings.general.pipeline_path.path')"
>
<template #default="{ id }">
<TextField
:id="id"
v-model="repoSettings.config_file"
:placeholder="$t('repo.settings.general.pipeline_path.default')"
/>
</template>
<template #description>
<i18n-t keypath="repo.settings.general.pipeline_path.desc" tag="p" class="text-sm text-wp-text-alt-100">
<span class="code-box-inline">{{ $t('repo.settings.general.pipeline_path.desc_path_example') }}</span>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
<span class="code-box-inline">/</span>
</i18n-t>
</template>
</InputField>
<InputField <InputField
docs-url="docs/usage/project-settings#cancel-previous-pipelines" docs-url="docs/usage/project-settings#cancel-previous-pipelines"
:label="$t('repo.settings.general.cancel_prev.cancel')" :label="$t('repo.settings.general.cancel_prev.cancel')"
@ -130,7 +155,7 @@ import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction'; import { useAsyncAction } from '~/compositions/useAsyncAction';
import useAuthentication from '~/compositions/useAuthentication'; import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import { RepoVisibility, WebhookEvents, type Repo, type RepoSettings } from '~/lib/api/types'; import { RepoRequireApproval, RepoVisibility, WebhookEvents, type Repo, type RepoSettings } from '~/lib/api/types';
import { useRepoStore } from '~/store/repos'; import { useRepoStore } from '~/store/repos';
const apiClient = useApiClient(); const apiClient = useApiClient();
@ -151,7 +176,7 @@ function loadRepoSettings() {
config_file: repo.value.config_file, config_file: repo.value.config_file,
timeout: repo.value.timeout, timeout: repo.value.timeout,
visibility: repo.value.visibility, visibility: repo.value.visibility,
gated: repo.value.gated, require_approval: repo.value.require_approval,
trusted: repo.value.trusted, trusted: repo.value.trusted,
allow_pr: repo.value.allow_pr, allow_pr: repo.value.allow_pr,
allow_deploy: repo.value.allow_deploy, allow_deploy: repo.value.allow_deploy,

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('registries.credentials')" :desc="$t('registries.desc')" docs-url="docs/usage/registries"> <Settings :title="$t('registries.credentials')" :description="$t('registries.desc')" docs-url="docs/usage/registries">
<template #titleActions> <template #headerActions>
<Button <Button
v-if="selectedRegistry" v-if="selectedRegistry"
:text="$t('registries.show')" :text="$t('registries.show')"

View file

@ -16,24 +16,14 @@
</span> </span>
</template> </template>
<Tab id="general" :title="$t('repo.settings.general.general')"> <Tab :to="{ name: 'repo-settings' }" :title="$t('repo.settings.general.general')" />
<GeneralTab /> <Tab :to="{ name: 'repo-settings-secrets' }" :title="$t('secrets.secrets')" />
</Tab> <Tab :to="{ name: 'repo-settings-registries' }" :title="$t('registries.registries')" />
<Tab id="secrets" :title="$t('secrets.secrets')"> <Tab :to="{ name: 'repo-settings-crons' }" :title="$t('repo.settings.crons.crons')" />
<SecretsTab /> <Tab :to="{ name: 'repo-settings-badge' }" :title="$t('repo.settings.badge.badge')" />
</Tab> <Tab :to="{ name: 'repo-settings-actions' }" :title="$t('repo.settings.actions.actions')" />
<Tab id="registries" :title="$t('registries.registries')">
<RegistriesTab /> <router-view />
</Tab>
<Tab id="crons" :title="$t('repo.settings.crons.crons')">
<CronTab />
</Tab>
<Tab id="badge" :title="$t('repo.settings.badge.badge')">
<BadgeTab />
</Tab>
<Tab id="actions" :title="$t('repo.settings.actions.actions')">
<ActionsTab />
</Tab>
</Scaffold> </Scaffold>
</template> </template>
@ -44,12 +34,6 @@ import { useRouter } from 'vue-router';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue'; import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue'; import Tab from '~/components/layout/scaffold/Tab.vue';
import ActionsTab from '~/components/repo/settings/ActionsTab.vue';
import BadgeTab from '~/components/repo/settings/BadgeTab.vue';
import CronTab from '~/components/repo/settings/CronTab.vue';
import GeneralTab from '~/components/repo/settings/GeneralTab.vue';
import RegistriesTab from '~/components/repo/settings/RegistriesTab.vue';
import SecretsTab from '~/components/repo/settings/SecretsTab.vue';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import { useRouteBack } from '~/compositions/useRouteBack'; import { useRouteBack } from '~/compositions/useRouteBack';
import type { Repo, RepoPermissions } from '~/lib/api/types'; import type { Repo, RepoPermissions } from '~/lib/api/types';

View file

@ -1,6 +1,6 @@
<template> <template>
<Settings :title="$t('secrets.secrets')" :desc="$t('secrets.desc')" docs-url="docs/usage/secrets"> <Settings :title="$t('secrets.secrets')" :description="$t('secrets.desc')" docs-url="docs/usage/secrets">
<template #titleActions> <template #headerActions>
<Button v-if="selectedSecret" :text="$t('secrets.show')" start-icon="back" @click="selectedSecret = undefined" /> <Button v-if="selectedSecret" :text="$t('secrets.show')" start-icon="back" @click="selectedSecret = undefined" />
<Button v-else :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" /> <Button v-else :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" />
</template> </template>

View file

@ -1,6 +1,6 @@
<template> <template>
<AgentManager <AgentManager
:desc="$t('user.settings.agents.desc')" :description="$t('user.settings.agents.desc')"
:load-agents="loadAgents" :load-agents="loadAgents"
:create-agent="createAgent" :create-agent="createAgent"
:update-agent="updateAgent" :update-agent="updateAgent"

View file

@ -1,7 +1,7 @@
<template> <template>
<Settings :title="$t('user.settings.cli_and_api.cli_and_api')" :desc="$t('user.settings.cli_and_api.desc')"> <Settings :title="$t('user.settings.cli_and_api.cli_and_api')" :description="$t('user.settings.cli_and_api.desc')">
<InputField :label="$t('user.settings.cli_and_api.cli_usage')"> <InputField :label="$t('user.settings.cli_and_api.cli_usage')">
<template #titleActions> <template #headerActions>
<a :href="cliDownload" target="_blank" class="ml-4 text-wp-link-100 hover:text-wp-link-200">{{ <a :href="cliDownload" target="_blank" class="ml-4 text-wp-link-100 hover:text-wp-link-200">{{
$t('user.settings.cli_and_api.download_cli') $t('user.settings.cli_and_api.download_cli')
}}</a> }}</a>
@ -10,14 +10,14 @@
</InputField> </InputField>
<InputField :label="$t('user.settings.cli_and_api.token')"> <InputField :label="$t('user.settings.cli_and_api.token')">
<template #titleActions> <template #headerActions>
<Button class="ml-auto" :text="$t('user.settings.cli_and_api.reset_token')" @click="resetToken" /> <Button class="ml-auto" :text="$t('user.settings.cli_and_api.reset_token')" @click="resetToken" />
</template> </template>
<pre class="code-box">{{ token }}</pre> <pre class="code-box">{{ token }}</pre>
</InputField> </InputField>
<InputField :label="$t('user.settings.cli_and_api.api_usage')"> <InputField :label="$t('user.settings.cli_and_api.api_usage')">
<template #titleActions> <template #headerActions>
<a <a
v-if="enableSwagger" v-if="enableSwagger"
:href="`${address}/swagger/index.html`" :href="`${address}/swagger/index.html`"

View file

@ -1,22 +1,18 @@
<template> <template>
<Panel> <Settings
<div class="flex flex-row border-b mb-4 pb-4 items-center dark:border-wp-background-100"> :title="$t('registries.registries')"
<div class="ml-2"> :description="$t('user.settings.registries.desc')"
<h1 class="text-xl text-wp-text-100">{{ $t('registries.registries') }}</h1> docs-url="docs/usage/registries"
<p class="text-sm text-wp-text-alt-100"> >
{{ $t('user.settings.registries.desc') }} <template #headerActions>
<DocsLink :topic="$t('registries.registries')" url="docs/usage/registries" />
</p>
</div>
<Button <Button
v-if="selectedRegistry" v-if="selectedRegistry"
class="ml-auto"
:text="$t('registries.show')" :text="$t('registries.show')"
start-icon="back" start-icon="back"
@click="selectedRegistry = undefined" @click="selectedRegistry = undefined"
/> />
<Button v-else class="ml-auto" :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" /> <Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</div> </template>
<RegistryList <RegistryList
v-if="!selectedRegistry" v-if="!selectedRegistry"
@ -33,7 +29,7 @@
@save="createRegistry" @save="createRegistry"
@cancel="selectedRegistry = undefined" @cancel="selectedRegistry = undefined"
/> />
</Panel> </Settings>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -42,8 +38,7 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import DocsLink from '~/components/atomic/DocsLink.vue'; import Settings from '~/components/layout/Settings.vue';
import Panel from '~/components/layout/Panel.vue';
import RegistryEdit from '~/components/registry/RegistryEdit.vue'; import RegistryEdit from '~/components/registry/RegistryEdit.vue';
import RegistryList from '~/components/registry/RegistryList.vue'; import RegistryList from '~/components/registry/RegistryList.vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';

View file

@ -1,22 +1,13 @@
<template> <template>
<Panel> <Settings
<div class="flex flex-row border-b mb-4 pb-4 items-center dark:border-wp-background-100"> :title="$t('secrets.secrets')"
<div class="ml-2"> :description="$t('user.settings.secrets.desc')"
<h1 class="text-xl text-wp-text-100">{{ $t('secrets.secrets') }}</h1> docs-url="docs/usage/secrets"
<p class="text-sm text-wp-text-alt-100"> >
{{ $t('user.settings.secrets.desc') }} <template #headerActions>
<DocsLink :topic="$t('secrets.secrets')" url="docs/usage/secrets" /> <Button v-if="selectedSecret" :text="$t('secrets.show')" start-icon="back" @click="selectedSecret = undefined" />
</p> <Button v-else :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" />
</div> </template>
<Button
v-if="selectedSecret"
class="ml-auto"
:text="$t('secrets.show')"
start-icon="back"
@click="selectedSecret = undefined"
/>
<Button v-else class="ml-auto" :text="$t('secrets.add')" start-icon="plus" @click="showAddSecret" />
</div>
<SecretList <SecretList
v-if="!selectedSecret" v-if="!selectedSecret"
@ -33,7 +24,7 @@
@save="createSecret" @save="createSecret"
@cancel="selectedSecret = undefined" @cancel="selectedSecret = undefined"
/> />
</Panel> </Settings>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -42,8 +33,7 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import DocsLink from '~/components/atomic/DocsLink.vue'; import Settings from '~/components/layout/Settings.vue';
import Panel from '~/components/layout/Panel.vue';
import SecretEdit from '~/components/secrets/SecretEdit.vue'; import SecretEdit from '~/components/secrets/SecretEdit.vue';
import SecretList from '~/components/secrets/SecretList.vue'; import SecretList from '~/components/secrets/SecretList.vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';

View file

@ -0,0 +1,27 @@
<template>
<Scaffold enable-tabs>
<template #title>{{ $t('user.settings.settings') }}</template>
<template #headerActions><Button :text="$t('logout')" :to="`${address}/logout`" /></template>
<Tab :to="{ name: 'user' }" :title="$t('user.settings.general.general')" />
<Tab :to="{ name: 'user-secrets' }" :title="$t('secrets.secrets')" />
<Tab :to="{ name: 'user-registries' }" :title="$t('registries.registries')" />
<Tab :to="{ name: 'user-cli-and-api' }" :title="$t('user.settings.cli_and_api.cli_and_api')" />
<Tab
v-if="useConfig().userRegisteredAgents"
:to="{ name: 'user-agents' }"
:title="$t('admin.settings.agents.agents')"
/>
<router-view />
</Scaffold>
</template>
<script lang="ts" setup>
import Button from '~/components/atomic/Button.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useConfig from '~/compositions/useConfig';
const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host
</script>

View file

@ -14,6 +14,27 @@
package woodpecker package woodpecker
type ApprovalMode string
var (
RequireApprovalNone ApprovalMode = "none" // require approval for no events
RequireApprovalForks ApprovalMode = "forks" // require approval for PRs from forks
RequireApprovalPullRequests ApprovalMode = "pull_requests" // require approval for all PRs (default)
RequireApprovalAllEvents ApprovalMode = "all_events" // require approval for all events
)
func (mode ApprovalMode) Valid() bool {
switch mode {
case RequireApprovalNone,
RequireApprovalForks,
RequireApprovalPullRequests,
RequireApprovalAllEvents:
return true
default:
return false
}
}
type ( type (
// User represents a user account. // User represents a user account.
User struct { User struct {
@ -27,37 +48,39 @@ type (
// Repo represents a repository. // Repo represents a repository.
Repo struct { Repo struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
ForgeRemoteID string `json:"forge_remote_id"` ForgeRemoteID string `json:"forge_remote_id"`
Owner string `json:"owner"` Owner string `json:"owner"`
Name string `json:"name"` Name string `json:"name"`
FullName string `json:"full_name"` FullName string `json:"full_name"`
Avatar string `json:"avatar_url,omitempty"` Avatar string `json:"avatar_url,omitempty"`
ForgeURL string `json:"forge_url,omitempty"` ForgeURL string `json:"forge_url,omitempty"`
Clone string `json:"clone_url,omitempty"` Clone string `json:"clone_url,omitempty"`
DefaultBranch string `json:"default_branch,omitempty"` DefaultBranch string `json:"default_branch,omitempty"`
SCMKind string `json:"scm,omitempty"` SCMKind string `json:"scm,omitempty"`
Timeout int64 `json:"timeout,omitempty"` Timeout int64 `json:"timeout,omitempty"`
Visibility string `json:"visibility"` Visibility string `json:"visibility"`
IsSCMPrivate bool `json:"private"` IsSCMPrivate bool `json:"private"`
IsTrusted bool `json:"trusted"` IsTrusted bool `json:"trusted"`
IsGated bool `json:"gated"` IsGated bool `json:"gated,omitempty"` // TODO: remove in next major release
IsActive bool `json:"active"` RequireApproval ApprovalMode `json:"require_approval"`
AllowPullRequests bool `json:"allow_pr"` IsActive bool `json:"active"`
Config string `json:"config_file"` AllowPullRequests bool `json:"allow_pr"`
CancelPreviousPipelineEvents []string `json:"cancel_previous_pipeline_events"` Config string `json:"config_file"`
NetrcOnlyTrusted bool `json:"netrc_only_trusted"` CancelPreviousPipelineEvents []string `json:"cancel_previous_pipeline_events"`
NetrcOnlyTrusted bool `json:"netrc_only_trusted"`
} }
// RepoPatch defines a repository patch request. // RepoPatch defines a repository patch request.
RepoPatch struct { RepoPatch struct {
Config *string `json:"config_file,omitempty"` Config *string `json:"config_file,omitempty"`
IsTrusted *bool `json:"trusted,omitempty"` IsTrusted *bool `json:"trusted,omitempty"`
IsGated *bool `json:"gated,omitempty"` IsGated *bool `json:"gated,omitempty"` // TODO: remove in next major release
Timeout *int64 `json:"timeout,omitempty"` RequireApproval *ApprovalMode `json:"require_approval,omitempty"`
Visibility *string `json:"visibility"` Timeout *int64 `json:"timeout,omitempty"`
AllowPull *bool `json:"allow_pr,omitempty"` Visibility *string `json:"visibility"`
PipelineCounter *int `json:"pipeline_counter,omitempty"` AllowPull *bool `json:"allow_pr,omitempty"`
PipelineCounter *int `json:"pipeline_counter,omitempty"`
} }
PipelineError struct { PipelineError struct {