diff --git a/tools/maintainer/train/land.sh b/tools/maintainer/train/land.sh index f6624484..0371d979 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,20 +51,25 @@ 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; } 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 @@ -76,7 +81,14 @@ 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 + ''|*[!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 done }