Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@
self-hosted-runner:
labels:
- macos-15-intel

# actionlint does not yet know the `concurrency.queue` key, which GitHub added
# and tend's generated `tend-review.yaml` uses. Upstream tracks it in
# rhysd/actionlint#654 and #657; the PR adding support (#661) was closed
# unmerged and the latest release (v1.7.12) still rejects it, so the nightly
# `uvx tend init` regeneration would otherwise land permanently red here.
#
# Scoped to the generated `tend-*` workflows so a genuine typo in a
# hand-written workflow is still caught. Remove once actionlint ships support.
paths:
.github/workflows/tend-*.yaml:
ignore:
- 'unexpected key "queue" for "concurrency" section'
4 changes: 2 additions & 2 deletions .github/workflows/tend-ci-fix.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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,
Expand Down Expand Up @@ -34,7 +34,7 @@ jobs:

- uses: ./.github/actions/tend-setup

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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,
Expand Down Expand Up @@ -420,7 +420,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.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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,
Expand Down Expand Up @@ -34,7 +34,7 @@ jobs:

- uses: ./.github/actions/tend-setup

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
135 changes: 37 additions & 98 deletions .github/workflows/tend-notifications.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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,
Expand All @@ -14,6 +14,9 @@ on:

jobs:
notifications:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
Expand All @@ -29,113 +32,47 @@ jobs:
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.
# Establish the repository's notification recovery queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# 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`
# 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
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN

COUNT=$(echo "$NOTIFS" | jq 'length')
if [ "$COUNT" = "0" ]; then
echo "count=0" >> "$GITHUB_OUTPUT"
echo "No unread notifications — skipping"
exit 0
fi
# Activity newer than this belongs to an event workflow that may still be
# running. The same cutoff is passed to the agent and, once every older item has
# a semantic outcome, to GitHub's repository-level mark-read endpoint. Newer
# activity therefore cannot be acknowledged by this run.
CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$CUTOFF" >> "$GITHUB_OUTPUT"

# --- 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)
# Watching makes a new issue or PR visible before the bot has participated in
# its thread. The installer sets this too; every poll repeats the idempotent PUT
# so a later settings change is repaired without additional state.
gh api "repos/$GITHUB_REPOSITORY/subscription" -X PUT \
-F subscribed=true -F ignored=false --silent \
|| echo "::warning::could not enable repository watching; retrying next cycle"

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
# Capture every unread page at the cutoff. GitHub occasionally returns an HTML
# error page even with a successful status, so validate the slurped page shape.
# A failed fetch leaves the queue untouched for the next scheduled cycle.
ENDPOINT="notifications?before=$CUTOFF&per_page=100"
if PAGES=$(gh api "$ENDPOINT" --paginate --slurp 2>/dev/null) \
&& NOTIFS=$(echo "$PAGES" | jq -ce \
'if type == "array" and all(.[]; type == "array") then add // [] else error("invalid pages") end'); then
COUNT=$(echo "$NOTIFS" | jq 'length')
else
COUNT=0
echo "::warning::notifications fetch failed; queue left for the next cycle"
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" = "$BOT_NAME" ] && [ "$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
echo "No notification work before $CUTOFF — skipping"
else
echo "$COUNT processable notification(s) — proceeding"
echo "$COUNT notification task(s) — proceeding"
fi

- uses: actions/checkout@v7
Expand All @@ -148,7 +85,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.20
- uses: max-sixty/tend/claude@0.1.22
if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch'
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
Expand All @@ -158,3 +95,5 @@ jobs:
model: opus
prompt: |
/tend-ci-runner:notifications

Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by tend 0.1.20. Regenerate with: uvx tend@latest init
# Generated by tend 0.1.22. 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,
Expand Down Expand Up @@ -34,7 +34,7 @@ jobs:

- uses: ./.github/actions/tend-setup

- uses: max-sixty/tend/claude@0.1.20
- uses: max-sixty/tend/claude@0.1.22
with:
github_token: ${{ secrets.TEND_BOT_TOKEN }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
Loading
Loading