From 2698f7ce90b565c0793c07646c81b8249a0e9a55 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:20:19 +0000 Subject: [PATCH 1/4] fix(nightly): settle mergeable before filtering, and stop treating UNKNOWN as clean Closes #897 --- plugins/tend-ci-runner/skills/nightly/SKILL.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/tend-ci-runner/skills/nightly/SKILL.md b/plugins/tend-ci-runner/skills/nightly/SKILL.md index 0913a3a9..ab5abb0e 100644 --- a/plugins/tend-ci-runner/skills/nightly/SKILL.md +++ b/plugins/tend-ci-runner/skills/nightly/SKILL.md @@ -76,15 +76,24 @@ a one-line reason) plus a `_Last refreshed: _` footer. Updates: Find conflicted PRs from this bot and from upstream dependency bots: +`mergeable` is computed lazily, not stored. The first query after `main` moves returns `UNKNOWN` for every PR whose merge hasn't been recomputed and *enqueues* the computation; a later query returns the real value. Filtering a cold read on `== "CONFLICTING"` therefore reports a conflicted PR as clean. Re-query until it settles: + ```bash BOT_LOGIN=$(gh api user --jq '.login') for author in "$BOT_LOGIN" app/dependabot app/renovate; do - gh pr list --author "$author" --json number,title,mergeable,headRefName,author \ - --jq '.[] | select(.mergeable == "CONFLICTING")' + out="/tmp/prs-${author//\//-}.json" # `app/dependabot` has a slash; strip it + for _ in 1 2 3 4 5; do + gh pr list --author "$author" --json number,title,mergeable,headRefName,author > "$out" + [ "$(jq '[.[] | select(.mergeable == "UNKNOWN")] | length' "$out")" -eq 0 ] && break + sleep 10 + done + jq -c '.[] | select(.mergeable == "CONFLICTING")' "$out" + jq -r '.[] | select(.mergeable == "UNKNOWN") + | "unsettled, treat as possibly conflicted: #\(.number) \(.title)"' "$out" done ``` -Skip the rest of this step if none of the queries return anything. +Skip the rest of this step only when every PR settled and none came back `CONFLICTING`. A PR still `UNKNOWN` after the retries is not a clean one — check it out and test-merge (`git merge --no-commit --no-ff origin/main`) before dismissing it, and report it as unverified rather than counting it clean. ### Upstream dependency bots: trigger the bot's own rebase From 5e1e820181e96847c6875cee3c6fab985f5f63da Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:28:01 +0000 Subject: [PATCH 2/4] fix(nightly): distinguish a failed mergeable query from a clean one --- plugins/tend-ci-runner/skills/nightly/SKILL.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/tend-ci-runner/skills/nightly/SKILL.md b/plugins/tend-ci-runner/skills/nightly/SKILL.md index ab5abb0e..ec2169a0 100644 --- a/plugins/tend-ci-runner/skills/nightly/SKILL.md +++ b/plugins/tend-ci-runner/skills/nightly/SKILL.md @@ -84,16 +84,20 @@ for author in "$BOT_LOGIN" app/dependabot app/renovate; do out="/tmp/prs-${author//\//-}.json" # `app/dependabot` has a slash; strip it for _ in 1 2 3 4 5; do gh pr list --author "$author" --json number,title,mergeable,headRefName,author > "$out" + # A failed query leaves `$out` empty, which reads as "no conflicts" — the + # same silent-miss shape. `[]` (no open PRs) is 3 bytes, a failure is 0. + [ -s "$out" ] || { sleep 10; continue; } [ "$(jq '[.[] | select(.mergeable == "UNKNOWN")] | length' "$out")" -eq 0 ] && break sleep 10 done + [ -s "$out" ] || echo "query for $author never landed — conflicts unverified" jq -c '.[] | select(.mergeable == "CONFLICTING")' "$out" jq -r '.[] | select(.mergeable == "UNKNOWN") | "unsettled, treat as possibly conflicted: #\(.number) \(.title)"' "$out" done ``` -Skip the rest of this step only when every PR settled and none came back `CONFLICTING`. A PR still `UNKNOWN` after the retries is not a clean one — check it out and test-merge (`git merge --no-commit --no-ff origin/main`) before dismissing it, and report it as unverified rather than counting it clean. +Skip the rest of this step only when every query landed, every PR settled, and none came back `CONFLICTING`. A PR still `UNKNOWN` after the retries is not a clean one — check it out and test-merge (`git merge --no-commit --no-ff origin/main`) before dismissing it, and report it as unverified rather than counting it clean. ### Upstream dependency bots: trigger the bot's own rebase From 988b4c12cff4cf6b40581f6e054164c42d625c05 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:40:27 +0000 Subject: [PATCH 3/4] fix(nightly): test-merge locally instead of polling for mergeable git merge-tree answers the same question synchronously, so the retry loop goes away entirely. --- .../tend-ci-runner/skills/nightly/SKILL.md | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/plugins/tend-ci-runner/skills/nightly/SKILL.md b/plugins/tend-ci-runner/skills/nightly/SKILL.md index ec2169a0..2026fd32 100644 --- a/plugins/tend-ci-runner/skills/nightly/SKILL.md +++ b/plugins/tend-ci-runner/skills/nightly/SKILL.md @@ -76,28 +76,37 @@ a one-line reason) plus a `_Last refreshed: _` footer. Updates: Find conflicted PRs from this bot and from upstream dependency bots: -`mergeable` is computed lazily, not stored. The first query after `main` moves returns `UNKNOWN` for every PR whose merge hasn't been recomputed and *enqueues* the computation; a later query returns the real value. Filtering a cold read on `== "CONFLICTING"` therefore reports a conflicted PR as clean. Re-query until it settles: +Don't filter `gh pr list --json mergeable` on `== "CONFLICTING"`. `mergeable` is computed lazily: the first query after `main` moves returns `UNKNOWN` and only *enqueues* the computation, so a cold read reports a conflicted PR as clean. There is no blocking read — [the REST docs](https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request) prescribe resubmitting the request until the value settles. Test-merge locally instead: `git merge-tree` answers the same question synchronously, in-process, with no retry loop. ```bash BOT_LOGIN=$(gh api user --jq '.login') +git fetch --quiet origin main for author in "$BOT_LOGIN" app/dependabot app/renovate; do out="/tmp/prs-${author//\//-}.json" # `app/dependabot` has a slash; strip it - for _ in 1 2 3 4 5; do - gh pr list --author "$author" --json number,title,mergeable,headRefName,author > "$out" - # A failed query leaves `$out` empty, which reads as "no conflicts" — the - # same silent-miss shape. `[]` (no open PRs) is 3 bytes, a failure is 0. - [ -s "$out" ] || { sleep 10; continue; } - [ "$(jq '[.[] | select(.mergeable == "UNKNOWN")] | length' "$out")" -eq 0 ] && break - sleep 10 + gh pr list --author "$author" --json number,title,headRefName,author > "$out" + # A failed query leaves `$out` empty, which reads as "no conflicts" — the + # same silent-miss shape. `[]` (no open PRs) is 3 bytes, a failure is 0. + [ -s "$out" ] || { echo "query for $author never landed — conflicts unverified"; continue; } + # One fetch for every head, forced because bot branches get force-pushed. + # `refs/pull/N/head` also covers PRs opened from a fork. + mapfile -t refs < <(jq -r '.[].number | "refs/pull/\(.)/head:refs/tend/pr/\(.)"' "$out") + [ "${#refs[@]}" -eq 0 ] || git fetch --quiet --force origin "${refs[@]}" + jq -r '.[] | "\(.number)\t\(.title)"' "$out" | while IFS=$'\t' read -r n title; do + # Clean merge: exit 0. Conflict: non-zero with the tree OID and conflicted + # paths on stdout. Anything else (ref never fetched, bad revision): non-zero + # with stdout empty — so test `$tree`, not the exit status alone. + if tree=$(git merge-tree --write-tree origin/main "refs/tend/pr/$n" 2>/dev/null); then + continue + elif [ -n "$tree" ]; then + echo "CONFLICTING: #$n $title" + else + echo "merge test never ran, conflicts unverified: #$n $title" + fi done - [ -s "$out" ] || echo "query for $author never landed — conflicts unverified" - jq -c '.[] | select(.mergeable == "CONFLICTING")' "$out" - jq -r '.[] | select(.mergeable == "UNKNOWN") - | "unsettled, treat as possibly conflicted: #\(.number) \(.title)"' "$out" done ``` -Skip the rest of this step only when every query landed, every PR settled, and none came back `CONFLICTING`. A PR still `UNKNOWN` after the retries is not a clean one — check it out and test-merge (`git merge --no-commit --no-ff origin/main`) before dismissing it, and report it as unverified rather than counting it clean. +Skip the rest of this step only when every query landed and nothing printed. A PR whose merge test never ran is not a clean one — investigate it and report it as unverified rather than counting it clean. ### Upstream dependency bots: trigger the bot's own rebase From 97ea7eb8cb50a5e5a6fde3f107451194dcfaae9c Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:48:56 +0000 Subject: [PATCH 4/4] fix(nightly): page the PR list, and name the author in each finding --- plugins/tend-ci-runner/skills/nightly/SKILL.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/tend-ci-runner/skills/nightly/SKILL.md b/plugins/tend-ci-runner/skills/nightly/SKILL.md index 2026fd32..f4143679 100644 --- a/plugins/tend-ci-runner/skills/nightly/SKILL.md +++ b/plugins/tend-ci-runner/skills/nightly/SKILL.md @@ -83,7 +83,9 @@ BOT_LOGIN=$(gh api user --jq '.login') git fetch --quiet origin main for author in "$BOT_LOGIN" app/dependabot app/renovate; do out="/tmp/prs-${author//\//-}.json" # `app/dependabot` has a slash; strip it - gh pr list --author "$author" --json number,title,headRefName,author > "$out" + # `--limit 100` is load-bearing: `gh pr list` defaults to 30 and truncates + # silently, so PR 31 onward would never be test-merged. + gh pr list --author "$author" --limit 100 --json number,title,headRefName,author > "$out" # A failed query leaves `$out` empty, which reads as "no conflicts" — the # same silent-miss shape. `[]` (no open PRs) is 3 bytes, a failure is 0. [ -s "$out" ] || { echo "query for $author never landed — conflicts unverified"; continue; } @@ -98,9 +100,9 @@ for author in "$BOT_LOGIN" app/dependabot app/renovate; do if tree=$(git merge-tree --write-tree origin/main "refs/tend/pr/$n" 2>/dev/null); then continue elif [ -n "$tree" ]; then - echo "CONFLICTING: #$n $title" + echo "CONFLICTING: $author #$n $title" else - echo "merge test never ran, conflicts unverified: #$n $title" + echo "merge test never ran, conflicts unverified: $author #$n $title" fi done done