Skip to content

Commit 38e68ad

Browse files
authored
fix/feat: Update removal operations for related resources to now gracefully handle deleting archived repositories (#2844)
* Update removal operations for related resources to now gracefully handle deleteing archived repositories * Add rep collab tests - requires GITHUB_TEST_COLLABORATOR to be set * More repo collab coverage * Adds coverage for deploy key and teams * Same docs/notes as before
1 parent 519e99a commit 38e68ad

13 files changed

+298
-14
lines changed

github/resource_github_repository_collaborator.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,14 @@ func resourceGithubRepositoryCollaboratorDelete(d *schema.ResourceData, meta int
196196
// Delete any pending invitations
197197
invitation, err := findRepoInvitation(client, ctx, owner, repoNameWithoutOwner, username)
198198
if err != nil {
199-
return err
199+
return handleArchivedRepoDelete(err, "repository collaborator invitation", username, owner, repoNameWithoutOwner)
200200
} else if invitation != nil {
201201
_, err = client.Repositories.DeleteInvitation(ctx, owner, repoNameWithoutOwner, invitation.GetID())
202-
return err
202+
return handleArchivedRepoDelete(err, "repository collaborator invitation", username, owner, repoNameWithoutOwner)
203203
}
204204

205205
_, err = client.Repositories.RemoveCollaborator(ctx, owner, repoNameWithoutOwner, username)
206-
return err
206+
return handleArchivedRepoDelete(err, "repository collaborator", username, owner, repoNameWithoutOwner)
207207
}
208208

209209
func findRepoInvitation(client *github.Client, ctx context.Context, owner, repo, collaborator string) (*github.RepositoryInvitation, error) {

github/resource_github_repository_collaborator_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,95 @@ func TestParseRepoName(t *testing.T) {
152152
})
153153
}
154154
}
155+
156+
func TestAccGithubRepositoryCollaboratorArchivedRepo(t *testing.T) {
157+
158+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
159+
160+
t.Run("can delete collaborators from archived repositories without error", func(t *testing.T) {
161+
162+
// Note: This test requires GITHUB_TEST_COLLABORATOR to be set to a valid GitHub username
163+
testCollaborator := os.Getenv("GITHUB_TEST_COLLABORATOR")
164+
if testCollaborator == "" {
165+
t.Skip("GITHUB_TEST_COLLABORATOR not set, skipping archived repository collaborator test")
166+
}
167+
168+
config := fmt.Sprintf(`
169+
resource "github_repository" "test" {
170+
name = "tf-acc-test-collab-archive-%s"
171+
auto_init = true
172+
}
173+
174+
resource "github_repository_collaborator" "test" {
175+
repository = github_repository.test.name
176+
username = "%s"
177+
permission = "pull"
178+
}
179+
`, randomID, testCollaborator)
180+
181+
archivedConfig := fmt.Sprintf(`
182+
resource "github_repository" "test" {
183+
name = "tf-acc-test-collab-archive-%s"
184+
auto_init = true
185+
archived = true
186+
}
187+
188+
resource "github_repository_collaborator" "test" {
189+
repository = github_repository.test.name
190+
username = "%s"
191+
permission = "pull"
192+
}
193+
`, randomID, testCollaborator)
194+
195+
testCase := func(t *testing.T, mode string) {
196+
resource.Test(t, resource.TestCase{
197+
PreCheck: func() { skipUnlessMode(t, mode) },
198+
Providers: testAccProviders,
199+
Steps: []resource.TestStep{
200+
{
201+
Config: config,
202+
Check: resource.ComposeTestCheckFunc(
203+
resource.TestCheckResourceAttr(
204+
"github_repository_collaborator.test", "username",
205+
testCollaborator,
206+
),
207+
resource.TestCheckResourceAttr(
208+
"github_repository_collaborator.test", "permission",
209+
"pull",
210+
),
211+
),
212+
},
213+
{
214+
Config: archivedConfig,
215+
Check: resource.ComposeTestCheckFunc(
216+
resource.TestCheckResourceAttr(
217+
"github_repository.test", "archived",
218+
"true",
219+
),
220+
),
221+
},
222+
// This step should succeed - the collaborator should be removed from state
223+
// without trying to actually delete it from the archived repo
224+
{
225+
Config: fmt.Sprintf(`
226+
resource "github_repository" "test" {
227+
name = "tf-acc-test-collab-archive-%s"
228+
auto_init = true
229+
archived = true
230+
}
231+
`, randomID),
232+
},
233+
},
234+
})
235+
}
236+
237+
t.Run("with individual mode", func(t *testing.T) {
238+
testCase(t, individual)
239+
})
240+
241+
t.Run("with organization mode", func(t *testing.T) {
242+
testCase(t, organization)
243+
})
244+
245+
})
246+
}

