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: Updated repo collaborators to support ignoring teams #2481

Merged
merged 1 commit into from
Jan 17, 2025
Merged
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
117 changes: 84 additions & 33 deletions github/resource_github_repository_collaborators.go
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ func resourceGithubRepositoryCollaborators() *schema.Resource {
"user": {
Type: schema.TypeSet,
Optional: true,
Description: "List of users",
Description: "List of users.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"permission": {
@@ -52,7 +52,7 @@ func resourceGithubRepositoryCollaborators() *schema.Resource {
"team": {
Type: schema.TypeSet,
Optional: true,
Description: "List of teams",
Description: "List of teams.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"permission": {
@@ -76,6 +76,20 @@ func resourceGithubRepositoryCollaborators() *schema.Resource {
},
Computed: true,
},
"ignore_team": {
Type: schema.TypeSet,
Optional: true,
Description: "List of teams to ignore.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"team_id": {
Type: schema.TypeString,
Description: "ID or slug of the team to ignore.",
Required: true,
},
},
},
},
},

CustomizeDiff: customdiff.Sequence(
@@ -145,16 +159,16 @@ func (c teamCollaborator) Empty() bool {
return c == teamCollaborator{}
}

func flattenTeamCollaborator(obj teamCollaborator, teamIDs []int64) interface{} {
func flattenTeamCollaborator(obj teamCollaborator, teamSlugs []string) interface{} {
if obj.Empty() {
return nil
}

var teamIDString string
if slices.Contains(teamIDs, obj.teamID) {
teamIDString = strconv.FormatInt(obj.teamID, 10)
} else {
if slices.Contains(teamSlugs, obj.teamSlug) {
teamIDString = obj.teamSlug
} else {
teamIDString = strconv.FormatInt(obj.teamID, 10)
}

transformed := map[string]interface{}{
@@ -165,7 +179,7 @@ func flattenTeamCollaborator(obj teamCollaborator, teamIDs []int64) interface{}
return transformed
}

func flattenTeamCollaborators(objs []teamCollaborator, teamIDs []int64) []interface{} {
func flattenTeamCollaborators(objs []teamCollaborator, teamSlugs []string) []interface{} {
if objs == nil {
return nil
}
@@ -176,14 +190,14 @@ func flattenTeamCollaborators(objs []teamCollaborator, teamIDs []int64) []interf

items := make([]interface{}, len(objs))
for i, obj := range objs {
items[i] = flattenTeamCollaborator(obj, teamIDs)
items[i] = flattenTeamCollaborator(obj, teamSlugs)
}

return items
}

func listUserCollaborators(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string) ([]userCollaborator, error) {
var userCollaborators []userCollaborator
userCollaborators := make([]userCollaborator, 0)
affiliations := []string{"direct", "outside"}
for _, affiliation := range affiliations {
opt := &github.ListCollaboratorsOptions{ListOptions: github.ListOptions{
@@ -217,7 +231,7 @@ func listUserCollaborators(client *github.Client, isOrg bool, ctx context.Contex
}

func listInvitations(client *github.Client, ctx context.Context, owner, repoName string) ([]invitedCollaborator, error) {
var invitedCollaborators []invitedCollaborator
invitedCollaborators := make([]invitedCollaborator, 0)

opt := &github.ListOptions{PerPage: maxPerPage}
for {
@@ -230,7 +244,8 @@ func listInvitations(client *github.Client, ctx context.Context, owner, repoName
permissionName := getPermission(i.GetPermissions())

invitedCollaborators = append(invitedCollaborators, invitedCollaborator{
userCollaborator{permissionName, i.GetInvitee().GetLogin()}, i.GetID()})
userCollaborator{permissionName, i.GetInvitee().GetLogin()}, i.GetID(),
})
}

if resp.NextPage == 0 {
@@ -241,11 +256,11 @@ func listInvitations(client *github.Client, ctx context.Context, owner, repoName
return invitedCollaborators, nil
}

func listTeams(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string) ([]teamCollaborator, error) {
var teamCollaborators []teamCollaborator
func listTeams(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string, ignoreTeamIds []int64) ([]teamCollaborator, error) {
allTeams := make([]teamCollaborator, 0)

if !isOrg {
return teamCollaborators, nil
return allTeams, nil
}

opt := &github.ListOptions{PerPage: maxPerPage}
@@ -256,20 +271,23 @@ func listTeams(client *github.Client, isOrg bool, ctx context.Context, owner, re
}

for _, t := range repoTeams {
permissionName := getPermission(t.GetPermission())
if slices.Contains(ignoreTeamIds, t.GetID()) {
continue
}

teamCollaborators = append(teamCollaborators, teamCollaborator{permissionName, t.GetID(), t.GetSlug()})
allTeams = append(allTeams, teamCollaborator{permission: getPermission(t.GetPermission()), teamID: t.GetID(), teamSlug: t.GetSlug()})
}

if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return teamCollaborators, nil

return allTeams, nil
}

func listAllCollaborators(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string) ([]userCollaborator, []invitedCollaborator, []teamCollaborator, error) {
func listAllCollaborators(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string, ignoreTeamIds []int64) ([]userCollaborator, []invitedCollaborator, []teamCollaborator, error) {
userCollaborators, err := listUserCollaborators(client, isOrg, ctx, owner, repoName)
if err != nil {
return nil, nil, nil, err
@@ -278,16 +296,14 @@ func listAllCollaborators(client *github.Client, isOrg bool, ctx context.Context
if err != nil {
return nil, nil, nil, err
}
teamCollaborators, err := listTeams(client, isOrg, ctx, owner, repoName)
teamCollaborators, err := listTeams(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return nil, nil, nil, err
}
return userCollaborators, invitations, teamCollaborators, err
}

func matchUserCollaboratorsAndInvites(
repoName string, want []interface{}, hasUsers []userCollaborator, hasInvites []invitedCollaborator,
meta interface{}) error {
func matchUserCollaboratorsAndInvites(repoName string, want []interface{}, hasUsers []userCollaborator, hasInvites []invitedCollaborator, meta interface{}) error {
client := meta.(*Owner).v3client

owner := meta.(*Owner).name
@@ -383,8 +399,7 @@ func matchUserCollaboratorsAndInvites(
return nil
}

func matchTeamCollaborators(
repoName string, want []interface{}, has []teamCollaborator, meta interface{}) error {
func matchTeamCollaborators(repoName string, want []interface{}, has []teamCollaborator, meta interface{}) error {
client := meta.(*Owner).v3client
orgID := meta.(*Owner).id
owner := meta.(*Owner).name
@@ -471,15 +486,15 @@ func resourceGithubRepositoryCollaboratorsCreate(d *schema.ResourceData, meta in
repoName := d.Get("repository").(string)
ctx := context.Background()

teamsMap := make(map[string]struct{})
teamsMap := make(map[string]struct{}, len(teams))
for _, team := range teams {
teamIDString := team.(map[string]interface{})["team_id"].(string)
if _, found := teamsMap[teamIDString]; found {
return fmt.Errorf("duplicate set member: %s", teamIDString)
}
teamsMap[teamIDString] = struct{}{}
}
usersMap := make(map[string]struct{})
usersMap := make(map[string]struct{}, len(users))
for _, user := range users {
username := user.(map[string]interface{})["username"].(string)
if _, found := usersMap[username]; found {
@@ -488,7 +503,12 @@ func resourceGithubRepositoryCollaboratorsCreate(d *schema.ResourceData, meta in
usersMap[username] = struct{}{}
}

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName)
ignoreTeamIds, err := getIgnoreTeamIds(d, meta)
if err != nil {
return err
}

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "repository collaborators (%s/%s)", owner, repoName)
}
@@ -516,7 +536,12 @@ func resourceGithubRepositoryCollaboratorsRead(d *schema.ResourceData, meta inte
repoName := d.Id()
ctx := context.WithValue(context.Background(), ctxId, d.Id())

userCollaborators, invitedCollaborators, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName)
ignoreTeamIds, err := getIgnoreTeamIds(d, meta)
if err != nil {
return err
}

userCollaborators, invitedCollaborators, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "repository collaborators (%s/%s)", owner, repoName)
}
@@ -526,9 +551,14 @@ func resourceGithubRepositoryCollaboratorsRead(d *schema.ResourceData, meta inte
invitationIds[i.username] = strconv.FormatInt(i.invitationID, 10)
}

teamIDs := make([]int64, len(teamCollaborators))
for i, t := range teamCollaborators {
teamIDs[i] = t.teamID
sourceTeams := d.Get("team").(*schema.Set).List()
teamSlugs := make([]string, len(sourceTeams))
for i, t := range sourceTeams {
teamIdString := t.(map[string]interface{})["team_id"].(string)
_, parseIntErr := strconv.ParseInt(teamIdString, 10, 64)
if parseIntErr != nil {
teamSlugs[i] = teamIdString
}
}

err = d.Set("repository", repoName)
@@ -539,7 +569,7 @@ func resourceGithubRepositoryCollaboratorsRead(d *schema.ResourceData, meta inte
if err != nil {
return err
}
err = d.Set("team", flattenTeamCollaborators(teamCollaborators, teamIDs))
err = d.Set("team", flattenTeamCollaborators(teamCollaborators, teamSlugs))
if err != nil {
return err
}
@@ -563,7 +593,12 @@ func resourceGithubRepositoryCollaboratorsDelete(d *schema.ResourceData, meta in
repoName := d.Get("repository").(string)
ctx := context.Background()

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName)
ignoreTeamIds, err := getIgnoreTeamIds(d, meta)
if err != nil {
return err
}

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "repository collaborators (%s/%s)", owner, repoName)
}
@@ -580,3 +615,19 @@ func resourceGithubRepositoryCollaboratorsDelete(d *schema.ResourceData, meta in
err = matchTeamCollaborators(repoName, nil, teamCollaborators, meta)
return err
}

func getIgnoreTeamIds(d *schema.ResourceData, meta interface{}) ([]int64, error) {
ignoreTeams := d.Get("ignore_team").(*schema.Set).List()
ignoreTeamIds := make([]int64, len(ignoreTeams))

for i, t := range ignoreTeams {
s := t.(map[string]interface{})["team_id"].(string)
id, err := getTeamID(s, meta)
if err != nil {
return nil, err
}
ignoreTeamIds[i] = id
}

return ignoreTeamIds, nil
}
191 changes: 160 additions & 31 deletions github/resource_github_repository_collaborators_test.go
Original file line number Diff line number Diff line change
@@ -14,30 +14,28 @@ import (
)

func TestAccGithubRepositoryCollaborators(t *testing.T) {

inOrgUser := os.Getenv("GITHUB_IN_ORG_USER")
inOrgUser2 := os.Getenv("GITHUB_IN_ORG_USER2")

if inOrgUser == "" || inOrgUser2 == "" {
t.Skip("set inOrgUser and inOrgUser2 to unskip this test run")
}

if inOrgUser == testOwnerFunc() || inOrgUser2 == testOwnerFunc() {
t.Skip("inOrgUser and inOrgUser2 can't be same as owner")
}

config := Config{BaseURL: "https://api.github.com/", Owner: testOwnerFunc(), Token: testToken}
meta, err := config.Meta()
if err != nil {
t.Fatalf("failed to return meta without error: %s", err.Error())
}

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

t.Run("creates collaborators without error", func(t *testing.T) {
if inOrgUser == "" {
t.Skip("set inOrgUser to unskip this test run")
}

if inOrgUser == testOwnerFunc() {
t.Skip("inOrgUser can't be same as owner")
}

randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
conn := meta.(*Owner).v3client
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)
teamName := fmt.Sprintf("tf-acc-test-team-%s", randomID)

individualConfig := fmt.Sprintf(`
resource "github_repository" "test" {
@@ -64,7 +62,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
}
resource "github_team" "test" {
name = "test"
name = "%s"
}
resource "github_repository_collaborators" "test_repo_collaborators" {
@@ -79,7 +77,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
permission = "pull"
}
}
`, repoName, inOrgUser)
`, repoName, teamName, inOrgUser)

testCase := func(t *testing.T, mode, config string, testCheck func(state *terraform.State) error) {
resource.Test(t, resource.TestCase{
@@ -192,9 +190,19 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
})

t.Run("updates collaborators without error", func(t *testing.T) {
if inOrgUser == "" || inOrgUser2 == "" {
t.Skip("set inOrgUser and inOrgUser2 to unskip this test run")
}

if inOrgUser == testOwnerFunc() || inOrgUser2 == testOwnerFunc() {
t.Skip("inOrgUser or inOrgUser2 can't be same as owner")
}

randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
conn := meta.(*Owner).v3client
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)
team0Name := fmt.Sprintf("tf-acc-test-team-0-%s", randomID)
team1Name := fmt.Sprintf("tf-acc-test-team-1-%s", randomID)

individualConfig := fmt.Sprintf(`
resource "github_repository" "test" {
@@ -237,12 +245,12 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
visibility = "private"
}
resource "github_team" "test" {
name = "test"
resource "github_team" "test_0" {
name = "%s"
}
resource "github_team" "test2" {
name = "test2"
resource "github_team" "test_1" {
name = "%s"
}
resource "github_repository_collaborators" "test_repo_collaborators" {
@@ -265,7 +273,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
permission = "pull"
}
}
`, repoName, inOrgUser, inOrgUser2)
`, repoName, team0Name, team1Name, inOrgUser, inOrgUser2)

orgConfigUpdate := fmt.Sprintf(`
resource "github_repository" "test" {
@@ -274,12 +282,12 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
visibility = "private"
}
resource "github_team" "test" {
name = "test"
resource "github_team" "test_0" {
name = "%s"
}
resource "github_team" "test2" {
name = "test2"
resource "github_team" "test_1" {
name = "%s"
}
resource "github_repository_collaborators" "test_repo_collaborators" {
@@ -290,11 +298,11 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
permission = "push"
}
team {
team_id = github_team.test.id
team_id = github_team.test_0.id
permission = "push"
}
}
`, repoName, inOrgUser)
`, repoName, team0Name, team1Name, inOrgUser)

testCase := func(t *testing.T, mode, config, configUpdate string, testCheck func(state *terraform.State) error) {
resource.Test(t, resource.TestCase{
@@ -319,9 +327,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
t.Run("with an individual account", func(t *testing.T) {
check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("github_repository_collaborators.test_repo_collaborators", "user.#"),
resource.TestCheckResourceAttrSet("github_repository_collaborators.test_repo_collaborators", "team.#"),
resource.TestCheckResourceAttr("github_repository_collaborators.test_repo_collaborators", "user.#", "1"),
resource.TestCheckResourceAttr("github_repository_collaborators.test_repo_collaborators", "team.#", "0"),
func(state *terraform.State) error {
owner := meta.(*Owner).name

@@ -364,7 +370,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
func(state *terraform.State) error {
owner := testOrganizationFunc()

teamAttrs := state.RootModule().Resources["github_team.test"].Primary.Attributes
teamAttrs := state.RootModule().Resources["github_team.test_0"].Primary.Attributes
collaborators := state.RootModule().Resources["github_repository_collaborators.test_repo_collaborators"].Primary
for name, val := range collaborators.Attributes {
if strings.HasPrefix(name, "user.") && strings.HasSuffix(name, ".username") && val != inOrgUser {
@@ -411,9 +417,18 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
})

t.Run("removes collaborators without error", func(t *testing.T) {
if inOrgUser == "" || inOrgUser2 == "" {
t.Skip("set inOrgUser and inOrgUser2 to unskip this test run")
}

if inOrgUser == testOwnerFunc() || inOrgUser2 == testOwnerFunc() {
t.Skip("inOrgUser or inOrgUser2 can't be same as owner")
}

randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
conn := meta.(*Owner).v3client
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)
teamName := fmt.Sprintf("tf-acc-test-team-%s", randomID)

individualConfig := fmt.Sprintf(`
resource "github_repository" "test" {
@@ -448,7 +463,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
}
resource "github_team" "test" {
name = "test"
name = "%s"
}
resource "github_repository_collaborators" "test_repo_collaborators" {
@@ -467,7 +482,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
permission = "pull"
}
}
`, repoName, inOrgUser, inOrgUser2)
`, repoName, teamName, inOrgUser, inOrgUser2)

orgConfigUpdate := fmt.Sprintf(`
resource "github_repository" "test" {
@@ -477,9 +492,9 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
}
resource "github_team" "test" {
name = "test"
name = "%s"
}
`, repoName)
`, repoName, teamName)

testCase := func(t *testing.T, mode, config, configUpdate string, testCheck func(state *terraform.State) error) {
resource.Test(t, resource.TestCase{
@@ -544,4 +559,118 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
testCase(t, organization, orgConfig, orgConfigUpdate, check)
})
})

t.Run("does not churn on team slug", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)
team0Name := fmt.Sprintf("tf-acc-test-team-0-%s", randomID)
team1Name := fmt.Sprintf("tf-acc-test-team-1-%s", randomID)

config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "%s"
auto_init = true
visibility = "private"
}
resource "github_team" "test_0" {
name = "%s"
}
resource "github_team" "test_1" {
name = "%s"
}
resource "github_repository_collaborators" "test_repo_collaborators" {
repository = "${github_repository.test.name}"
team {
team_id = github_team.test_0.id
permission = "pull"
}
team {
team_id = github_team.test_1.name
permission = "pull"
}
}
`, repoName, team0Name, team1Name)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, organization) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("github_repository_collaborators.test_repo_collaborators", "team.#"),
resource.TestCheckResourceAttr("github_repository_collaborators.test_repo_collaborators", "team.#", "2"),
),
},
{
Config: config,
ExpectNonEmptyPlan: false,
},
},
})
})

t.Run("ignores specified teams", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)
team0Name := fmt.Sprintf("tf-acc-test-team-0-%s", randomID)
team1Name := fmt.Sprintf("tf-acc-test-team-1-%s", randomID)

config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "%s"
auto_init = true
visibility = "private"
}
resource "github_team" "test_0" {
name = "%s"
}
resource "github_team_repository" "some_team_repo" {
team_id = github_team.test_0.id
repository = github_repository.test.name
}
resource "github_team" "test_1" {
name = "%s"
}
resource "github_repository_collaborators" "test_repo_collaborators" {
repository = "${github_repository.test.name}"
team {
team_id = github_team.test_1.id
permission = "pull"
}
ignore_team {
team_id = github_team.test_0.id
}
}
`, repoName, team0Name, team1Name)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, organization) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("github_repository_collaborators.test_repo_collaborators", "team.#"),
resource.TestCheckResourceAttr("github_repository_collaborators.test_repo_collaborators", "team.#", "1"),
),
},
{
Config: config,
ExpectNonEmptyPlan: false,
},
},
})
})
}
25 changes: 15 additions & 10 deletions website/docs/r/repository_collaborators.html.markdown
Original file line number Diff line number Diff line change
@@ -14,10 +14,10 @@ github_team_repository or they will fight over what your policy should be.

This resource allows you to manage all collaborators for repositories in your
organization or personal account. For organization repositories, collaborators can
have explicit (and differing levels of) read, write, or administrator access to
specific repositories, without giving the user full organization membership.
have explicit (and differing levels of) read, write, or administrator access to
specific repositories, without giving the user full organization membership.
For personal repositories, collaborators can only be granted write
(implicitly includes read) permission.
(implicitly includes read) permission.

When applied, an invitation will be sent to the user to become a collaborators
on a repository. When destroyed, either the invitation will be cancelled or the
@@ -31,7 +31,7 @@ Further documentation on GitHub collaborators:
- [Adding outside collaborators to your personal repositories](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/managing-access-to-your-personal-repositories)
- [Adding outside collaborators to repositories in your organization](https://help.github.com/articles/adding-outside-collaborators-to-repositories-in-your-organization/)
- [Converting an organization member to an outside collaborators](https://help.github.com/articles/converting-an-organization-member-to-an-outside-collaborator/)

## Example Usage

```hcl
@@ -52,7 +52,7 @@ resource "github_repository_collaborators" "some_repo_collaborators" {
permission = "admin"
username = "SomeUser"
}
team {
permission = "pull"
team_id = github_team.some_team.slug
@@ -64,9 +64,10 @@ resource "github_repository_collaborators" "some_repo_collaborators" {

The following arguments are supported:

* `repository` - (Required) The GitHub repository
* `user` - (Optional) List of users
* `team` - (Optional) List of teams
* `repository` - (Required) The GitHub repository.
* `user` - (Optional) List of users to grant access to the repository.
* `team` - (Optional) List of teams to grant access to the repository.
* `ignore_team` - (Optional) List of teams to ignore when checking for repository access. This supports ignoring teams granted access at an organizational level.

The `user` block supports:

@@ -77,16 +78,20 @@ The `user` block supports:

The `team` block supports:

* `team_id` - (Required) The GitHub team id or the GitHub team slug
* `team_id` - (Required) The GitHub team id or the GitHub team slug.
* `permission` - (Optional) The permission of the outside collaborators for the repository.
Must be one of `pull`, `triage`, `push`, `maintain`, `admin` or the name of an existing [custom repository role](https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-peoples-access-to-your-organization-with-roles/managing-custom-repository-roles-for-an-organization) within the organisation. Defaults to `pull`.
Must be `push` for personal repositories. Defaults to `push`.

The `team_ignore` block supports:

* `team_id` - (Required) The GitHub team id or the GitHub team slug.

## Attribute Reference

In addition to the above arguments, the following attributes are exported:

* `invitation_ids` - Map of usernames to invitation ID for any users added as part of creation of this resource to
* `invitation_ids` - Map of usernames to invitation ID for any users added as part of creation of this resource to
be used in [`github_user_invitation_accepter`](./user_invitation_accepter.html).

## Import