From 8a3178d8199fc3560e72ac7930f8709dfc71b02d Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 19:51:42 +0200 Subject: [PATCH 01/23] Refactor to use Context-aware functions Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 44 ++++++++++-------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 388ce4f9f8..2267d75ba2 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -7,15 +7,16 @@ import ( "net/http" "github.com/google/go-github/v88/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubBranchDefault() *schema.Resource { return &schema.Resource{ - Create: resourceGithubBranchDefaultCreate, - Read: resourceGithubBranchDefaultRead, - Delete: resourceGithubBranchDefaultDelete, - Update: resourceGithubBranchDefaultUpdate, + CreateContext: resourceGithubBranchDefaultCreate, + ReadContext: resourceGithubBranchDefaultRead, + DeleteContext: resourceGithubBranchDefaultDelete, + UpdateContext: resourceGithubBranchDefaultUpdate, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -51,24 +52,22 @@ func resourceGithubBranchDefault() *schema.Resource { } } -func resourceGithubBranchDefaultCreate(d *schema.ResourceData, meta any) error { +func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Get("repository").(string) defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) - ctx := context.Background() - repository, _, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } if *repository.DefaultBranch != defaultBranch { if rename { if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, *repository.DefaultBranch, defaultBranch); err != nil { - return err + return diag.FromErr(err) } } else { repository := &github.Repository{ @@ -76,22 +75,21 @@ func resourceGithubBranchDefaultCreate(d *schema.ResourceData, meta any) error { } if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { - return err + return diag.FromErr(err) } } } d.SetId(repoName) - return resourceGithubBranchDefaultRead(d, meta) + return resourceGithubBranchDefaultRead(ctx, d, meta) } -func resourceGithubBranchDefaultRead(d *schema.ResourceData, meta any) error { +func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Id() - ctx := context.WithValue(context.Background(), ctxId, d.Id()) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -110,7 +108,7 @@ func resourceGithubBranchDefaultRead(d *schema.ResourceData, meta any) error { return nil } } - return err + return diag.FromErr(err) } if repository.DefaultBranch == nil { @@ -124,7 +122,7 @@ func resourceGithubBranchDefaultRead(d *schema.ResourceData, meta any) error { return nil } -func resourceGithubBranchDefaultDelete(d *schema.ResourceData, meta any) error { +func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Id() @@ -133,28 +131,24 @@ func resourceGithubBranchDefaultDelete(d *schema.ResourceData, meta any) error { DefaultBranch: nil, } - ctx := context.Background() - _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) - return err + return diag.FromErr(err) } -func resourceGithubBranchDefaultUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Id() defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) - ctx := context.Background() - if rename { repository, _, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, *repository.DefaultBranch, defaultBranch); err != nil { - return err + return diag.FromErr(err) } } else { repository := &github.Repository{ @@ -162,9 +156,9 @@ func resourceGithubBranchDefaultUpdate(d *schema.ResourceData, meta any) error { } if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { - return err + return diag.FromErr(err) } } - return resourceGithubBranchDefaultRead(d, meta) + return resourceGithubBranchDefaultRead(ctx, d, meta) } From 81060075c054d645528fad63afb362cf1579de3b Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 19:52:14 +0200 Subject: [PATCH 02/23] Sort CRUD functions Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 2267d75ba2..958ea325da 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -15,8 +15,8 @@ func resourceGithubBranchDefault() *schema.Resource { return &schema.Resource{ CreateContext: resourceGithubBranchDefaultCreate, ReadContext: resourceGithubBranchDefaultRead, - DeleteContext: resourceGithubBranchDefaultDelete, UpdateContext: resourceGithubBranchDefaultUpdate, + DeleteContext: resourceGithubBranchDefaultDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -122,19 +122,6 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData return nil } -func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - repoName := d.Id() - - repository := &github.Repository{ - DefaultBranch: nil, - } - - _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) - return diag.FromErr(err) -} - func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name @@ -162,3 +149,16 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa return resourceGithubBranchDefaultRead(ctx, d, meta) } + +func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + owner := meta.(*Owner).name + repoName := d.Id() + + repository := &github.Repository{ + DefaultBranch: nil, + } + + _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) + return diag.FromErr(err) +} From 84923133faa5b3f5d1ba08be17255ad00d1d1b60 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 20:08:26 +0200 Subject: [PATCH 03/23] Updates naming and error handling Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 75 +++++++++++++++++------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 958ea325da..ca2457e3e6 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -19,6 +19,7 @@ func resourceGithubBranchDefault() *schema.Resource { DeleteContext: resourceGithubBranchDefaultDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, + // StateContext: resourceGithubBranchDefaultImport, }, Schema: map[string]*schema.Schema{ @@ -33,6 +34,7 @@ func resourceGithubBranchDefault() *schema.Resource { ForceNew: true, Description: "The GitHub repository.", }, + // TODO add repository_id and diffRepository to handle repository renames "rename": { Type: schema.TypeBool, Optional: true, @@ -52,9 +54,11 @@ func resourceGithubBranchDefault() *schema.Resource { } } -func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name +func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + repoName := d.Get("repository").(string) defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) @@ -64,14 +68,14 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } - if *repository.DefaultBranch != defaultBranch { + if repository.GetDefaultBranch() != defaultBranch { if rename { - if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, *repository.DefaultBranch, defaultBranch); err != nil { + if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { return diag.FromErr(err) } } else { repository := &github.Repository{ - DefaultBranch: &defaultBranch, + DefaultBranch: github.Ptr(defaultBranch), } if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { @@ -85,9 +89,12 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa return resourceGithubBranchDefaultRead(ctx, d, meta) } -func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name +func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + // repoName := d.Get("repository").(string) repoName := d.Id() if !d.IsNewResource() { @@ -116,15 +123,24 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData return nil } - _ = d.Set("etag", resp.Header.Get("ETag")) - _ = d.Set("branch", *repository.DefaultBranch) - _ = d.Set("repository", *repository.Name) + if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + if err := d.Set("branch", repository.GetDefaultBranch()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("repository", repository.GetName()); err != nil { + return diag.FromErr(err) + } return nil } -func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name +func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + // repoName := d.Get("repository").(string) repoName := d.Id() defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) @@ -134,12 +150,12 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa if err != nil { return diag.FromErr(err) } - if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, *repository.DefaultBranch, defaultBranch); err != nil { + if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { return diag.FromErr(err) } } else { repository := &github.Repository{ - DefaultBranch: &defaultBranch, + DefaultBranch: github.Ptr(defaultBranch), } if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { @@ -150,9 +166,11 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa return resourceGithubBranchDefaultRead(ctx, d, meta) } -func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name +func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + // repoName := d.Get("repository").(string) repoName := d.Id() repository := &github.Repository{ @@ -162,3 +180,20 @@ func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceDa _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) return diag.FromErr(err) } + +// func resourceGithubBranchDefaultImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { +// repoName, defaultBranch, err := parseID2(d.Id()) +// if err != nil { +// return nil, err +// } + +// d.SetId(repoName) +// if err := d.Set("branch", defaultBranch); err != nil { +// return nil, err +// } +// if err := d.Set("repository", repoName); err != nil { +// return nil, err +// } + +// return []*schema.ResourceData{d}, nil +// } From 48528a1b62024d96c71cf82415b2722a065db4d6 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 21:16:30 +0200 Subject: [PATCH 04/23] Adds test cases for Update and Import functionality Signed-off-by: Timo Sand --- github/resource_github_branch_default_test.go | 178 ++++++++++++++++-- 1 file changed, 158 insertions(+), 20 deletions(-) diff --git a/github/resource_github_branch_default_test.go b/github/resource_github_branch_default_test.go index 47855e32b4..a512a18485 100644 --- a/github/resource_github_branch_default_test.go +++ b/github/resource_github_branch_default_test.go @@ -4,14 +4,19 @@ import ( "fmt" "testing" + "github.com/hashicorp/terraform-plugin-testing/compare" "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/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubBranchDefault(t *testing.T) { - t.Run("creates and manages branch defaults", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("creates_as_import_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { @@ -48,9 +53,9 @@ func TestAccGithubBranchDefault(t *testing.T) { }) }) - t.Run("replaces the default_branch of a repository", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("creates_default_branch_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { name = "%s" @@ -69,20 +74,15 @@ func TestAccGithubBranchDefault(t *testing.T) { `, repoName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_branch_default.test", "branch", - "test", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_branch_default.test", "branch", "test"), + ), }, { Config: ` @@ -96,9 +96,9 @@ func TestAccGithubBranchDefault(t *testing.T) { }) }) - t.Run("creates and manages branch defaults even if rename is set", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("creates_as_import_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { name = "%s" @@ -135,9 +135,9 @@ func TestAccGithubBranchDefault(t *testing.T) { }) }) - t.Run("replaces the default_branch of a repository without creating a branch resource prior to", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-branch-def-%s", testResourcePrefix, randomID) + t.Run("creates_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_repository" "test" { name = "%s" @@ -162,4 +162,142 @@ func TestAccGithubBranchDefault(t *testing.T) { }, }) }) + + t.Run("updates_default_branch_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + + config := ` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + resource "github_branch" "test" { + repository = github_repository.test.name + branch = "test" + } + + resource "github_branch_default" "test" { + repository = github_repository.test.name + branch = "%s" + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, "main"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "github_branch_default.test", tfjsonpath.New("branch"), + "github_repository.test", tfjsonpath.New("default_branch"), + compare.ValuesSame(), + ), + }, + }, + { + Config: fmt.Sprintf(config, repoName, "test"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_branch_default.test", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("test")), + }, + }, + { + Config: ` + removed { + from = github_branch.test + lifecycle { destroy = false } + } + `, + }, + }, + }) + }) + + t.Run("updates_default_branch_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + + config := ` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test" { + repository = github_repository.test.name + branch = "%s" + rename = true + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, "main"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "github_branch_default.test", tfjsonpath.New("branch"), + "github_repository.test", tfjsonpath.New("default_branch"), + compare.ValuesSame(), + ), + }, + }, + { + Config: fmt.Sprintf(config, repoName, "development"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_branch_default.test", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + }, + }) + }) + t.Run("imports_with_rename_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test"{ + repository = github_repository.test.name + branch = "development" + rename = true + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + { + ResourceName: "github_branch_default.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"rename", "etag"}, + }, + }, + }) + }) } From 0ca131d1aa80f77fb711d4a6a56988df27f5917a Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 21:27:52 +0200 Subject: [PATCH 05/23] Refactor to use `tflog` Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 60 ++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index ca2457e3e6..1c84f58c31 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -3,10 +3,10 @@ package github import ( "context" "errors" - "log" "net/http" "github.com/google/go-github/v88/github" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -63,17 +63,30 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) + tflog.Trace(ctx, "Creating default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + "branch": defaultBranch, + "rename": rename, + }) + repository, _, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { return diag.FromErr(err) } + tflog.Debug(ctx, "Fetched repository", map[string]any{ + "current_default_branch": repository.GetDefaultBranch(), + }) + if repository.GetDefaultBranch() != defaultBranch { if rename { + tflog.Debug(ctx, "Renaming branch to new default") if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { return diag.FromErr(err) } } else { + tflog.Debug(ctx, "Setting new default branch") repository := &github.Repository{ DefaultBranch: github.Ptr(defaultBranch), } @@ -82,14 +95,20 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } } + } else { + tflog.Debug(ctx, "Default branch already set to desired branch, skipping update") } d.SetId(repoName) + tflog.Trace(ctx, "Finished creating default branch resource", map[string]any{ + "resource_id": d.Id(), + }) return resourceGithubBranchDefaultRead(ctx, d, meta) } func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "resource_id", d.Id()) meta := m.(*Owner) client := meta.v3client owner := meta.name @@ -97,6 +116,11 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData // repoName := d.Get("repository").(string) repoName := d.Id() + tflog.Trace(ctx, "Reading default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + }) + if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -106,11 +130,14 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotModified { + tflog.Debug(ctx, "Repository not modified, skipping read") return nil } if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[INFO] Removing repository %s/%s from state because it no longer exists in GitHub", - owner, repoName) + tflog.Info(ctx, "Removing repository from state because it no longer exists in GitHub", map[string]any{ + "owner": owner, + "repository": repoName, + }) d.SetId("") return nil } @@ -119,6 +146,7 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData } if repository.DefaultBranch == nil { + tflog.Warn(ctx, "Default branch is nil, removing resource from state") d.SetId("") return nil } @@ -132,10 +160,13 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData if err := d.Set("repository", repository.GetName()); err != nil { return diag.FromErr(err) } + + tflog.Trace(ctx, "Finished reading default branch resource") return nil } func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "resource_id", d.Id()) meta := m.(*Owner) client := meta.v3client owner := meta.name @@ -145,7 +176,15 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) + tflog.Trace(ctx, "Updating default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + "branch": defaultBranch, + "rename": rename, + }) + if rename { + tflog.Debug(ctx, "Renaming branch to new default") repository, _, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { return diag.FromErr(err) @@ -154,6 +193,7 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } } else { + tflog.Debug(ctx, "Setting new default branch") repository := &github.Repository{ DefaultBranch: github.Ptr(defaultBranch), } @@ -163,22 +203,34 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa } } + tflog.Trace(ctx, "Finished updating default branch resource") return resourceGithubBranchDefaultRead(ctx, d, meta) } func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "resource_id", d.Id()) meta := m.(*Owner) client := meta.v3client owner := meta.name // repoName := d.Get("repository").(string) repoName := d.Id() + tflog.Trace(ctx, "Deleting default branch resource", map[string]any{ + "owner": owner, + "repository": repoName, + }) + repository := &github.Repository{ DefaultBranch: nil, } _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) - return diag.FromErr(err) + if err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, "Finished deleting default branch resource") + return nil } // func resourceGithubBranchDefaultImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { From 7b201e363fd3836fd7ba601d99e761cecc4ec978 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 21:35:00 +0200 Subject: [PATCH 06/23] Remove `Read` calls from `Update` and `Create` Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 1c84f58c31..946000913b 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -70,7 +70,7 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa "rename": rename, }) - repository, _, err := client.Repositories.Get(ctx, owner, repoName) + repository, resp, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { return diag.FromErr(err) } @@ -101,10 +101,15 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa d.SetId(repoName) + if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } + tflog.Trace(ctx, "Finished creating default branch resource", map[string]any{ "resource_id": d.Id(), }) - return resourceGithubBranchDefaultRead(ctx, d, meta) + + return nil } func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { @@ -183,12 +188,15 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa "rename": rename, }) + var etag string + if rename { tflog.Debug(ctx, "Renaming branch to new default") - repository, _, err := client.Repositories.Get(ctx, owner, repoName) + repository, resp, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { return diag.FromErr(err) } + etag = resp.Header.Get("ETag") if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { return diag.FromErr(err) } @@ -198,13 +206,18 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa DefaultBranch: github.Ptr(defaultBranch), } - if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { + if _, resp, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { return diag.FromErr(err) + } else { + etag = resp.Header.Get("ETag") } } + if err := d.Set("etag", etag); err != nil { + return diag.FromErr(err) + } tflog.Trace(ctx, "Finished updating default branch resource") - return resourceGithubBranchDefaultRead(ctx, d, meta) + return nil } func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { From c665e57d5c671736d7d2654a070e446cfafa26a6 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 21:57:23 +0200 Subject: [PATCH 07/23] Update tests to use `ConfigStateChecks` Signed-off-by: Timo Sand --- github/resource_github_branch_default_test.go | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/github/resource_github_branch_default_test.go b/github/resource_github_branch_default_test.go index a512a18485..0b993912cb 100644 --- a/github/resource_github_branch_default_test.go +++ b/github/resource_github_branch_default_test.go @@ -30,24 +30,18 @@ func TestAccGithubBranchDefault(t *testing.T) { } `, repoName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_branch_default.test", "branch", - "main", - ), - resource.TestCheckResourceAttr( - "github_branch_default.test", "repository", - repoName, - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("branch"), "github_repository.test", tfjsonpath.New("default_branch"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, }, }, }) @@ -80,9 +74,12 @@ func TestAccGithubBranchDefault(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_branch_default.test", "branch", "test"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("branch"), "github_branch.test", tfjsonpath.New("branch"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, }, { Config: ` @@ -112,24 +109,18 @@ func TestAccGithubBranchDefault(t *testing.T) { } `, repoName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_branch_default.test", "branch", - "main", - ), - resource.TestCheckResourceAttr( - "github_branch_default.test", "repository", - repoName, - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("branch"), "github_repository.test", tfjsonpath.New("default_branch"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, }, }, }) @@ -157,7 +148,12 @@ func TestAccGithubBranchDefault(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc(resource.TestCheckResourceAttr("github_branch_default.test", "branch", "development")), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs("github_branch_default.test", tfjsonpath.New("repository"), "github_repository.test", tfjsonpath.New("name"), compare.ValuesSame()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("repository_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("etag"), knownvalue.NotNull()), + }, }, }, }) From 85f792411bfdd33e691247308b93852d974f6b2d Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 21:58:28 +0200 Subject: [PATCH 08/23] Adds `repository_id` field Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 946000913b..59a38cf821 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -22,6 +22,8 @@ func resourceGithubBranchDefault() *schema.Resource { // StateContext: resourceGithubBranchDefaultImport, }, + CustomizeDiff: diffRepository, + Schema: map[string]*schema.Schema{ "branch": { Type: schema.TypeString, @@ -31,10 +33,13 @@ func resourceGithubBranchDefault() *schema.Resource { "repository": { Type: schema.TypeString, Required: true, - ForceNew: true, Description: "The GitHub repository.", }, - // TODO add repository_id and diffRepository to handle repository renames + "repository_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The GitHub repository ID.", + }, "rename": { Type: schema.TypeBool, Optional: true, @@ -101,6 +106,9 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa d.SetId(repoName) + if err := d.Set("repository_id", int(repository.GetID())); err != nil { + return diag.FromErr(err) + } if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { return diag.FromErr(err) } @@ -156,13 +164,15 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData return nil } - if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { + if err := d.Set("repository_id", int(repository.GetID())); err != nil { return diag.FromErr(err) } - if err := d.Set("branch", repository.GetDefaultBranch()); err != nil { + + if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { return diag.FromErr(err) } - if err := d.Set("repository", repository.GetName()); err != nil { + + if err := d.Set("branch", repository.GetDefaultBranch()); err != nil { return diag.FromErr(err) } From 27e59113a33a57e64f54fbdfba397cad480d9208 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 21:59:36 +0200 Subject: [PATCH 09/23] Adds custom `Import` function Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 37 +++++++++--------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 59a38cf821..58d9d2467e 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -18,8 +18,7 @@ func resourceGithubBranchDefault() *schema.Resource { UpdateContext: resourceGithubBranchDefaultUpdate, DeleteContext: resourceGithubBranchDefaultDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - // StateContext: resourceGithubBranchDefaultImport, + StateContext: resourceGithubBranchDefaultImport, }, CustomizeDiff: diffRepository, @@ -126,8 +125,7 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData client := meta.v3client owner := meta.name - // repoName := d.Get("repository").(string) - repoName := d.Id() + repoName := d.Get("repository").(string) tflog.Trace(ctx, "Reading default branch resource", map[string]any{ "owner": owner, @@ -186,8 +184,7 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa client := meta.v3client owner := meta.name - // repoName := d.Get("repository").(string) - repoName := d.Id() + repoName := d.Get("repository").(string) defaultBranch := d.Get("branch").(string) rename := d.Get("rename").(bool) @@ -235,8 +232,7 @@ func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceDa meta := m.(*Owner) client := meta.v3client owner := meta.name - // repoName := d.Get("repository").(string) - repoName := d.Id() + repoName := d.Get("repository").(string) tflog.Trace(ctx, "Deleting default branch resource", map[string]any{ "owner": owner, @@ -256,19 +252,12 @@ func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceDa return nil } -// func resourceGithubBranchDefaultImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { -// repoName, defaultBranch, err := parseID2(d.Id()) -// if err != nil { -// return nil, err -// } - -// d.SetId(repoName) -// if err := d.Set("branch", defaultBranch); err != nil { -// return nil, err -// } -// if err := d.Set("repository", repoName); err != nil { -// return nil, err -// } - -// return []*schema.ResourceData{d}, nil -// } +func resourceGithubBranchDefaultImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + repoName := d.Id() + + if err := d.Set("repository", repoName); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} From 8de8716ff3ca8cd22fdcdbaa61d2e9d76aa149b3 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 21 Feb 2026 22:00:03 +0200 Subject: [PATCH 10/23] Updates docs Signed-off-by: Timo Sand --- docs/resources/branch_default.md | 8 +++++++- templates/resources/branch_default.md.tmpl | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/resources/branch_default.md b/docs/resources/branch_default.md index d0c4e8e092..49ff7a7da1 100644 --- a/docs/resources/branch_default.md +++ b/docs/resources/branch_default.md @@ -58,9 +58,15 @@ The following arguments are supported: - `branch` - (Required) The branch (e.g. `main`) - `rename` - (Optional) Indicate if it should rename the branch rather than use an existing branch. Defaults to `false`. +## Attribute Reference + +The following attributes are exported: + +- `repository_id` - The GitHub repository ID. + ## Import -GitHub Branch Defaults can be imported using an ID made up of `repository`, e.g. +GitHub Branch Defaults can be imported using the repository name, e.g. ```shell terraform import github_branch_default.branch_default my-repo diff --git a/templates/resources/branch_default.md.tmpl b/templates/resources/branch_default.md.tmpl index ab3fb7fe27..7a9748654e 100644 --- a/templates/resources/branch_default.md.tmpl +++ b/templates/resources/branch_default.md.tmpl @@ -30,9 +30,15 @@ The following arguments are supported: - `branch` - (Required) The branch (e.g. `main`) - `rename` - (Optional) Indicate if it should rename the branch rather than use an existing branch. Defaults to `false`. +## Attribute Reference + +The following attributes are exported: + +- `repository_id` - The GitHub repository ID. + ## Import -GitHub Branch Defaults can be imported using an ID made up of `repository`, e.g. +GitHub Branch Defaults can be imported using the repository name, e.g. ```shell terraform import github_branch_default.branch_default my-repo From 0b285e2771d3b37f634c57dcf9c239b2cbf794ca Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Mon, 11 May 2026 05:09:09 +0300 Subject: [PATCH 11/23] `gofmt` Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 58d9d2467e..580355bbef 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -92,7 +92,7 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa } else { tflog.Debug(ctx, "Setting new default branch") repository := &github.Repository{ - DefaultBranch: github.Ptr(defaultBranch), + DefaultBranch: new(defaultBranch), } if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { @@ -210,7 +210,7 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa } else { tflog.Debug(ctx, "Setting new default branch") repository := &github.Repository{ - DefaultBranch: github.Ptr(defaultBranch), + DefaultBranch: new(defaultBranch), } if _, resp, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { From 391b0d54f4388f90fd7d43a7b0ef759679670f01 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Wed, 3 Jun 2026 21:25:27 +0300 Subject: [PATCH 12/23] Make `forcetypeassert` linter happy Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 60 +++++++++++++++++++----- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 580355bbef..805fc0a4be 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -59,13 +59,25 @@ func resourceGithubBranchDefault() *schema.Resource { } func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { - meta := m.(*Owner) + meta, ok := m.(*Owner) + if !ok { + return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) + } client := meta.v3client owner := meta.name - repoName := d.Get("repository").(string) - defaultBranch := d.Get("branch").(string) - rename := d.Get("rename").(bool) + repoName, ok := d.Get("repository").(string) + if !ok { + return diag.FromErr(errors.New("repository must be a string")) + } + defaultBranch, ok := d.Get("branch").(string) + if !ok { + return diag.FromErr(errors.New("branch must be a string")) + } + rename, ok := d.Get("rename").(bool) + if !ok { + return diag.FromErr(errors.New("rename must be a boolean")) + } tflog.Trace(ctx, "Creating default branch resource", map[string]any{ "owner": owner, @@ -121,11 +133,17 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { ctx = tflog.SetField(ctx, "resource_id", d.Id()) - meta := m.(*Owner) + meta, ok := m.(*Owner) + if !ok { + return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) + } client := meta.v3client owner := meta.name - repoName := d.Get("repository").(string) + repoName, ok := d.Get("repository").(string) + if !ok { + return diag.FromErr(errors.New("repository must be a string")) + } tflog.Trace(ctx, "Reading default branch resource", map[string]any{ "owner": owner, @@ -180,13 +198,25 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { ctx = tflog.SetField(ctx, "resource_id", d.Id()) - meta := m.(*Owner) + meta, ok := m.(*Owner) + if !ok { + return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) + } client := meta.v3client owner := meta.name - repoName := d.Get("repository").(string) - defaultBranch := d.Get("branch").(string) - rename := d.Get("rename").(bool) + repoName, ok := d.Get("repository").(string) + if !ok { + return diag.FromErr(errors.New("repository must be a string")) + } + defaultBranch, ok := d.Get("branch").(string) + if !ok { + return diag.FromErr(errors.New("branch must be a string")) + } + rename, ok := d.Get("rename").(bool) + if !ok { + return diag.FromErr(errors.New("rename must be a boolean")) + } tflog.Trace(ctx, "Updating default branch resource", map[string]any{ "owner": owner, @@ -229,10 +259,16 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { ctx = tflog.SetField(ctx, "resource_id", d.Id()) - meta := m.(*Owner) + meta, ok := m.(*Owner) + if !ok { + return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) + } client := meta.v3client owner := meta.name - repoName := d.Get("repository").(string) + repoName, ok := d.Get("repository").(string) + if !ok { + return diag.FromErr(errors.New("repository must be a string")) + } tflog.Trace(ctx, "Deleting default branch resource", map[string]any{ "owner": owner, From 08d8c2791c7085961cfc824463af28bbb29cc100 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Wed, 3 Jun 2026 23:45:40 +0300 Subject: [PATCH 13/23] Rename branch only if it's different from current default branch Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 11 +++-- github/resource_github_branch_default_test.go | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 805fc0a4be..4343b76009 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -228,14 +228,17 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa var etag string if rename { - tflog.Debug(ctx, "Renaming branch to new default") + tflog.Debug(ctx, "Rename enabled, checking if branch rename is needed") repository, resp, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { return diag.FromErr(err) } - etag = resp.Header.Get("ETag") - if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { - return diag.FromErr(err) + if repository.GetDefaultBranch() != defaultBranch { + tflog.Debug(ctx, "Renaming branch to new default") + etag = resp.Header.Get("ETag") + if _, _, err := client.Repositories.RenameBranch(ctx, owner, repoName, repository.GetDefaultBranch(), defaultBranch); err != nil { + return diag.FromErr(err) + } } } else { tflog.Debug(ctx, "Setting new default branch") diff --git a/github/resource_github_branch_default_test.go b/github/resource_github_branch_default_test.go index 0b993912cb..648d38ece4 100644 --- a/github/resource_github_branch_default_test.go +++ b/github/resource_github_branch_default_test.go @@ -296,4 +296,45 @@ func TestAccGithubBranchDefault(t *testing.T) { }, }) }) + t.Run("regression_prevent_trying_rename_to_same_name", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + config := ` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test"{ + repository = github_repository.test.name + branch = "development" + rename = %t + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, true), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + { + Config: fmt.Sprintf(config, repoName, false), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + { + Config: fmt.Sprintf(config, repoName, true), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + }, + }) + }) } From 6933c467eda4eb20334f6eb35f9652201491abef Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Thu, 4 Jun 2026 11:44:17 +0300 Subject: [PATCH 14/23] Fix documentation error Signed-off-by: Timo Sand --- ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8875deb388..a7604ee515 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -95,7 +95,7 @@ func resourceExampleRead(ctx context.Context, d *schema.ResourceData, m any) dia // Single API call to get all needed data resource, _, err := client.Resources.Get(ctx, owner, name) if err != nil { - if ghErr, ok := errors.AsType[github.ErrorResponse](err); ok { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok { if ghErr.Response.StatusCode == http.StatusNotFound { tflog.Info(ctx, "Removing resource from state because it no longer exists", map[string]any{"name": name}) d.SetId("") @@ -337,7 +337,7 @@ Handle 404s gracefully by removing from state: ```go resource, _, err := client.Resources.Get(ctx, owner, name) if err != nil { - if ghErr, ok := errors.AsType[github.ErrorResponse](err); ok { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok { if ghErr.Response.StatusCode == http.StatusNotFound { tflog.Info(ctx, "Removing resource from state because it no longer exists", map[string]any{"name": name}) d.SetId("") From fedf817502530aec7971e500fd45353fc2d10a29 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Thu, 4 Jun 2026 14:15:41 +0300 Subject: [PATCH 15/23] Add acc tests to verify Destroy behaviour Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 7 + github/resource_github_branch_default_test.go | 132 ++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 4343b76009..02a7729f70 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -284,6 +284,13 @@ func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceDa _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository) if err != nil { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok { + if ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "Removing resource from state because repository no longer exists upstream", map[string]any{"owner": owner, "repository": repoName}) + d.SetId("") + return nil + } + } return diag.FromErr(err) } diff --git a/github/resource_github_branch_default_test.go b/github/resource_github_branch_default_test.go index 648d38ece4..fa0462d77b 100644 --- a/github/resource_github_branch_default_test.go +++ b/github/resource_github_branch_default_test.go @@ -296,6 +296,138 @@ func TestAccGithubBranchDefault(t *testing.T) { }, }) }) + t.Run("destroys_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + + repoOnlyConfig := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + `, repoName) + + fullConfig := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test" { + repository = github_repository.test.name + branch = "main" + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fullConfig, + }, + { + Config: repoOnlyConfig, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository.test", tfjsonpath.New("default_branch"), knownvalue.StringExact("main")), + }, + }, + }, + }) + }) + + t.Run("destroys_does_not_modify_remote_branch", func(t *testing.T) { + // The Delete function sends a nil DefaultBranch in the PATCH body, which + // go-github omits via omitempty — making it a no-op on the GitHub side. + // This test pins that behavior: the remote default branch must be unchanged + // after the resource is removed from Terraform state. + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + + repoOnlyConfig := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + `, repoName) + + fullConfig := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test" { + repository = github_repository.test.name + branch = "development" + rename = true + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fullConfig, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_branch_default.test", tfjsonpath.New("branch"), knownvalue.StringExact("development")), + }, + }, + { + Config: repoOnlyConfig, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_repository.test", tfjsonpath.New("default_branch"), knownvalue.StringExact("development")), + }, + }, + }, + }) + }) + + t.Run("gracefully_handles_repository_deleted_out_of_band", func(t *testing.T) { + // This tests the Read 404 path: when the repository is deleted externally, + // a state refresh discovers the 404, removes both resources from state + // without error, and does not attempt to call Delete. + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + } + + resource "github_branch_default" "test" { + repository = github_repository.test.name + branch = "main" + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + PreConfig: func() { + meta, err := getTestMeta() + if err != nil { + t.Errorf("failed to get test meta: %s", err) + return + } + if _, err := meta.v3client.Repositories.Delete(t.Context(), meta.name, repoName); err != nil { + t.Errorf("failed to delete repository out-of-band: %s", err) + } + }, + RefreshState: true, + ExpectNonEmptyPlan: true, + }, + }, + }) + }) + t.Run("regression_prevent_trying_rename_to_same_name", func(t *testing.T) { randomID := acctest.RandString(5) repoName := fmt.Sprintf("%sbranch-def-%s", testResourcePrefix, randomID) From 326af968d59acc1fd27138638f50816c395e01cc Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 5 Jun 2026 07:01:49 +0300 Subject: [PATCH 16/23] Address review comments Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 57 +++++------------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 02a7729f70..514c3d723d 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -59,10 +59,7 @@ func resourceGithubBranchDefault() *schema.Resource { } func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { - meta, ok := m.(*Owner) - if !ok { - return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) - } + meta, _ := m.(*Owner) client := meta.v3client owner := meta.name @@ -79,21 +76,14 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa return diag.FromErr(errors.New("rename must be a boolean")) } - tflog.Trace(ctx, "Creating default branch resource", map[string]any{ - "owner": owner, - "repository": repoName, - "branch": defaultBranch, - "rename": rename, - }) + tflog.Trace(ctx, "Creating default branch resource", map[string]any{"owner": owner, "repository": repoName, "branch": defaultBranch, "rename": rename}) repository, resp, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { return diag.FromErr(err) } - tflog.Debug(ctx, "Fetched repository", map[string]any{ - "current_default_branch": repository.GetDefaultBranch(), - }) + tflog.Debug(ctx, "Fetched repository", map[string]any{"current_default_branch": repository.GetDefaultBranch()}) if repository.GetDefaultBranch() != defaultBranch { if rename { @@ -124,19 +114,14 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } - tflog.Trace(ctx, "Finished creating default branch resource", map[string]any{ - "resource_id": d.Id(), - }) + tflog.Trace(ctx, "Finished creating default branch resource", map[string]any{"resource_id": d.Id()}) return nil } func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { ctx = tflog.SetField(ctx, "resource_id", d.Id()) - meta, ok := m.(*Owner) - if !ok { - return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) - } + meta, _ := m.(*Owner) client := meta.v3client owner := meta.name @@ -145,10 +130,7 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData return diag.FromErr(errors.New("repository must be a string")) } - tflog.Trace(ctx, "Reading default branch resource", map[string]any{ - "owner": owner, - "repository": repoName, - }) + tflog.Trace(ctx, "Reading default branch resource", map[string]any{"owner": owner, "repository": repoName}) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) @@ -163,10 +145,7 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData return nil } if ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, "Removing repository from state because it no longer exists in GitHub", map[string]any{ - "owner": owner, - "repository": repoName, - }) + tflog.Info(ctx, "Removing repository from state because it no longer exists in GitHub", map[string]any{"owner": owner, "repository": repoName}) d.SetId("") return nil } @@ -198,10 +177,7 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { ctx = tflog.SetField(ctx, "resource_id", d.Id()) - meta, ok := m.(*Owner) - if !ok { - return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) - } + meta, _ := m.(*Owner) client := meta.v3client owner := meta.name @@ -218,12 +194,7 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa return diag.FromErr(errors.New("rename must be a boolean")) } - tflog.Trace(ctx, "Updating default branch resource", map[string]any{ - "owner": owner, - "repository": repoName, - "branch": defaultBranch, - "rename": rename, - }) + tflog.Trace(ctx, "Updating default branch resource", map[string]any{"owner": owner, "repository": repoName, "branch": defaultBranch, "rename": rename}) var etag string @@ -262,10 +233,7 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { ctx = tflog.SetField(ctx, "resource_id", d.Id()) - meta, ok := m.(*Owner) - if !ok { - return diag.FromErr(errors.New("unexpected meta type, expected *Owner")) - } + meta, _ := m.(*Owner) client := meta.v3client owner := meta.name repoName, ok := d.Get("repository").(string) @@ -273,10 +241,7 @@ func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceDa return diag.FromErr(errors.New("repository must be a string")) } - tflog.Trace(ctx, "Deleting default branch resource", map[string]any{ - "owner": owner, - "repository": repoName, - }) + tflog.Trace(ctx, "Deleting default branch resource", map[string]any{"owner": owner, "repository": repoName}) repository := &github.Repository{ DefaultBranch: nil, From 330a466bb84cae5b3e2b0e40c6f28a66233609a2 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 5 Jun 2026 21:52:50 +0300 Subject: [PATCH 17/23] Use `errors.AsType` pattern Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 514c3d723d..55f1b69639 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -138,8 +138,7 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData repository, resp, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok { if ghErr.Response.StatusCode == http.StatusNotModified { tflog.Debug(ctx, "Repository not modified, skipping read") return nil From f88a8935331c4a1f75208cfec92bf340524ef843 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 5 Jun 2026 22:17:06 +0300 Subject: [PATCH 18/23] Address review comments Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 55f1b69639..8b34987c44 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -63,10 +63,7 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa client := meta.v3client owner := meta.name - repoName, ok := d.Get("repository").(string) - if !ok { - return diag.FromErr(errors.New("repository must be a string")) - } + repoName, _ := d.Get("repository").(string) defaultBranch, ok := d.Get("branch").(string) if !ok { return diag.FromErr(errors.New("branch must be a string")) @@ -83,6 +80,8 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } + etag := resp.Header.Get("ETag") + tflog.Debug(ctx, "Fetched repository", map[string]any{"current_default_branch": repository.GetDefaultBranch()}) if repository.GetDefaultBranch() != defaultBranch { @@ -97,9 +96,12 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa DefaultBranch: new(defaultBranch), } - if _, _, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { + if _, resp, err := client.Repositories.Edit(ctx, owner, repoName, repository); err != nil { return diag.FromErr(err) + } else { + etag = resp.Header.Get("ETag") } + } } else { tflog.Debug(ctx, "Default branch already set to desired branch, skipping update") @@ -110,7 +112,7 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa if err := d.Set("repository_id", int(repository.GetID())); err != nil { return diag.FromErr(err) } - if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { + if err := d.Set("etag", etag); err != nil { return diag.FromErr(err) } From f59df2b95c7494606a26f0adc9bada6677398b20 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 5 Jun 2026 22:42:02 +0300 Subject: [PATCH 19/23] Add state migration and tests Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 9 ++ ...esource_github_branch_default_migration.go | 72 ++++++++++++++ ...ce_github_branch_default_migration_test.go | 96 +++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 github/resource_github_branch_default_migration.go create mode 100644 github/resource_github_branch_default_migration_test.go diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 8b34987c44..ce2da5d456 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -21,6 +21,15 @@ func resourceGithubBranchDefault() *schema.Resource { StateContext: resourceGithubBranchDefaultImport, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubBranchDefaultV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubBranchDefaultStateUpgradeV0, + Version: 0, + }, + }, + CustomizeDiff: diffRepository, Schema: map[string]*schema.Schema{ diff --git a/github/resource_github_branch_default_migration.go b/github/resource_github_branch_default_migration.go new file mode 100644 index 0000000000..e5211c07be --- /dev/null +++ b/github/resource_github_branch_default_migration.go @@ -0,0 +1,72 @@ +package github + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubBranchDefaultV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "branch": { + Type: schema.TypeString, + Required: true, + }, + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "rename": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "etag": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceGithubBranchDefaultStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + tflog.Debug(ctx, "state upgrade v0: State before v0 migration", rawState) + + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName := "" + if v, ok := rawState["repository"]; ok { + if s, ok := v.(string); ok && s != "" { + repoName = s + } + } + if repoName == "" { + if v, ok := rawState["id"]; ok { + if s, ok := v.(string); ok && s != "" { + repoName = s + } + } + } + if repoName == "" { + return nil, fmt.Errorf("state upgrade v0: repository is not a string or not set") + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, fmt.Errorf("state upgrade v0: failed to retrieve repository '%s': %w", repoName, err) + } + + rawState["repository"] = repoName + rawState["repository_id"] = int(repo.GetID()) + + tflog.Debug(ctx, "state upgrade v0: State after v0 migration", rawState) + + return rawState, nil +} diff --git a/github/resource_github_branch_default_migration_test.go b/github/resource_github_branch_default_migration_test.go new file mode 100644 index 0000000000..3d3624fe1b --- /dev/null +++ b/github/resource_github_branch_default_migration_test.go @@ -0,0 +1,96 @@ +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v88/github" +) + +func buildMockResponsesForBranchDefaultMigrationV0toV1(mockOwner, mockRepo string, wantRepoID int) []*mockResponse { + responseBodyJSON, err := json.Marshal(github.Repository{ + ID: new(int64(wantRepoID)), + Name: new(mockRepo), + }) + if err != nil { + panic(fmt.Sprintf("error marshalling repository response: %s", err)) + } + + return []*mockResponse{{ + ExpectedUri: fmt.Sprintf("/repos/%s/%s", mockOwner, mockRepo), + ExpectedHeaders: map[string]string{ + "Accept": "application/vnd.github.scarlet-witch-preview+json, application/vnd.github.mercy-preview+json, application/vnd.github.baptiste-preview+json, application/vnd.github.nebula-preview+json", + }, + ResponseBody: string(responseBodyJSON), + StatusCode: http.StatusOK, + }} +} + +func Test_resourceGithubBranchDefaultStateUpgradeV0toV1(t *testing.T) { + t.Parallel() + + for _, d := range []struct { + testName string + rawState map[string]any + want map[string]any + shouldError bool + }{ + { + testName: "adds_repository_id", + rawState: map[string]any{ + "id": "test-repo", + "repository": "test-repo", + "branch": "main", + "rename": false, + "etag": `W/\"etag\"`, + }, + want: map[string]any{ + "id": "test-repo", + "repository": "test-repo", + "repository_id": 1234567890, + "branch": "main", + "rename": false, + "etag": `W/\"etag\"`, + }, + }, + { + testName: "falls_back_to_id_for_imported_state", + rawState: map[string]any{ + "id": "test-repo", + "branch": "main", + "rename": true, + }, + want: map[string]any{ + "id": "test-repo", + "repository": "test-repo", + "repository_id": 1234567890, + "branch": "main", + "rename": true, + }, + }, + } { + t.Run(d.testName, func(t *testing.T) { + t.Parallel() + + meta := &Owner{name: "test-org"} + ts := githubApiMock(buildMockResponsesForBranchDefaultMigrationV0toV1(meta.name, "test-repo", 1234567890)) + defer ts.Close() + + meta.v3client = mustGitHubClient(t, ts.URL) + + // ctx := tflogtest.RootLogger(t.Context(), log.Writer()) // This pattern can be used to capture logs during testing if needed + + got, err := resourceGithubBranchDefaultStateUpgradeV0(t.Context(), d.rawState, meta) + if (err != nil) != d.shouldError { + t.Fatalf("unexpected error state: got error %v, shouldError %v", err, d.shouldError) + } + + if diff := cmp.Diff(got, d.want); diff != "" && !d.shouldError { + t.Fatalf("got %+v, want %+v, diff %s", got, d.want, diff) + } + }) + } +} From 4137ac70d94cef4a458589c90566737d8a80631d Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 5 Jun 2026 23:06:36 +0300 Subject: [PATCH 20/23] Update docs template to be mostly generated Signed-off-by: Timo Sand --- docs/resources/branch_default.md | 38 +++++++++--------- .../resources/github_branch_default/import.sh | 1 + .../resource_basic.tf} | 2 + .../resource_rename_nonexistent_branch.tf} | 2 + github/resource_github_branch_default.go | 17 ++++---- templates/resources/branch_default.md.tmpl | 39 ++++++------------- 6 files changed, 46 insertions(+), 53 deletions(-) create mode 100644 examples/resources/github_branch_default/import.sh rename examples/resources/{branch_default/example_1.tf => github_branch_default/resource_basic.tf} (96%) rename examples/resources/{branch_default/example_2.tf => github_branch_default/resource_rename_nonexistent_branch.tf} (86%) diff --git a/docs/resources/branch_default.md b/docs/resources/branch_default.md index 49ff7a7da1..70a541ad00 100644 --- a/docs/resources/branch_default.md +++ b/docs/resources/branch_default.md @@ -1,22 +1,20 @@ --- page_title: "github_branch_default (Resource) - GitHub" description: |- - Provides a GitHub branch default for a given repository. + Configures the default branch for a GitHub repository. --- # github_branch_default (Resource) -Provides a GitHub branch default resource. +Configures the default branch for a GitHub repository. -This resource allows you to set the default branch for a given repository. - -Note that use of this resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. +~> **Note:** This resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. ## Example Usage -Basic usage: - ```terraform +# Basic usage + resource "github_repository" "example" { name = "example" description = "My awesome codebase" @@ -34,9 +32,9 @@ resource "github_branch_default" "default" { } ``` -Renaming to a branch that doesn't exist: - ```terraform +# Renaming to a branch that doesn't exist + resource "github_repository" "example" { name = "example" description = "My awesome codebase" @@ -50,23 +48,27 @@ resource "github_branch_default" "default" { } ``` -## Argument Reference + +## Schema + +### Required -The following arguments are supported: +- `branch` (String) The name of the branch to set as the default (e.g. 'main'). +- `repository` (String) The name of the GitHub repository. -- `repository` - (Required) The GitHub repository -- `branch` - (Required) The branch (e.g. `main`) -- `rename` - (Optional) Indicate if it should rename the branch rather than use an existing branch. Defaults to `false`. +### Optional -## Attribute Reference +- `etag` (String) The ETag header for the repository API response. +- `rename` (Boolean) Indicate if the current default branch should be renamed rather than switching to an existing branch. Defaults to 'false'. -The following attributes are exported: +### Read-Only -- `repository_id` - The GitHub repository ID. +- `id` (String) The ID of this resource. +- `repository_id` (Number) The ID of the GitHub repository. ## Import -GitHub Branch Defaults can be imported using the repository name, e.g. +GitHub Branch Defaults can be imported using the repository name with the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), for example: ```shell terraform import github_branch_default.branch_default my-repo diff --git a/examples/resources/github_branch_default/import.sh b/examples/resources/github_branch_default/import.sh new file mode 100644 index 0000000000..915a02509e --- /dev/null +++ b/examples/resources/github_branch_default/import.sh @@ -0,0 +1 @@ +terraform import github_branch_default.branch_default my-repo diff --git a/examples/resources/branch_default/example_1.tf b/examples/resources/github_branch_default/resource_basic.tf similarity index 96% rename from examples/resources/branch_default/example_1.tf rename to examples/resources/github_branch_default/resource_basic.tf index 4606efe8ae..10fdeb5273 100644 --- a/examples/resources/branch_default/example_1.tf +++ b/examples/resources/github_branch_default/resource_basic.tf @@ -1,3 +1,5 @@ +# Basic usage + resource "github_repository" "example" { name = "example" description = "My awesome codebase" diff --git a/examples/resources/branch_default/example_2.tf b/examples/resources/github_branch_default/resource_rename_nonexistent_branch.tf similarity index 86% rename from examples/resources/branch_default/example_2.tf rename to examples/resources/github_branch_default/resource_rename_nonexistent_branch.tf index 3974b93ca7..91f2f60ed4 100644 --- a/examples/resources/branch_default/example_2.tf +++ b/examples/resources/github_branch_default/resource_rename_nonexistent_branch.tf @@ -1,3 +1,5 @@ +# Renaming to a branch that doesn't exist + resource "github_repository" "example" { name = "example" description = "My awesome codebase" diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index ce2da5d456..4a6a18d140 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -32,32 +32,35 @@ func resourceGithubBranchDefault() *schema.Resource { CustomizeDiff: diffRepository, + Description: "Configures the default branch for a GitHub repository.", + Schema: map[string]*schema.Schema{ "branch": { Type: schema.TypeString, Required: true, - Description: "The branch (e.g. 'main').", + Description: "The name of the branch to set as the default (e.g. 'main').", }, "repository": { Type: schema.TypeString, Required: true, - Description: "The GitHub repository.", + Description: "The name of the GitHub repository.", }, "repository_id": { Type: schema.TypeInt, Computed: true, - Description: "The GitHub repository ID.", + Description: "The ID of the GitHub repository.", }, "rename": { Type: schema.TypeBool, Optional: true, Default: false, - Description: "Indicate if it should rename the branch rather than use an existing branch. Defaults to 'false'.", + Description: "Indicate if the current default branch should be renamed rather than switching to an existing branch. Defaults to 'false'.", }, "etag": { - Type: schema.TypeString, - Optional: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The ETag header for the repository API response.", DiffSuppressFunc: func(k, o, n string, d *schema.ResourceData) bool { return true }, diff --git a/templates/resources/branch_default.md.tmpl b/templates/resources/branch_default.md.tmpl index 7a9748654e..4ebacc6a75 100644 --- a/templates/resources/branch_default.md.tmpl +++ b/templates/resources/branch_default.md.tmpl @@ -1,45 +1,28 @@ --- page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" description: |- - Provides a GitHub branch default for a given repository. +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) -Provides a GitHub branch default resource. +{{ .Description | trimspace }} -This resource allows you to set the default branch for a given repository. - -Note that use of this resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. +~> **Note:** This resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. +{{ if .HasExamples -}} ## Example Usage -Basic usage: - -{{ tffile "examples/resources/branch_default/example_1.tf" }} - -Renaming to a branch that doesn't exist: - -{{ tffile "examples/resources/branch_default/example_2.tf" }} - -## Argument Reference - -The following arguments are supported: - -- `repository` - (Required) The GitHub repository -- `branch` - (Required) The branch (e.g. `main`) -- `rename` - (Optional) Indicate if it should rename the branch rather than use an existing branch. Defaults to `false`. - -## Attribute Reference +{{- range .ExampleFiles }} -The following attributes are exported: +{{ tffile . }} +{{- end }} +{{- end }} -- `repository_id` - The GitHub repository ID. +{{ .SchemaMarkdown | trimspace }} ## Import -GitHub Branch Defaults can be imported using the repository name, e.g. +GitHub Branch Defaults can be imported using the repository name with the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), for example: -```shell -terraform import github_branch_default.branch_default my-repo -``` +{{codefile "shell" .ImportFile }} From ba987f0c9749915ac0e8d727cc19e2fc969f60a8 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 6 Jun 2026 08:34:09 +0300 Subject: [PATCH 21/23] Ensure that ID gets updated if reponame changes Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index 4a6a18d140..b18fcad87f 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -236,6 +236,11 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa etag = resp.Header.Get("ETag") } } + + if d.HasChange("repository") { + d.SetId(repoName) + } + if err := d.Set("etag", etag); err != nil { return diag.FromErr(err) } From 0a739c75ab8fa6eaa78633d1f6576fb823372232 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 6 Jun 2026 08:39:52 +0300 Subject: [PATCH 22/23] Remove typeasserts from schema fields Signed-off-by: Timo Sand --- github/resource_github_branch_default.go | 35 +++++------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index b18fcad87f..d63a72875b 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -76,14 +76,8 @@ func resourceGithubBranchDefaultCreate(ctx context.Context, d *schema.ResourceDa owner := meta.name repoName, _ := d.Get("repository").(string) - defaultBranch, ok := d.Get("branch").(string) - if !ok { - return diag.FromErr(errors.New("branch must be a string")) - } - rename, ok := d.Get("rename").(bool) - if !ok { - return diag.FromErr(errors.New("rename must be a boolean")) - } + defaultBranch, _ := d.Get("branch").(string) + rename, _ := d.Get("rename").(bool) tflog.Trace(ctx, "Creating default branch resource", map[string]any{"owner": owner, "repository": repoName, "branch": defaultBranch, "rename": rename}) @@ -139,10 +133,7 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData client := meta.v3client owner := meta.name - repoName, ok := d.Get("repository").(string) - if !ok { - return diag.FromErr(errors.New("repository must be a string")) - } + repoName, _ := d.Get("repository").(string) tflog.Trace(ctx, "Reading default branch resource", map[string]any{"owner": owner, "repository": repoName}) @@ -194,18 +185,9 @@ func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceDa client := meta.v3client owner := meta.name - repoName, ok := d.Get("repository").(string) - if !ok { - return diag.FromErr(errors.New("repository must be a string")) - } - defaultBranch, ok := d.Get("branch").(string) - if !ok { - return diag.FromErr(errors.New("branch must be a string")) - } - rename, ok := d.Get("rename").(bool) - if !ok { - return diag.FromErr(errors.New("rename must be a boolean")) - } + repoName, _ := d.Get("repository").(string) + defaultBranch, _ := d.Get("branch").(string) + rename, _ := d.Get("rename").(bool) tflog.Trace(ctx, "Updating default branch resource", map[string]any{"owner": owner, "repository": repoName, "branch": defaultBranch, "rename": rename}) @@ -254,10 +236,7 @@ func resourceGithubBranchDefaultDelete(ctx context.Context, d *schema.ResourceDa meta, _ := m.(*Owner) client := meta.v3client owner := meta.name - repoName, ok := d.Get("repository").(string) - if !ok { - return diag.FromErr(errors.New("repository must be a string")) - } + repoName, _ := d.Get("repository").(string) tflog.Trace(ctx, "Deleting default branch resource", map[string]any{"owner": owner, "repository": repoName}) From 6b9e94a057af0a657ee3e03b89cb529e0c325acb Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 12 Jun 2026 01:25:03 +0300 Subject: [PATCH 23/23] Address review comments Signed-off-by: Timo Sand --- docs/resources/branch_default.md | 15 ++++++++++-- .../import-by-string-id.tf | 4 ++++ github/resource_github_branch_default.go | 1 - templates/resources/branch_default.md.tmpl | 24 +++++++++++++++++-- 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 examples/resources/github_branch_default/import-by-string-id.tf diff --git a/docs/resources/branch_default.md b/docs/resources/branch_default.md index 70a541ad00..7c1601254c 100644 --- a/docs/resources/branch_default.md +++ b/docs/resources/branch_default.md @@ -8,7 +8,7 @@ description: |- Configures the default branch for a GitHub repository. -~> **Note:** This resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. +~> This resource is incompatible with the `default_branch` option of the [`github_repository`](repository) resource. Using both will result in plans always showing a diff. ## Example Usage @@ -68,7 +68,18 @@ resource "github_branch_default" "default" { ## Import -GitHub Branch Defaults can be imported using the repository name with the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), for example: +Import is supported using the following syntax: + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +```terraform +import { + to = github_branch_default.default + id = "example" +} +``` + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: ```shell terraform import github_branch_default.branch_default my-repo diff --git a/examples/resources/github_branch_default/import-by-string-id.tf b/examples/resources/github_branch_default/import-by-string-id.tf new file mode 100644 index 0000000000..bdf85550dc --- /dev/null +++ b/examples/resources/github_branch_default/import-by-string-id.tf @@ -0,0 +1,4 @@ +import { + to = github_branch_default.default + id = "example" +} diff --git a/github/resource_github_branch_default.go b/github/resource_github_branch_default.go index d63a72875b..5a422daf6d 100644 --- a/github/resource_github_branch_default.go +++ b/github/resource_github_branch_default.go @@ -180,7 +180,6 @@ func resourceGithubBranchDefaultRead(ctx context.Context, d *schema.ResourceData } func resourceGithubBranchDefaultUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { - ctx = tflog.SetField(ctx, "resource_id", d.Id()) meta, _ := m.(*Owner) client := meta.v3client owner := meta.name diff --git a/templates/resources/branch_default.md.tmpl b/templates/resources/branch_default.md.tmpl index 4ebacc6a75..4f99fbd754 100644 --- a/templates/resources/branch_default.md.tmpl +++ b/templates/resources/branch_default.md.tmpl @@ -8,7 +8,7 @@ description: |- {{ .Description | trimspace }} -~> **Note:** This resource is incompatible with the `default_branch` option of the `github_repository` resource. Using both will result in plans always showing a diff. +~> This resource is incompatible with the `default_branch` option of the [`github_repository`](repository) resource. Using both will result in plans always showing a diff. {{ if .HasExamples -}} ## Example Usage @@ -20,9 +20,29 @@ description: |- {{- end }} {{ .SchemaMarkdown | trimspace }} +{{- if or .HasImport .HasImportIDConfig .HasImportIdentityConfig }} ## Import -GitHub Branch Defaults can be imported using the repository name with the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), for example: +Import is supported using the following syntax: +{{- end }} +{{- if .HasImportIdentityConfig }} + +In Terraform v1.12.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `identity` attribute, for example: + +{{tffile .ImportIdentityConfigFile }} + +{{ .IdentitySchemaMarkdown | trimspace }} +{{- end }} +{{- if .HasImportIDConfig }} + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +{{tffile .ImportIDConfigFile }} +{{- end }} +{{- if .HasImport }} + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: {{codefile "shell" .ImportFile }} +{{- end }}