github/resource_github_repository_collaborators.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,10 @@ func matchUserCollaboratorsAndInvites(repoName string, want []interface{}, hasUs
322322
log.Printf("[DEBUG] Removing user %s from repo: %s.", has.username, repoName)
323323
_, err := client.Repositories.RemoveCollaborator(ctx, owner, repoName, has.username)
324324
if err != nil {
325-
return err
325+
err = handleArchivedRepoDelete(err, "repository collaborator", has.username, owner, repoName)
326+
if err != nil {
327+
return err
328+
}
326329
}
327330
} else if wantPermission != has.permission { // permission should be updated
328331
log.Printf("[DEBUG] Updating user %s permission from %s to %s for repo: %s.", has.username, has.permission, wantPermission, repoName)
@@ -350,7 +353,10 @@ func matchUserCollaboratorsAndInvites(repoName string, want []interface{}, hasUs
350353
log.Printf("[DEBUG] Deleting invite for user %s from repo: %s.", has.username, repoName)
351354
_, err := client.Repositories.DeleteInvitation(ctx, owner, repoName, has.invitationID)
352355
if err != nil {
353-
return err
356+
err = handleArchivedRepoDelete(err, "repository collaborator invitation", has.username, owner, repoName)
357+
if err != nil {
358+
return err
359+
}
354360
}
355361
} else if wantPermission != has.permission { // permission should be updated
356362
log.Printf("[DEBUG] Updating invite for user %s permission from %s to %s for repo: %s.", has.username, has.permission, wantPermission, repoName)
@@ -469,7 +475,10 @@ func matchTeamCollaborators(repoName string, want []interface{}, has []teamColla
469475
log.Printf("[DEBUG] Removing team %d from repo: %s.", team.teamID, repoName)
470476
_, err := client.Teams.RemoveTeamRepoByID(ctx, orgID, team.teamID, owner, repoName)
471477
if err != nil {
472-
return err
478+
err = handleArchivedRepoDelete(err, "team repository access", fmt.Sprintf("team %d", team.teamID), owner, repoName)
479+
if err != nil {
480+
return err
481+
}
473482
}
474483
}
475484

github/resource_github_repository_deploy_key.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,7 @@ func resourceGithubRepositoryDeployKeyDelete(d *schema.ResourceData, meta interf
153153
ctx := context.WithValue(context.Background(), ctxId, d.Id())
154154

155155
_, err = client.Repositories.DeleteKey(ctx, owner, repoName, id)
156-
if err != nil {
157-
return err
158-
}
159-
160-
return err
156+
return handleArchivedRepoDelete(err, "repository deploy key", idString, owner, repoName)
161157
}
162158

163159
func suppressDeployKeyDiff(k, oldV, newV string, d *schema.ResourceData) bool {

github/resource_github_repository_deploy_key_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,88 @@ resource "github_repository_deploy_key" "test_repo_deploy_key" {
167167
}
168168
`, name, keyPath)
169169
}
170+
171+
func TestAccGithubRepositoryDeployKeyArchivedRepo(t *testing.T) {
172+
173+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
174+
175+
t.Run("can delete deploy keys from archived repositories without error", func(t *testing.T) {
176+
177+
// Create a TEMP SSH key for testing only
178+
key := `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+7E/lL5ZWD7TCnNHfQWfyZ+/g1J0+E2u5R1d8K3/WKXGmI4DXk5JHZv+/rj+1J5HL5+3rJ4Z5bGF4e1z8E9JqHzF+8lQ3EI8E3z+9CQ5E5SYPeZPLxFk= [email protected]`
179+
180+
config := fmt.Sprintf(`
181+
resource "github_repository" "test" {
182+
name = "tf-acc-test-deploy-key-archive-%s"
183+
auto_init = true
184+
}
185+
186+
resource "github_repository_deploy_key" "test" {
187+
key = "%s"
188+
read_only = true
189+
repository = github_repository.test.name
190+
title = "test-archived-deploy-key"
191+
}
192+
`, randomID, key)
193+
194+
archivedConfig := fmt.Sprintf(`
195+
resource "github_repository" "test" {
196+
name = "tf-acc-test-deploy-key-archive-%s"
197+
auto_init = true
198+
archived = true
199+
}
200+
201+
resource "github_repository_deploy_key" "test" {
202+
key = "%s"
203+
read_only = true
204+
repository = github_repository.test.name
205+
title = "test-archived-deploy-key"
206+
}
207+
`, randomID, key)
208+
209+
testCase := func(t *testing.T, mode string) {
210+
resource.Test(t, resource.TestCase{
211+
PreCheck: func() { skipUnlessMode(t, mode) },
212+
Providers: testAccProviders,
213+
Steps: []resource.TestStep{
214+
{
215+
Config: config,
216+
Check: resource.ComposeTestCheckFunc(
217+
resource.TestCheckResourceAttr(
218+
"github_repository_deploy_key.test", "title",
219+
"test-archived-deploy-key",
220+
),
221+
),
222+
},
223+
{
224+
Config: archivedConfig,
225+
Check: resource.ComposeTestCheckFunc(
226+
resource.TestCheckResourceAttr(
227+
"github_repository.test", "archived",
228+
"true",
229+
),
230+
),
231+
},
232+
{
233+
Config: fmt.Sprintf(`
234+
resource "github_repository" "test" {
235+
name = "tf-acc-test-deploy-key-archive-%s"
236+
auto_init = true
237+
archived = true
238+
}
239+
`, randomID),
240+
},
241+
},
242+
})
243+
}
244+
245+
t.Run("with individual mode", func(t *testing.T) {
246+
testCase(t, individual)
247+
})
248+
249+
t.Run("with organization mode", func(t *testing.T) {
250+
testCase(t, organization)
251+
})
252+
253+
})
254+
}

github/resource_github_repository_webhook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,5 +215,5 @@ func resourceGithubRepositoryWebhookDelete(d *schema.ResourceData, meta interfac
215215
ctx := context.WithValue(context.Background(), ctxId, d.Id())
216216

217217
_, err = client.Repositories.DeleteHook(ctx, owner, repoName, hookID)
218-
return err
218+
return handleArchivedRepoDelete(err, "repository webhook", d.Id(), owner, repoName)
219219
}

github/resource_github_team_repository.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package github
22

33
import (
44
"context"
5+
"fmt"
56
"log"
67
"net/http"
78
"strconv"
@@ -233,9 +234,9 @@ func resourceGithubTeamRepositoryDelete(d *schema.ResourceData, meta interface{}
233234
"Try deleting team repository again.",
234235
repoName, newRepoName)
235236
_, err := client.Teams.RemoveTeamRepoByID(ctx, orgId, teamId, orgName, newRepoName)
236-
return err
237+
return handleArchivedRepoDelete(err, "team repository access", fmt.Sprintf("team %s", teamIdString), orgName, newRepoName)
237238
}
238239
}
239240

240-
return err
241+
return handleArchivedRepoDelete(err, "team repository access", fmt.Sprintf("team %s", teamIdString), orgName, repoName)
241242
}

github/resource_github_team_repository_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,94 @@ func TestAccGithubTeamRepository(t *testing.T) {
171171
})
172172
})
173173
}
174+
175+
func TestAccGithubTeamRepositoryArchivedRepo(t *testing.T) {
176+
177+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
178+
179+
t.Run("can delete team repository access from archived repositories without error", func(t *testing.T) {
180+
181+
config := fmt.Sprintf(`
182+
resource "github_team" "test" {
183+
name = "tf-acc-test-team-archive-%s"
184+
description = "test team for archived repo"
185+
}
186+
187+
resource "github_repository" "test" {
188+
name = "tf-acc-test-team-archive-%[1]s"
189+
auto_init = true
190+
}
191+
192+
resource "github_team_repository" "test" {
193+
team_id = github_team.test.id
194+
repository = github_repository.test.name
195+
permission = "pull"
196+
}
197+
`, randomID)
198+
199+
archivedConfig := fmt.Sprintf(`
200+
resource "github_team" "test" {
201+
name = "tf-acc-test-team-archive-%s"
202+
description = "test team for archived repo"
203+
}
204+
205+
resource "github_repository" "test" {
206+
name = "tf-acc-test-team-archive-%[1]s"
207+
auto_init = true
208+
archived = true
209+
}
210+
211+
resource "github_team_repository" "test" {
212+
team_id = github_team.test.id
213+
repository = github_repository.test.name
214+
permission = "pull"
215+
}
216+
`, randomID)
217+
218+
testCase := func(t *testing.T, mode string) {
219+
resource.Test(t, resource.TestCase{
220+
PreCheck: func() { skipUnlessMode(t, mode) },
221+
Providers: testAccProviders,
222+
Steps: []resource.TestStep{
223+
{
224+
Config: config,
225+
Check: resource.ComposeTestCheckFunc(
226+
resource.TestCheckResourceAttr(
227+
"github_team_repository.test", "permission",
228+
"pull",
229+
),
230+
),
231+
},
232+
{
233+
Config: archivedConfig,
234+
Check: resource.ComposeTestCheckFunc(
235+
resource.TestCheckResourceAttr(
236+
"github_repository.test", "archived",
237+
"true",
238+
),
239+
),
240+
},
241+
{
242+
Config: fmt.Sprintf(`
243+
resource "github_team" "test" {
244+
name = "tf-acc-test-team-archive-%s"
245+
description = "test team for archived repo"
246+
}
247+
248+
resource "github_repository" "test" {
249+
name = "tf-acc-test-team-archive-%[1]s"
250+
auto_init = true
251+
archived = true
252+
}
253+
`, randomID),
254+
},
255+
},
256+
})
257+
}
258+
259+
t.Run("with an organization account", func(t *testing.T) {
260+
testCase(t, organization)
261+
})
262+
263+
})
264+
}

website/docs/r/repository_collaborator.html.markdown

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ When applied, an invitation will be sent to the user to become a collaborator
2323
on a repository. When destroyed, either the invitation will be cancelled or the
2424
collaborator will be removed from the repository.
2525

26+
~> **Note on Archived Repositories**: When a repository is archived, GitHub makes it read-only, preventing collaborator modifications. If you attempt to destroy resources associated with archived repositories, the provider will gracefully handle the operation by logging an informational message and removing the resource from Terraform state without attempting to modify the archived repository.
27+
2628
This resource is non-authoritative, for managing ALL collaborators of a repo, use github_repository_collaborators
2729
instead.
2830

website/docs/r/repository_collaborators.html.markdown

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ When applied, an invitation will be sent to the user to become a collaborators
2323
on a repository. When destroyed, either the invitation will be cancelled or the
2424
collaborators will be removed from the repository.
2525

26+
~> **Note on Archived Repositories**: When a repository is archived, GitHub makes it read-only, preventing collaborator modifications. If you attempt to destroy resources associated with archived repositories, the provider will gracefully handle the operation by logging an informational message and removing the resource from Terraform state without attempting to modify the archived repository.
27+
2628
This resource is authoritative. For adding a collaborator to a repo in a non-authoritative manner, use
2729
github_repository_collaborator instead.
2830

0 commit comments

Comments
 (0)