From 0eb1669b0f1958963bf3676af4ee9fc6672cbb8f Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:00:12 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20update=20tend=20workflows=20(0.1.14=20?= =?UTF-8?q?=E2=86=92=200.1.15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tend-ci-fix.yaml | 4 +- .github/workflows/tend-mention.yaml | 106 ++++++++++++++++++---- .github/workflows/tend-nightly.yaml | 4 +- .github/workflows/tend-notifications.yaml | 20 +++- .github/workflows/tend-review-runs.yaml | 4 +- .github/workflows/tend-review.yaml | 88 +++++++++++++++++- .github/workflows/tend-triage.yaml | 6 +- .github/workflows/tend-weekly.yaml | 4 +- 8 files changed, 201 insertions(+), 35 deletions(-) diff --git a/.github/workflows/tend-ci-fix.yaml b/.github/workflows/tend-ci-fix.yaml index 42ed2fddf9eb..1dd5aed94733 100644 --- a/.github/workflows/tend-ci-fix.yaml +++ b/.github/workflows/tend-ci-fix.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-mention.yaml b/.github/workflows/tend-mention.yaml index d1825da6cf6a..725a430b1e54 100644 --- a/.github/workflows/tend-mention.yaml +++ b/.github/workflows/tend-mention.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -90,20 +90,20 @@ jobs: -f "client_payload[id]=${{ github.event.review.id || github.event.comment.id }}" verify: - # Skip comments on `tend-outage` issues: the action's Report-failure step - # auto-comments on those when Claude invocation fails, and without this - # guard those comments re-trigger tend-mention during a persistent outage - # (e.g. Anthropic 401), producing a self-sustaining ~1 run/minute loop - # until the outage clears. The prompt's self-loop guard can't help here - # because the model never executes — the action fails before Claude starts. - # A relayed review enters as `repository_dispatch` and is judged in the - # check step below, against the record the API holds. + # Skip comments on the issues tend files about its own health: the action + # auto-comments on those when a run fails or is refused, and without this + # guard those comments re-trigger tend-mention, producing a + # self-sustaining ~1 run/minute loop until the underlying condition + # clears. The prompt's self-loop guard can't help here because the model + # never executes — the action fails before Claude starts. A relayed review + # enters as `repository_dispatch` and is judged in the check step below, + # against the record the API holds. if: | github.event_name == 'repository_dispatch' || (github.event_name == 'issues' && contains(github.event.issue.body, '@prql-bot')) || (github.event_name == 'issue_comment' && - !contains(github.event.issue.labels.*.name, 'tend-outage')) + contains(github.event.issue.labels.*.name, 'tend-outage') == false && contains(github.event.issue.labels.*.name, 'tend-rate-limit') == false) runs-on: ubuntu-24.04 environment: name: tend @@ -195,8 +195,11 @@ jobs: # comments on purpose: the pull_request_review *submission* kind is # deliberately left out, since a review the bot leaves on its own PR # is its reviewer role (the prompt is told to action it), not a - # self-loop. The only self-review skip is the terminal empty-body - # APPROVED gate below. + # self-loop. The three self-review skips below are author-keyed, but + # each is also narrowed to a case that leaves this run nothing to do: + # the terminal empty-body APPROVED gate, the synthetic reply + # container, and a bot review on a PR the bot did not author. None of + # them licenses a blanket self-review skip. if { [ "$KIND" = "issue_comment" ] || [ "$KIND" = "pull_request_review_comment" ]; } \ && [ "$COMMENT_AUTHOR" = "prql-bot" ]; then echo "should_run=false" >> "$GITHUB_OUTPUT" @@ -242,14 +245,58 @@ jobs: # A review's record includes review.body (checked above) but NOT the # bodies of the inline comments attached to the review. Fetch them # so a first-contact @-mention inside an inline comment is detected - # on PRs where the bot has no prior engagement. + # on PRs where the bot has no prior engagement. One object per line, + # so `--paginate` concatenates pages instead of reducing within one. + # Keep the `{body, in_reply_to_id}` construction: `in_reply_to_id` is + # an *optional* property, absent rather than null on a fresh comment, + # and building the object normalizes absent to null so the `== null` + # select below counts both shapes. A bare `.in_reply_to_id` stream + # would emit nothing for a fresh comment and skip every container. if [ "$KIND" = "pull_request_review" ]; then - if gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR/reviews/$PAYLOAD_ID/comments" \ - --jq '.[].body' | grep -qF '@prql-bot'; then + INLINE=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR/reviews/$PAYLOAD_ID/comments" \ + --jq '.[] | {body, in_reply_to_id}') + + if printf '%s\n' "$INLINE" | jq -r '.body' | grep -qF '@prql-bot'; then echo "should_run=true" >> "$GITHUB_OUTPUT" echo "reason=mention" >> "$GITHUB_OUTPUT" exit 0 fi + + # Replying to a review thread wraps the reply in a synthetic + # zero-body COMMENTED review, so the bot's own inline reply arrives + # here as a `pull_request_review` submission as well as the + # `pull_request_review_comment` the skip above already drops. Same + # artifact, second event path: without this the engagement heuristic + # below sees a bot-authored PR (or BOT_REVIEWS > 0) and spins up a + # full handle job that the prompt's self-loop guard then exits. + # Narrower than the comment skip on purpose — a real review the bot + # submits with inline comments and no body carries actionable signal, + # so require that *every* inline comment be a reply + # (`in_reply_to_id` set). A container with any fresh inline comment + # still fires. + if [ "$REVIEW_AUTHOR" = "prql-bot" ] \ + && [ -z "$COMMENT_BODY" ] \ + && [ "$(printf '%s\n' "$INLINE" | jq -s '[.[] | select(.in_reply_to_id == null)] | length')" = "0" ]; then + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Somebody else's content-free approval is terminal too. The gate + # above is author-keyed, so a human's bare APPROVED falls through to + # the PR-author short-circuit (on a bot-authored PR) or to + # BOT_REVIEWS (on one the bot has reviewed) and starts a session + # whose only possible outcome is a silent exit: an approval with no + # body and no inline comments asks for nothing, and the bot cannot + # merge on its own. Unlike the bot-authored gates this one requires + # `$INLINE` to be empty rather than reply-only — an approval whose + # nits live inline is a request to the PR's author, which on a + # bot-authored PR is a role the bot has to act in. + if [ "$REVIEW_STATE" = "approved" ] \ + && [ -z "$COMMENT_BODY" ] \ + && [ -z "$INLINE" ]; then + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi fi # Non-mention: check bot engagement @@ -290,6 +337,26 @@ jobs: echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0 fi + # A review the bot leaves on someone else's PR leaves this run + # nothing to do: whatever the review warranted, the tend-review + # session that submitted it has already done — left the findings for + # a human author to act on (pushing to their branch unbidden is + # barred by conduct rules), or, on a dependency-bot PR where no + # author will act, pushed the fix itself. The BOT_REVIEWS heuristic + # below counts this very review, so without this gate the session + # always starts and always exits silently. Keyed on author alone — + # the APPROVED + empty-body gate above is the same shape narrowed to + # its one terminal leg, and reusing those clauses here would let every + # bodied COMMENTED review through. Placement carries the rest of the + # design: *after* the PR_AUTHOR short-circuit, which has already + # exited when the PR is the bot's own, so the reviewer-to-author + # handoff on a bot PR still fires; *after* the body and inline + # @-mention scans, so an explicit summons still wins. + if [ "$KIND" = "pull_request_review" ] \ + && [ "$REVIEW_AUTHOR" = "prql-bot" ]; then + echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0 + fi + # Captured, not counted — see the note on the issue-comment lookup above. BOT_REVIEWS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \ --jq '.[] | select(.user.login == "prql-bot") | .id') @@ -391,7 +458,7 @@ jobs: # the API record — the dispatch payload never carries one to spoof. EVENT_TS: ${{ github.event.comment.updated_at || needs.verify.outputs.ts || github.event.issue.updated_at }} - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} @@ -420,6 +487,13 @@ jobs: || format('A user commented on an issue/PR where you previously participated ({0}). Read the full context. Only respond if the comment is directed at you, asks a question you can help with, or requests changes you can make. If the conversation is between other participants, exit silently.', github.event.comment.html_url) }} + - name: Restore local setup actions for POST cleanup + if: always() + run: | + dir=.github/actions/tend-setup + git checkout "$GITHUB_SHA" -- "$dir" || + echo "::warning::could not restore $dir from $GITHUB_SHA; POST cleanup of the local action may fail" + - name: Remove eyes reaction if: | always() diff --git a/.github/workflows/tend-nightly.yaml b/.github/workflows/tend-nightly.yaml index 6fab2bb1a5d5..26bde9da4151 100644 --- a/.github/workflows/tend-nightly.yaml +++ b/.github/workflows/tend-nightly.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-notifications.yaml b/.github/workflows/tend-notifications.yaml index 73cc54f23756..3071412d5d69 100644 --- a/.github/workflows/tend-notifications.yaml +++ b/.github/workflows/tend-notifications.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -27,7 +27,19 @@ jobs: steps: - name: Check for unread notifications id: check + env: + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + BOT_NAME: prql-bot run: | + # shellcheck shell=bash + # Pre-check for tend-notifications: decide whether the agent needs to boot at + # all, and clear inbox noise no agent run is needed for. + # + # Inlined into the generated workflow (adopter repos have no copy of this + # file), so it stays self-contained: env in, GITHUB_OUTPUT out. + # + # env: BOT_NAME, GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN + # Fetch notifications once, tolerating transient non-JSON responses. # GitHub occasionally returns an HTML error page (even with a 200) # during a brief API blip; under `bash -e` an untolerated `gh api` @@ -95,7 +107,7 @@ jobs: PR_INFO=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUM" --jq '"\(.user.login) \(.state)"' 2>/dev/null) || continue PR_AUTHOR=${PR_INFO%% *} PR_STATE=${PR_INFO##* } - if [ "$PR_AUTHOR" = "prql-bot" ] && [ "$PR_STATE" = "closed" ]; then + if [ "$PR_AUTHOR" = "$BOT_NAME" ] && [ "$PR_STATE" = "closed" ]; then gh api "notifications/threads/$tid" -X PATCH || true fi done @@ -125,8 +137,6 @@ jobs: else echo "$COUNT processable notification(s) — proceeding" fi - env: - GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} - uses: actions/checkout@v7 if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' @@ -138,7 +148,7 @@ jobs: - uses: ./.github/actions/tend-setup if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' with: github_token: ${{ secrets.TEND_BOT_TOKEN }} diff --git a/.github/workflows/tend-review-runs.yaml b/.github/workflows/tend-review-runs.yaml index a2e974d29df5..6816e91fb044 100644 --- a/.github/workflows/tend-review-runs.yaml +++ b/.github/workflows/tend-review-runs.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-review.yaml b/.github/workflows/tend-review.yaml index cb05d6b52cdb..fcbadded54b0 100644 --- a/.github/workflows/tend-review.yaml +++ b/.github/workflows/tend-review.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -15,7 +15,11 @@ jobs: review: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true + # A push mid-review queues a replacement run rather than killing the + # session: the running review folds the push in and stamps the commits + # it examined, and the gate step below lets the queued run exit without + # booting an agent when its HEAD is already covered. + cancel-in-progress: false runs-on: ubuntu-24.04 environment: name: tend @@ -26,16 +30,84 @@ jobs: actions: read issues: write steps: + - name: Skip when the live HEAD is already examined + id: gate + env: + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + PR: ${{ github.event.pull_request.number }} + EVENT_ACTION: ${{ github.event.action }} + run: | + # shellcheck shell=bash + # Pre-check for tend-review: decide whether the agent needs to boot at all. + # + # The review job runs without cancel-in-progress, so a push mid-review queues + # a replacement run while the live session keeps going, folds the push in, and + # stamps each commit it examined with a `tend-review/` commit status (review + # skill, "Stamp examined HEADs"). The concurrency group holds this run until + # that session ends; by then the live HEAD is usually stamped and there is + # nothing left to do. Judged against the live PR, not the event payload — a + # queued run's payload is stale by construction. + # + # Inlined into the generated workflow (adopter repos have no copy of this + # file), so it stays self-contained: env in, GITHUB_OUTPUT out. Any write-scoped + # actor could forge the stamp to suppress a review; that actor can already + # cancel the run itself, so the merge gate — not this check — remains the + # security boundary. + # + # env: PR, EVENT_ACTION, GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN + + # Only `synchronize` can be a stale duplicate of an examination that already + # happened: `opened` has no prior run, and `reopened` / `ready_for_review` + # ask for a fresh pass even on a stamped commit. + if [ "$EVENT_ACTION" != "synchronize" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Fail open on API errors: a redundant agent run beats a silently skipped + # review. The parse belongs inside the guard — GitHub sometimes returns an + # HTML error page with a 200 during a blip, so a zero `gh` exit doesn't mean + # the body is JSON, and an unguarded `jq` under the run block's `bash -e` + # would fail the step (fail-closed) instead. + if ! PR_INFO=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" 2>/dev/null) \ + || ! STATE=$(echo "$PR_INFO" | jq -re '.state'); then + echo "PR #$PR fetch failed — proceeding without the pre-check" + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "$STATE" != "open" ]; then + echo "PR #$PR is $STATE — skipping" + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # The stamp context carries the PR number: one branch can be two open PRs + # (same head, different base), and each base means a different diff, so an + # examination of one must not gate the other. + HEAD=$(echo "$PR_INFO" | jq -r '.head.sha') + STAMPED=$(gh api "repos/$GITHUB_REPOSITORY/commits/$HEAD/status?per_page=100" 2>/dev/null \ + | jq --arg ctx "tend-review/$PR" \ + '[.statuses[]? | select(.context == $ctx and .state == "success")] | length' \ + || echo 0) + if [ "${STAMPED:-0}" -gt 0 ]; then + echo "HEAD $HEAD already examined (tend-review/$PR) — skipping" + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "should_run=true" >> "$GITHUB_OUTPUT" # Two checkouts: `setup:` runs against the base tree, and the PR's own # tree lands after it. Setup executes as the runner user, outside the # containment the harness builds for the contributor's code. - uses: actions/checkout@v7 + if: steps.gate.outputs.should_run == 'true' with: fetch-depth: 0 fetch-tags: true token: ${{ secrets.TEND_BOT_TOKEN }} - uses: ./.github/actions/tend-setup + if: steps.gate.outputs.should_run == 'true' # GitHub only materializes refs/pull/N/merge for mergeable PRs — on # conflicting PRs it 404s and every downstream step cascades as skipped. @@ -44,6 +116,7 @@ jobs: # tree. - name: Resolve PR checkout ref id: pr_ref + if: steps.gate.outputs.should_run == 'true' env: GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} PR: ${{ github.event.pull_request.number }} @@ -55,6 +128,7 @@ jobs: echo "::notice::refs/pull/$PR/merge unavailable (likely merge conflict); falling back to /head" fi - uses: actions/checkout@v7 + if: steps.gate.outputs.should_run == 'true' with: ref: ${{ steps.pr_ref.outputs.ref }} allow-unsafe-pr-checkout: true @@ -63,7 +137,8 @@ jobs: fetch-tags: true token: ${{ secrets.TEND_BOT_TOKEN }} - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 + if: steps.gate.outputs.should_run == 'true' with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} @@ -72,3 +147,10 @@ jobs: model: opus prompt: >- ${{ format('/tend-ci-runner:review {0}', github.event.pull_request.number) }} + + - name: Restore local setup actions for POST cleanup + if: always() && (steps.gate.outputs.should_run == 'true') + run: | + dir=.github/actions/tend-setup + git checkout "$GITHUB_SHA" -- "$dir" || + echo "::warning::could not restore $dir from $GITHUB_SHA; POST cleanup of the local action may fail" diff --git a/.github/workflows/tend-triage.yaml b/.github/workflows/tend-triage.yaml index ad3b4ca1a502..4c166a936095 100644 --- a/.github/workflows/tend-triage.yaml +++ b/.github/workflows/tend-triage.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -17,7 +17,7 @@ concurrency: jobs: triage: - if: github.repository_owner == 'PRQL' && contains(github.event.issue.labels.*.name, 'tend-outage') == false + if: github.repository_owner == 'PRQL' && contains(github.event.issue.labels.*.name, 'tend-outage') == false && contains(github.event.issue.labels.*.name, 'tend-rate-limit') == false runs-on: ubuntu-24.04 environment: name: tend @@ -37,7 +37,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-weekly.yaml b/.github/workflows/tend-weekly.yaml index faea814b23ad..a75842b81841 100644 --- a/.github/workflows/tend-weekly.yaml +++ b/.github/workflows/tend-weekly.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.14. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.14 + - uses: max-sixty/tend/claude@0.1.15 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}