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

Fix: add GitHub repository collaborators when repository name contains the org name #2013

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
32 changes: 23 additions & 9 deletions github/resource_github_repository_collaborator.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,16 @@ func resourceGithubRepositoryCollaborator() *schema.Resource {
func resourceGithubRepositoryCollaboratorCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client

owner := meta.(*Owner).name
username := d.Get("username").(string)
repoName := d.Get("repository").(string)

owner, repoNameWithoutOwner := parseRepoName(repoName, meta.(*Owner).name)

ctx := context.Background()

_, _, err := client.Repositories.AddCollaborator(ctx,
owner,
repoName,
repoNameWithoutOwner,
username,
&github.RepositoryAddCollaboratorOptions{
Permission: d.Get("permission").(string),
Expand All @@ -94,15 +96,15 @@ func resourceGithubRepositoryCollaboratorCreate(d *schema.ResourceData, meta int
func resourceGithubRepositoryCollaboratorRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client

owner := meta.(*Owner).name
repoName, username, err := parseTwoPartID(d.Id(), "repository", "username")
owner, repoNameWithoutOwner := parseRepoName(repoName, meta.(*Owner).name)
if err != nil {
return err
}
ctx := context.WithValue(context.Background(), ctxId, d.Id())

// First, check if the user has been invited but has not yet accepted
invitation, err := findRepoInvitation(client, ctx, owner, repoName, username)
invitation, err := findRepoInvitation(client, ctx, owner, repoNameWithoutOwner, username)
if err != nil {
if ghErr, ok := err.(*github.ErrorResponse); ok {
if ghErr.Response.StatusCode == http.StatusNotFound {
Expand Down Expand Up @@ -135,7 +137,7 @@ func resourceGithubRepositoryCollaboratorRead(d *schema.ResourceData, meta inter

for {
collaborators, resp, err := client.Repositories.ListCollaborators(ctx,
owner, repoName, opt)
owner, repoNameWithoutOwner, opt)
if err != nil {
return err
}
Expand Down Expand Up @@ -170,22 +172,23 @@ func resourceGithubRepositoryCollaboratorUpdate(d *schema.ResourceData, meta int
func resourceGithubRepositoryCollaboratorDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client

owner := meta.(*Owner).name
username := d.Get("username").(string)
repoName := d.Get("repository").(string)

owner, repoNameWithoutOwner := parseRepoName(repoName, meta.(*Owner).name)

ctx := context.WithValue(context.Background(), ctxId, d.Id())

// Delete any pending invitations
invitation, err := findRepoInvitation(client, ctx, owner, repoName, username)
invitation, err := findRepoInvitation(client, ctx, owner, repoNameWithoutOwner, username)
if err != nil {
return err
} else if invitation != nil {
_, err = client.Repositories.DeleteInvitation(ctx, owner, repoName, invitation.GetID())
_, err = client.Repositories.DeleteInvitation(ctx, owner, repoNameWithoutOwner, invitation.GetID())
return err
}

_, err = client.Repositories.RemoveCollaborator(ctx, owner, repoName, username)
_, err = client.Repositories.RemoveCollaborator(ctx, owner, repoNameWithoutOwner, username)
return err
}

Expand All @@ -210,3 +213,14 @@ func findRepoInvitation(client *github.Client, ctx context.Context, owner, repo,
}
return nil, nil
}

func parseRepoName(repoName string, defaultOwner string) (string, string) {
// GitHub replaces '/' with '-' for a repo name, so it is safe to assume that if repo name contains '/'
// then first part will be the owner name and second part will be the repo name
if strings.Contains(repoName, "/") {
parts := strings.Split(repoName, "/")
return parts[0], parts[1]
} else {
return defaultOwner, repoName
}
}
89 changes: 89 additions & 0 deletions github/resource_github_repository_collaborator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package github

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
Expand Down Expand Up @@ -62,4 +63,92 @@ func TestAccGithubRepositoryCollaborator(t *testing.T) {
})
})

t.Run("creates invitations when repository contains the org name", func(t *testing.T) {

orgName := os.Getenv("GITHUB_ORGANIZATION")

if orgName == "" {
t.Skip("Set GITHUB_ORGANIZATION to unskip this test run")
}

configWithOwner := fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-%s"
auto_init = true
}

resource "github_repository_collaborator" "test_repo_collaborator_2" {
repository = "%s/${github_repository.test.name}"
username = "<username>"
permission = "triage"
}
`, randomID, orgName)

checkWithOwner := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository_collaborator.test_repo_collaborator_2", "permission",
"triage",
),
)

testCase := func(t *testing.T, mode string) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, mode) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: configWithOwner,
Check: checkWithOwner,
},
},
})
}

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)
})
})
}

func TestParseRepoName(t *testing.T) {
tests := []struct {
name string
repoName string
defaultOwner string
wantOwner string
wantRepoName string
}{
{
name: "Repo name without owner",
repoName: "example-repo",
defaultOwner: "default-owner",
wantOwner: "default-owner",
wantRepoName: "example-repo",
},
{
name: "Repo name with owner",
repoName: "owner-name/example-repo",
defaultOwner: "default-owner",
wantOwner: "owner-name",
wantRepoName: "example-repo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotOwner, gotRepoName := parseRepoName(tt.repoName, tt.defaultOwner)
if gotOwner != tt.wantOwner || gotRepoName != tt.wantRepoName {
t.Errorf("parseRepoName(%q, %q) = %q, %q, want %q, %q",
tt.repoName, tt.defaultOwner, gotOwner, gotRepoName, tt.wantOwner, tt.wantRepoName)
}
})
}
}
3 changes: 3 additions & 0 deletions website/docs/r/repository_collaborator.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ resource "github_repository_collaborator" "a_repo_collaborator" {
The following arguments are supported:

* `repository` - (Required) The GitHub repository

~> Note: The owner of the repository can be passed as part of the repository name e.g. `owner-org-name/repo-name`. If owner is not supplied as part of the repository name, it may also be supplied by setting the environment variable `GITHUB_OWNER`.

* `username` - (Required) The user to add to the repository as a collaborator.
* `permission` - (Optional) The permission of the outside collaborator for the repository.
Must be one of `pull`, `push`, `maintain`, `triage` or `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 organization for organization-owned repositories.
Expand Down