diff --git a/github/resource_github_branch_protection.go b/github/resource_github_branch_protection.go index 8de96413fc..1470462530 100644 --- a/github/resource_github_branch_protection.go +++ b/github/resource_github_branch_protection.go @@ -151,6 +151,12 @@ func resourceGithubBranchProtection() *schema.Resource { 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.", Elem: &schema.Schema{Type: schema.TypeString}, }, + PROTECTION_FORCE_PUSHES_BYPASSERS: { + Type: schema.TypeSet, + Optional: true, + 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.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, Create: resourceGithubBranchProtectionCreate, @@ -185,7 +191,7 @@ func resourceGithubBranchProtectionCreate(d *schema.ResourceData, meta interface return err } - var reviewIds, pushIds, bypassIds []string + var reviewIds, pushIds, bypassForcePushIds, bypassPullRequestIds []string reviewIds, err = getActorIds(data.ReviewDismissalActorIDs, meta) if err != nil { return err @@ -196,19 +202,26 @@ func resourceGithubBranchProtectionCreate(d *schema.ResourceData, meta interface return err } - bypassIds, err = getActorIds(data.BypassPullRequestActorIDs, meta) + bypassForcePushIds, err = getActorIds(data.BypassForcePushActorIDs, meta) + if err != nil { + return err + } + + bypassPullRequestIds, err = getActorIds(data.BypassPullRequestActorIDs, meta) if err != nil { return err } data.PushActorIDs = pushIds data.ReviewDismissalActorIDs = reviewIds - data.BypassPullRequestActorIDs = bypassIds + data.BypassForcePushActorIDs = bypassForcePushIds + data.BypassPullRequestActorIDs = bypassPullRequestIds input := githubv4.CreateBranchProtectionRuleInput{ AllowsDeletions: githubv4.NewBoolean(githubv4.Boolean(data.AllowsDeletions)), AllowsForcePushes: githubv4.NewBoolean(githubv4.Boolean(data.AllowsForcePushes)), BlocksCreations: githubv4.NewBoolean(githubv4.Boolean(data.BlocksCreations)), + BypassForcePushActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassForcePushActorIDs)), BypassPullRequestActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassPullRequestActorIDs)), DismissesStaleReviews: githubv4.NewBoolean(githubv4.Boolean(data.DismissesStaleReviews)), IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(data.IsAdminEnforced)), @@ -331,6 +344,12 @@ func resourceGithubBranchProtectionRead(d *schema.ResourceData, meta interface{} log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_RESTRICTS_PUSHES, protection.Repository.Name, protection.Pattern, d.Id()) } + forcePushBypassers := setForcePushBypassers(protection, data, meta) + err = d.Set(PROTECTION_FORCE_PUSHES_BYPASSERS, forcePushBypassers) + if err != nil { + log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_FORCE_PUSHES_BYPASSERS, protection.Repository.Name, protection.Pattern, d.Id()) + } + err = d.Set(PROTECTION_REQUIRES_LAST_PUSH_APPROVAL, protection.RequireLastPushApproval) if err != nil { 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 return err } - var reviewIds, pushIds, bypassIds []string + var reviewIds, pushIds, bypassForcePushIds, bypassPullRequestIds []string reviewIds, err = getActorIds(data.ReviewDismissalActorIDs, meta) if err != nil { return err @@ -368,20 +387,27 @@ func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta interface return err } - bypassIds, err = getActorIds(data.BypassPullRequestActorIDs, meta) + bypassForcePushIds, err = getActorIds(data.BypassForcePushActorIDs, meta) + if err != nil { + return err + } + + bypassPullRequestIds, err = getActorIds(data.BypassPullRequestActorIDs, meta) if err != nil { return err } data.PushActorIDs = pushIds data.ReviewDismissalActorIDs = reviewIds - data.BypassPullRequestActorIDs = bypassIds + data.BypassForcePushActorIDs = bypassForcePushIds + data.BypassPullRequestActorIDs = bypassPullRequestIds input := githubv4.UpdateBranchProtectionRuleInput{ BranchProtectionRuleID: d.Id(), AllowsDeletions: githubv4.NewBoolean(githubv4.Boolean(data.AllowsDeletions)), AllowsForcePushes: githubv4.NewBoolean(githubv4.Boolean(data.AllowsForcePushes)), BlocksCreations: githubv4.NewBoolean(githubv4.Boolean(data.BlocksCreations)), + BypassForcePushActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassForcePushActorIDs)), BypassPullRequestActorIDs: githubv4NewIDSlice(githubv4IDSliceEmpty(data.BypassPullRequestActorIDs)), DismissesStaleReviews: githubv4.NewBoolean(githubv4.Boolean(data.DismissesStaleReviews)), IsAdminEnforced: githubv4.NewBoolean(githubv4.Boolean(data.IsAdminEnforced)), diff --git a/github/resource_github_branch_protection_test.go b/github/resource_github_branch_protection_test.go index f787674b94..7d883babcc 100644 --- a/github/resource_github_branch_protection_test.go +++ b/github/resource_github_branch_protection_test.go @@ -556,6 +556,118 @@ func TestAccGithubBranchProtection(t *testing.T) { }) + t.Run("configures non-empty list of force push bypassers", func(t *testing.T) { + + config := fmt.Sprintf(` + + resource "github_repository" "test" { + name = "tf-acc-test-%s" + auto_init = true + } + + data "github_user" "test" { + username = "%s" + } + + resource "github_branch_protection" "test" { + + repository_id = github_repository.test.node_id + pattern = "main" + + force_push_bypassers = [ + data.github_user.test.node_id + ] + + } + + `, randomID, testOwnerFunc()) + + check := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "github_branch_protection.test", "force_push_bypassers.#", "1", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + }) + + t.Run("configures empty list of force push bypassers", func(t *testing.T) { + + config := fmt.Sprintf(` + + resource "github_repository" "test" { + name = "tf-acc-test-%s" + auto_init = true + } + + resource "github_branch_protection" "test" { + + repository_id = github_repository.test.node_id + pattern = "main" + + force_push_bypassers = [] + + } + + `, randomID) + + check := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "github_branch_protection.test", "force_push_bypassers.#", "0", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + }) + t.Run("configures non-empty list of pull request bypassers", func(t *testing.T) { config := fmt.Sprintf(` diff --git a/github/util_v4_branch_protection.go b/github/util_v4_branch_protection.go index 158e9260d4..79aff4d823 100644 --- a/github/util_v4_branch_protection.go +++ b/github/util_v4_branch_protection.go @@ -29,6 +29,14 @@ type DismissalActorTypes struct { } } +type BypassForcePushActorTypes struct { + Actor struct { + App Actor `graphql:"... on App"` + Team Actor `graphql:"... on Team"` + User ActorUser `graphql:"... on User"` + } +} + type BypassPullRequestActorTypes struct { Actor struct { App Actor `graphql:"... on App"` @@ -56,6 +64,9 @@ type BranchProtectionRule struct { ReviewDismissalAllowances struct { Nodes []DismissalActorTypes } `graphql:"reviewDismissalAllowances(first: 100)"` + BypassForcePushAllowances struct { + Nodes []BypassForcePushActorTypes + } `graphql:"bypassForcePushAllowances(first: 100)"` BypassPullRequestAllowances struct { Nodes []BypassPullRequestActorTypes } `graphql:"bypassPullRequestAllowances(first: 100)"` @@ -86,6 +97,7 @@ type BranchProtectionResourceData struct { AllowsForcePushes bool BlocksCreations bool BranchProtectionRuleID string + BypassForcePushActorIDs []string BypassPullRequestActorIDs []string DismissesStaleReviews bool IsAdminEnforced bool @@ -237,6 +249,18 @@ func branchProtectionResourceData(d *schema.ResourceData, meta interface{}) (Bra } } + if v, ok := d.GetOk(PROTECTION_FORCE_PUSHES_BYPASSERS); ok { + bypassForcePushActorIDs := make([]string, 0) + vL := v.(*schema.Set).List() + for _, v := range vL { + bypassForcePushActorIDs = append(bypassForcePushActorIDs, v.(string)) + } + if len(bypassForcePushActorIDs) > 0 { + data.BypassForcePushActorIDs = bypassForcePushActorIDs + data.AllowsForcePushes = false + } + } + if v, ok := d.GetOk(PROTECTION_LOCK_BRANCH); ok { data.LockBranch = v.(bool) } @@ -293,6 +317,19 @@ func branchProtectionResourceDataActors(d *schema.ResourceData, meta interface{} data.RestrictsPushes = true } } + + if v, ok := d.GetOk(PROTECTION_FORCE_PUSHES_BYPASSERS); ok { + bypassForcePushActorIDs := make([]string, 0) + vL := v.(*schema.Set).List() + for _, v := range vL { + bypassForcePushActorIDs = append(bypassForcePushActorIDs, v.(string)) + } + if len(bypassForcePushActorIDs) > 0 { + data.BypassForcePushActorIDs = bypassForcePushActorIDs + data.AllowsForcePushes = false + } + } + return data, nil } @@ -322,6 +359,37 @@ func setDismissalActorIDs(actors []DismissalActorTypes, data BranchProtectionRes return dismissalActors } +func setBypassForcePushActorIDs(actors []BypassForcePushActorTypes, data BranchProtectionResourceData, meta interface{}) []string { + bypassActors := make([]string, 0, len(actors)) + + orgName := meta.(*Owner).name + + for _, a := range actors { + IsID := false + for _, v := range data.BypassForcePushActorIDs { + 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) { + bypassActors = append(bypassActors, v) + IsID = true + break + } + } + if !IsID { + if a.Actor.Team.Slug != "" { + bypassActors = append(bypassActors, orgName+"/"+string(a.Actor.Team.Slug)) + continue + } + if a.Actor.User.Login != "" { + bypassActors = append(bypassActors, "/"+string(a.Actor.User.Login)) + continue + } + if a.Actor.App != (Actor{}) { + bypassActors = append(bypassActors, a.Actor.App.ID.(string)) + } + } + } + return bypassActors +} + func setBypassPullRequestActorIDs(actors []BypassPullRequestActorTypes, data BranchProtectionResourceData, meta interface{}) []string { bypassActors := make([]string, 0, len(actors)) @@ -434,6 +502,16 @@ func setPushes(protection BranchProtectionRule, data BranchProtectionResourceDat return pushActors } +func setForcePushBypassers(protection BranchProtectionRule, data BranchProtectionResourceData, meta interface{}) []string { + if protection.AllowsForcePushes { + return nil + } + bypassForcePushAllowances := protection.BypassForcePushAllowances.Nodes + bypassForcePushActors := setBypassForcePushActorIDs(bypassForcePushAllowances, data, meta) + + return bypassForcePushActors +} + func getBranchProtectionID(repoID githubv4.ID, pattern string, meta interface{}) (githubv4.ID, error) { var query struct { Node struct { diff --git a/github/util_v4_consts.go b/github/util_v4_consts.go index 74540f9ab0..0d073de51d 100644 --- a/github/util_v4_consts.go +++ b/github/util_v4_consts.go @@ -19,6 +19,7 @@ const ( PROTECTION_RESTRICTS_PUSHES = "push_restrictions" PROTECTION_RESTRICTS_REVIEW_DISMISSALS = "restrict_dismissals" PROTECTION_RESTRICTS_REVIEW_DISMISSERS = "dismissal_restrictions" + PROTECTION_FORCE_PUSHES_BYPASSERS = "force_push_bypassers" PROTECTION_PULL_REQUESTS_BYPASSERS = "pull_request_bypassers" PROTECTION_LOCK_BRANCH = "lock_branch" PROTECTION_REQUIRES_LAST_PUSH_APPROVAL = "require_last_push_approval" diff --git a/website/docs/r/branch_protection.html.markdown b/website/docs/r/branch_protection.html.markdown index 839cbafd24..9ee41c62ea 100644 --- a/website/docs/r/branch_protection.html.markdown +++ b/website/docs/r/branch_protection.html.markdown @@ -51,6 +51,14 @@ resource "github_branch_protection" "example" { # github_team.example.node_id ] + force_push_bypassers = [ + data.github_user.example.node_id, + "/exampleuser", + "exampleorganization/exampleteam", + # limited to a list of one type of restriction (user, team, app) + # github_team.example.node_id + ] + } resource "github_repository" "example" { @@ -85,6 +93,7 @@ The following arguments are supported: * `required_status_checks` - (Optional) Enforce restrictions for required status checks. See [Required Status Checks](#required-status-checks) below for details. * `required_pull_request_reviews` - (Optional) Enforce restrictions for pull request reviews. See [Required Pull Request Reviews](#required-pull-request-reviews) below for details. * `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. +* `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. * `allows_deletions` - (Optional) Boolean, setting this to `true` to allow the branch to be deleted. * `allows_force_pushes` - (Optional) Boolean, setting this to `true` to allow force pushes on the branch. * `blocks_creations` - (Optional) Boolean, setting this to `true` to block creating the branch.