From e90dc6192c45f5111cce89a3366258daafce3d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20R=C3=BCegsegger?= Date: Mon, 17 Nov 2025 19:07:15 +0100 Subject: [PATCH 1/4] feat: Add update functionality to rename existing GitHub branches and update documentation --- github/resource_github_branch.go | 25 ++++++++++- github/resource_github_branch_test.go | 60 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/github/resource_github_branch.go b/github/resource_github_branch.go index 318e5979b6..5a3901f0ed 100644 --- a/github/resource_github_branch.go +++ b/github/resource_github_branch.go @@ -15,6 +15,7 @@ func resourceGithubBranch() *schema.Resource { return &schema.Resource{ Create: resourceGithubBranchCreate, Read: resourceGithubBranchRead, + Update: resourceGithubBranchUpdate, Delete: resourceGithubBranchDelete, Importer: &schema.ResourceImporter{ State: resourceGithubBranchImport, @@ -30,7 +31,6 @@ func resourceGithubBranch() *schema.Resource { "branch": { Type: schema.TypeString, Required: true, - ForceNew: true, Description: "The repository branch to create.", }, "source_branch": { @@ -184,6 +184,29 @@ func resourceGithubBranchDelete(d *schema.ResourceData, meta interface{}) error return nil } +func resourceGithubBranchUpdate(d *schema.ResourceData, meta interface{}) error { + if !d.HasChange("branch") { + return resourceGithubBranchRead(d, meta) + } + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + repoName, oldBranchName, err := parseTwoPartID(d.Id(), "repository", "branch") + if err != nil { + return err + } + newBranchName := d.Get("branch").(string) + + if _, _, err := client.Repositories.RenameBranch(ctx, orgName, repoName, oldBranchName, newBranchName); err != nil { + return fmt.Errorf("error renaming GitHub branch %s/%s (%s -> %s): %w", orgName, repoName, oldBranchName, newBranchName, err) + } + + d.SetId(buildTwoPartID(repoName, newBranchName)) + + return resourceGithubBranchRead(d, meta) +} + func resourceGithubBranchImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { repoName, branchName, err := parseTwoPartID(d.Id(), "repository", "branch") if err != nil { diff --git a/github/resource_github_branch_test.go b/github/resource_github_branch_test.go index b5844a6e6e..694335e087 100644 --- a/github/resource_github_branch_test.go +++ b/github/resource_github_branch_test.go @@ -209,4 +209,64 @@ func TestAccGithubBranch(t *testing.T) { }) + t.Run("renames a branch without replacement", func(t *testing.T) { + + initialConfig := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-%[1]s" + auto_init = true + } + + resource "github_branch" "test" { + repository = github_repository.test.id + branch = "initial" + } + `, randomID) + + renamedConfig := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-%[1]s" + auto_init = true + } + + resource "github_branch" "test" { + repository = github_repository.test.id + branch = "renamed" + } + `, randomID) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: initialConfig, + }, + { + Config: renamedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_branch.test", "branch", "renamed", + ), + ), + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + }) + } From 80da967799de1bc5a53b3dfb255fa5ed5d8e5281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20R=C3=BCegsegger?= Date: Thu, 20 Nov 2025 13:15:09 +0100 Subject: [PATCH 2/4] gofmt --- github/resource_github_branch_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/github/resource_github_branch_test.go b/github/resource_github_branch_test.go index 8dcbd1f109..7de895f1cb 100644 --- a/github/resource_github_branch_test.go +++ b/github/resource_github_branch_test.go @@ -203,7 +203,6 @@ func TestAccGithubBranch(t *testing.T) { }) t.Run("renames a branch without replacement", func(t *testing.T) { - initialConfig := fmt.Sprintf(` resource "github_repository" "test" { name = "tf-acc-test-%[1]s" @@ -259,7 +258,5 @@ func TestAccGithubBranch(t *testing.T) { t.Run("with an organization account", func(t *testing.T) { testCase(t, organization) }) - }) - } From fbbb3fb45e71681e0e944938998c5b9d379d9db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20R=C3=BCegsegger?= Date: Fri, 21 Nov 2025 17:37:16 +0100 Subject: [PATCH 3/4] fix: Update function signatures to use 'any' instead of 'interface{}' --- github/resource_github_branch.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/resource_github_branch.go b/github/resource_github_branch.go index 116916b8f2..298ae3e2e7 100644 --- a/github/resource_github_branch.go +++ b/github/resource_github_branch.go @@ -186,7 +186,7 @@ func resourceGithubBranchDelete(d *schema.ResourceData, meta any) error { return nil } -func resourceGithubBranchUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceGithubBranchUpdate(d *schema.ResourceData, meta any) error { if !d.HasChange("branch") { return resourceGithubBranchRead(d, meta) } @@ -209,7 +209,7 @@ func resourceGithubBranchUpdate(d *schema.ResourceData, meta interface{}) error return resourceGithubBranchRead(d, meta) } -func resourceGithubBranchImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { +func resourceGithubBranchImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { repoName, branchName, err := parseTwoPartID(d.Id(), "repository", "branch") if err != nil { return nil, err From 37db1fdc003a775afe4cab5db82adbf4e1aea607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20R=C3=BCegsegger?= Date: Mon, 24 Nov 2025 18:20:53 +0100 Subject: [PATCH 4/4] test: Add validation tests for GitHub branch resource schema --- .../resource_github_branch_validation_test.go | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 github/resource_github_branch_validation_test.go diff --git a/github/resource_github_branch_validation_test.go b/github/resource_github_branch_validation_test.go new file mode 100644 index 0000000000..690dfb28fe --- /dev/null +++ b/github/resource_github_branch_validation_test.go @@ -0,0 +1,51 @@ +package github + +import "testing" + +func TestGithubBranchIsUpdatedWhenBranchChanges(t *testing.T) { + resource := resourceGithubBranch() + + branchSchema := resource.Schema["branch"] + if branchSchema == nil { + t.Fatal("branch field should exist in schema") + } + if branchSchema.ForceNew { + t.Error("branch field should not be ForceNew so renames are handled via update") + } +} + +func TestGithubBranchIsRecreatedWhenRepositoryChanges(t *testing.T) { + resource := resourceGithubBranch() + + repositorySchema := resource.Schema["repository"] + if repositorySchema == nil { + t.Fatal("repository field should exist in schema") + } + if !repositorySchema.ForceNew { + t.Error("repository field should be ForceNew so changes recreate the resource") + } +} + +func TestGithubBranchIsRecreatedWhenSourceBranchChanges(t *testing.T) { + resource := resourceGithubBranch() + + sourceBranchSchema := resource.Schema["source_branch"] + if sourceBranchSchema == nil { + t.Fatal("source_branch field should exist in schema") + } + if !sourceBranchSchema.ForceNew { + t.Error("source_branch field should be ForceNew so changes recreate the resource") + } +} + +func TestGithubBranchIsRecreatedWhenSourceSHAChanges(t *testing.T) { + resource := resourceGithubBranch() + + sourceSHASchema := resource.Schema["source_sha"] + if sourceSHASchema == nil { + t.Fatal("source_sha field should exist in schema") + } + if !sourceSHASchema.ForceNew { + t.Error("source_sha field should be ForceNew so changes recreate the resource") + } +}