Skip to content

Commit

Permalink
feat(github_branch_protection_v3): Add support for bypass_pull_reques…
Browse files Browse the repository at this point in the history
…t_allowances (#1578)

* feat(github_branch_protection_v3): Add support for bypass_pull_request_allowances

* fix: Get bpra

* chore: Remove debug

* chore: Remove debug

* fix: Lint

* fix: Test

* Test rename for convention

---------

Co-authored-by: Nick Floyd <[email protected]>
Co-authored-by: Keegan Campbell <[email protected]>
  • Loading branch information
3 people authored Apr 3, 2023
1 parent 0546397 commit 70a0a5a
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 10 deletions.
24 changes: 24 additions & 0 deletions github/resource_github_branch_protection_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ func resourceGithubBranchProtectionV3() *schema.Resource {
Description: "Require 'x' number of approvals to satisfy branch protection requirements. If this is specified it must be a number between 0-6.",
ValidateFunc: validation.IntBetween(0, 6),
},
"bypass_pull_request_allowances": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"users": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"teams": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"apps": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
},
},
},
Expand Down
86 changes: 80 additions & 6 deletions github/resource_github_branch_protection_v3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,10 @@ func TestAccGithubBranchProtectionV3_required_status_checks(t *testing.T) {
})
}
func TestAccGithubBranchProtectionV3_required_pull_request_reviews(t *testing.T) {

randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

t.Run("configures required pull request reviews", func(t *testing.T) {

config := fmt.Sprintf(`
resource "github_repository" "test" {
Expand All @@ -228,8 +228,8 @@ func TestAccGithubBranchProtectionV3_required_pull_request_reviews(t *testing.T)
branch = "main"
required_pull_request_reviews {
dismiss_stale_reviews = true
require_code_owner_reviews = true
dismiss_stale_reviews = true
require_code_owner_reviews = true
}
}
Expand Down Expand Up @@ -279,6 +279,80 @@ func TestAccGithubBranchProtectionV3_required_pull_request_reviews(t *testing.T)
})
}

func TestAccGithubBranchProtectionV3RequiredPullRequestReviewsBypassAllowances(t *testing.T) {

randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

t.Run("configures required pull request reviews with bypass allowances", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-%s"
auto_init = true
}
resource "github_team" "test" {
name = "tf-acc-test-%[1]s"
}
resource "github_team_repository" "test" {
team_id = github_team.test.id
repository = github_repository.test.name
permission = "admin"
}
resource "github_branch_protection_v3" "test" {
repository = github_repository.test.name
branch = "main"
required_pull_request_reviews {
bypass_pull_request_allowances {
teams = [github_team.test.slug]
}
}
depends_on = [github_team_repository.test]
}
`, randomID)

check := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(
"github_branch_protection_v3.test", "required_pull_request_reviews.#", "1",
),
resource.TestCheckResourceAttr(
"github_branch_protection_v3.test", "required_pull_request_reviews.0.bypass_pull_request_allowances.#", "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) {
t.Skip("individual account not supported for this operation")
})

t.Run("with an organization account", func(t *testing.T) {
testCase(t, organization)
})

})
}

