Skip to content

tend-notifications #5316

tend-notifications

tend-notifications #5316

# Generated by tend 0.1.24. 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:
concurrency:
group: tend-notifications
cancel-in-progress: false
if: github.repository_owner == 'PRQL'
runs-on: ubuntu-24.04
environment:
name: tend
deployment: false
permissions:
contents: write
pull-requests: write
actions: read
issues: write
steps:
- name: Check for unread notifications and conflicted PRs
id: check
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
run: |
# shellcheck shell=bash
# Establish the repository's frequent maintenance queue and decide whether the
# agent needs to boot. Inlined into the generated workflow: env in,
# GITHUB_OUTPUT out.
#
# env: GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN
# 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"
# 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"
# 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
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
# GitHub computes mergeability lazily after the base moves. UNKNOWN therefore
# means "worth a synchronous local test", not "clean". This is only a cheap
# boot gate; the agent test-merges every candidate before changing a branch.
# Read the newest comments: a deferral is normally the PR's latest activity.
# An older marker can waste boots, but the resolver paginates before acting.
# shellcheck disable=SC2016 # $q is a GraphQL variable, not a shell variable.
if BOT_LOGIN=$(gh api user --jq .login 2>/dev/null) \
&& PRS=$(gh api graphql -f query='
query($q: String!) {
search(query: $q, type: ISSUE, first: 100) {
nodes { ... on PullRequest {
mergeable headRefOid
comments(last: 100) { nodes { author { login } body } }
} }
}
}' -f q="repo:$GITHUB_REPOSITORY author:$BOT_LOGIN is:pr is:open" \
--jq '.data.search.nodes' 2>/dev/null) \
&& CONFLICT_COUNT=$(jq -er --arg bot "$BOT_LOGIN" '
[.[]
| select(.mergeable != "MERGEABLE")
| . as $pr
| "<!-- tend-conflict-deferred head=\($pr.headRefOid) -->" as $marker
| select(any($pr.comments.nodes[]?;
.author.login == $bot
and (((.body // "") | sub("\\s+$"; "") | split("\n") | last) == $marker))
| not)]
| length' <<<"$PRS"); then
:
else
CONFLICT_COUNT=0
echo "::warning::bot PR conflict scan failed; retrying next cycle"
fi
echo "conflict_count=$CONFLICT_COUNT" >> "$GITHUB_OUTPUT"
if [ "$COUNT" = "0" ] && [ "$CONFLICT_COUNT" = "0" ]; then
echo "No notification or conflict work — skipping"
else
[ "$COUNT" = "0" ] || \
echo "$COUNT notification task(s) — proceeding"
[ "$CONFLICT_COUNT" = "0" ] || \
echo "$CONFLICT_COUNT possible conflicted bot PR(s) — proceeding"
fi
- uses: actions/checkout@v7
if: steps.check.outputs.count != '0' || steps.check.outputs.conflict_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' || steps.check.outputs.conflict_count != '0' || github.event_name == 'workflow_dispatch'
- uses: max-sixty/tend/claude@0.1.24
if: steps.check.outputs.count != '0' || steps.check.outputs.conflict_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
sandbox_setup: |2
mkdir -p ~/.local/bin
curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ~/.local/bin
cargo-nextest --version
curl -LsSf https://github.com/mitsuhiko/insta/releases/latest/download/cargo-insta-installer.sh | CARGO_HOME=~/.local sh
cargo-insta --version
prompt: |2
/tend-ci-runner:notifications
Notification snapshot cutoff: ${{ steps.check.outputs.cutoff }}
Possible conflicted bot PR count: ${{ steps.check.outputs.conflict_count }}