diff --git a/github/repository_utils.go b/github/repository_utils.go index 00d6f3e23c..1c62465491 100644 --- a/github/repository_utils.go +++ b/github/repository_utils.go @@ -9,11 +9,16 @@ import ( "strings" "github.com/google/go-github/v82/github" + "github.com/hashicorp/terraform-plugin-log/tflog" ) // checkRepositoryBranchExists tests if a branch exists in a repository. -func checkRepositoryBranchExists(client *github.Client, owner, repo, branch string) error { - ctx := context.WithValue(context.Background(), ctxId, buildTwoPartID(repo, branch)) +func checkRepositoryBranchExists(ctx context.Context, client *github.Client, owner, repo, branch string) error { + tflog.Debug(ctx, "Checking if branch exists", map[string]any{ + "branch": branch, + "owner": owner, + "repo": repo, + }) _, _, err := client.Repositories.GetBranch(ctx, owner, repo, branch, 2) if err != nil { var ghErr *github.ErrorResponse diff --git a/github/resource_github_repository_file.go b/github/resource_github_repository_file.go index 80a169cf2d..3a31edb55e 100644 --- a/github/resource_github_repository_file.go +++ b/github/resource_github_repository_file.go @@ -2,7 +2,6 @@ package github import ( "context" - "errors" "fmt" "net/http" "net/url" @@ -23,15 +22,28 @@ func resourceGithubRepositoryFile() *schema.Resource { StateContext: resourceGithubRepositoryFileImport, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubRepositoryFileV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubRepositoryFileStateUpgradeV0, + Version: 0, + }, + }, + Description: "This resource allows you to create and manage files within a GitHub repository.", Schema: map[string]*schema.Schema{ "repository": { Type: schema.TypeString, Required: true, - ForceNew: true, Description: "The repository name", }, + "repository_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The repository ID", + }, "file": { Type: schema.TypeString, Required: true, @@ -47,6 +59,7 @@ func resourceGithubRepositoryFile() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, Description: "The branch name, defaults to the repository's default branch", }, "ref": { @@ -97,6 +110,7 @@ func resourceGithubRepositoryFile() *schema.Resource { 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, DiffSuppressFunc: autoBranchDiffSuppressFunc, + Deprecated: "Use `github_branch` resource instead", }, "autocreate_branch_source_branch": { Type: schema.TypeString, @@ -105,6 +119,7 @@ func resourceGithubRepositoryFile() *schema.Resource { Description: "The branch name to start from, if 'autocreate_branch' is set. Defaults to 'main'.", RequiredWith: []string{"autocreate_branch"}, DiffSuppressFunc: autoBranchDiffSuppressFunc, + Deprecated: "Use `github_branch` resource instead", }, "autocreate_branch_source_sha": { Type: schema.TypeString, @@ -113,8 +128,10 @@ func resourceGithubRepositoryFile() *schema.Resource { 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"}, DiffSuppressFunc: autoBranchDiffSuppressFunc, + Deprecated: "Use `github_branch` resource instead", }, }, + CustomizeDiff: diffRepository, } } @@ -161,11 +178,18 @@ func resourceGithubRepositoryFileCreate(ctx context.Context, d *schema.ResourceD checkOpt := github.RepositoryContentGetOptions{} - if branch, ok := d.GetOk("branch"); ok { + repoInfo, _, err := client.Repositories.Get(ctx, owner, repo) + if err != nil { + return diag.FromErr(err) + } + var branch string + + if branchFieldVal, ok := d.GetOk("branch"); ok { + branch = branchFieldVal.(string) tflog.Debug(ctx, "Using explicitly set branch", map[string]any{ - "branch": branch.(string), + "branch": branch, }) - if err := checkRepositoryBranchExists(client, owner, repo, branch.(string)); err != nil { + if err := checkRepositoryBranchExists(ctx, client, owner, repo, branch); err != nil { if d.Get("autocreate_branch").(bool) { if err := resourceGithubRepositoryFileCreateBranch(ctx, d, meta); err != nil { return diag.FromErr(err) @@ -174,9 +198,12 @@ func resourceGithubRepositoryFileCreate(ctx context.Context, d *schema.ResourceD return diag.FromErr(err) } } - checkOpt.Ref = branch.(string) + } else { + branch = repoInfo.GetDefaultBranch() } + checkOpt.Ref = branch + opts := resourceGithubRepositoryFileOptions(d) if opts.Message == nil { @@ -215,10 +242,25 @@ func resourceGithubRepositoryFileCreate(ctx context.Context, d *schema.ResourceD return diag.FromErr(err) } - d.SetId(fmt.Sprintf("%s/%s", repo, file)) + newResourceID, err := buildID(repo, file, branch) + if err != nil { + return diag.FromErr(err) + } + tflog.Debug(ctx, "Setting ID", map[string]any{ + "id": newResourceID, + }) + d.SetId(newResourceID) + + // Set computed values after the resource is created and in state if err = d.Set("commit_sha", create.GetSHA()); err != nil { return diag.FromErr(err) } + if err := d.Set("branch", branch); err != nil { + return diag.FromErr(err) + } + if err := d.Set("repository_id", int(repoInfo.GetID())); err != nil { + return diag.FromErr(err) + } return resourceGithubRepositoryFileRead(ctx, d, meta) } @@ -227,35 +269,22 @@ func resourceGithubRepositoryFileRead(ctx context.Context, d *schema.ResourceDat client := meta.(*Owner).v3client owner := meta.(*Owner).name - repo, file := splitRepoFilePath(d.Id()) - ctx = tflog.SetField(ctx, "repository", repo) + repoName := d.Get("repository").(string) + file := d.Get("file").(string) + branch := d.Get("branch").(string) + + ctx = tflog.SetField(ctx, "repository", repoName) ctx = tflog.SetField(ctx, "file", file) ctx = tflog.SetField(ctx, "owner", owner) + ctx = tflog.SetField(ctx, "owner", owner) opts := &github.RepositoryContentGetOptions{} - if branch, ok := d.GetOk("branch"); ok { - tflog.Debug(ctx, "Using explicitly set branch", map[string]any{ - "branch": branch.(string), - }) - if err := checkRepositoryBranchExists(client, owner, repo, branch.(string)); err != nil { - if d.Get("autocreate_branch").(bool) { - branch = d.Get("autocreate_branch_source_branch").(string) - } else { - tflog.Info(ctx, "Removing repository path from state because the branch no longer exists in GitHub") - d.SetId("") - return nil - } - } - opts.Ref = branch.(string) - } + opts.Ref = branch - fc, _, _, err := client.Repositories.GetContents(ctx, owner, repo, file, opts) + fc, _, _, err := client.Repositories.GetContents(ctx, owner, repoName, file, opts) if err != nil { - var errorResponse *github.ErrorResponse - if errors.As(err, &errorResponse) && errorResponse.Response.StatusCode == http.StatusTooManyRequests { - return diag.FromErr(err) - } + return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "repository file %s/%s:%s:%s", owner, repoName, file, branch)) } if fc == nil { tflog.Info(ctx, "Removing repository path from state because it no longer exists in GitHub") @@ -271,12 +300,7 @@ func resourceGithubRepositoryFileRead(ctx context.Context, d *schema.ResourceDat if err = d.Set("content", content); err != nil { return diag.FromErr(err) } - if err = d.Set("repository", repo); err != nil { - return diag.FromErr(err) - } - if err = d.Set("file", file); err != nil { - return diag.FromErr(err) - } + if err = d.Set("sha", fc.GetSHA()); err != nil { return diag.FromErr(err) } @@ -301,10 +325,10 @@ func resourceGithubRepositoryFileRead(ctx context.Context, d *schema.ResourceDat tflog.Debug(ctx, "Using known commit SHA", map[string]any{ "commit_sha": sha.(string), }) - commit, _, err = client.Repositories.GetCommit(ctx, owner, repo, sha.(string), nil) + commit, _, err = client.Repositories.GetCommit(ctx, owner, repoName, sha.(string), nil) } else { tflog.Debug(ctx, "Commit SHA unknown for file, looking for commit") - commit, err = getFileCommit(ctx, client, owner, repo, file, ref) + commit, err = getFileCommit(ctx, client, owner, repoName, file, ref) } if err != nil { return diag.FromErr(err) @@ -345,24 +369,12 @@ func resourceGithubRepositoryFileUpdate(ctx context.Context, d *schema.ResourceD repo := d.Get("repository").(string) file := d.Get("file").(string) + branch := d.Get("branch").(string) + ctx = tflog.SetField(ctx, "repository", repo) ctx = tflog.SetField(ctx, "file", file) ctx = tflog.SetField(ctx, "owner", owner) - - if branch, ok := d.GetOk("branch"); ok { - tflog.Debug(ctx, "Using explicitly set branch", map[string]any{ - "branch": branch.(string), - }) - if err := checkRepositoryBranchExists(client, owner, repo, branch.(string)); err != nil { - if d.Get("autocreate_branch").(bool) { - if err := resourceGithubRepositoryFileCreateBranch(ctx, d, meta); err != nil { - return diag.FromErr(err) - } - } else { - return diag.FromErr(err) - } - } - } + ctx = tflog.SetField(ctx, "branch", branch) opts := resourceGithubRepositoryFileOptions(d) @@ -370,15 +382,23 @@ func resourceGithubRepositoryFileUpdate(ctx context.Context, d *schema.ResourceD opts.Message = github.Ptr(fmt.Sprintf("Update %s", file)) } - create, _, err := client.Repositories.CreateFile(ctx, owner, repo, file, opts) + update, _, err := client.Repositories.UpdateFile(ctx, owner, repo, file, opts) if err != nil { return diag.FromErr(err) } - if err = d.Set("commit_sha", create.GetSHA()); err != nil { + if err = d.Set("commit_sha", update.GetSHA()); err != nil { return diag.FromErr(err) } + if d.HasChanges("repository", "file", "branch") { + newResourceID, err := buildID(repo, file, branch) + if err != nil { + return diag.FromErr(err) + } + d.SetId(newResourceID) + } + return resourceGithubRepositoryFileRead(ctx, d, meta) } @@ -395,22 +415,8 @@ func resourceGithubRepositoryFileDelete(ctx context.Context, d *schema.ResourceD opts.Message = github.Ptr(fmt.Sprintf("Delete %s", file)) } - if b, ok := d.GetOk("branch"); ok { - tflog.Debug(ctx, "Using explicitly set branch", map[string]any{ - "branch": b.(string), - }) - if err := checkRepositoryBranchExists(client, owner, repo, b.(string)); err != nil { - if d.Get("autocreate_branch").(bool) { - if err := resourceGithubRepositoryFileCreateBranch(ctx, d, meta); err != nil { - return diag.FromErr(err) - } - } else { - return diag.FromErr(err) - } - } - branch := b.(string) - opts.Branch = github.Ptr(branch) - } + branch := d.Get("branch").(string) + opts.Branch = github.Ptr(branch) _, _, err := client.Repositories.DeleteFile(ctx, owner, repo, file, opts) return diag.FromErr(handleArchivedRepoDelete(err, "repository file", file, owner, repo)) @@ -427,28 +433,56 @@ func autoBranchDiffSuppressFunc(k, _, _ string, d *schema.ResourceData) bool { } func resourceGithubRepositoryFileImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { - repoFilePath, branch, err := parseID2(d.Id()) + repo, filePath, branch, err := parseID3(d.Id()) if err != nil { - return nil, fmt.Errorf("invalid ID specified. Supplied ID must be written as /:. %w", err) + return nil, fmt.Errorf("invalid ID specified. Supplied ID must be written as :: (when branch is default) or ::. %w", err) } client := meta.(*Owner).v3client owner := meta.(*Owner).name - repo, file := splitRepoFilePath(repoFilePath) - opts := &github.RepositoryContentGetOptions{Ref: branch} - if err := d.Set("branch", branch); err != nil { + opts := &github.RepositoryContentGetOptions{} + + repoInfo, _, err := client.Repositories.Get(ctx, owner, repo) + if err != nil { return nil, err } - fc, _, _, err := client.Repositories.GetContents(ctx, owner, repo, file, opts) + if branch == "" { + branch = repoInfo.GetDefaultBranch() + } + + opts.Ref = branch + + fc, _, _, err := client.Repositories.GetContents(ctx, owner, repo, filePath, opts) if err != nil { return nil, err } if fc == nil { - return nil, fmt.Errorf("file %s is not a file in repository %s/%s or repository is not readable", file, owner, repo) + return nil, fmt.Errorf("filePath %s is not a file in repository %s/%s or repository is not readable", filePath, owner, repo) } - d.SetId(fmt.Sprintf("%s/%s", repo, file)) + if err := d.Set("repository", repo); err != nil { + return nil, err + } + if err := d.Set("file", filePath); err != nil { + return nil, err + } + + newResourceID, err := buildID(repo, filePath, branch) + if err != nil { + return nil, err + } + tflog.Debug(ctx, "Setting ID", map[string]any{ + "id": newResourceID, + }) + d.SetId(newResourceID) + + if err := d.Set("branch", branch); err != nil { + return nil, err + } + if err := d.Set("repository_id", int(repoInfo.GetID())); err != nil { + return nil, err + } if err = d.Set("overwrite_on_create", false); err != nil { return nil, err } diff --git a/github/resource_github_repository_file_migration.go b/github/resource_github_repository_file_migration.go new file mode 100644 index 0000000000..1c6739e58d --- /dev/null +++ b/github/resource_github_repository_file_migration.go @@ -0,0 +1,123 @@ +package github + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubRepositoryFileV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "file": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "content": { + Type: schema.TypeString, + Required: true, + }, + "branch": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "ref": { + Type: schema.TypeString, + Computed: true, + ForceNew: true, + }, + "commit_sha": { + Type: schema.TypeString, + Computed: true, + }, + "commit_message": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "commit_author": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"commit_email"}, + }, + "commit_email": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"commit_author"}, + }, + "sha": { + Type: schema.TypeString, + Computed: true, + }, + "overwrite_on_create": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "autocreate_branch": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "autocreate_branch_source_branch": { + Type: schema.TypeString, + Default: "main", + Optional: true, + RequiredWith: []string{"autocreate_branch"}, + }, + "autocreate_branch_source_sha": { + Type: schema.TypeString, + Optional: true, + Computed: true, + RequiredWith: []string{"autocreate_branch"}, + }, + }, + } +} + +func resourceGithubRepositoryFileStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + tflog.Debug(ctx, "GitHub Repository File State before migration", map[string]any{ + "rawState": rawState, + }) + + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName, ok := rawState["repository"].(string) + if !ok { + return nil, fmt.Errorf("repository not found or is not a string") + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve repository '%s': %w", repoName, err) + } + + rawState["repository_id"] = int(repo.GetID()) + + // If branch is missing or empty, fetch the default branch from the repository + if branch, ok := rawState["branch"].(string); !ok || branch == "" { + rawState["branch"] = repo.GetDefaultBranch() + } + + newResourceID, err := buildID(rawState["repository"].(string), rawState["file"].(string), rawState["branch"].(string)) + if err != nil { + return nil, fmt.Errorf("failed to build ID: %w", err) + } + rawState["id"] = newResourceID + + tflog.Debug(ctx, "GitHub Repository File State after migration", map[string]any{ + "rawState": rawState, + }) + return rawState, nil +} diff --git a/github/resource_github_repository_file_migration_test.go b/github/resource_github_repository_file_migration_test.go new file mode 100644 index 0000000000..ba02db041f --- /dev/null +++ b/github/resource_github_repository_file_migration_test.go @@ -0,0 +1,155 @@ +package github + +// TODO: Enable this test once we have a way to mock the GitHub API + +// import ( +// "context" +// "encoding/json" +// "fmt" +// "net/http" +// "net/url" +// "testing" + +// "github.com/google/go-cmp/cmp" +// "github.com/google/go-github/v82/github" +// ) + +// func buildMockResponsesForRepositoryFileMigrationV0toV1(mockOwner, mockRepo string, wantRepoID int) []*mockResponse { +// responseBodyJson, err := json.Marshal(github.Repository{ +// ID: github.Ptr(int64(wantRepoID)), +// DefaultBranch: github.Ptr("main"), +// Name: github.Ptr(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_resourceGithubRepositoryFileStateUpgradeV0toV1(t *testing.T) { +// t.Parallel() + +// for _, d := range []struct { +// testName string +// rawState map[string]any +// want map[string]any +// shouldError bool +// }{ +// { +// testName: "preserves_existing_branch", +// rawState: map[string]any{ +// "id": "test-repo/path/to/file.txt", +// "repository": "test-repo", +// "file": "path/to/file.txt", +// "content": "file content", +// "branch": "main", +// "commit_sha": "abc123", +// "sha": "def456", +// "overwrite_on_create": false, +// }, +// want: map[string]any{ +// "id": "test-repo:path/to/file.txt:main", +// "repository": "test-repo", +// "repository_id": 1234567890, +// "file": "path/to/file.txt", +// "content": "file content", +// "branch": "main", +// "commit_sha": "abc123", +// "sha": "def456", +// "overwrite_on_create": false, +// }, +// shouldError: false, +// }, +// { +// testName: "preserves_custom_branch", +// rawState: map[string]any{ +// "id": "test-repo/README.md", +// "repository": "test-repo", +// "file": "README.md", +// "content": "# README", +// "branch": "develop", +// }, +// want: map[string]any{ +// "id": "test-repo:README.md:develop", +// "repository": "test-repo", +// "repository_id": 1234567890, +// "file": "README.md", +// "content": "# README", +// "branch": "develop", +// }, +// shouldError: false, +// }, +// { +// testName: "migrates_with_missing_branch", +// rawState: map[string]any{ +// "id": "test-repo/path/to/file.txt", +// "repository": "test-repo", +// "file": "path/to/file.txt", +// "content": "file content", +// }, +// want: map[string]any{ +// "id": "test-repo:path/to/file.txt:main", +// "repository": "test-repo", +// "repository_id": 1234567890, +// "file": "path/to/file.txt", +// "content": "file content", +// "branch": "main", // fetched from API +// }, +// shouldError: false, +// }, +// { +// testName: "migrates_with_empty_branch", +// rawState: map[string]any{ +// "id": "test-repo/path/to/file.txt", +// "repository": "test-repo", +// "file": "path/to/file.txt", +// "content": "file content", +// "branch": "", +// }, +// want: map[string]any{ +// "id": "test-repo:path/to/file.txt:main", +// "repository": "test-repo", +// "repository_id": 1234567890, +// "file": "path/to/file.txt", +// "content": "file content", +// "branch": "main", // fetched from API +// }, +// shouldError: false, +// }, +// } { +// t.Run(d.testName, func(t *testing.T) { +// t.Parallel() + +// meta := &Owner{ +// name: "test-org", +// } + +// ts := githubApiMock(buildMockResponsesForRepositoryFileMigrationV0toV1(meta.name, d.want["repository"].(string), d.want["repository_id"].(int))) +// defer ts.Close() + +// httpCl := http.DefaultClient +// httpCl.Transport = http.DefaultTransport + +// client := github.NewClient(httpCl) +// u, _ := url.Parse(ts.URL + "/") +// client.BaseURL = u +// meta.v3client = client + +// got, err := resourceGithubRepositoryFileStateUpgradeV0(context.Background(), 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) +// } +// }) +// } +// } diff --git a/github/resource_github_repository_file_test.go b/github/resource_github_repository_file_test.go index 2f11e27311..8dba39abba 100644 --- a/github/resource_github_repository_file_test.go +++ b/github/resource_github_repository_file_test.go @@ -33,30 +33,13 @@ func TestAccGithubRepositoryFile(t *testing.T) { } `, repoName) 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", - "main", - ), - 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", - ), + resource.TestCheckResourceAttr("github_repository_file.test", "content", "bar"), + resource.TestCheckResourceAttr("github_repository_file.test", "sha", "ba0e162e1c47469e3fe4b393a8bf8c569f302116"), + resource.TestCheckResourceAttr("github_repository_file.test", "ref", "main"), + 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"), resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch"), resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch_source_branch"), resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch_source_sha"), @@ -272,7 +255,7 @@ func TestAccGithubRepositoryFile(t *testing.T) { t.Run("creates and manages files on auto created branch if branch does not exist", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%srepo-file-%s", testResourcePrefix, randomID) - config := fmt.Sprintf(` + config := ` resource "github_repository" "test" { name = "%s" auto_init = true @@ -287,53 +270,33 @@ func TestAccGithubRepositoryFile(t *testing.T) { commit_message = "Managed by Terraform" commit_author = "Terraform User" commit_email = "terraform@example.com" - autocreate_branch = false + autocreate_branch = %t } - `, repoName) - - 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", - ), - resource.TestCheckResourceAttr("github_repository_file.test", "autocreate_branch", "true"), - resource.TestCheckResourceAttr("github_repository_file.test", "autocreate_branch_source_branch", "main"), - resource.TestCheckResourceAttrSet("github_repository_file.test", "autocreate_branch_source_sha"), - ) + ` resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, + Config: fmt.Sprintf(config, repoName, false), ExpectError: regexp.MustCompile(`unexpected status code: 404 Not Found`), }, { - Config: strings.Replace(config, - "autocreate_branch = false", - "autocreate_branch = true", 1), - Check: check, + Config: fmt.Sprintf(config, repoName, true), + 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"), + resource.TestCheckResourceAttr("github_repository_file.test", "autocreate_branch", "true"), + resource.TestCheckResourceAttr("github_repository_file.test", "autocreate_branch_source_branch", "main"), + resource.TestCheckResourceAttrSet("github_repository_file.test", "autocreate_branch_source_sha"), + resource.TestCheckResourceAttrSet("github_repository_file.test", "repository_id"), + ), }, }, }) @@ -371,19 +334,13 @@ func TestAccGithubRepositoryFile(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_repository_file.test", "file", - "archived-test.md", - ), + resource.TestCheckResourceAttr("github_repository_file.test", "file", "archived-test.md"), ), }, { Config: archivedConfig, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_repository.test", "archived", - "true", - ), + resource.TestCheckResourceAttr("github_repository.test", "archived", "true"), ), }, // This step should succeed - the file should be removed from state @@ -400,4 +357,102 @@ func TestAccGithubRepositoryFile(t *testing.T) { }, }) }) + t.Run("imports_files_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sfile-import-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + vulnerability_alerts = true + } + + resource "github_repository_file" "test" { + repository = github_repository.test.name + file = "test" + content = "bar" + commit_message = "Managed by Terraform" + commit_author = "Terraform User" + commit_email = "terraform@example.com" + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_repository_file.test", "content", "bar"), + resource.TestCheckResourceAttrSet("github_repository_file.test", "sha"), + resource.TestCheckResourceAttr("github_repository_file.test", "ref", "main"), + 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"), + resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch"), + resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch_source_branch"), + resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch_source_sha"), + resource.TestCheckResourceAttrSet("github_repository_file.test", "repository_id"), + ), + }, + { + ResourceName: "github_repository_file.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"commit_author", "commit_email"}, // For some reason `d` doesn't contain the commit author and email when importing. + }, + }, + }) + }) + t.Run("imports_files_with_branch_in_id_without_error", func(t *testing.T) { + randomID := acctest.RandString(5) + repoName := fmt.Sprintf("%sfile-import-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + auto_init = true + vulnerability_alerts = true + } + + resource "github_repository_file" "test" { + repository = github_repository.test.name + file = "test" + content = "bar" + commit_message = "Managed by Terraform" + commit_author = "Terraform User" + commit_email = "terraform@example.com" + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_repository_file.test", "content", "bar"), + resource.TestCheckResourceAttrSet("github_repository_file.test", "sha"), + resource.TestCheckResourceAttr("github_repository_file.test", "ref", "main"), + 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"), + resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch"), + resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch_source_branch"), + resource.TestCheckNoResourceAttr("github_repository_file.test", "autocreate_branch_source_sha"), + resource.TestCheckResourceAttrSet("github_repository_file.test", "repository_id"), + ), + }, + { + ResourceName: "github_repository_file.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"commit_author", "commit_email"}, // For some reason `d` doesn't contain the commit author and email when importing. + }, + }, + }) + }) } diff --git a/github/util.go b/github/util.go index e277299715..559f999e80 100644 --- a/github/util.go +++ b/github/util.go @@ -242,11 +242,6 @@ func (e *unconvertibleIdError) Error() string { e.OriginalId, e.OriginalError.Error()) } -func splitRepoFilePath(path string) (string, string) { - parts := strings.Split(path, "/") - return parts[0], strings.Join(parts[1:], "/") -} - func getTeamID(teamIDString string, meta any) (int64, error) { // Given a string that is either a team id or team slug, return the // id of the team it is referring to. diff --git a/website/docs/r/repository_file.html.markdown b/website/docs/r/repository_file.html.markdown index cf614f9912..8bd65b16fb 100644 --- a/website/docs/r/repository_file.html.markdown +++ b/website/docs/r/repository_file.html.markdown @@ -15,10 +15,11 @@ GitHub repository. ## Example Usage ### Existing Branch + ```hcl resource "github_repository" "foo" { - name = "tf-acc-test-%s" + name = "example" auto_init = true } @@ -36,10 +37,11 @@ resource "github_repository_file" "foo" { ``` ### Auto Created Branch + ```hcl resource "github_repository" "foo" { - name = "tf-acc-test-%s" + name = "example" auto_init = true } @@ -57,55 +59,55 @@ resource "github_repository_file" "foo" { ``` - ## Argument Reference The following arguments are supported: -* `repository` - (Required) The repository to create the file in. +- `repository` - (Required) The repository to create the file in. -* `file` - (Required) The path of the file to manage. +- `file` - (Required) The path of the file to manage. -* `content` - (Required) The file content. +- `content` - (Required) The file content. -* `branch` - (Optional) Git branch (defaults to the repository's default branch). +- `branch` - (Optional) Git branch (defaults to the repository's default branch). 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. +- `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. -* `commit_email` - (Optional) Committer email address to use. **NOTE:** GitHub app users may omit author and email information so GitHub can verify commits as the GitHub App. This may be useful when a branch protection rule requires signed commits. +- `commit_email` - (Optional) Committer email address to use. **NOTE:** GitHub app users may omit author and email information so GitHub can verify commits as the GitHub App. This may be useful when a branch protection rule requires signed commits. -* `commit_message` - (Optional) The commit message when creating, updating or deleting the managed file. +- `commit_message` - (Optional) The commit message when creating, updating or deleting the managed file. -* `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. +- `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` - (Optional) **Deprecated** 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'. Use the `github_branch` resource instead. -* `autocreate_branch_source_branch` - (Optional) The branch name to start from, if 'autocreate_branch' is set. Defaults to 'main'. +- `autocreate_branch_source_branch` - (Optional) **Deprecated** The branch name to start from, if 'autocreate_branch' is set. Defaults to 'main'. Use the `github_branch` resource instead. -* `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. +- `autocreate_branch_source_sha` - (Optional) **Deprecated** 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. Use the `github_branch` resource instead. ## Attributes Reference The following additional attributes are exported: -* `commit_sha` - The SHA of the commit that modified the file. +- `commit_sha` - The SHA of the commit that modified the file. -* `sha` - The SHA blob of the file. +- `repository_id` - The ID of the repository. -* `ref` - The name of the commit/branch/tag. +- `sha` - The SHA blob of the file. +- `ref` - The name of the commit/branch/tag. ## Import -Repository files can be imported using a combination of the `repo` and `file`, e.g. +Repository files can be imported using a combination of the `repo`, `file` and `branch` or empty branch for the default branch, e.g. -``` -$ terraform import github_repository_file.gitignore example/.gitignore +```sh +terraform import github_repository_file.gitignore example:.gitignore:feature-branch ``` -To import a file from a branch other than the default branch, append `:` and the branch name, e.g. +and using default branch: -``` -$ terraform import github_repository_file.gitignore example/.gitignore:dev +```sh +terraform import github_repository_file.gitignore example:.gitignore: ```