mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-22 16:36:30 +00:00
Extend approval options (#3348)
This commit is contained in:
parent
2a445a6663
commit
5e2fa8164b
27 changed files with 431 additions and 93 deletions
|
@ -65,6 +65,7 @@ Visibility: {{ .Visibility }}
|
|||
Private: {{ .IsSCMPrivate }}
|
||||
Trusted: {{ .IsTrusted }}
|
||||
Gated: {{ .IsGated }}
|
||||
Require approval for: {{ .RequireApproval }}
|
||||
Clone url: {{ .Clone }}
|
||||
Allow pull-requests: {{ .AllowPullRequests }}
|
||||
`
|
||||
|
|
|
@ -39,6 +39,10 @@ var repoUpdateCmd = &cli.Command{
|
|||
Name: "gated",
|
||||
Usage: "repository is gated",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "require-approval",
|
||||
Usage: "repository requires approval for",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "timeout",
|
||||
Usage: "repository timeout",
|
||||
|
@ -79,6 +83,7 @@ func repoUpdate(ctx context.Context, c *cli.Command) error {
|
|||
timeout = c.Duration("timeout")
|
||||
trusted = c.Bool("trusted")
|
||||
gated = c.Bool("gated")
|
||||
requireApproval = c.String("require-approval")
|
||||
pipelineCounter = int(c.Int("pipeline-counter"))
|
||||
unsafe = c.Bool("unsafe")
|
||||
)
|
||||
|
@ -87,8 +92,29 @@ func repoUpdate(ctx context.Context, c *cli.Command) error {
|
|||
if c.IsSet("trusted") {
|
||||
patch.IsTrusted = &trusted
|
||||
}
|
||||
// TODO: remove isGated in next major release
|
||||
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") {
|
||||
v := int64(timeout / time.Minute)
|
||||
|
|
|
@ -4905,6 +4905,9 @@ const docTemplate = `{
|
|||
"forge_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"from_fork": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
@ -5068,9 +5071,6 @@ const docTemplate = `{
|
|||
"full_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"gated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
@ -5092,6 +5092,9 @@ const docTemplate = `{
|
|||
"private": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"require_approval": {
|
||||
"$ref": "#/definitions/model.ApprovalMode"
|
||||
},
|
||||
"scm": {
|
||||
"$ref": "#/definitions/SCMKind"
|
||||
},
|
||||
|
@ -5125,11 +5128,15 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
},
|
||||
"gated": {
|
||||
"description": "TODO: deprecated in favor of RequireApproval =\u003e Remove in next major release",
|
||||
"type": "boolean"
|
||||
},
|
||||
"netrc_only_trusted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"require_approval": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"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": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
|
@ -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.
|
||||
:::
|
||||
|
||||
## 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.
|
||||
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.
|
||||
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`.
|
||||
|
||||
## Trusted
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 353 KiB |
|
@ -91,6 +91,7 @@ func PostRepo(c *gin.Context) {
|
|||
repo.Update(from)
|
||||
} else {
|
||||
repo = from
|
||||
repo.RequireApproval = model.RequireApprovalForks
|
||||
repo.AllowPull = true
|
||||
repo.AllowDeploy = false
|
||||
repo.NetrcOnlyTrusted = true
|
||||
|
@ -250,8 +251,20 @@ func PatchRepo(c *gin.Context) {
|
|||
if in.AllowDeploy != nil {
|
||||
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 {
|
||||
repo.Timeout = *in.Timeout
|
||||
|
|
|
@ -183,6 +183,7 @@ func convertPullHook(from *internal.PullRequestHook) *model.Pipeline {
|
|||
Author: from.Actor.Login,
|
||||
Sender: from.Actor.Login,
|
||||
Timestamp: from.PullRequest.Updated.UTC().Unix(),
|
||||
FromFork: from.PullRequest.Source.Repo.UUID != from.PullRequest.Dest.Repo.UUID,
|
||||
}
|
||||
|
||||
if from.PullRequest.State == stateClosed {
|
||||
|
|
|
@ -123,6 +123,7 @@ func convertPullRequestEvent(ev *bb.PullRequestEvent, baseURL string) *model.Pip
|
|||
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),
|
||||
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 {
|
||||
|
|
|
@ -171,6 +171,7 @@ func pipelineFromPullRequest(hook *pullRequestHook) *model.Pipeline {
|
|||
hook.PullRequest.Base.Ref,
|
||||
),
|
||||
PullRequestLabels: convertLabels(hook.PullRequest.Labels),
|
||||
FromFork: hook.PullRequest.Head.RepoID != hook.PullRequest.Base.RepoID,
|
||||
}
|
||||
|
||||
return pipeline
|
||||
|
|
|
@ -172,6 +172,7 @@ func pipelineFromPullRequest(hook *pullRequestHook) *model.Pipeline {
|
|||
hook.PullRequest.Base.Ref,
|
||||
),
|
||||
PullRequestLabels: convertLabels(hook.PullRequest.Labels),
|
||||
FromFork: hook.PullRequest.Head.RepoID != hook.PullRequest.Base.RepoID,
|
||||
}
|
||||
|
||||
return pipeline
|
||||
|
|
|
@ -157,6 +157,8 @@ func parsePullHook(hook *github.PullRequestEvent, merge bool) (*github.PullReque
|
|||
event = model.EventPullClosed
|
||||
}
|
||||
|
||||
fromFork := hook.GetPullRequest().GetHead().GetRepo().GetID() != hook.GetPullRequest().GetBase().GetRepo().GetID()
|
||||
|
||||
pipeline := &model.Pipeline{
|
||||
Event: event,
|
||||
Commit: hook.GetPullRequest().GetHead().GetSHA(),
|
||||
|
@ -173,6 +175,7 @@ func parsePullHook(hook *github.PullRequestEvent, merge bool) (*github.PullReque
|
|||
hook.GetPullRequest().GetBase().GetRef(),
|
||||
),
|
||||
PullRequestLabels: convertLabels(hook.GetPullRequest().Labels),
|
||||
FromFork: fromFork,
|
||||
}
|
||||
if merge {
|
||||
pipeline.Ref = fmt.Sprintf(mergeRefs, hook.GetPullRequest().GetNumber())
|
||||
|
|
|
@ -138,6 +138,7 @@ func convertMergeRequestHook(hook *gitlab.MergeEvent, req *http.Request) (int, *
|
|||
pipeline.Title = obj.Title
|
||||
pipeline.ForgeURL = obj.URL
|
||||
pipeline.PullRequestLabels = convertLabels(hook.Labels)
|
||||
pipeline.FromFork = target.PathWithNamespace != source.PathWithNamespace
|
||||
|
||||
return obj.IID, repo, pipeline, nil
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ type Pipeline struct {
|
|||
AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"`
|
||||
PullRequestLabels []string `json:"pr_labels,omitempty" xorm:"json 'pr_labels'"`
|
||||
IsPrerelease bool `json:"is_prerelease,omitempty" xorm:"is_prerelease"`
|
||||
FromFork bool `json:"from_fork,omitempty" xorm:"from_fork"`
|
||||
} // @name Pipeline
|
||||
|
||||
// TableName return database table name for xorm.
|
||||
|
|
|
@ -20,6 +20,27 @@ import (
|
|||
"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.
|
||||
type Repo struct {
|
||||
ID int64 `json:"id,omitempty" xorm:"pk autoincr 'id'"`
|
||||
|
@ -42,7 +63,7 @@ type Repo struct {
|
|||
Visibility RepoVisibility `json:"visibility" xorm:"varchar(10) 'visibility'"`
|
||||
IsSCMPrivate bool `json:"private" xorm:"private"`
|
||||
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"`
|
||||
AllowPull bool `json:"allow_pr" xorm:"allow_pr"`
|
||||
AllowDeploy bool `json:"allow_deploy" xorm:"allow_deploy"`
|
||||
|
@ -109,7 +130,8 @@ func (r *Repo) Update(from *Repo) {
|
|||
// RepoPatch represents a repository patch object.
|
||||
type RepoPatch struct {
|
||||
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"`
|
||||
Visibility *string `json:"visibility,omitempty"`
|
||||
AllowPull *bool `json:"allow_pr,omitempty"`
|
||||
|
|
|
@ -27,8 +27,7 @@ import (
|
|||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||
)
|
||||
|
||||
// Approve update the status to pending for a blocked pipeline because of a gated repo
|
||||
// and start them afterward.
|
||||
// Approve update the status to pending for a blocked pipeline so it can be executed.
|
||||
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 {
|
||||
return nil, ErrBadRequest{Msg: fmt.Sprintf("cannot approve a pipeline with status %s", currentPipeline.Status)}
|
||||
|
|
|
@ -68,7 +68,7 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline
|
|||
// update some pipeline fields
|
||||
pipeline.RepoID = repo.ID
|
||||
pipeline.Status = model.StatusCreated
|
||||
setGatedState(repo, pipeline)
|
||||
setApprovalState(repo, pipeline)
|
||||
err = _store.CreatePipeline(pipeline)
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName)
|
||||
|
|
|
@ -26,7 +26,7 @@ import (
|
|||
"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) {
|
||||
forge, err := server.Config.Services.Manager.ForgeFromRepo(repo)
|
||||
if err != nil {
|
||||
|
|
|
@ -16,11 +16,40 @@ package pipeline
|
|||
|
||||
import "go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
|
||||
func setGatedState(repo *model.Repo, pipeline *model.Pipeline) {
|
||||
// TODO(336): extend gated feature with an allow/block List
|
||||
if repo.IsGated &&
|
||||
// events created by woodpecker itself should run right away
|
||||
pipeline.Event != model.EventCron && pipeline.Event != model.EventManual {
|
||||
pipeline.Status = model.StatusBlocked
|
||||
func setApprovalState(repo *model.Repo, pipeline *model.Pipeline) {
|
||||
if !needsApproval(repo, pipeline) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
78
server/pipeline/gated_test.go
Normal file
78
server/pipeline/gated_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
},
|
||||
}
|
|
@ -47,6 +47,7 @@ var migrationTasks = []*xormigrate.Migration{
|
|||
&addCustomLabelsToAgent,
|
||||
&splitTrusted,
|
||||
&correctPotentialCorruptOrgsUsersRelation,
|
||||
&gatedToRequireApproval,
|
||||
}
|
||||
|
||||
var allBeans = []any{
|
||||
|
|
|
@ -88,10 +88,6 @@
|
|||
"allow": "Allow deployments",
|
||||
"desc": "Allow deployments from successful pipelines. Only use if you trust all users with push access."
|
||||
},
|
||||
"protected": {
|
||||
"protected": "Protected",
|
||||
"desc": "Every pipeline needs to be approved before being executed."
|
||||
},
|
||||
"netrc_only_trusted": {
|
||||
"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."
|
||||
|
@ -504,5 +500,14 @@
|
|||
"internal_error": "Some internal error occurred",
|
||||
"registration_closed": "The registration is closed",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
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"
|
||||
:value="option.value"
|
||||
:checked="innerValue.includes(option.value)"
|
||||
:checked="innerValue?.includes(option.value)"
|
||||
@click="innerValue = option.value"
|
||||
/>
|
||||
<div class="flex flex-col ml-4">
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
<template>
|
||||
<Panel>
|
||||
<h1 class="text-xl text-wp-text-100">{{ title }}</h1>
|
||||
<div class="flex flex-col gap-4 border-b mb-4 pb-4">
|
||||
<div class="flex flex-col sm:flex-row gap-4 sm:gap-12 md:justify-between dark:border-wp-background-100">
|
||||
<div v-if="desc" class="flex items-center gap-x-2 text-sm text-wp-text-alt-100">
|
||||
<span class="flex flex-grow-0">{{ desc }}</span>
|
||||
<DocsLink v-if="docsUrl" class="flex flex-grow-0" :topic="title" :url="docsUrl" />
|
||||
</div>
|
||||
<div class="flex flex-col border-b mb-4 pb-4 justify-center dark:border-wp-background-100">
|
||||
<h1 class="text-xl text-wp-text-100 flex items-center gap-1">
|
||||
{{ title }}
|
||||
<DocsLink v-if="docsUrl" :topic="title" :url="docsUrl" />
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
<slot v-if="$slots.titleActions" name="titleActions" />
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-2 items-center justify-between">
|
||||
<p v-if="desc" class="text-sm text-wp-text-alt-100">{{ desc }}</p>
|
||||
<div v-if="$slots.titleActions">
|
||||
<slot name="titleActions" />
|
||||
</div>
|
||||
</div>
|
||||
<Warning v-if="warning" class="text-sm mt-1" :text="warning" />
|
||||
|
||||
<Warning v-if="warning" class="text-sm mt-4" :text="warning" />
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
|
|
|
@ -1,26 +1,6 @@
|
|||
<template>
|
||||
<Settings :title="$t('repo.settings.general.general')">
|
||||
<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
|
||||
docs-url="docs/usage/project-settings#project-settings-1"
|
||||
:label="$t('repo.settings.general.project')"
|
||||
|
@ -35,11 +15,6 @@
|
|||
:label="$t('repo.settings.general.allow_deploy.allow')"
|
||||
: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
|
||||
v-model="repoSettings.netrc_only_trusted"
|
||||
:label="$t('repo.settings.general.netrc_only_trusted.netrc_only_trusted')"
|
||||
|
@ -69,6 +44,36 @@
|
|||
/>
|
||||
</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
|
||||
docs-url="docs/usage/project-settings#project-visibility"
|
||||
:label="$t('repo.settings.general.visibility.visibility')"
|
||||
|
@ -87,6 +92,26 @@
|
|||
</div>
|
||||
</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
|
||||
docs-url="docs/usage/project-settings#cancel-previous-pipelines"
|
||||
:label="$t('repo.settings.general.cancel_prev.cancel')"
|
||||
|
@ -130,7 +155,7 @@ import useApiClient from '~/compositions/useApiClient';
|
|||
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||
import useAuthentication from '~/compositions/useAuthentication';
|
||||
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';
|
||||
|
||||
const apiClient = useApiClient();
|
||||
|
@ -151,7 +176,7 @@ function loadRepoSettings() {
|
|||
config_file: repo.value.config_file,
|
||||
timeout: repo.value.timeout,
|
||||
visibility: repo.value.visibility,
|
||||
gated: repo.value.gated,
|
||||
require_approval: repo.value.require_approval,
|
||||
trusted: repo.value.trusted,
|
||||
allow_pr: repo.value.allow_pr,
|
||||
allow_deploy: repo.value.allow_deploy,
|
||||
|
|
|
@ -67,7 +67,7 @@ export interface Repo {
|
|||
|
||||
last_pipeline: number;
|
||||
|
||||
gated: boolean;
|
||||
require_approval: RepoRequireApproval;
|
||||
|
||||
// Events that will cancel running pipelines before starting a new one
|
||||
cancel_previous_pipeline_events: string[];
|
||||
|
@ -81,6 +81,13 @@ export enum RepoVisibility {
|
|||
Private = 'private',
|
||||
Internal = 'internal',
|
||||
}
|
||||
|
||||
export enum RepoRequireApproval {
|
||||
None = 'none',
|
||||
Forks = 'forks',
|
||||
PullRequests = 'pull_requests',
|
||||
AllEvents = 'all_events',
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export type RepoSettings = Pick<
|
||||
|
@ -89,7 +96,7 @@ export type RepoSettings = Pick<
|
|||
| 'timeout'
|
||||
| 'visibility'
|
||||
| 'trusted'
|
||||
| 'gated'
|
||||
| 'require_approval'
|
||||
| 'allow_pr'
|
||||
| 'allow_deploy'
|
||||
| 'cancel_previous_pipeline_events'
|
||||
|
|
|
@ -14,6 +14,27 @@
|
|||
|
||||
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 (
|
||||
// User represents a user account.
|
||||
User struct {
|
||||
|
@ -27,37 +48,39 @@ type (
|
|||
|
||||
// Repo represents a repository.
|
||||
Repo struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
ForgeRemoteID string `json:"forge_remote_id"`
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Avatar string `json:"avatar_url,omitempty"`
|
||||
ForgeURL string `json:"forge_url,omitempty"`
|
||||
Clone string `json:"clone_url,omitempty"`
|
||||
DefaultBranch string `json:"default_branch,omitempty"`
|
||||
SCMKind string `json:"scm,omitempty"`
|
||||
Timeout int64 `json:"timeout,omitempty"`
|
||||
Visibility string `json:"visibility"`
|
||||
IsSCMPrivate bool `json:"private"`
|
||||
IsTrusted bool `json:"trusted"`
|
||||
IsGated bool `json:"gated"`
|
||||
IsActive bool `json:"active"`
|
||||
AllowPullRequests bool `json:"allow_pr"`
|
||||
Config string `json:"config_file"`
|
||||
CancelPreviousPipelineEvents []string `json:"cancel_previous_pipeline_events"`
|
||||
NetrcOnlyTrusted bool `json:"netrc_only_trusted"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
ForgeRemoteID string `json:"forge_remote_id"`
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Avatar string `json:"avatar_url,omitempty"`
|
||||
ForgeURL string `json:"forge_url,omitempty"`
|
||||
Clone string `json:"clone_url,omitempty"`
|
||||
DefaultBranch string `json:"default_branch,omitempty"`
|
||||
SCMKind string `json:"scm,omitempty"`
|
||||
Timeout int64 `json:"timeout,omitempty"`
|
||||
Visibility string `json:"visibility"`
|
||||
IsSCMPrivate bool `json:"private"`
|
||||
IsTrusted bool `json:"trusted"`
|
||||
IsGated bool `json:"gated,omitempty"` // TODO: remove in next major release
|
||||
RequireApproval ApprovalMode `json:"require_approval"`
|
||||
IsActive bool `json:"active"`
|
||||
AllowPullRequests bool `json:"allow_pr"`
|
||||
Config string `json:"config_file"`
|
||||
CancelPreviousPipelineEvents []string `json:"cancel_previous_pipeline_events"`
|
||||
NetrcOnlyTrusted bool `json:"netrc_only_trusted"`
|
||||
}
|
||||
|
||||
// RepoPatch defines a repository patch request.
|
||||
RepoPatch struct {
|
||||
Config *string `json:"config_file,omitempty"`
|
||||
IsTrusted *bool `json:"trusted,omitempty"`
|
||||
IsGated *bool `json:"gated,omitempty"`
|
||||
Timeout *int64 `json:"timeout,omitempty"`
|
||||
Visibility *string `json:"visibility"`
|
||||
AllowPull *bool `json:"allow_pr,omitempty"`
|
||||
PipelineCounter *int `json:"pipeline_counter,omitempty"`
|
||||
Config *string `json:"config_file,omitempty"`
|
||||
IsTrusted *bool `json:"trusted,omitempty"`
|
||||
IsGated *bool `json:"gated,omitempty"` // TODO: remove in next major release
|
||||
RequireApproval *ApprovalMode `json:"require_approval,omitempty"`
|
||||
Timeout *int64 `json:"timeout,omitempty"`
|
||||
Visibility *string `json:"visibility"`
|
||||
AllowPull *bool `json:"allow_pr,omitempty"`
|
||||
PipelineCounter *int `json:"pipeline_counter,omitempty"`
|
||||
}
|
||||
|
||||
PipelineError struct {
|
||||
|
|
Loading…
Reference in a new issue