Skip to content

Commit 42c2a60

Browse files
authored
feat(branch_protection): Add support for force push bypassers (#1529)
* feat(branch_protection): Add support for force push bypassers Fixes #1085. * Fixes from @pkrzaczkowski-hippo #1529 (comment)
1 parent c5c8a15 commit 42c2a60

5 files changed

+232
-6
lines changed

github/resource_github_branch_protection.go

+32-6
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ func resourceGithubBranchProtection() *schema.Resource {
151151
Description: "The list of actor Names/IDs that may push to the branch. Actor names must either begin with a '/' for users or the organization name followed by a '/' for teams.",
152152
Elem: &schema.Schema{Type: schema.TypeString},
153153
},
154+
PROTECTION_FORCE_PUSHES_BYPASSERS: {
155+
Type: schema.TypeSet,
156+
Optional: true,
157+
Description: "The list of actor Names/IDs that are allowed to bypass force push restrictions. Actor names must either begin with a '/' for users or the organization name followed by a '/' for teams.",
158+
Elem: &schema.Schema{Type: schema.TypeString},
159+
},
154160
},
155161

156162
Create: resourceGithubBranchProtectionCreate,
@@ -185,7 +191,7 @@ func resourceGithubBranchProtectionCreate(d *schema.ResourceData, meta interface
185191
return err
186192
}
187193

188-
var reviewIds, pushIds, bypassIds []string
194+
var reviewIds, pushIds, bypassForcePushIds, bypassPullRequestIds []string
189195
reviewIds, err = getActorIds(data.ReviewDismissalActorIDs, meta)
190196
if err != nil {
191197
return err
@@ -196,19 +202,26 @@ func resourceGithubBranchProtectionCreate(d *schema.ResourceData, meta interface
196202
return err
197203
}
198204

199-
bypassIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
205+
bypassForcePushIds, err = getActorIds(data.BypassForcePushActorIDs, meta)
206+
if err != nil {
207+
return err
208+
}
209+
210+
bypassPullRequestIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
200211
if err != nil {
201212
return err
202213
}
203214

204215
data.PushActorIDs = pushIds
205216
data.ReviewDismissalActorIDs = reviewIds
206-
data.BypassPullRequestActorIDs = bypassIds
217+
data.BypassForcePushActorIDs = bypassForcePushIds
218+
data.BypassPullRequestActorIDs = bypassPullRequestIds
207219

208220
input := githubv4.CreateBranchProtectionRuleInput{
209221
AllowsDeletions: githubv4.NewBoolean(githubv4.Boolean(data.AllowsDeletions)),
210222
AllowsForcePushes: githubv4.NewBoolean(githubv4.Boolean(data.AllowsForcePushes)),
211223
BlocksCreations: githubv4.NewBoolean(githubv4.Boolean(data.BlocksCreations)),
224+
BypassForcePushActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassForcePushActorIDs)),
212225
BypassPullRequestActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassPullRequestActorIDs)),
213226
DismissesStaleReviews: githubv4.NewBoolean(githubv4.Boolean(data.DismissesStaleReviews)),
214227
IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(data.IsAdminEnforced)),
@@ -331,6 +344,12 @@ func resourceGithubBranchProtectionRead(d *schema.ResourceData, meta interface{}
331344
log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_RESTRICTS_PUSHES, protection.Repository.Name, protection.Pattern, d.Id())
332345
}
333346

347+
forcePushBypassers := setForcePushBypassers(protection, data, meta)
348+
err = d.Set(PROTECTION_FORCE_PUSHES_BYPASSERS, forcePushBypassers)
349+
if err != nil {
350+
log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_FORCE_PUSHES_BYPASSERS, protection.Repository.Name, protection.Pattern, d.Id())
351+
}
352+
334353
err = d.Set(PROTECTION_REQUIRES_LAST_PUSH_APPROVAL, protection.RequireLastPushApproval)
335354
if err != nil {
336355
log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_REQUIRES_LAST_PUSH_APPROVAL, protection.Repository.Name, protection.Pattern, d.Id())
@@ -357,7 +376,7 @@ func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta interface
357376
return err
358377
}
359378

360-
var reviewIds, pushIds, bypassIds []string
379+
var reviewIds, pushIds, bypassForcePushIds, bypassPullRequestIds []string
361380
reviewIds, err = getActorIds(data.ReviewDismissalActorIDs, meta)
362381
if err != nil {
363382
return err
@@ -368,20 +387,27 @@ func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta interface
368387
return err
369388
}
370389

