From 716a3f8172afbc3592fca78cba4ba7f4538d32d9 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:54:46 +0000 Subject: [PATCH 1/4] fix(weekly): don't trust a re-anchored approval on a rebased dependency PR --- plugins/tend-ci-runner/skills/weekly/SKILL.md | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/plugins/tend-ci-runner/skills/weekly/SKILL.md b/plugins/tend-ci-runner/skills/weekly/SKILL.md index eb15003f..0063be89 100644 --- a/plugins/tend-ci-runner/skills/weekly/SKILL.md +++ b/plugins/tend-ci-runner/skills/weekly/SKILL.md @@ -28,10 +28,26 @@ If no dependency PRs are open, note "0 dependency PRs to process" and continue t ```bash HEAD_SHA=$(gh pr view --json commits --jq '.commits[-1].oid') BOT_LOGIN=$(gh api user --jq '.login') - LAST_APPROVAL_SHA=$(gh pr view --json reviews \ - --jq "[.reviews[] | select(.author.login == \"$BOT_LOGIN\" and .state == \"APPROVED\")] | last | .commit.oid // empty") + REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner') - if [ "$LAST_APPROVAL_SHA" = "$HEAD_SHA" ]; then + # A force-push re-points an existing review's commit anchor at the NEW head, + # so an approval of the pre-rebase code reports the current `$HEAD_SHA` and + # this guard skips a commit the bot never read — leaving the rebased PR + # carrying an approval it never earned. Dependency PRs are the population + # tend rewrites on purpose (`nightly` posts `@dependabot recreate` and ticks + # renovate's rebase-check), so ignore approvals older than the newest + # rewrite. REST reviews carry `.commit_id`/`.submitted_at`, NOT the + # `.commit.oid` that `gh pr view --json reviews` returns; `gh api --jq` + # accepts no `--arg`, so pipe to `jq`. + LAST_FORCE_PUSH_AT=$(gh api --paginate "repos/$REPO/issues//timeline" \ + | jq -rs 'add | [.[] | select(.event == "head_ref_force_pushed") | .created_at] | max // ""') + LAST_APPROVAL_SHA=$(gh api --paginate "repos/$REPO/pulls//reviews" \ + | jq -rs --arg bot "$BOT_LOGIN" --arg fp "$LAST_FORCE_PUSH_AT" \ + 'add | [.[] | select(.user.login == $bot and .state == "APPROVED") + | select($fp == "" or .submitted_at > $fp)] + | last | .commit_id // empty') + + if [ -n "$LAST_APPROVAL_SHA" ] && [ "$LAST_APPROVAL_SHA" = "$HEAD_SHA" ]; then echo "Already approved on this commit; skipping." else # Compose a one-line review body naming the package, bump type, and what you From 6281a54b6485883e93223957189e53d8a3e7eb29 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:02:55 +0000 Subject: [PATCH 2/4] fix(weekly): dismiss a re-anchored approval on the comment-and-skip paths --- plugins/tend-ci-runner/skills/weekly/SKILL.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plugins/tend-ci-runner/skills/weekly/SKILL.md b/plugins/tend-ci-runner/skills/weekly/SKILL.md index 0063be89..e75ec6c2 100644 --- a/plugins/tend-ci-runner/skills/weekly/SKILL.md +++ b/plugins/tend-ci-runner/skills/weekly/SKILL.md @@ -57,6 +57,25 @@ If no dependency PRs are open, note "0 dependency PRs to process" and continue t ``` 4. If CI is failing, comment with the failure summary and skip 5. If a major version bump, comment noting it needs manual review and skip +6. On either skip path (4 or 5), dismiss an approval that predates the newest rewrite before you leave. Both paths are reachable *because* a rebase changed something, and neither passes through item 3's guard — so the pre-rewrite approval stays the bot's latest review, re-anchored onto the current head, and the PR still reads as bot-approved while you comment that it isn't mergeable: + ```bash + BOT_LOGIN=$(gh api user --jq '.login') + REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner') + LAST_FORCE_PUSH_AT=$(gh api --paginate "repos/$REPO/issues//timeline" \ + | jq -rs 'add | [.[] | select(.event == "head_ref_force_pushed") | .created_at] | max // ""') + STALE_APPROVAL_ID=$(gh api --paginate "repos/$REPO/pulls//reviews" \ + | jq -rs --arg bot "$BOT_LOGIN" --arg fp "$LAST_FORCE_PUSH_AT" \ + 'add | [.[] | select(.user.login == $bot and .state == "APPROVED") + | select($fp != "" and .submitted_at < $fp)] + | last | .id // empty') + + if [ -n "$STALE_APPROVAL_ID" ]; then + # PUT, not POST — the dismiss endpoint requires it. + gh api "repos/$REPO/pulls//reviews/$STALE_APPROVAL_ID/dismissals" \ + -X PUT -f message="Rebased since this approval; re-reviewing." + fi + ``` + A dismissed review reports `DISMISSED` rather than `APPROVED`, so the filter stops matching it and a later run re-dismisses nothing. ## Step 3: Repo-specific weekly tasks From f3a5bed2b2487c81dec8656474129613ade6981f Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:13:33 +0000 Subject: [PATCH 3/4] fix(weekly): test the newest approval for staleness, not any stale one --- plugins/tend-ci-runner/skills/weekly/SKILL.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/plugins/tend-ci-runner/skills/weekly/SKILL.md b/plugins/tend-ci-runner/skills/weekly/SKILL.md index e75ec6c2..3cce5f3a 100644 --- a/plugins/tend-ci-runner/skills/weekly/SKILL.md +++ b/plugins/tend-ci-runner/skills/weekly/SKILL.md @@ -63,16 +63,23 @@ If no dependency PRs are open, note "0 dependency PRs to process" and continue t REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner') LAST_FORCE_PUSH_AT=$(gh api --paginate "repos/$REPO/issues//timeline" \ | jq -rs 'add | [.[] | select(.event == "head_ref_force_pushed") | .created_at] | max // ""') + # `last` BEFORE the staleness test, not after: the question is whether the + # newest approval is stale, not whether some stale approval exists. Filtering + # first would return the pre-rewrite id even when a later approval already + # superseded it — dismissing a review that no longer sets the PR's state + # while the one that does stands untouched. STALE_APPROVAL_ID=$(gh api --paginate "repos/$REPO/pulls//reviews" \ | jq -rs --arg bot "$BOT_LOGIN" --arg fp "$LAST_FORCE_PUSH_AT" \ - 'add | [.[] | select(.user.login == $bot and .state == "APPROVED") - | select($fp != "" and .submitted_at < $fp)] - | last | .id // empty') + 'add | [.[] | select(.user.login == $bot and .state == "APPROVED")] + | last + | select(. != null and $fp != "" and .submitted_at < $fp) + | .id') if [ -n "$STALE_APPROVAL_ID" ]; then - # PUT, not POST — the dismiss endpoint requires it. + # PUT, not POST — the dismiss endpoint requires it. Keep the message to what + # these paths actually do: they comment and stop, so don't promise a re-review. gh api "repos/$REPO/pulls//reviews/$STALE_APPROVAL_ID/dismissals" \ - -X PUT -f message="Rebased since this approval; re-reviewing." + -X PUT -f message="Rebased since this approval; the new head is unreviewed." fi ``` A dismissed review reports `DISMISSED` rather than `APPROVED`, so the filter stops matching it and a later run re-dismisses nothing. From ef874ebb5a534b7ef11cf0f47eaa86f552fa2fe4 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:20:58 +0000 Subject: [PATCH 4/4] docs(weekly): note why item 3's filter order is equivalent --- plugins/tend-ci-runner/skills/weekly/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/tend-ci-runner/skills/weekly/SKILL.md b/plugins/tend-ci-runner/skills/weekly/SKILL.md index 3cce5f3a..b7bda81a 100644 --- a/plugins/tend-ci-runner/skills/weekly/SKILL.md +++ b/plugins/tend-ci-runner/skills/weekly/SKILL.md @@ -67,7 +67,9 @@ If no dependency PRs are open, note "0 dependency PRs to process" and continue t # newest approval is stale, not whether some stale approval exists. Filtering # first would return the pre-rewrite id even when a later approval already # superseded it — dismissing a review that no longer sets the PR's state - # while the one that does stands untouched. + # while the one that does stands untouched. Item 3 keeps the opposite order + # and is still correct: `submitted_at` is monotonic here, so its `> $fp` keeps + # a suffix (last-of-suffix == last-of-list) while this `< $fp` keeps a prefix. STALE_APPROVAL_ID=$(gh api --paginate "repos/$REPO/pulls//reviews" \ | jq -rs --arg bot "$BOT_LOGIN" --arg fp "$LAST_FORCE_PUSH_AT" \ 'add | [.[] | select(.user.login == $bot and .state == "APPROVED")]