Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(github_branch_protection_v3): Add support for bypass_pull_request_allowances #1578

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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