371-
bypassIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
390+
bypassForcePushIds, err = getActorIds(data.BypassForcePushActorIDs, meta)
391+
if err != nil {
392+
return err
393+
}
394+
395+
bypassPullRequestIds, err = getActorIds(data.BypassPullRequestActorIDs, meta)
372396
if err != nil {
373397
return err
374398
}
375399

376400
data.PushActorIDs = pushIds
377401
data.ReviewDismissalActorIDs = reviewIds
378-
data.BypassPullRequestActorIDs = bypassIds
402+
data.BypassForcePushActorIDs = bypassForcePushIds
403+
data.BypassPullRequestActorIDs = bypassPullRequestIds
379404

380405
input := githubv4.UpdateBranchProtectionRuleInput{
381406
BranchProtectionRuleID: d.Id(),
382407
AllowsDeletions: githubv4.NewBoolean(githubv4.Boolean(data.AllowsDeletions)),
383408
AllowsForcePushes: githubv4.NewBoolean(githubv4.Boolean(data.AllowsForcePushes)),
384409
BlocksCreations: githubv4.NewBoolean(githubv4.Boolean(data.BlocksCreations)),
410+
BypassForcePushActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassForcePushActorIDs)),
385411
BypassPullRequestActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassPullRequestActorIDs)),
386412
DismissesStaleReviews: githubv4.NewBoolean(githubv4.Boolean(data.DismissesStaleReviews)),
387413
IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(data.IsAdminEnforced)),

github/resource_github_branch_protection_test.go

+112
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,118 @@ func TestAccGithubBranchProtection(t *testing.T) {
556556

557557
})
558558

