tend-notifications #3491
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Generated by tend 0.1.12. 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, | |
| # `running-tend`) in this repo's .claude/skills/ directory, or open an issue at | |
| # https://github.com/max-sixty/tend/issues for changes that need to | |
| # happen upstream in the tend-ci-runner plugin. | |
| name: tend-notifications | |
| on: | |
| schedule: | |
| - cron: "*/15 * * * *" | |
| workflow_dispatch: | |
| jobs: | |
| notifications: | |
| if: github.repository_owner == 'PRQL' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| actions: read | |
| issues: write | |
| steps: | |
| - name: Check for unread notifications | |
| id: check | |
| run: | | |
| # 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` | |
| # failure would abort the whole step red. A failed fetch just means | |
| # this cycle can't enumerate — the next scheduled cycle picks up | |
| # anything missed — so retry briefly, then skip cleanly. | |
| # | |
| # On every failed attempt, log what actually came back: the plain | |
| # fetch hides the body behind `2>/dev/null`, and a transient blip | |
| # that recovers on retry would otherwise leave no trace of the bad | |
| # response. `gh api -i` re-fetches with the HTTP status line + | |
| # headers ahead of the body, so a recurring failure shows the real | |
| # status code and body — not just jq's parse complaint. It exits | |
| # non-zero on an error status, hence `|| true`. The extra call runs | |
| # only on the (rare) failing attempt; the happy path is one fetch. | |
| NOTIFS="" | |
| for attempt in 1 2 3; do | |
| if NOTIFS=$(gh api notifications 2>/dev/null) && echo "$NOTIFS" | jq -e . >/dev/null 2>&1; then | |
| break | |
| fi | |
| NOTIFS="" | |
| echo "--- notifications fetch attempt $attempt failed; actual response (status + body head) ---" | |
| gh api notifications -i 2>&1 | head -c 1000 || true | |
| echo | |
| [ "$attempt" -lt 3 ] && sleep "$attempt" | |
| done | |
| if [ -z "$NOTIFS" ]; then | |
| echo "count=0" >> "$GITHUB_OUTPUT" | |
| echo "notifications fetch failed after retries — skipping this cycle" | |
| exit 0 | |
| fi | |
| COUNT=$(echo "$NOTIFS" | jq 'length') | |
| if [ "$COUNT" = "0" ]; then | |
| echo "count=0" >> "$GITHUB_OUTPUT" | |
| echo "No unread notifications — skipping" | |
| exit 0 | |
| fi | |
| # --- Layer B: drop notifications shadowed by recent dedicated runs --- | |
| # Event workflows mark their own notifications read via the harness | |
| # action's post-step on success; this sweeps the case where Claude failed | |
| # (post-step is gated by `if: success()`) so the notification still | |
| # gets cleared without burning Claude turns to rediscover it. | |
| SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ) | |
| RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true) | |
| if [ -n "$RECENT_PRS" ]; then | |
| for pr in $RECENT_PRS; do | |
| echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do | |
| [ -n "$tid" ] || continue | |
| gh api "notifications/threads/$tid" -X PATCH || true | |
| done | |
| done | |
| fi | |
| # --- Layer C: drop notifications on bot-authored closed PRs --- | |
| # The bot auto-subscribes to its own PRs. After merge/close, leftover | |
| # subscription notifications are pure noise — no action needed. | |
| # Reuses the snapshot fetched above; marking a thread read is | |
| # idempotent, so a stale snapshot at worst re-PATCHes a read thread. | |
| echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do | |
| [ -n "$tid" ] || continue | |
| PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last') | |
| 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 | |
| gh api "notifications/threads/$tid" -X PATCH || true | |
| fi | |
| done | |
| # --- Layer D: count processable notifications --- | |
| # Same-repo notifications younger than 10 minutes are deferred: a | |
| # dedicated workflow (tend-review/mention/triage/ci-fix) is likely | |
| # still starting up or mid-flight and hasn't posted its response yet. | |
| # Processing them now risks duplicating work. Cross-repo notifications | |
| # are exempt — no dedicated workflow handles them. | |
| CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ) | |
| # Re-fetch so the count reflects threads marked read in Layers B/C. | |
| # Tolerate a transient failure by falling back to the pre-mutation | |
| # snapshot — an over-count at worst spends one agent run, never fails. | |
| if ! REMAINING=$(gh api notifications 2>/dev/null) || ! echo "$REMAINING" | jq -e . >/dev/null 2>&1; then | |
| REMAINING="$NOTIFS" | |
| fi | |
| COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length') | |
| echo "count=$COUNT" >> "$GITHUB_OUTPUT" | |
| if [ "$COUNT" = "0" ]; then | |
| TOTAL=$(echo "$REMAINING" | jq 'length') | |
| if [ "$TOTAL" = "0" ]; then | |
| echo "All notifications handled by pre-checks — skipping" | |
| else | |
| echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping" | |
| fi | |
| 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' | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| token: ${{ secrets.TEND_BOT_TOKEN }} | |
| - uses: ./.github/actions/tend-setup | |
| if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' | |
| - uses: max-sixty/tend/claude@0.1.12 | |
| if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' | |
| with: | |
| github_token: ${{ secrets.TEND_BOT_TOKEN }} | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| bot_name: prql-bot | |
| model: opus | |
| prompt: | | |
| /tend-ci-runner:notifications |