diff --git a/github/resource_github_repository_file.go b/github/resource_github_repository_file.go index d3820c8cad..a9892e3973 100644 --- a/github/resource_github_repository_file.go +++ b/github/resource_github_repository_file.go @@ -119,6 +119,26 @@ func resourceGithubRepositoryFile() *schema.Resource { Description: "Enable overwriting existing files, defaults to \"false\"", Default: false, }, + "autocreate_branch": { + Type: schema.TypeBool, + Optional: true, + Description: "Automatically create the branch if it could not be found. Subsequent reads if the branch is deleted will occur from 'autocreate_branch_source_branch'", + Default: false, + }, + "autocreate_branch_source_branch": { + Type: schema.TypeString, + Default: "main", + Optional: true, + Description: "The branch name to start from, if 'autocreate_branch' is set. Defaults to 'main'.", + RequiredWith: []string{"autocreate_branch"}, + }, + "autocreate_branch_source_sha": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The commit hash to start from, if 'autocreate_branch' is set. Defaults to the tip of 'autocreate_branch_source_branch'. If provided, 'autocreate_branch_source_branch' is ignored.", + RequiredWith: []string{"autocreate_branch"}, + }, }, } } @@ -177,7 +197,29 @@ func resourceGithubRepositoryFileCreate(d *schema.ResourceData, meta interface{} if branch, ok := d.GetOk("branch"); ok { log.Printf("[DEBUG] Using explicitly set branch: %s", branch.(string)) if err := checkRepositoryBranchExists(client, owner, repo, branch.(string)); err != nil { - return err + if d.Get("autocreate_branch").(bool) { + branchRefName := "refs/heads/" + branch.(string) + sourceBranchName := d.Get("autocreate_branch_source_branch").(string) + sourceBranchRefName := "refs/heads/" + sourceBranchName + + if _, hasSourceSHA := d.GetOk("autocreate_branch_source_sha"); !hasSourceSHA { + ref, _, err := client.Git.GetRef(ctx, owner, repo, sourceBranchRefName) + if err != nil { + return fmt.Errorf("error querying GitHub branch reference %s/%s (%s): %s", + owner, repo, sourceBranchRefName, err) + } + d.Set("autocreate_branch_source_sha", *ref.Object.SHA) + } + sourceBranchSHA := d.Get("autocreate_branch_source_sha").(string) + if _, _, err := client.Git.CreateRef(ctx, owner, repo, &github.Reference{ + Ref: &branchRefName, + Object: &github.GitObject{SHA: &sourceBranchSHA}, + }); err != nil { + return err + } + } else { + return err + } } checkOpt.Ref = branch.(string) } @@ -244,10 +286,14 @@ func resourceGithubRepositoryFileRead(d *schema.ResourceData, meta interface{}) if branch, ok := d.GetOk("branch"); ok { log.Printf("[DEBUG] Using explicitly set branch: %s", branch.(string)) if err := checkRepositoryBranchExists(client, owner, repo, branch.(string)); err != nil { - log.Printf("[INFO] Removing repository path %s/%s/%s from state because the branch no longer exists in GitHub", - owner, repo, file) - d.SetId("") - return nil + if d.Get("autocreate_branch").(bool) { + branch = d.Get("autocreate_branch_source_branch").(string) + } else { + log.Printf("[INFO] Removing repository path %s/%s/%s from state because the branch no longer exists in GitHub", + owner, repo, file) + d.SetId("") + return nil + } } opts.Ref = branch.(string) } @@ -344,7 +390,29 @@ func resourceGithubRepositoryFileUpdate(d *schema.ResourceData, meta interface{} if branch, ok := d.GetOk("branch"); ok { log.Printf("[DEBUG] Using explicitly set branch: %s", branch.(string)) if err := checkRepositoryBranchExists(client, owner, repo, branch.(string)); err != nil { - return err + if d.Get("autocreate_branch").(bool) { + branchRefName := "refs/heads/" + branch.(string) + sourceBranchName := d.Get("autocreate_branch_source_branch").(string) + sourceBranchRefName := "refs/heads/" + sourceBranchName + + if _, hasSourceSHA := d.GetOk("autocreate_branch_source_sha"); !hasSourceSHA { + ref, _, err := client.Git.GetRef(ctx, owner, repo, sourceBranchRefName) + if err != nil { + return fmt.Errorf("error querying GitHub branch reference %s/%s (%s): %s", + owner, repo, sourceBranchRefName, err) + } + d.Set("autocreate_branch_source_sha", *ref.Object.SHA) + } + sourceBranchSHA := d.Get("autocreate_branch_source_sha").(string) + if _, _, err := client.Git.CreateRef(ctx, owner, repo, &github.Reference{ + Ref: &branchRefName, + Object: &github.GitObject{SHA: &sourceBranchSHA}, + }); err != nil { + return err + } + } else { + return err + } } } @@ -395,6 +463,31 @@ func resourceGithubRepositoryFileDelete(d *schema.ResourceData, meta interface{} if b, ok := d.GetOk("branch"); ok { log.Printf("[DEBUG] Using explicitly set branch: %s", b.(string)) + if err := checkRepositoryBranchExists(client, owner, repo, b.(string)); err != nil { + if d.Get("autocreate_branch").(bool) { + branchRefName := "refs/heads/" + b.(string) + sourceBranchName := d.Get("autocreate_branch_source_branch").(string) + sourceBranchRefName := "refs/heads/" + sourceBranchName + + if _, hasSourceSHA := d.GetOk("autocreate_branch_source_sha"); !hasSourceSHA { + ref, _, err := client.Git.GetRef(ctx, owner, repo, sourceBranchRefName) + if err != nil { + return fmt.Errorf("error querying GitHub branch reference %s/%s (%s): %s", + owner, repo, sourceBranchRefName, err) + } + d.Set("autocreate_branch_source_sha", *ref.Object.SHA) + } + sourceBranchSHA := d.Get("autocreate_branch_source_sha").(string) + if _, _, err := client.Git.CreateRef(ctx, owner, repo, &github.Reference{ + Ref: &branchRefName, + Object: &github.GitObject{SHA: &sourceBranchSHA}, + }); err != nil { + return err + } + } else { + return err + } + } branch = b.(string) opts.Branch = &branch } diff --git a/github/resource_github_repository_file_test.go b/github/resource_github_repository_file_test.go index e0357622bd..af12f6a00e 100644 --- a/github/resource_github_repository_file_test.go +++ b/github/resource_github_repository_file_test.go @@ -246,4 +246,84 @@ func TestAccGithubRepositoryFile(t *testing.T) { }) }) + + t.Run("creates and manages files on auto created branch if branch does not exist", func(t *testing.T) { + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-%s" + auto_init = true + } + + resource "github_repository_file" "test" { + repository = github_repository.test.name + branch = "does/not/exist" + file = "test" + content = "bar" + commit_message = "Managed by Terraform" + commit_author = "Terraform User" + commit_email = "terraform@example.com" + autocreate_branch = false + } + `, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository_file.test", "content", + "bar", + ), + resource.TestCheckResourceAttr( + "github_repository_file.test", "sha", + "ba0e162e1c47469e3fe4b393a8bf8c569f302116", + ), + resource.TestCheckResourceAttr( + "github_repository_file.test", "ref", + "does/not/exist", + ), + resource.TestCheckResourceAttrSet( + "github_repository_file.test", "commit_author", + ), + resource.TestCheckResourceAttrSet( + "github_repository_file.test", "commit_email", + ), + resource.TestCheckResourceAttrSet( + "github_repository_file.test", "commit_message", + ), + resource.TestCheckResourceAttrSet( + "github_repository_file.test", "commit_sha", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile(`unexpected status code: 404 Not Found`), + }, + { + Config: strings.Replace(config, + "autocreate_branch = false", + "autocreate_branch = true", 1), + Check: check, + }, + }, + }) + } + + 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/website/docs/r/repository_file.html.markdown b/website/docs/r/repository_file.html.markdown index 4e573b33d4..907c7209aa 100644 --- a/website/docs/r/repository_file.html.markdown +++ b/website/docs/r/repository_file.html.markdown @@ -13,6 +13,7 @@ GitHub repository. ## Example Usage +### Existing Branch ```hcl resource "github_repository" "foo" { @@ -33,6 +34,28 @@ resource "github_repository_file" "foo" { ``` +### Auto Created Branch +```hcl + +resource "github_repository" "foo" { + name = "tf-acc-test-%s" + auto_init = true +} + +resource "github_repository_file" "foo" { + repository = github_repository.foo.name + branch = "does/not/exist" + file = ".gitignore" + content = "**/*.tfstate" + commit_message = "Managed by Terraform" + commit_author = "Terraform User" + commit_email = "terraform@example.com" + overwrite_on_create = true + autocreate_branch = true +} + +``` + ## Argument Reference @@ -45,7 +68,7 @@ The following arguments are supported: * `content` - (Required) The file content. * `branch` - (Optional) Git branch (defaults to the repository's default branch). - The branch must already exist, it will not be created if it does not already exist. + The branch must already exist, it will only be created automatically if 'autocreate_branch' is set true. * `commit_author` - (Optional) Committer author name to use. **NOTE:** GitHub app users may omit author and email information so GitHub can verify commits as the GitHub App. This maybe useful when a branch protection rule requires signed commits. @@ -55,6 +78,12 @@ The following arguments are supported: * `overwrite_on_create` - (Optional) Enable overwriting existing files. If set to `true` it will overwrite an existing file with the same name. If set to `false` it will fail if there is an existing file with the same name. +* `autocreate_branch` - (Optional) Automatically create the branch if it could not be found. Defaults to false. Subsequent reads if the branch is deleted will occur from 'autocreate_branch_source_branch'. + +* `autocreate_branch_source_branch` - (Optional) The branch name to start from, if 'autocreate_branch' is set. Defaults to 'main'. + +* `autocreate_branch_source_sha` - (Optional) The commit hash to start from, if 'autocreate_branch' is set. Defaults to the tip of 'autocreate_branch_source_branch'. If provided, 'autocreate_branch_source_branch' is ignored. + ## Attributes Reference The following additional attributes are exported: