diff --git a/github/resource_github_branch.go b/github/resource_github_branch.go index 47314dd9e..298ae3e2e 100644 --- a/github/resource_github_branch.go +++ b/github/resource_github_branch.go @@ -16,6 +16,7 @@ func resourceGithubBranch() *schema.Resource { return &schema.Resource{ Create: resourceGithubBranchCreate, Read: resourceGithubBranchRead, + Update: resourceGithubBranchUpdate, Delete: resourceGithubBranchDelete, Importer: &schema.ResourceImporter{ State: resourceGithubBranchImport, @@ -31,7 +32,6 @@ func resourceGithubBranch() *schema.Resource { "branch": { Type: schema.TypeString, Required: true, - ForceNew: true, Description: "The repository branch to create.", }, "source_branch": { @@ -186,6 +186,29 @@ func resourceGithubBranchDelete(d *schema.ResourceData, meta any) error { return nil } +func resourceGithubBranchUpdate(d *schema.ResourceData, meta any) 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 any) ([]*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 bf75cf7d0..7de895f1c 100644 --- a/github/resource_github_branch_test.go +++ b/github/resource_github_branch_test.go @@ -201,4 +201,62 @@ func TestAccGithubBranch(t *testing.T) { testCase(t, organization) }) }) + + 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) + }) + }) } diff --git a/github/resource_github_branch_validation_test.go b/github/resource_github_branch_validation_test.go new file mode 100644 index 000000000..690dfb28f --- /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") + } +}