Skip to content

Commit 0f3f802

Browse files
authored
feat/fix: Handle error management on resources when dealing with archived repos. (#2837)
* Adds utility to handle the various needs of error management when dealing with archived repos. * Properly handle label deletes of archived repos * coverage * Updates docs detailing the behavior * Apply the handling for repo files
1 parent b15919c commit 0f3f802

10 files changed

+267
-8
lines changed

github/repository_utils.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package github
33
import (
44
"context"
55
"fmt"
6+
"log"
67
"net/http"
78
"strings"
89

@@ -124,6 +125,38 @@ func listAutolinks(client *github.Client, owner, repo string) ([]*github.Autolin
124125
return allAutolinks, nil
125126
}
126127

128+
// isArchivedRepositoryError checks if an error is a 403 "repository archived" error.
129+
// Returns true if the repository is archived.
130+
func isArchivedRepositoryError(err error) bool {
131+
if ghErr, ok := err.(*github.ErrorResponse); ok {
132+
if ghErr.Response.StatusCode == http.StatusForbidden {
133+
return strings.Contains(strings.ToLower(ghErr.Message), "archived")
134+
}
135+
}
136+
return false
137+
}
138+
139+
// handleArchivedRepositoryError handles errors for operations on archived repositories.
140+
// If the repository is archived, it logs a message and returns nil, otherwise, it returns the original error.
141+
func handleArchivedRepositoryError(err error, operation, resource, owner, repo string) error {
142+
if err == nil {
143+
return nil
144+
}
145+
146+
if isArchivedRepositoryError(err) {
147+
log.Printf("[INFO] Skipping %s of %s from archived repository %s/%s", operation, resource, owner, repo)
148+
return nil
149+
}
150+
151+
return err
152+
}
153+
154+
// handleArchivedRepoDelete is a convenience wrapper for handleArchivedRepositoryError
155+
// specifically for delete operations, which is the most common use case.
156+
func handleArchivedRepoDelete(err error, resourceType, resourceName, owner, repo string) error {
157+
return handleArchivedRepositoryError(err, "deletion", fmt.Sprintf("%s %s", resourceType, resourceName), owner, repo)
158+
}
159+
127160
// get the list of retriable errors
128161
func getDefaultRetriableErrors() map[int]bool {
129162
return map[int]bool{

github/resource_github_issue_label.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ func resourceGithubIssueLabelDelete(d *schema.ResourceData, meta interface{}) er
197197
name := d.Get("name").(string)
198198
ctx := context.WithValue(context.Background(), ctxId, d.Id())
199199

200-
_, err := client.Issues.DeleteLabel(ctx,
201-
orgName, repoName, name)
202-
return err
200+
_, err := client.Issues.DeleteLabel(ctx, orgName, repoName, name)
201+
return handleArchivedRepoDelete(err, "issue label", name, orgName, repoName)
203202
}

github/resource_github_issue_label_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,72 @@ func TestAccGithubIssueLabel(t *testing.T) {
8080
})
8181
})
8282

83+
t.Run("can delete labels from archived repositories without error", func(t *testing.T) {
84+
85+
config := fmt.Sprintf(`
86+
resource "github_repository" "test" {
87+
name = "tf-acc-test-archive-%s"
88+
auto_init = true
89+
}
90+
91+
resource "github_issue_label" "test" {
92+
repository = github_repository.test.name
93+
name = "archived-test-label"
94+
color = "ff0000"
95+
description = "Test label for archived repo"
96+
}
97+
`, randomID)
98+
99+
archivedConfig := strings.Replace(config,
100+
`auto_init = true`,
101+
`auto_init = true
102+
archived = true`, 1)
103+
104+
testCase := func(t *testing.T, mode string) {
105+
resource.Test(t, resource.TestCase{
106+
PreCheck: func() { skipUnlessMode(t, mode) },
107+
Providers: testAccProviders,
108+
Steps: []resource.TestStep{
109+
{
110+
Config: config,
111+
Check: resource.ComposeTestCheckFunc(
112+
resource.TestCheckResourceAttr(
113+
"github_issue_label.test", "name",
114+
"archived-test-label",
115+
),
116+
),
117+
},
118+
{
119+
Config: archivedConfig,
120+
Check: resource.ComposeTestCheckFunc(
121+
resource.TestCheckResourceAttr(
122+
"github_repository.test", "archived",
123+
"true",
124+
),
125+
),
126+
},
127+
// This step should succeed - the label should be removed from state
128+
// without trying to actually delete it from the archived repo
129+
{
130+
Config: fmt.Sprintf(`
131+
resource "github_repository" "test" {
132+
name = "tf-acc-test-archive-%s"
133+
auto_init = true
134+
archived = true
135+
}
136+
`, randomID),
137+
},
138+
},
139+
})
140+
}
141+
142+
t.Run("with an individual account", func(t *testing.T) {
143+
testCase(t, individual)
144+
})
145+
146+
t.Run("with an organization account", func(t *testing.T) {
147+
testCase(t, organization)
148+
})
149+
})
150+
83151
}

