feat: Add bypass allowlist for branch protection (#36514)

- Introduce a “Bypass Protection Allowlist” on branch rules
(users/teams) alongside admins, with BlockAdminMergeOverride
  still respected.
- Surface the allowlist in API (create/edit options, structs) and
settings UI; merge box now shows the red button +
  message for bypass-capable users.
- Apply bypass logic to merge checks and pre-receive so allowlisted
users can override unmet approvals/status checks/
  protected files when force-merging.
- Add migration for new columns, locale strings, and unit tests (bypass
helper; queue test tweak).

<img width="1069" height="218" alt="image"
src="https://github.com/user-attachments/assets/0b61bc2a-a27f-47f3-a923-613688008e65"
/>


Fixes #36476

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Codex GPT-5.3 <codex@openai.com>
Co-authored-by: GPT-5.2 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
Nicolas
2026-05-16 16:23:42 +02:00
committed by GitHub
parent 54ff68b0a9
commit eb93981d45
23 changed files with 572 additions and 40 deletions
+41
View File
@@ -43,6 +43,9 @@ type ProtectedBranch struct {
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false"`
BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
@@ -204,6 +207,29 @@ func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch,
return in
}
// CanBypassBranchProtection reports whether the user can bypass branch protection checks (status checks, approvals, protected files)
// Either a repo admin (when not blocked) or a user/team on the bypass allowlist can bypass.
func CanBypassBranchProtection(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User, isRepoAdmin bool) bool {
if isRepoAdmin && !protectBranch.BlockAdminMergeOverride {
return true
}
if !protectBranch.EnableBypassAllowlist {
return false
}
if slices.Contains(protectBranch.BypassAllowlistUserIDs, user.ID) {
return true
}
if len(protectBranch.BypassAllowlistTeamIDs) == 0 {
return false
}
in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.BypassAllowlistTeamIDs)
if err != nil {
log.Error("IsUserInTeams failed: userID=%d, repoID=%d, allowlistTeamIDs=%v, err=%v", user.ID, protectBranch.RepoID, protectBranch.BypassAllowlistTeamIDs, err)
return false
}
return in
}
// IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals)
func IsUserOfficialReviewer(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User) (bool, error) {
repo, err := repo_model.GetRepositoryByID(ctx, protectBranch.RepoID)
@@ -347,6 +373,9 @@ type WhitelistOptions struct {
ApprovalsUserIDs []int64
ApprovalsTeamIDs []int64
BypassUserIDs []int64
BypassTeamIDs []int64
}
// UpdateProtectBranch saves branch protection options of repository.
@@ -387,6 +416,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
}
protectBranch.ApprovalsWhitelistUserIDs = whitelist
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.BypassAllowlistUserIDs, opts.BypassUserIDs)
if err != nil {
return err
}
protectBranch.BypassAllowlistUserIDs = whitelist
// if the repo is in an organization
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
if err != nil {
@@ -412,6 +447,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.BypassAllowlistTeamIDs, opts.BypassTeamIDs)
if err != nil {
return err
}
protectBranch.BypassAllowlistTeamIDs = whitelist
// Looks like it's a new rule
if protectBranch.ID == 0 {
// as it's a new rule and if priority was not set, we need to calc it.