Fix non-zero exit on error and debounce transient mergeable=false
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

agentpr/watchpr already propagated command errors to a non-zero exit, but
that behaviour had no regression coverage and the root command was not
constructible outside main(). watchpr also fired a spurious conflict alert
because Gitea computes mergeability asynchronously and can briefly report
mergeable=false right after a push. The docs additionally printed the
AppRole role_id literal UUID.

- Extract newRootCmd() in both cmd/agentpr and cmd/watchpr so main() only
  runs Execute and exits non-zero on error; add tests asserting Execute
  returns an error for a bad PR ref / malformed --repo / no args.
- Debounce mergeability loss in MeaningfulChange: only alert when
  mergeable=false persists across two consecutive polls (both prev and cur
  false, still open); update the table test for one-poll-false (benign),
  false-persisting (alert), and recovered false->true (benign).
- Refer to AGENT_APPROLE_ROLE_ID by env var in README.md/AGENTS.md without
  printing the literal role_id; keep the code default and env override.
This commit is contained in:
2026-08-12 22:17:10 +10:00
parent 3f03e0d84f
commit 05594113a2
8 changed files with 102 additions and 13 deletions
+22 -1
View File
@@ -17,6 +17,7 @@ func base() PRState {
func TestMeaningfulChange(t *testing.T) {
tests := []struct {
name string
mutatePrev func(s *PRState)
mutate func(s *PRState)
wantChange bool
}{
@@ -56,10 +57,27 @@ func TestMeaningfulChange(t *testing.T) {
wantChange: true,
},
{
name: "lost mergeability alerts",
// A single mergeable=false poll is debounced: Gitea often reports
// this transiently right after a push.
name: "mergeable true to false for one poll is benign",
mutate: func(s *PRState) { s.Mergeable = false },
wantChange: false,
},
{
// mergeable=false persisting into a second consecutive poll is a
// real conflict and alerts.
name: "mergeable false persisting a second poll alerts",
mutatePrev: func(s *PRState) { s.Mergeable = false },
mutate: func(s *PRState) { s.Mergeable = false },
wantChange: true,
},
{
// mergeable recovered (false then true) must not alert.
name: "mergeable recovered false to true is benign",
mutatePrev: func(s *PRState) { s.Mergeable = false },
mutate: func(s *PRState) {},
wantChange: false,
},
{
name: "new head sha alone is benign",
mutate: func(s *PRState) { s.HeadSHA = "def456" },
@@ -69,6 +87,9 @@ func TestMeaningfulChange(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prev := base()
if tt.mutatePrev != nil {
tt.mutatePrev(&prev)
}
cur := base()
tt.mutate(&cur)
got, reason := MeaningfulChange(prev, cur)