github/resource_github_issue_labels.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ func resourceGithubIssueLabelsDelete(d *schema.ResourceData, meta interface{}) e
189189

190190
_, err := client.Issues.DeleteLabel(ctx, owner, repository, name)
191191
if err != nil {
192+
if isArchivedRepositoryError(err) {
193+
log.Printf("[INFO] Skipping deletion of remaining issue labels from archived repository %s/%s", owner, repository)
194+
break // Skip deleting remaining labels
195+
}
192196
return err
193197
}
194198
}

github/resource_github_issue_labels_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package github
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"testing"
78

89
"github.com/google/go-github/v67/github"
@@ -159,3 +160,84 @@ func testAccGithubIssueLabelsAddLabel(repository, label string) error {
159160
_, _, err := client.Issues.CreateLabel(ctx, orgName, repository, &github.Label{Name: github.String(label)})
160161
return err
161162
}
163+
164+
func TestAccGithubIssueLabelsArchived(t *testing.T) {
165+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
166+
167+
t.Run("can delete labels from archived repositories without error", func(t *testing.T) {
168+
169+
repoName := fmt.Sprintf("tf-acc-test-labels-archive-%s", randomID)
170+
171+
config := fmt.Sprintf(`
172+
resource "github_repository" "test" {
173+
name = "%s"
174+
auto_init = true
175+
}
176+
177+
resource "github_issue_labels" "test" {
178+
repository = github_repository.test.name
179+
label {
180+
name = "archived-label-1"
181+
color = "ff0000"
182+
description = "First test label"
183+
}
184+
label {
185+
name = "archived-label-2"
186+
color = "00ff00"
187+
description = "Second test label"
188+
}
189+
}
190+
`, repoName)
191+
192+
archivedConfig := strings.Replace(config,
193+
`auto_init = true`,
194+
`auto_init = true
195+
archived = true`, 1)
196+
197+
testCase := func(t *testing.T, mode string) {
198+
resource.Test(t, resource.TestCase{
199+
PreCheck: func() { skipUnlessMode(t, mode) },
200+
Providers: testAccProviders,
201+
Steps: []resource.TestStep{
202+
{
203+
Config: config,
204+
Check: resource.ComposeTestCheckFunc(
205+
resource.TestCheckResourceAttr(
206+
"github_issue_labels.test", "label.#",
207+
"2",
208+
),
209+
),
210+
},
211+
{
212+
Config: archivedConfig,
213+
Check: resource.ComposeTestCheckFunc(
214+
resource.TestCheckResourceAttr(
215+
"github_repository.test", "archived",
216+
"true",
217+
),
218+
),
219+
},
220+
// This step should succeed - the labels should be removed from state
221+
// without trying to actually delete them from the archived repo
222+
{
223+
Config: fmt.Sprintf(`
224+
resource "github_repository" "test" {
225+
name = "%s"
226+
auto_init = true
227+
archived = true
228+
}
229+
`, repoName),
230+
},
231+
},
232+
})
233+
}
234+
235+
t.Run("with an individual account", func(t *testing.T) {
236+
testCase(t, individual)
237+
})
238+
239+
t.Run("with an organization account", func(t *testing.T) {
240+
testCase(t, organization)
241+
})
242+
})
243+
}

github/resource_github_repository_file.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -504,11 +504,7 @@ func resourceGithubRepositoryFileDelete(d *schema.ResourceData, meta interface{}
504504
}
505505

