mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-25 19:31:05 +00:00
Extend approval options (#3348)
This commit is contained in:
parent
2cbd9268f6
commit
d95e6a33bf
26 changed files with 418 additions and 86 deletions
|
@ -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 }}
|
||||||
`
|
`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -4671,6 +4671,9 @@ const docTemplate = `{
|
||||||
"forge_url": {
|
"forge_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"from_fork": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
@ -4861,15 +4864,15 @@ const docTemplate = `{
|
||||||
"private": {
|
"private": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"require_approval": {
|
||||||
|
"$ref": "#/definitions/model.ApprovalMode"
|
||||||
|
},
|
||||||
"scm": {
|
"scm": {
|
||||||
"$ref": "#/definitions/SCMKind"
|
"$ref": "#/definitions/SCMKind"
|
||||||
},
|
},
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"trusted": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"$ref": "#/definitions/RepoVisibility"
|
"$ref": "#/definitions/RepoVisibility"
|
||||||
}
|
}
|
||||||
|
@ -4899,12 +4902,12 @@ const docTemplate = `{
|
||||||
"netrc_only_trusted": {
|
"netrc_only_trusted": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"require_approval": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"trusted": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -5163,6 +5166,27 @@ const docTemplate = `{
|
||||||
"EventManual"
|
"EventManual"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": [
|
||||||
|
|
|
@ -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 |
|
@ -90,6 +90,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
|
||||||
|
@ -236,8 +237,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.IsTrusted != nil {
|
if in.IsTrusted != nil {
|
||||||
repo.IsTrusted = *in.IsTrusted
|
repo.IsTrusted = *in.IsTrusted
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'"`
|
||||||
|
@ -41,7 +62,7 @@ type Repo struct {
|
||||||
Timeout int64 `json:"timeout,omitempty" xorm:"timeout"`
|
Timeout int64 `json:"timeout,omitempty" xorm:"timeout"`
|
||||||
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"`
|
||||||
IsTrusted bool `json:"trusted" xorm:"trusted"`
|
RequireApproval ApprovalMode `json:"require_approval" xorm:"varchar(50) require_approval"`
|
||||||
IsGated bool `json:"gated" xorm:"gated"`
|
IsGated bool `json:"gated" xorm:"gated"`
|
||||||
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"`
|
||||||
|
@ -109,7 +130,7 @@ 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"`
|
||||||
IsTrusted *bool `json:"trusted,omitempty"`
|
RequireApproval *string `json:"require_approval,omitempty"`
|
||||||
IsGated *bool `json:"gated,omitempty"`
|
IsGated *bool `json:"gated,omitempty"`
|
||||||
Timeout *int64 `json:"timeout,omitempty"`
|
Timeout *int64 `json:"timeout,omitempty"`
|
||||||
Visibility *string `json:"visibility,omitempty"`
|
Visibility *string `json:"visibility,omitempty"`
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
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")
|
||||||
|
},
|
||||||
|
}
|
|
@ -64,6 +64,7 @@ var migrationTasks = []*xormigrate.Migration{
|
||||||
&unifyColumnsTables,
|
&unifyColumnsTables,
|
||||||
&alterTableRegistriesFixRequiredFields,
|
&alterTableRegistriesFixRequiredFields,
|
||||||
&correctPotentialCorruptOrgsUsersRelation,
|
&correctPotentialCorruptOrgsUsersRelation,
|
||||||
|
&gatedToRequireApproval,
|
||||||
}
|
}
|
||||||
|
|
||||||
var allBeans = []any{
|
var allBeans = []any{
|
||||||
|
|
|
@ -87,10 +87,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 containers",
|
"netrc_only_trusted": "Only inject netrc credentials into trusted containers",
|
||||||
"desc": "Only inject netrc credentials into trusted containers (recommended)."
|
"desc": "Only inject netrc credentials into trusted containers (recommended)."
|
||||||
|
@ -469,5 +465,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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 px-1">{{ $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 px-1">/</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')"
|
||||||
|
@ -53,6 +28,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')"
|
||||||
|
@ -71,6 +76,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')"
|
||||||
|
@ -114,7 +139,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();
|
||||||
|
@ -135,7 +160,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,
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue