From 7e6024334e205e1f07a166924f5462c8cc8f4349 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:05:29 -0400 Subject: [PATCH 1/3] fix(ci): land.sh hands a PR back when a review thread opens after auto-merge The ruleset holds a PR with an unresolved thread, and land.sh's wait loop only watched for merge, DIRTY, a moved main and failing checks, so a thread the reviewer opened after auto-merge went on (#258, 2026-10-26) stalled the train until the deadline. The loop now turns auto-merge off and exits 2 on a positive unresolved count; a failed query counts as none. --- tools/maintainer/train/land.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tools/maintainer/train/land.sh b/tools/maintainer/train/land.sh index f6624484..7eb0e245 100755 --- a/tools/maintainer/train/land.sh +++ b/tools/maintainer/train/land.sh @@ -8,7 +8,7 @@ # main: when every generated file comes out unchanged the PR keeps its CI results and lands; # otherwise auto-merge goes off and it exits 8 for a real refresh. # -# Exit: 0 merged, 1 failing check, 2 unresolved threads, 3 timeout, 4 someone else pushed, +# Exit: 0 merged, 1 failing check, 2 unresolved threads (before or after auto-merge), 3 timeout, 4 someone else pushed, # 5 auto-merge refused, 6 closed, 7 went DIRTY, 8 main moved under it (refresh again). { P=$1 @@ -51,9 +51,12 @@ while :; do esac sleep 60 done -u=$(gh api graphql -F owner="${R%/*}" -F name="${R#*/}" -F n="$P" \ - -f query='query($owner:String!,$name:String!,$n:Int!){repository(owner:$owner,name:$name){pullRequest(number:$n){reviewThreads(first:100){nodes{isResolved}}}}}' \ - --jq '[.data.repository.pullRequest.reviewThreads.nodes[]|select(.isResolved==false)]|length') +unresolved() { + gh api graphql -F owner="${R%/*}" -F name="${R#*/}" -F n="$P" \ + -f query='query($owner:String!,$name:String!,$n:Int!){repository(owner:$owner,name:$name){pullRequest(number:$n){reviewThreads(first:100){nodes{isResolved}}}}}' \ + --jq '[.data.repository.pullRequest.reviewThreads.nodes[]|select(.isResolved==false)]|length' 2>/dev/null +} +u=$(unresolved) [ "$u" != 0 ] && { echo "#$P Corpus green but $u unresolved review thread(s)"; exit 2; } if moved && ! still_valid; then echo "#$P main moved under it since the refresh"; exit 8; fi gh pr merge "$P" -R "$R" --squash --auto > /dev/null 2>&1 || { echo "#$P could not enable auto-merge"; exit 5; } @@ -77,6 +80,12 @@ while :; do fi f=$(gh pr checks "$P" -R "$R" --json name,state --jq '[.[]|select(.state=="FAILURE")|.name]|join(", ")' 2>/dev/null) [ -n "$f" ] && { echo "#$P failing: $f"; exit 1; } + # A reviewer can open a thread after auto-merge went on; the ruleset then holds the PR until + # the deadline, so hand it back instead of stalling the train. + case $(unresolved) in + [1-9]|[1-9][0-9]) gh pr merge "$P" -R "$R" --disable-auto > /dev/null 2>&1 + echo "#$P review thread opened after auto-merge went on; auto-merge off"; exit 2 ;; + esac sleep 60 done } From b3a8df36b19118d07f2e8319b6595a050a960b23 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:55:09 -0400 Subject: [PATCH 2/3] fix(ci): land.sh treats any positive thread count as open, not just 1-99 --- tools/maintainer/train/land.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/maintainer/train/land.sh b/tools/maintainer/train/land.sh index 7eb0e245..855367bd 100755 --- a/tools/maintainer/train/land.sh +++ b/tools/maintainer/train/land.sh @@ -83,7 +83,8 @@ while :; do # A reviewer can open a thread after auto-merge went on; the ruleset then holds the PR until # the deadline, so hand it back instead of stalling the train. case $(unresolved) in - [1-9]|[1-9][0-9]) gh pr merge "$P" -R "$R" --disable-auto > /dev/null 2>&1 + ''|*[!0-9]*|0) ;; + *) gh pr merge "$P" -R "$R" --disable-auto > /dev/null 2>&1 echo "#$P review thread opened after auto-merge went on; auto-merge off"; exit 2 ;; esac sleep 60 From 3680cc86b648fcf2bfeff4173ac7eec513be6cb5 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:56:12 -0400 Subject: [PATCH 3/3] fix(ci): land.sh turns auto-merge off on every failed exit after enabling it A timeout or a failing check left auto-merge on, so the abandoned PR could merge while the train refreshed the next one (#298 review). A branch that strict up-to-date reports BEHIND now exits 8 for a real refresh instead of waiting out the deadline. --- tools/maintainer/train/land.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/maintainer/train/land.sh b/tools/maintainer/train/land.sh index 855367bd..0371d979 100755 --- a/tools/maintainer/train/land.sh +++ b/tools/maintainer/train/land.sh @@ -62,12 +62,14 @@ if moved && ! still_valid; then echo "#$P main moved under it since the refresh" gh pr merge "$P" -R "$R" --squash --auto > /dev/null 2>&1 || { echo "#$P could not enable auto-merge"; exit 5; } echo "#$P Corpus green, 0 threads, auto-merge on" while :; do - [ "$(date +%s)" -gt $((deadline + 3600)) ] && { echo "#$P TIMEOUT waiting for merge"; exit 3; } + [ "$(date +%s)" -gt $((deadline + 3600)) ] && { gh pr merge "$P" -R "$R" --disable-auto > /dev/null 2>&1; echo "#$P TIMEOUT waiting for merge; auto-merge off"; exit 3; } s=$(gh pr view "$P" -R "$R" --json state,mergeStateStatus --jq '"\(.state) \(.mergeStateStatus)"' 2>/dev/null) case $s in MERGED*) echo "#$P MERGED as $(gh pr view "$P" -R "$R" --json mergeCommit --jq '.mergeCommit.oid[0:9]')"; exit 0 ;; CLOSED*) echo "#$P CLOSED without merge"; exit 6 ;; *DIRTY) gh pr merge "$P" -R "$R" --disable-auto > /dev/null 2>&1; echo "#$P went DIRTY before merging"; exit 7 ;; + # With strict up-to-date on, a branch behind main never merges; only a real refresh pushes main in. + *BEHIND) gh pr merge "$P" -R "$R" --disable-auto > /dev/null 2>&1; echo "#$P is behind main; auto-merge off"; exit 8 ;; esac if moved; then [ "$(gh pr view "$P" -R "$R" --json state --jq .state 2>/dev/null)" = MERGED ] && continue @@ -79,7 +81,7 @@ while :; do echo "#$P main moved under it while waiting; auto-merge off"; exit 8 fi f=$(gh pr checks "$P" -R "$R" --json name,state --jq '[.[]|select(.state=="FAILURE")|.name]|join(", ")' 2>/dev/null) - [ -n "$f" ] && { echo "#$P failing: $f"; exit 1; } + [ -n "$f" ] && { gh pr merge "$P" -R "$R" --disable-auto > /dev/null 2>&1; echo "#$P failing: $f; auto-merge off"; exit 1; } # A reviewer can open a thread after auto-merge went on; the ruleset then holds the PR until # the deadline, so hand it back instead of stalling the train. case $(unresolved) in