506506
_, _, err := client.Repositories.DeleteFile(ctx, owner, repo, file, opts)
507-
if err != nil {
508-
return nil
509-
}
510-
511-
return nil
507+
return handleArchivedRepoDelete(err, "repository file", file, owner, repo)
512508
}
513509

514510
func autoBranchDiffSuppressFunc(k, _, _ string, d *schema.ResourceData) bool {

github/resource_github_repository_file_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,76 @@ func TestAccGithubRepositoryFile(t *testing.T) {
344344
})
345345

346346
})
347+
348+
t.Run("can delete files from archived repositories without error", func(t *testing.T) {
349+
350+
config := fmt.Sprintf(`
351+
resource "github_repository" "test" {
352+
name = "tf-acc-test-file-archive-%s"
353+
auto_init = true
354+
}
355+
356+
resource "github_repository_file" "test" {
357+
repository = github_repository.test.name
358+
branch = "main"
359+
file = "archived-test.md"
360+
content = "# Test file for archived repo"
361+
commit_message = "Add test file"
362+
commit_author = "Terraform User"
363+
commit_email = "[email protected]"
364+
}
365+
`, randomID)
366+
367+
archivedConfig := strings.Replace(config,
368+
`auto_init = true`,
369+
`auto_init = true
370+
archived = true`, 1)
371+
372+
testCase := func(t *testing.T, mode string) {
373+
resource.Test(t, resource.TestCase{
374+
PreCheck: func() { skipUnlessMode(t, mode) },
375+
Providers: testAccProviders,
376+
Steps: []resource.TestStep{
377+
{
378+
Config: config,
379+
Check: resource.ComposeTestCheckFunc(
380+
resource.TestCheckResourceAttr(
381+
"github_repository_file.test", "file",
382+
"archived-test.md",
383+
),
384+
),
385+
},
386+
{
387+
Config: archivedConfig,
388+
Check: resource.ComposeTestCheckFunc(
389+
resource.TestCheckResourceAttr(
390+
"github_repository.test", "archived",
391+
"true",
392+
),
393+
),
394+
},
395+
// This step should succeed - the file should be removed from state
396+
// without trying to actually delete it from the archived repo
397+
{
398+
Config: fmt.Sprintf(`
399+
resource "github_repository" "test" {
400+
name = "tf-acc-test-file-archive-%s"
401+
auto_init = true
402+
archived = true
403+
}
404+
`, randomID),
405+
},
406+
},
407+
})
408+
}
409+
410+
t.Run("with an individual account", func(t *testing.T) {
411+
testCase(t, individual)
412+
})
413+
414+
t.Run("with an organization account", func(t *testing.T) {
415+
testCase(t, organization)
416+
})
417+
418+
})
347419
}

website/docs/r/issue_label.html.markdown

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ and those labels easily conflict with custom ones.
2020
This resource will first check if the label exists, and then issue an update,
2121
otherwise it will create.
2222

23+
~> **Note:** When a repository is archived, Terraform will skip deletion of issue labels to avoid API errors, as archived repositories are read-only. The labels will be removed from Terraform state without attempting to delete them from GitHub.
24+
2325
## Example Usage
2426

2527
```hcl

website/docs/r/issue_labels.html.markdown

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ This resource is authoritative. For adding a label to a repo in a non-authoritat
1818

1919
If you change the case of a label's name, its' color, or description, this resource will edit the existing label to match the new values. However, if you change the name of a label, this resource will create a new label with the new name and delete the old label. Beware that this will remove the label from any issues it was previously attached to.
2020

21+
~> **Note:** When a repository is archived, Terraform will skip deletion of issue labels to avoid API errors, as archived repositories are read-only. The labels will be removed from Terraform state without attempting to delete them from GitHub.
22+
2123
## Example Usage
2224

2325
```hcl

website/docs/r/repository_file.html.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ description: |-
1010
This resource allows you to create and manage files within a
1111
GitHub repository.
1212

13+
~> **Note:** When a repository is archived, Terraform will skip deletion of repository files to avoid API errors, as archived repositories are read-only. The files will be removed from Terraform state without attempting to delete them from GitHub.
1314

1415
## Example Usage
1516

0 commit comments

Comments
 (0)