diff --git a/plugins/tend-ci-runner/skills/weekly/SKILL.md b/plugins/tend-ci-runner/skills/weekly/SKILL.md index eb15003f..b7bda81a 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 @@ -41,6 +57,34 @@ 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 // ""') + # `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. 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")] + | last + | select(. != null and $fp != "" and .submitted_at < $fp) + | .id') + + if [ -n "$STALE_APPROVAL_ID" ]; then + # 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; 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. ## Step 3: Repo-specific weekly tasks