diff --git a/cmd/server/openapi/docs.go b/cmd/server/openapi/docs.go
index 074fafbab..9c0092a23 100644
--- a/cmd/server/openapi/docs.go
+++ b/cmd/server/openapi/docs.go
@@ -5054,6 +5054,12 @@ const docTemplate = `{
"allow_pr": {
"type": "boolean"
},
+ "approval_allowed_users": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"avatar_url": {
"type": "string"
},
@@ -5135,6 +5141,12 @@ const docTemplate = `{
"allow_pr": {
"type": "boolean"
},
+ "approval_allowed_users": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"cancel_previous_pipeline_events": {
"type": "array",
"items": {
diff --git a/server/api/repo.go b/server/api/repo.go
index c9127c219..e9e49648a 100644
--- a/server/api/repo.go
+++ b/server/api/repo.go
@@ -259,6 +259,9 @@ func PatchRepo(c *gin.Context) {
return
}
}
+ if in.ApprovalAllowedUsers != nil {
+ repo.ApprovalAllowedUsers = *in.ApprovalAllowedUsers
+ }
if in.Timeout != nil {
repo.Timeout = *in.Timeout
}
diff --git a/server/model/repo.go b/server/model/repo.go
index e61646216..ce944afa1 100644
--- a/server/model/repo.go
+++ b/server/model/repo.go
@@ -63,6 +63,7 @@ type Repo struct {
IsSCMPrivate bool `json:"private" xorm:"private"`
Trusted TrustedConfiguration `json:"trusted" xorm:"json 'trusted'"`
RequireApproval ApprovalMode `json:"require_approval" xorm:"varchar(50) require_approval"`
+ ApprovalAllowedUsers []string `json:"approval_allowed_users" xorm:"json approval_allowed_users"`
IsActive bool `json:"active" xorm:"active"`
AllowPull bool `json:"allow_pr" xorm:"allow_pr"`
AllowDeploy bool `json:"allow_deploy" xorm:"allow_deploy"`
@@ -129,6 +130,7 @@ func (r *Repo) Update(from *Repo) {
type RepoPatch struct {
Config *string `json:"config_file,omitempty"`
RequireApproval *string `json:"require_approval,omitempty"`
+ ApprovalAllowedUsers *[]string `json:"approval_allowed_users,omitempty"`
Timeout *int64 `json:"timeout,omitempty"`
Visibility *string `json:"visibility,omitempty"`
AllowPull *bool `json:"allow_pr,omitempty"`
diff --git a/server/pipeline/gated.go b/server/pipeline/gated.go
index 4a4f3dc36..8804b8c2b 100644
--- a/server/pipeline/gated.go
+++ b/server/pipeline/gated.go
@@ -14,7 +14,11 @@
package pipeline
-import "go.woodpecker-ci.org/woodpecker/v3/server/model"
+import (
+ "slices"
+
+ "go.woodpecker-ci.org/woodpecker/v3/server/model"
+)
func setApprovalState(repo *model.Repo, pipeline *model.Pipeline) {
if !needsApproval(repo, pipeline) {
@@ -31,6 +35,12 @@ func needsApproval(repo *model.Repo, pipeline *model.Pipeline) bool {
return false
}
+ // skip if user is allowed
+ // It's enough to check the username as the repo matches the forge of the pipeline already (no username clashes from different forges possible)
+ if slices.Contains(repo.ApprovalAllowedUsers, pipeline.Author) {
+ return false
+ }
+
switch repo.RequireApproval {
// repository allows all events without approval
case model.RequireApprovalNone:
diff --git a/server/pipeline/gated_test.go b/server/pipeline/gated_test.go
index 76004ff3d..c429c9a03 100644
--- a/server/pipeline/gated_test.go
+++ b/server/pipeline/gated_test.go
@@ -69,6 +69,18 @@ func TestSetGatedState(t *testing.T) {
},
expectBlocked: true,
},
+ {
+ name: "require approval for everything with allowed user",
+ repo: &model.Repo{
+ RequireApproval: model.RequireApprovalAllEvents,
+ ApprovalAllowedUsers: []string{"user"},
+ },
+ pipeline: &model.Pipeline{
+ Event: model.EventPush,
+ Author: "user",
+ },
+ expectBlocked: false,
+ },
}
for _, tc := range testCases {
diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json
index d97f82b2c..a11287cad 100644
--- a/web/src/assets/locales/en.json
+++ b/web/src/assets/locales/en.json
@@ -509,7 +509,11 @@
"none_desc": "Every event triggers pipelines, including pull requests. This setting can be dangerous and is only recommended for private instances.",
"forks": "Pull request from forked repository",
"pull_requests": "All pull requests",
- "all_events": "All events from forge"
+ "all_events": "All events from forge",
+ "allowed_users": {
+ "allowed_users": "Allowed users",
+ "desc": "Pipelines created by the listed users never require approval."
+ }
},
"all_repositories": "All repositories",
"no_search_results": "No results found"
diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts
index 18698aaae..c2cb8d61b 100644
--- a/web/src/lib/api/types/repo.ts
+++ b/web/src/lib/api/types/repo.ts
@@ -73,6 +73,8 @@ export interface Repo {
require_approval: RepoRequireApproval;
+ approval_allowed_users: string[];
+
// Events that will cancel running pipelines before starting a new one
cancel_previous_pipeline_events: string[];
@@ -101,6 +103,7 @@ export type RepoSettings = Pick<
| 'visibility'
| 'trusted'
| 'require_approval'
+ | 'approval_allowed_users'
| 'allow_pr'
| 'allow_deploy'
| 'cancel_previous_pipeline_events'
diff --git a/web/src/views/repo/settings/General.vue b/web/src/views/repo/settings/General.vue
index b6edd8815..b42a8f040 100644
--- a/web/src/views/repo/settings/General.vue
+++ b/web/src/views/repo/settings/General.vue
@@ -88,6 +88,27 @@
+
+
+
+
+
+ {{ $t('require_approval.allowed_users.desc') }}
+
+
+
@@ -191,6 +212,7 @@ function loadRepoSettings() {
visibility: repo.value.visibility,
require_approval: repo.value.require_approval,
trusted: repo.value.trusted,
+ approval_allowed_users: repo.value.approval_allowed_users || [],
allow_pr: repo.value.allow_pr,
allow_deploy: repo.value.allow_deploy,
cancel_previous_pipeline_events: repo.value.cancel_previous_pipeline_events || [],
@@ -268,4 +290,20 @@ function removeImage(image: string) {
repoSettings.value.netrc_trusted = repoSettings.value.netrc_trusted.filter((i) => i !== image);
}
+
+const newUser = ref('');
+function addNewUser() {
+ if (!newUser.value) {
+ return;
+ }
+ repoSettings.value?.approval_allowed_users.push(newUser.value);
+ newUser.value = '';
+}
+function removeUser(user: string) {
+ if (!repoSettings.value) {
+ throw new Error('Unexpected: repoSettings should be set');
+ }
+
+ repoSettings.value.approval_allowed_users = repoSettings.value.approval_allowed_users.filter((i) => i !== user);
+}