func TestAccGithubBranchProtectionV3_branch_push_restrictions(t *testing.T) {

randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
Expand All @@ -294,7 +368,7 @@ func TestAccGithubBranchProtectionV3_branch_push_restrictions(t *testing.T) {
resource "github_team" "test" {
name = "tf-acc-test-%[1]s"
}
resource "github_team_repository" "test" {
team_id = "${github_team.test.id}"
repository = "${github_repository.test.name}"
Expand All @@ -307,9 +381,9 @@ func TestAccGithubBranchProtectionV3_branch_push_restrictions(t *testing.T) {
branch = "main"
restrictions {
teams = ["${github_team.test.slug}"]
teams = ["${github_team.test.slug}"]
}
}
`, randomID)

Expand Down
78 changes: 76 additions & 2 deletions github/resource_github_branch_protection_v3_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"github.com/google/go-github/v50/github"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"log"
"strconv"
"strings"

"github.com/google/go-github/v50/github"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func buildProtectionRequest(d *schema.ResourceData) (*github.ProtectionRequest, error) {
Expand Down Expand Up @@ -122,6 +123,40 @@ func requireSignedCommitsUpdate(d *schema.ResourceData, meta interface{}) (err e
return err
}

func flattenBypassPullRequestAllowances(bpra *github.BypassPullRequestAllowances) []interface{} {
if bpra == nil {
return nil
}
users := make([]interface{}, 0, len(bpra.Users))
for _, u := range bpra.Users {
if u.Login != nil {
users = append(users, *u.Login)
}
}

teams := make([]interface{}, 0, len(bpra.Teams))
for _, t := range bpra.Teams {
if t.Slug != nil {
teams = append(teams, *t.Slug)
}
}

apps := make([]interface{}, 0, len(bpra.Apps))
for _, t := range bpra.Apps {
if t.Slug != nil {
apps = append(apps, *t.Slug)
}
}

return []interface{}{
map[string]interface{}{
"users": schema.NewSet(schema.HashString, users),
"teams": schema.NewSet(schema.HashString, teams),
"apps": schema.NewSet(schema.HashString, apps),
},
}
}

func flattenAndSetRequiredPullRequestReviews(d *schema.ResourceData, protection *github.Protection) error {
rprr := protection.GetRequiredPullRequestReviews()
if rprr != nil {
Expand All @@ -143,13 +178,16 @@ func flattenAndSetRequiredPullRequestReviews(d *schema.ResourceData, protection
}
}

bpra := flattenBypassPullRequestAllowances(rprr.GetBypassPullRequestAllowances())

return d.Set("required_pull_request_reviews", []interface{}{
map[string]interface{}{
"dismiss_stale_reviews": rprr.DismissStaleReviews,
"dismissal_users": schema.NewSet(schema.HashString, users),
"dismissal_teams": schema.NewSet(schema.HashString, teams),
"require_code_owner_reviews": rprr.RequireCodeOwnerReviews,
"required_approving_review_count": rprr.RequiredApprovingReviewCount,
"bypass_pull_request_allowances": bpra,
},
})
}
Expand Down Expand Up @@ -292,10 +330,16 @@ func expandRequiredPullRequestReviews(d *schema.ResourceData) (*github.PullReque
drr.Teams = &teams
}

bpra, err := expandBypassPullRequestAllowances(m)
if err != nil {
return nil, err
}

rprr.DismissalRestrictionsRequest = drr
rprr.DismissStaleReviews = m["dismiss_stale_reviews"].(bool)
rprr.RequireCodeOwnerReviews = m["require_code_owner_reviews"].(bool)
rprr.RequiredApprovingReviewCount = m["required_approving_review_count"].(int)
rprr.BypassPullRequestAllowancesRequest = bpra
}

return rprr, nil
Expand Down Expand Up @@ -336,6 +380,36 @@ func expandRestrictions(d *schema.ResourceData) (*github.BranchRestrictionsReque
return nil, nil
}

func expandBypassPullRequestAllowances(m map[string]interface{}) (*github.BypassPullRequestAllowancesRequest, error) {
if m["bypass_pull_request_allowances"] == nil {
return nil, nil
}

vL := m["bypass_pull_request_allowances"].([]interface{})
if len(vL) > 1 {
return nil, errors.New("cannot specify bypass_pull_request_allowances more than one time")
}

var bpra *github.BypassPullRequestAllowancesRequest

for _, v := range vL {
if v == nil {
return nil, errors.New("invalid bypass_pull_request_allowances")
}
bpra = new(github.BypassPullRequestAllowancesRequest)
m := v.(map[string]interface{})

users := expandNestedSet(m, "users")
bpra.Users = users
teams := expandNestedSet(m, "teams")
bpra.Teams = teams
apps := expandNestedSet(m, "apps")
bpra.Apps = apps
}

return bpra, nil
}

func checkBranchRestrictionsUsers(actual *github.BranchRestrictions, expected *github.BranchRestrictionsRequest) error {
if expected == nil {
return nil
Expand Down
19 changes: 17 additions & 2 deletions website/docs/r/branch_protection_v3.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: |-
Protects a GitHub branch using the v3 / REST implementation. The `github_branch_protection` resource has moved to the GraphQL API, while this resource will continue to leverage the REST API
---

# github\_branch\_protection\_v3
# github_branch_protection_v3

Protects a GitHub branch.

Expand All @@ -29,7 +29,7 @@ resource "github_branch_protection_v3" "example" {

```hcl
# Protect the main branch of the foo repository. Additionally, require that
# the "ci/check" check ran by the Github Actions app is passing and only allow
# the "ci/check" check ran by the Github Actions app is passing and only allow
# the engineers team merge to the branch.
resource "github_branch_protection_v3" "example" {
Expand All @@ -48,6 +48,12 @@ resource "github_branch_protection_v3" "example" {
dismiss_stale_reviews = true
dismissal_users = ["foo-user"]
dismissal_teams = [github_team.example.slug]
bypass_pull_request_allowances {
users = ["foo-user"]
teams = [github_team.example.slug]
apps = ["foo-app"]
}
}
restrictions {
Expand Down Expand Up @@ -103,6 +109,7 @@ The following arguments are supported:
Always use `slug` of the team, **not** its name. Each team already **has** to have access to the repository.
* `require_code_owner_reviews`: (Optional) Require an approved review in pull requests including files with a designated code owner. Defaults to `false`.
* `required_approving_review_count`: (Optional) Require x number of approvals to satisfy branch protection requirements. If this is specified it must be a number between 0-6. This requirement matches GitHub's API, see the upstream [documentation](https://developer.github.com/v3/repos/branches/#parameters-1) for more information.
* `bypass_pull_request_allowances`: (Optional) Allow specific users, teams, or apps to bypass pull request requirements. See [Bypass Pull Request Allowances](#bypass-pull-request-allowances) below for details.

### Restrictions

Expand All @@ -115,6 +122,14 @@ The following arguments are supported:

`restrictions` is only available for organization-owned repositories.

### Bypass Pull Request Allowances

`bypass_pull_request_allowances` supports the following arguments:

- `users`: (Optional) The list of user logins allowed to bypass pull request requirements.
- `teams`: (Optional) The list of team slugs allowed to bypass pull request requirements.
- `apps`: (Optional) The list of app slugs allowed to bypass pull request requirements.

## Import

GitHub Branch Protection can be imported using an ID made up of `repository:branch`, e.g.
Expand Down

0 comments on commit 70a0a5a

Please sign in to comment.