559+
t.Run("configures non-empty list of force push bypassers", func(t *testing.T) {
560+
561+
config := fmt.Sprintf(`
562+
563+
resource "github_repository" "test" {
564+
name = "tf-acc-test-%s"
565+
auto_init = true
566+
}
567+
568+
data "github_user" "test" {
569+
username = "%s"
570+
}
571+
572+
resource "github_branch_protection" "test" {
573+
574+
repository_id = github_repository.test.node_id
575+
pattern = "main"
576+
577+
force_push_bypassers = [
578+
data.github_user.test.node_id
579+
]
580+
581+
}
582+
583+
`, randomID, testOwnerFunc())
584+
585+
check := resource.ComposeAggregateTestCheckFunc(
586+
resource.TestCheckResourceAttr(
587+
"github_branch_protection.test", "force_push_bypassers.#", "1",
588+
),
589+
)
590+
591+
testCase := func(t *testing.T, mode string) {
592+
resource.Test(t, resource.TestCase{
593+
PreCheck: func() { skipUnlessMode(t, mode) },
594+
Providers: testAccProviders,
595+
Steps: []resource.TestStep{
596+
{
597+
Config: config,
598+
Check: check,
599+
},
600+
},
601+
})
602+
}
603+
604+
t.Run("with an anonymous account", func(t *testing.T) {
605+
t.Skip("anonymous account not supported for this operation")
606+
})
607+
608+
t.Run("with an individual account", func(t *testing.T) {
609+
testCase(t, individual)
610+
})
611+
612+
t.Run("with an organization account", func(t *testing.T) {
613+
testCase(t, organization)
614+
})
615+
616+
})
617+
618+
t.Run("configures empty list of force push bypassers", func(t *testing.T) {
619+
620+
config := fmt.Sprintf(`
621+
622+
resource "github_repository" "test" {
623+
name = "tf-acc-test-%s"
624+
auto_init = true
625+
}
626+
627+
resource "github_branch_protection" "test" {
628+
629+
repository_id = github_repository.test.node_id
630+
pattern = "main"
631+
632+
force_push_bypassers = []
633+
634+
}
635+
636+
`, randomID)
637+
638+
check := resource.ComposeAggregateTestCheckFunc(
639+
resource.TestCheckResourceAttr(
640+
"github_branch_protection.test", "force_push_bypassers.#", "0",
641+
),
642+
)
643+
644+
testCase := func(t *testing.T, mode string) {
645+
resource.Test(t, resource.TestCase{
646+
PreCheck: func() { skipUnlessMode(t, mode) },
647+
Providers: testAccProviders,
648+
Steps: []resource.TestStep{
649+
{
650+
Config: config,
651+
Check: check,
652+
},
653+
},
654+
})
655+
}
656+
657+
t.Run("with an anonymous account", func(t *testing.T) {
658+
t.Skip("anonymous account not supported for this operation")
659+
})
660+
661+
t.Run("with an individual account", func(t *testing.T) {
662+
testCase(t, individual)
663+
})
664+
665+
t.Run("with an organization account", func(t *testing.T) {
666+
testCase(t, organization)
667+
})
668+
669+
})
670+
559671
t.Run("configures non-empty list of pull request bypassers", func(t *testing.T) {
560672

561673
config := fmt.Sprintf(`

github/util_v4_branch_protection.go

+78
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ type DismissalActorTypes struct {
2929
}
3030
}
3131

32+
type BypassForcePushActorTypes struct {
33+
Actor struct {
34+
App Actor `graphql:"... on App"`
35+
Team Actor `graphql:"... on Team"`
36+
User ActorUser `graphql:"... on User"`
37+
}
38+
}
39+
3240
type BypassPullRequestActorTypes struct {
3341
Actor struct {
3442
App Actor `graphql:"... on App"`
@@ -56,6 +64,9 @@ type BranchProtectionRule struct {
5664
ReviewDismissalAllowances struct {
5765
Nodes []DismissalActorTypes
5866
} `graphql:"reviewDismissalAllowances(first: 100)"`
67+
BypassForcePushAllowances struct {
68+
Nodes []BypassForcePushActorTypes
69+
} `graphql:"bypassForcePushAllowances(first: 100)"`
5970
BypassPullRequestAllowances struct {
6071
Nodes []BypassPullRequestActorTypes
6172
} `graphql:"bypassPullRequestAllowances(first: 100)"`
@@ -86,6 +97,7 @@ type BranchProtectionResourceData struct {
8697
AllowsForcePushes bool
8798
BlocksCreations bool
8899
BranchProtectionRuleID string
100+
BypassForcePushActorIDs []string
89101
BypassPullRequestActorIDs []string
90102
DismissesStaleReviews bool
91103
IsAdminEnforced bool
@@ -237,6 +249,18 @@ func branchProtectionResourceData(d *schema.ResourceData, meta interface{}) (Bra
237249
}
238250
}
239251

252+
if v, ok := d.GetOk(PROTECTION_FORCE_PUSHES_BYPASSERS); ok {
253+
bypassForcePushActorIDs := make([]string, 0)
254+
vL := v.(*schema.Set).List()
255+
for _, v := range vL {
256+
bypassForcePushActorIDs = append(bypassForcePushActorIDs, v.(string))
257+
}
258+
if len(bypassForcePushActorIDs) > 0 {
259+
data.BypassForcePushActorIDs = bypassForcePushActorIDs
260+
data.AllowsForcePushes = false
261+
}
262+
}
263+
240264
if v, ok := d.GetOk(PROTECTION_LOCK_BRANCH); ok {
241265
data.LockBranch = v.(bool)
242266
}
@@ -293,6 +317,19 @@ func branchProtectionResourceDataActors(d *schema.ResourceData, meta interface{}
293317
data.RestrictsPushes = true
294318
}
295319
}
320+
321+
if v, ok := d.GetOk(PROTECTION_FORCE_PUSHES_BYPASSERS); ok {
322+
bypassForcePushActorIDs := make([]string, 0)
323+
vL := v.(*schema.Set).List()
324+
for _, v := range vL {
325+
bypassForcePushActorIDs = append(bypassForcePushActorIDs, v.(string))
326+
}
327+
if len(bypassForcePushActorIDs) > 0 {
328+
data.BypassForcePushActorIDs = bypassForcePushActorIDs
329+
data.AllowsForcePushes = false
330+
}
331+
}
332+
296333
return data, nil
297334
}
298335

@@ -322,6 +359,37 @@ func setDismissalActorIDs(actors []DismissalActorTypes, data BranchProtectionRes
322359
return dismissalActors
323360
}
324361

362+
func setBypassForcePushActorIDs(actors []BypassForcePushActorTypes, data BranchProtectionResourceData, meta interface{}) []string {
363+
bypassActors := make([]string, 0, len(actors))
364+
365+
orgName := meta.(*Owner).name
366+
367+
for _, a := range actors {
368+
IsID := false
369+
for _, v := range data.BypassForcePushActorIDs {
370+
if (a.Actor.Team.ID != nil && a.Actor.Team.ID.(string) == v) || (a.Actor.User.ID != nil && a.Actor.User.ID.(string) == v) || (a.Actor.App.ID != nil && a.Actor.App.ID.(string) == v) {
371+
bypassActors = append(bypassActors, v)
372+
IsID = true
373+
break
374+
}
375+
}
376+
if !IsID {
377+
if a.Actor.Team.Slug != "" {
378+
bypassActors = append(bypassActors, orgName+"/"+string(a.Actor.Team.Slug))
379+
continue
380+
}
381+
if a.Actor.User.Login != "" {
382+
bypassActors = append(bypassActors, "/"+string(a.Actor.User.Login))
383+
continue
384+
}
385+
if a.Actor.App != (Actor{}) {
386+
bypassActors = append(bypassActors, a.Actor.App.ID.(string))
387+
}
388+
}
389+
}
390+
return bypassActors
391+
}
392+
325393
func setBypassPullRequestActorIDs(actors []BypassPullRequestActorTypes, data BranchProtectionResourceData, meta interface{}) []string {
326394
bypassActors := make([]string, 0, len(actors))
327395

@@ -434,6 +502,16 @@ func setPushes(protection BranchProtectionRule, data BranchProtectionResourceDat
434502
return pushActors
435503
}
436504

505+
func setForcePushBypassers(protection BranchProtectionRule, data BranchProtectionResourceData, meta interface{}) []string {
506+
if protection.AllowsForcePushes {
507+
return nil
508+
}
509+
bypassForcePushAllowances := protection.BypassForcePushAllowances.Nodes
510+
bypassForcePushActors := setBypassForcePushActorIDs(bypassForcePushAllowances, data, meta)
511+
512+
return bypassForcePushActors
513+
}
514+
437515
func getBranchProtectionID(repoID githubv4.ID, pattern string, meta interface{}) (githubv4.ID, error) {
438516
var query struct {
439517
Node struct {

github/util_v4_consts.go

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
PROTECTION_RESTRICTS_PUSHES = "push_restrictions"
2020
PROTECTION_RESTRICTS_REVIEW_DISMISSALS = "restrict_dismissals"
2121
PROTECTION_RESTRICTS_REVIEW_DISMISSERS = "dismissal_restrictions"
22+
PROTECTION_FORCE_PUSHES_BYPASSERS = "force_push_bypassers"
2223
PROTECTION_PULL_REQUESTS_BYPASSERS = "pull_request_bypassers"
2324
PROTECTION_LOCK_BRANCH = "lock_branch"
2425
PROTECTION_REQUIRES_LAST_PUSH_APPROVAL = "require_last_push_approval"

website/docs/r/branch_protection.html.markdown

+9
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ resource "github_branch_protection" "example" {
5151
# github_team.example.node_id
5252
]
5353
54+
force_push_bypassers = [
55+
data.github_user.example.node_id,
56+
"/exampleuser",
57+
"exampleorganization/exampleteam",
58+
# limited to a list of one type of restriction (user, team, app)
59+
# github_team.example.node_id
60+
]
61+
5462
}
5563
5664
resource "github_repository" "example" {
@@ -85,6 +93,7 @@ The following arguments are supported:
8593
* `required_status_checks` - (Optional) Enforce restrictions for required status checks. See [Required Status Checks](#required-status-checks) below for details.
8694
* `required_pull_request_reviews` - (Optional) Enforce restrictions for pull request reviews. See [Required Pull Request Reviews](#required-pull-request-reviews) below for details.
8795
* `push_restrictions` - (Optional) The list of actor Names/IDs that may push to the branch. Actor names must either begin with a "/" for users or the organization name followed by a "/" for teams.
96+
* `force_push_bypassers` - (Optional) The list of actor Names/IDs that are allowed to bypass force push restrictions. Actor names must either begin with a "/" for users or the organization name followed by a "/" for teams.
8897
* `allows_deletions` - (Optional) Boolean, setting this to `true` to allow the branch to be deleted.
8998
* `allows_force_pushes` - (Optional) Boolean, setting this to `true` to allow force pushes on the branch.
9099
* `blocks_creations` - (Optional) Boolean, setting this to `true` to block creating the branch.

0 commit comments

Comments
 (0)