diff --git a/github/resource_github_repository_file.go b/github/resource_github_repository_file.go index d02bb7e294..550efc5f16 100644 --- a/github/resource_github_repository_file.go +++ b/github/resource_github_repository_file.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -331,6 +332,19 @@ func resourceGithubRepositoryFileRead(ctx context.Context, d *schema.ResourceDat commit, err = getFileCommit(ctx, client, owner, repoName, file, ref) } if err != nil { + // The commit recorded in state, or any commit still containing the file, + // may be unreachable because the repository, branch or history is gone. + // That is "the resource no longer exists", not a failure to read it. + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response != nil && ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "Removing repository file from state because its commit no longer exists in GitHub") + d.SetId("") + return nil + } + if errors.Is(err, errFileCommitNotFound) { + tflog.Info(ctx, "Removing repository file from state because no commit contains it") + d.SetId("") + return nil + } return diag.FromErr(err) } tflog.Debug(ctx, "Found file in commit", map[string]any{ diff --git a/github/util_repo.go b/github/util_repo.go index f8c30b4430..a809b46806 100644 --- a/github/util_repo.go +++ b/github/util_repo.go @@ -33,6 +33,11 @@ func checkRepositoryBranchExists(ctx context.Context, client *github.Client, own return nil } +// errFileCommitNotFound reports that no commit reachable from the requested ref +// still contains the file, so callers can remove the resource from state rather +// than surfacing a hard error. +var errFileCommitNotFound = errors.New("no commit contains the file") + func getFileCommit(ctx context.Context, client *github.Client, owner, repo, file, branch string) (*github.RepositoryCommit, error) { opts := &github.CommitsListOptions{ SHA: branch, @@ -89,7 +94,7 @@ func getFileCommit(ctx context.Context, client *github.Client, owner, repo, file } } - return nil, fmt.Errorf("cannot find file %s in repo %s/%s", file, owner, repo) + return nil, fmt.Errorf("cannot find file %s in repo %s/%s: %w", file, owner, repo, errFileCommitNotFound) } // getAutolinkByKeyPrefix returns a single autolink reference by key prefix that was configured for the given repository. diff --git a/github/util_repo_test.go b/github/util_repo_test.go new file mode 100644 index 0000000000..3bc476d6cd --- /dev/null +++ b/github/util_repo_test.go @@ -0,0 +1,34 @@ +package github + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +// Test_getFileCommit_NoCommitContainsFile locks in the errFileCommitNotFound +// sentinel. resourceGithubRepositoryFileRead relies on it to remove the resource +// from state instead of returning a hard error, so unwrapping this error would +// silently reintroduce that failure. +func Test_getFileCommit_NoCommitContainsFile(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err := w.Write([]byte("[]")); err != nil { + t.Errorf("failed to write response: %s", err) + } + })) + defer ts.Close() + + client := mustCreateTestGitHubClient(t, ts.URL+"/") + + _, err := getFileCommit(context.Background(), client, "owner", "repo", "some/file.txt", "main") + if err == nil { + t.Fatal("expected an error when no commit contains the file, got nil") + } + + if !errors.Is(err, errFileCommitNotFound) { + t.Errorf("errors.Is(err, errFileCommitNotFound) = false, want true; got error: %s", err) + } +}