diff --git a/github/resource_github_branch_protection.go b/github/resource_github_branch_protection.go index 7faa34320f..1ee5873689 100644 --- a/github/resource_github_branch_protection.go +++ b/github/resource_github_branch_protection.go @@ -133,6 +133,8 @@ func resourceGithubBranchProtection() *schema.Resource { PROTECTION_REQUIRED_STATUS_CHECK_CONTEXTS: { Type: schema.TypeSet, Optional: true, + Computed: true, + Deprecated: "GitHub is deprecating the use of `contexts`. Use a `checks` array instead.", Description: "The list of status checks to require in order to merge into this branch. No status checks are required by default.", Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -293,6 +295,12 @@ func resourceGithubBranchProtectionRead(d *schema.ResourceData, meta any) error } protection := query.Node.Node + if protection.Repository.IsArchived { + log.Printf("[INFO] Removing branch protection (%s) from state because the repository (%s) is archived", d.Id(), protection.Repository.Name) + d.SetId("") + return nil + } + err = d.Set(PROTECTION_PATTERN, protection.Pattern) if err != nil { log.Printf("[DEBUG] Problem setting '%s' in %s %s branch protection (%s)", PROTECTION_PATTERN, protection.Repository.Name, protection.Pattern, d.Id()) diff --git a/github/resource_github_branch_protection_test.go b/github/resource_github_branch_protection_test.go index 6fa000258d..ea24146145 100644 --- a/github/resource_github_branch_protection_test.go +++ b/github/resource_github_branch_protection_test.go @@ -9,7 +9,10 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubBranchProtectionV4(t *testing.T) { @@ -164,6 +167,54 @@ func TestAccGithubBranchProtectionV4(t *testing.T) { }) }) + t.Run("removes from state when repository is archived", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + testRepoName := fmt.Sprintf("%sbranch-protection-%s", testResourcePrefix, randomID) + + configTemplate := ` + resource "github_repository" "test" { + name = "%s" + auto_init = true + archived = %t + } + + resource "github_branch_protection" "test" { + repository_id = github_repository.test.node_id + pattern = "main" + } + ` + + config := fmt.Sprintf(configTemplate, testRepoName, false) + configArchived := fmt.Sprintf(configTemplate, testRepoName, true) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_protection.test", tfjsonpath.New("pattern"), knownvalue.StringExact("main")), + }, + }, + { + Config: configArchived, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository.test", tfjsonpath.New("archived"), knownvalue.Bool(true)), + }, + }, + { + Config: configArchived, + ResourceName: "github_branch_protection.test", + ImportState: true, + ImportStateVerify: false, // Should fail to import because it's removed from state + ExpectError: regexp.MustCompile(`could not find a branch protection rule`), + ImportStateIdFunc: importBranchProtectionByRepoID("github_repository.test", "main"), + }, + }, + }) + }) + t.Run("configures required status checks", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) testRepoName := fmt.Sprintf("%sbranch-protection-%s", testResourcePrefix, randomID) diff --git a/github/resource_github_branch_protection_v3.go b/github/resource_github_branch_protection_v3.go index 411ae6534e..7f1c0f9ad9 100644 --- a/github/resource_github_branch_protection_v3.go +++ b/github/resource_github_branch_protection_v3.go @@ -275,6 +275,26 @@ func resourceGithubBranchProtectionV3Read(d *schema.ResourceData, meta any) erro orgName := meta.(*Owner).name ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + repo, _, err := client.Repositories.Get(ctx, orgName, repoName) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode == http.StatusNotFound { + log.Printf("[INFO] Removing branch protection %s/%s (%s) from state because the repository no longer exists", + orgName, repoName, branch) + d.SetId("") + return nil + } + } + return err + } + if repo.GetArchived() { + log.Printf("[INFO] Removing branch protection %s/%s (%s) from state because the repository is archived", orgName, repoName, branch) + d.SetId("") + return nil + } + if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } diff --git a/github/resource_github_branch_protection_v3_test.go b/github/resource_github_branch_protection_v3_test.go index 6ef6f8cb82..6c84141861 100644 --- a/github/resource_github_branch_protection_v3_test.go +++ b/github/resource_github_branch_protection_v3_test.go @@ -414,6 +414,100 @@ func TestAccGithubBranchProtectionV3(t *testing.T) { }) }) + t.Run("removes from state when repository is archived", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + testRepoName := fmt.Sprintf("%sbranch-protection-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_protection_v3" "test" { + repository = github_repository.test.name + branch = "main" + } + `, testRepoName) + + configArchived := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + archived = true + } + + resource "github_branch_protection_v3" "test" { + repository = github_repository.test.name + branch = "main" + } + `, testRepoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_branch_protection_v3.test", "branch", "main"), + ), + }, + { + Config: configArchived, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "archived", "true"), + ), + }, + { + Config: configArchived, + ResourceName: "github_branch_protection_v3.test", + ImportState: true, + ImportStateVerify: false, // Should fail to import because it's removed from state + ExpectError: nil, // Terraform usually succeeds with an empty ID if we return nil in Read + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return fmt.Sprintf("%s:main", testRepoName), nil + }, + }, + }, + }) + }) + + t.Run("fallbacks from checks to contexts", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + testRepoName := fmt.Sprintf("%sbranch-protection-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_protection_v3" "test" { + repository = github_repository.test.name + branch = "main" + + required_status_checks { + strict = true + checks = ["ci/test"] + } + } + `, testRepoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_branch_protection_v3.test", "required_status_checks.0.checks.#", "1"), + resource.TestCheckResourceAttr("github_branch_protection_v3.test", "required_status_checks.0.contexts.#", "1"), + resource.TestCheckTypeSetElemAttr("github_branch_protection_v3.test", "required_status_checks.0.contexts.*", "ci/test"), + ), + }, + }, + }) + }) + t.Run("configures required status checks", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) testRepoName := fmt.Sprintf("%sbranch-protection-%s", testResourcePrefix, randomID) diff --git a/github/resource_github_branch_protection_v3_utils.go b/github/resource_github_branch_protection_v3_utils.go index 45cb269316..ea98f7dce6 100644 --- a/github/resource_github_branch_protection_v3_utils.go +++ b/github/resource_github_branch_protection_v3_utils.go @@ -50,18 +50,29 @@ func flattenAndSetRequiredStatusChecks(d *schema.ResourceData, protection *githu // TODO: Remove once contexts is fully deprecated. // Flatten contexts - for _, c := range *rsc.Contexts { - // Parse into contexts - contexts = append(contexts, c) + if rsc.Contexts != nil { + for _, c := range *rsc.Contexts { + // Parse into contexts + contexts = append(contexts, c) + } + } + + // Fallback to populating contexts from checks if it's empty (e.g. archived repo) + if len(contexts) == 0 && rsc.Checks != nil { + for _, chk := range *rsc.Checks { + contexts = append(contexts, chk.Context) + } } // Flatten checks - for _, chk := range *rsc.Checks { - // Parse into checks - if chk.AppID != nil { - checks = append(checks, fmt.Sprintf("%s:%d", chk.Context, *chk.AppID)) - } else { - checks = append(checks, chk.Context) + if rsc.Checks != nil { + for _, chk := range *rsc.Checks { + // Parse into checks + if chk.AppID != nil { + checks = append(checks, fmt.Sprintf("%s:%d", chk.Context, *chk.AppID)) + } else { + checks = append(checks, chk.Context) + } } } diff --git a/github/util_v4_branch_protection.go b/github/util_v4_branch_protection.go index 5cf91536e5..48f215dda1 100644 --- a/github/util_v4_branch_protection.go +++ b/github/util_v4_branch_protection.go @@ -56,8 +56,9 @@ type PushActorTypes struct { type BranchProtectionRule struct { Repository struct { - ID githubv4.String - Name githubv4.String + ID githubv4.String + Name githubv4.String + IsArchived githubv4.Boolean } PushAllowances struct { Nodes []PushActorTypes