Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
33 changes: 33 additions & 0 deletions github/repository_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package github
import (
"context"
"fmt"
"log"
"net/http"
"strings"

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

// isArchivedRepositoryError checks if an error is a 403 "repository archived" error.
// Returns true if the repository is archived.
func isArchivedRepositoryError(err error) bool {
if ghErr, ok := err.(*github.ErrorResponse); ok {
if ghErr.Response.StatusCode == http.StatusForbidden {
return strings.Contains(strings.ToLower(ghErr.Message), "archived")
}
}
return false
}

// handleArchivedRepositoryError handles errors for operations on archived repositories.
// If the repository is archived, it logs a message and returns nil, otherwise, it returns the original error.
func handleArchivedRepositoryError(err error, operation, resource, owner, repo string) error {
if err == nil {
return nil
}

if isArchivedRepositoryError(err) {
log.Printf("[INFO] Skipping %s of %s from archived repository %s/%s", operation, resource, owner, repo)
return nil
}

return err
}

// handleArchivedRepoDelete is a convenience wrapper for handleArchivedRepositoryError
// specifically for delete operations, which is the most common use case.
func handleArchivedRepoDelete(err error, resourceType, resourceName, owner, repo string) error {
return handleArchivedRepositoryError(err, "deletion", fmt.Sprintf("%s %s", resourceType, resourceName), owner, repo)
}

// get the list of retriable errors
func getDefaultRetriableErrors() map[int]bool {
return map[int]bool{
Expand Down
5 changes: 2 additions & 3 deletions github/resource_github_issue_label.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@ func resourceGithubIssueLabelDelete(d *schema.ResourceData, meta interface{}) er
name := d.Get("name").(string)
ctx := context.WithValue(context.Background(), ctxId, d.Id())

_, err := client.Issues.DeleteLabel(ctx,
orgName, repoName, name)
return err
_, err := client.Issues.DeleteLabel(ctx, orgName, repoName, name)
return handleArchivedRepoDelete(err, "issue label", name, orgName, repoName)
}
68 changes: 68 additions & 0 deletions github/resource_github_issue_label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,72 @@ func TestAccGithubIssueLabel(t *testing.T) {
})
})

t.Run("can delete labels from archived repositories without error", func(t *testing.T) {

config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-archive-%s"
auto_init = true
}
resource "github_issue_label" "test" {
repository = github_repository.test.name
name = "archived-test-label"
color = "ff0000"
description = "Test label for archived repo"
}
`, randomID)

archivedConfig := strings.Replace(config,
`auto_init = true`,
`auto_init = true
archived = true`, 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: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_issue_label.test", "name",
"archived-test-label",
),
),
},
{
Config: archivedConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository.test", "archived",
"true",
),
),
},
// This step should succeed - the label should be removed from state
// without trying to actually delete it from the archived repo
{
Config: fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-archive-%s"
auto_init = true
archived = true
}
`, randomID),
},
},
})
}

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

}
4 changes: 4 additions & 0 deletions github/resource_github_issue_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ func resourceGithubIssueLabelsDelete(d *schema.ResourceData, meta interface{}) e

_, err := client.Issues.DeleteLabel(ctx, owner, repository, name)
if err != nil {
if isArchivedRepositoryError(err) {
log.Printf("[INFO] Skipping deletion of remaining issue labels from archived repository %s/%s", owner, repository)
break // Skip deleting remaining labels
}
return err
}
}
Expand Down
82 changes: 82 additions & 0 deletions github/resource_github_issue_labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package github
import (
"context"
"fmt"
"strings"
"testing"

"github.com/google/go-github/v67/github"
Expand Down Expand Up @@ -159,3 +160,84 @@ func testAccGithubIssueLabelsAddLabel(repository, label string) error {
_, _, err := client.Issues.CreateLabel(ctx, orgName, repository, &github.Label{Name: github.String(label)})
return err
}

func TestAccGithubIssueLabelsArchived(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

t.Run("can delete labels from archived repositories without error", func(t *testing.T) {

repoName := fmt.Sprintf("tf-acc-test-labels-archive-%s", randomID)

config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "%s"
auto_init = true
}

resource "github_issue_labels" "test" {
repository = github_repository.test.name
label {
name = "archived-label-1"
color = "ff0000"
description = "First test label"
}
label {
name = "archived-label-2"
color = "00ff00"
description = "Second test label"
}
}
`, repoName)

archivedConfig := strings.Replace(config,
`auto_init = true`,
`auto_init = true
archived = true`, 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: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_issue_labels.test", "label.#",
"2",
),
),
},
{
Config: archivedConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository.test", "archived",
"true",
),
),
},
// This step should succeed - the labels should be removed from state
// without trying to actually delete them from the archived repo
{
Config: fmt.Sprintf(`
resource "github_repository" "test" {
name = "%s"
auto_init = true
archived = true
}
`, repoName),
},
},
})
}

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)
})
})
}
6 changes: 1 addition & 5 deletions github/resource_github_repository_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,11 +504,7 @@ func resourceGithubRepositoryFileDelete(d *schema.ResourceData, meta interface{}
}

_, _, err := client.Repositories.DeleteFile(ctx, owner, repo, file, opts)
if err != nil {
return nil
}

return nil
return handleArchivedRepoDelete(err, "repository file", file, owner, repo)
}

func autoBranchDiffSuppressFunc(k, _, _ string, d *schema.ResourceData) bool {
Expand Down
72 changes: 72 additions & 0 deletions github/resource_github_repository_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,4 +344,76 @@ func TestAccGithubRepositoryFile(t *testing.T) {
})

})

t.Run("can delete files from archived repositories without error", func(t *testing.T) {

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

resource "github_repository_file" "test" {
repository = github_repository.test.name
branch = "main"
file = "archived-test.md"
content = "# Test file for archived repo"
commit_message = "Add test file"
commit_author = "Terraform User"
commit_email = "[email protected]"
}
`, randomID)

archivedConfig := strings.Replace(config,
`auto_init = true`,
`auto_init = true
archived = true`, 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: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository_file.test", "file",
"archived-test.md",
),
),
},
{
Config: archivedConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository.test", "archived",
"true",
),
),
},
// This step should succeed - the file should be removed from state
// without trying to actually delete it from the archived repo
{
Config: fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-file-archive-%s"
auto_init = true
archived = true
}
`, randomID),
},
},
})
}

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

})
}
2 changes: 2 additions & 0 deletions website/docs/r/issue_label.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ and those labels easily conflict with custom ones.
This resource will first check if the label exists, and then issue an update,
otherwise it will create.

~> **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.

## Example Usage

```hcl
Expand Down
2 changes: 2 additions & 0 deletions website/docs/r/issue_labels.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This resource is authoritative. For adding a label to a repo in a non-authoritat

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.

~> **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.

## Example Usage

```hcl
Expand Down
1 change: 1 addition & 0 deletions website/docs/r/repository_file.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ description: |-
This resource allows you to create and manage files within a
GitHub repository.

~> **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.

## Example Usage

Expand Down
Loading