From a58deefe40b745b22ba64f6af943ba076847ebbe Mon Sep 17 00:00:00 2001 From: Youqing Han Date: Wed, 25 Mar 2026 16:32:37 +0800 Subject: [PATCH 1/4] Update branch protection for archiving repos --- github/resource_github_branch_protection.go | 46 +++++++++++++++++++ .../resource_github_branch_protection_v3.go | 38 +++++++++++++++ ...ource_github_branch_protection_v3_utils.go | 29 ++++++++---- github/util_v4_branch_protection.go | 5 +- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/github/resource_github_branch_protection.go b/github/resource_github_branch_protection.go index 7faa34320f..3be08df5e5 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()) @@ -366,6 +374,25 @@ func resourceGithubBranchProtectionRead(d *schema.ResourceData, meta any) error } func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta any) error { + var query struct { + Node struct { + Node BranchProtectionRule `graphql:"... on BranchProtectionRule"` + } `graphql:"node(id: $id)"` + } + variables := map[string]any{ + "id": d.Id(), + } + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + client := meta.(*Owner).v4client + err := client.Query(ctx, &query, variables) + if err == nil { + protection := query.Node.Node + if protection.Repository.IsArchived { + log.Printf("[INFO] Skipping update of branch protection (%s) because the repository (%s) is archived", d.Id(), protection.Repository.Name) + return nil + } + } + var mutate struct { UpdateBranchProtectionRule struct { BranchProtectionRule struct { @@ -444,6 +471,25 @@ func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta any) erro } func resourceGithubBranchProtectionDelete(d *schema.ResourceData, meta any) error { + var query struct { + Node struct { + Node BranchProtectionRule `graphql:"... on BranchProtectionRule"` + } `graphql:"node(id: $id)"` + } + variables := map[string]any{ + "id": d.Id(), + } + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + client := meta.(*Owner).v4client + err := client.Query(ctx, &query, variables) + if err == nil { + protection := query.Node.Node + if protection.Repository.IsArchived { + log.Printf("[INFO] Skipping deletion of branch protection (%s) because the repository (%s) is archived", d.Id(), protection.Repository.Name) + return nil + } + } + var mutate struct { DeleteBranchProtectionRule struct { // Empty struct does not work ClientMutationId githubv4.ID diff --git a/github/resource_github_branch_protection_v3.go b/github/resource_github_branch_protection_v3.go index 411ae6534e..7b06c2da90 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)) } @@ -349,6 +369,16 @@ func resourceGithubBranchProtectionV3Update(d *schema.ResourceData, meta any) er if err != nil { return err } + orgName := meta.(*Owner).name + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + repo, _, err := client.Repositories.Get(ctx, orgName, repoName) + if err == nil { + if repo.GetArchived() { + log.Printf("[INFO] Skipping update of branch protection %s/%s (%s) because the repository is archived", orgName, repoName, branch) + return nil + } + } protectionRequest, err := buildProtectionRequest(d) if err != nil { @@ -407,6 +437,14 @@ func resourceGithubBranchProtectionV3Delete(d *schema.ResourceData, meta any) er orgName := meta.(*Owner).name ctx := context.WithValue(context.Background(), ctxId, d.Id()) + repo, _, err := client.Repositories.Get(ctx, orgName, repoName) + if err == nil { + if repo.GetArchived() { + log.Printf("[INFO] Skipping deletion of branch protection %s/%s (%s) because the repository is archived", orgName, repoName, branch) + return nil + } + } + _, err = client.Repositories.RemoveBranchProtection(ctx, orgName, repoName, branch) return err 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 From 8907ba6c9518b8afdfb9f2e0aca23a1eddb8cec7 Mon Sep 17 00:00:00 2001 From: Youqing Han Date: Fri, 27 Mar 2026 10:19:53 +0800 Subject: [PATCH 2/4] Remove unnecessary changes as suggestion --- github/resource_github_branch_protection.go | 38 ------------------- .../resource_github_branch_protection_v3.go | 18 --------- 2 files changed, 56 deletions(-) diff --git a/github/resource_github_branch_protection.go b/github/resource_github_branch_protection.go index 3be08df5e5..1ee5873689 100644 --- a/github/resource_github_branch_protection.go +++ b/github/resource_github_branch_protection.go @@ -374,25 +374,6 @@ func resourceGithubBranchProtectionRead(d *schema.ResourceData, meta any) error } func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta any) error { - var query struct { - Node struct { - Node BranchProtectionRule `graphql:"... on BranchProtectionRule"` - } `graphql:"node(id: $id)"` - } - variables := map[string]any{ - "id": d.Id(), - } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - client := meta.(*Owner).v4client - err := client.Query(ctx, &query, variables) - if err == nil { - protection := query.Node.Node - if protection.Repository.IsArchived { - log.Printf("[INFO] Skipping update of branch protection (%s) because the repository (%s) is archived", d.Id(), protection.Repository.Name) - return nil - } - } - var mutate struct { UpdateBranchProtectionRule struct { BranchProtectionRule struct { @@ -471,25 +452,6 @@ func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta any) erro } func resourceGithubBranchProtectionDelete(d *schema.ResourceData, meta any) error { - var query struct { - Node struct { - Node BranchProtectionRule `graphql:"... on BranchProtectionRule"` - } `graphql:"node(id: $id)"` - } - variables := map[string]any{ - "id": d.Id(), - } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - client := meta.(*Owner).v4client - err := client.Query(ctx, &query, variables) - if err == nil { - protection := query.Node.Node - if protection.Repository.IsArchived { - log.Printf("[INFO] Skipping deletion of branch protection (%s) because the repository (%s) is archived", d.Id(), protection.Repository.Name) - return nil - } - } - var mutate struct { DeleteBranchProtectionRule struct { // Empty struct does not work ClientMutationId githubv4.ID diff --git a/github/resource_github_branch_protection_v3.go b/github/resource_github_branch_protection_v3.go index 7b06c2da90..7f1c0f9ad9 100644 --- a/github/resource_github_branch_protection_v3.go +++ b/github/resource_github_branch_protection_v3.go @@ -369,16 +369,6 @@ func resourceGithubBranchProtectionV3Update(d *schema.ResourceData, meta any) er if err != nil { return err } - orgName := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - - repo, _, err := client.Repositories.Get(ctx, orgName, repoName) - if err == nil { - if repo.GetArchived() { - log.Printf("[INFO] Skipping update of branch protection %s/%s (%s) because the repository is archived", orgName, repoName, branch) - return nil - } - } protectionRequest, err := buildProtectionRequest(d) if err != nil { @@ -437,14 +427,6 @@ func resourceGithubBranchProtectionV3Delete(d *schema.ResourceData, meta any) er orgName := meta.(*Owner).name ctx := context.WithValue(context.Background(), ctxId, d.Id()) - repo, _, err := client.Repositories.Get(ctx, orgName, repoName) - if err == nil { - if repo.GetArchived() { - log.Printf("[INFO] Skipping deletion of branch protection %s/%s (%s) because the repository is archived", orgName, repoName, branch) - return nil - } - } - _, err = client.Repositories.RemoveBranchProtection(ctx, orgName, repoName, branch) return err From 9d0b42feb4b81a827040c86f78ef5cb2abc6dd02 Mon Sep 17 00:00:00 2001 From: Youqing Han Date: Sat, 28 Mar 2026 10:49:08 +0800 Subject: [PATCH 3/4] Add test --- .../resource_github_branch_protection_test.go | 56 +++++++++++ ...source_github_branch_protection_v3_test.go | 94 +++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/github/resource_github_branch_protection_test.go b/github/resource_github_branch_protection_test.go index 6fa000258d..d03f871359 100644 --- a/github/resource_github_branch_protection_test.go +++ b/github/resource_github_branch_protection_test.go @@ -164,6 +164,62 @@ 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) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_protection" "test" { + repository_id = github_repository.test.node_id + pattern = "main" + } + `, testRepoName) + + configArchived := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + archived = true + } + + resource "github_branch_protection" "test" { + repository_id = github_repository.test.node_id + pattern = "main" + } + `, testRepoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_branch_protection.test", "pattern", "main"), + ), + }, + { + Config: configArchived, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "archived", "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_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) From cde58f4d384ab9021ea4193f7b91a3f7ce9022b1 Mon Sep 17 00:00:00 2001 From: Youqing Han Date: Mon, 30 Mar 2026 15:52:03 +0800 Subject: [PATCH 4/4] Update test as suggested --- .../resource_github_branch_protection_test.go | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/github/resource_github_branch_protection_test.go b/github/resource_github_branch_protection_test.go index d03f871359..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) { @@ -167,30 +170,22 @@ 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) - config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - auto_init = true - } - resource "github_branch_protection" "test" { - repository_id = github_repository.test.node_id - pattern = "main" - } - `, testRepoName) - - configArchived := fmt.Sprintf(` + configTemplate := ` resource "github_repository" "test" { name = "%s" auto_init = true - archived = true + archived = %t } resource "github_branch_protection" "test" { repository_id = github_repository.test.node_id pattern = "main" } - `, testRepoName) + ` + + config := fmt.Sprintf(configTemplate, testRepoName, false) + configArchived := fmt.Sprintf(configTemplate, testRepoName, true) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, @@ -198,15 +193,15 @@ func TestAccGithubBranchProtectionV4(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("github_branch_protection.test", "pattern", "main"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_protection.test", tfjsonpath.New("pattern"), knownvalue.StringExact("main")), + }, }, { Config: configArchived, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("github_repository.test", "archived", "true"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository.test", tfjsonpath.New("archived"), knownvalue.Bool(true)), + }, }, { Config: configArchived,