watchpr: detect merge/close in poll loop (was hanging after baseline)
watchpr only reported meaningful changes as transitions from the poll baseline. A PR already merged or closed when the watch started was captured as the baseline and never produced a transition, so the loop polled the dead PR forever (process alive, never exiting) -- the single-PR --interval case observed in production. Add a terminal-state check applied to the baseline snapshot: a PR that is already merged or closed the moment watchpr starts is reported and exits immediately, since it can never change again. Extract the baseline+poll loop into agent.Watch behind a StateFetcher interface so the loop, its open->merged/close detection, and its poll-error resilience are unit-testable with a fake client.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package agent
|
||||
|
||||
import "time"
|
||||
|
||||
// PRState is a point-in-time snapshot of the PR attributes watchpr tracks.
|
||||
type PRState struct {
|
||||
Ref PRRef `json:"ref"`
|
||||
@@ -41,6 +43,78 @@ func FetchState(c *GiteaClient, ref PRRef, agentLogin string) (PRState, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StateFetcher fetches the current PRState for a ref. *GiteaClient satisfies it
|
||||
// via its FetchState method; tests inject fakes.
|
||||
type StateFetcher interface {
|
||||
FetchState(ref PRRef, agentLogin string) (PRState, error)
|
||||
}
|
||||
|
||||
// FetchState makes *GiteaClient a StateFetcher.
|
||||
func (c *GiteaClient) FetchState(ref PRRef, agentLogin string) (PRState, error) {
|
||||
return FetchState(c, ref, agentLogin)
|
||||
}
|
||||
|
||||
// WatchResult is the change that ended a watch.
|
||||
type WatchResult struct {
|
||||
Ref PRRef
|
||||
Reason string
|
||||
State PRState
|
||||
}
|
||||
|
||||
// terminalState reports whether a PR has reached a final state from which no
|
||||
// further meaningful change is possible, with a human-readable reason. Unlike a
|
||||
// transition (see MeaningfulChange) this holds for a single snapshot, so it also
|
||||
// catches a PR that is already merged/closed the moment watchpr starts.
|
||||
func terminalState(st PRState) (bool, string) {
|
||||
if st.Merged {
|
||||
return true, "PR merged"
|
||||
}
|
||||
if st.State == "closed" {
|
||||
return true, "PR closed without merging"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Watch establishes a baseline for each ref, then polls on every tick until a
|
||||
// tracked PR changes meaningfully, returning the first such change. A PR that is
|
||||
// already terminal (merged/closed) at baseline is reported immediately rather
|
||||
// than polled forever. Poll errors are handed to onError and never stop the
|
||||
// loop; only a baseline fetch error aborts. onBaseline, if set, fires once after
|
||||
// all baselines are captured and before the first tick.
|
||||
func Watch(f StateFetcher, refs []PRRef, agentLogin string, ticks <-chan time.Time, onBaseline func(), onError func(PRRef, error)) (WatchResult, error) {
|
||||
prev := make(map[string]PRState, len(refs))
|
||||
for _, ref := range refs {
|
||||
st, err := f.FetchState(ref, agentLogin)
|
||||
if err != nil {
|
||||
return WatchResult{}, err
|
||||
}
|
||||
if terminal, reason := terminalState(st); terminal {
|
||||
return WatchResult{Ref: ref, Reason: reason, State: st}, nil
|
||||
}
|
||||
prev[ref.String()] = st
|
||||
}
|
||||
if onBaseline != nil {
|
||||
onBaseline()
|
||||
}
|
||||
for range ticks {
|
||||
for _, ref := range refs {
|
||||
key := ref.String()
|
||||
cur, err := f.FetchState(ref, agentLogin)
|
||||
if err != nil {
|
||||
if onError != nil {
|
||||
onError(ref, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if changed, reason := MeaningfulChange(prev[key], cur); changed {
|
||||
return WatchResult{Ref: ref, Reason: reason, State: cur}, nil
|
||||
}
|
||||
prev[key] = cur
|
||||
}
|
||||
}
|
||||
return WatchResult{}, nil
|
||||
}
|
||||
|
||||
// countNonAgentComments counts comments authored by anyone other than agentLogin.
|
||||
func countNonAgentComments(comments []Comment, agentLogin string) int {
|
||||
n := 0
|
||||
|
||||
Reference in New Issue
Block a user