watchpr: fix watch mode hanging when a merged PR's head commit is gone
FetchState fetched the PR (merged=true) but then failed the whole state fetch when CommitStatus 404'd for a head commit that no longer existed (the branch was deleted after a squash/rebase merge). The merge signal was discarded, so the watch loop treated every post-merge poll as a transient error and never exited -- the 37-minute hang seen in production. - Add a typed APIError carrying the HTTP status so callers can detect a 404 without parsing error strings. - FetchState now tolerates a 404 from CommitStatus (commit gone => no CI status) and returns the authoritative merged/closed PR state. - Regression tests: FetchState survives a 404 status; the full watch loop, driven through a real client, detects a merge whose head commit is gone (both fail/hang before the fix).
This commit is contained in:
@@ -163,6 +163,59 @@ func TestFetchState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A merged PR whose branch was deleted leaves its head commit unreachable, so
|
||||
// the commit-status endpoint 404s. FetchState must still return the (merged)
|
||||
// state rather than failing, otherwise the watch loop never sees the merge.
|
||||
func TestFetchStateToleratesMissingCommit(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/v1/repos/unkin/repo/pulls/7", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, `{"number":7,"state":"closed","merged":true,"mergeable":true,"title":"feat","html_url":"u","head":{"sha":"cafebabecafebabe"}}`)
|
||||
})
|
||||
mux.HandleFunc("/api/v1/repos/unkin/repo/commits/cafebabecafebabe/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = io.WriteString(w, `{"message":"not found"}`)
|
||||
})
|
||||
mux.HandleFunc("/api/v1/repos/unkin/repo/issues/7/comments", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, `[]`)
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()}
|
||||
ref := PRRef{Owner: "unkin", Repo: "repo", Number: 7}
|
||||
st, err := FetchState(c, ref, "unkin-agent")
|
||||
if err != nil {
|
||||
t.Fatalf("FetchState must tolerate a 404 status for a gone commit: %v", err)
|
||||
}
|
||||
if !st.Merged || st.State != "closed" {
|
||||
t.Errorf("merged/state = %t/%q, want true/closed", st.Merged, st.State)
|
||||
}
|
||||
if st.CIStatus != "" {
|
||||
t.Errorf("CIStatus = %q, want empty (no status for a gone commit)", st.CIStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// A non-404 error from the status endpoint is still fatal: only "commit gone" is
|
||||
// tolerated, not, say, an auth or server failure.
|
||||
func TestFetchStateFailsOnNon404StatusError(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/v1/repos/unkin/repo/pulls/7", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, `{"number":7,"state":"open","merged":false,"mergeable":true,"head":{"sha":"cafebabecafebabe"}}`)
|
||||
})
|
||||
mux.HandleFunc("/api/v1/repos/unkin/repo/commits/cafebabecafebabe/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = io.WriteString(w, `{"message":"boom"}`)
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()}
|
||||
ref := PRRef{Owner: "unkin", Repo: "repo", Number: 7}
|
||||
if _, err := FetchState(c, ref, "unkin-agent"); err == nil {
|
||||
t.Fatal("FetchState should surface a 500 from the status endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGiteaAPIError(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/v1/repos/unkin/repo/pulls", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user