From b6b4ceaf868d623021b12ec86ed12924944e7576 Mon Sep 17 00:00:00 2001 From: Erik Miller Date: Wed, 29 Jul 2026 13:31:32 -0700 Subject: [PATCH] fix: remove repository file from state when its commit no longer exists resourceGithubRepositoryFileRead surfaced a 404 from the commit lookup, and getFileCommit's 'no commit contains the file' case, as read failures. Terraform's contract for read is to clear the ID when the remote object is gone, which the other repository resources already do. Erroring instead leaves the resource unable to refresh or be removed without editing state by hand. Wrap getFileCommit's exhaustion case in an errFileCommitNotFound sentinel so the read can distinguish it from a genuine failure, and translate both it and a GitHub 404 into d.SetId(""). Every other error keeps propagating unchanged. --- github/resource_github_repository_file.go | 14 ++++++++++ github/util_repo.go | 7 ++++- github/util_repo_test.go | 34 +++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 github/util_repo_test.go 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) + } +}