Skip to content

tend-notifications #3491

tend-notifications

tend-notifications #3491

# 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