Skip to content

chore: bump minijinja from 2.21.0 to 2.23.0 #960

chore: bump minijinja from 2.21.0 to 2.23.0

chore: bump minijinja from 2.21.0 to 2.23.0 #960

Workflow file for this run

# Generated by tend 0.1.17. 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-mention
on:
issues:
types: [edited]
issue_comment:
types: [created, edited]
# The review events reach only the `relay` job below: their runs carry
# `refs/pull/N/merge`, which the operational secrets' environment does not
# admit, so the secret-bearing jobs see them re-entered as a
# `repository_dispatch` instead. Same-repo PRs only — the notifications poll
# covers fork PRs.
pull_request_review:
types: [submitted]
# `created` is intentionally absent. Modern GitHub fires *both*
# pull_request_review and pull_request_review_comment for every newly-created
# inline comment (the standalone POST /pulls/PR/comments endpoint, the
# /replies endpoint, the "Add single comment" UI button, and reviews
# submitted with inline comments — all empirically verified). Subscribing to
# `created` would produce a duplicate dispatch whose handle runs collide on
# the tend-mention-handle-PR concurrency group, with the loser cancelled and
# posted as a CANCELLED check_run on the PR head SHA — which renders the
# PR's statusCheckRollup as FAILURE even though the bot did its job from the
# sibling run. Edits have no sibling event (review submissions don't fire on
# edits), so we still need to listen for `edited` to catch edit-to-summon
# ("@bot" added to an existing comment after the fact).
pull_request_review_comment:
types: [edited]
# The relay's re-entry point. GitHub creates this run even though a
# `GITHUB_TOKEN` triggered it — `workflow_dispatch` and `repository_dispatch`
# are the two events exempt from the rule that token-triggered events start
# no workflow — and it carries the default branch, which the environment
# admits.
repository_dispatch:
types: [tend-mention-review]
jobs:
# The secretless half of the review path: move the event onto a ref the
# environment admits, and nothing else. The merge ref itself can never be
# admitted — a same-repo `pull_request` run executes the PR head's own
# workflow files on that same ref, so admitting it would hand a pushed
# workflow the secrets the environment exists to deny. All judgement lives
# in `verify`, which re-reads the review or comment from the API, so a
# skipped event costs one extra short run rather than a second copy of the
# heuristics here.
relay:
# Fork PRs are excluded to hold long-standing behaviour: the notifications
# poll covers them, and it applies author-association tiers that this path
# does not. What the filter buys is that no fork head is checked out into a
# secret-bearing run; it is not an authorship gate, since a mention from
# any user with read access already wakes `handle` on a same-repo PR.
# Widening this is a separate decision, and moot while a fork run's token
# is refused the dispatch POST (403, probed). A dispatched run fails the
# event check, so the relay cannot re-enter itself.
if: |
(github.event_name == 'pull_request_review' ||
github.event_name == 'pull_request_review_comment') &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
# No secrets here. `contents: write` is what the dispatch POST needs, and
# is the whole of what this token can do — probed both ways on a live
# repo: an otherwise identical same-repo run declaring `contents: read`
# is refused with 403, so a narrower token would leave every review
# mention unanswered rather than merely unprivileged. It grants nobody a
# new capability: only someone who can already push a branch can open the
# same-repo PR whose workflow file this is, and they could declare any
# permissions they liked in it. The merge restriction is what bounds that,
# here as everywhere.
permissions:
contents: write
steps:
# Identifiers only: `verify` re-reads the review or comment from the
# API, so the words the bot weighs and acts on are the ones GitHub
# holds, and a forged dispatch faces the same scrutiny as a relayed one.
- name: Re-enter on an admitted ref
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api "repos/$GITHUB_REPOSITORY/dispatches" \
-f event_type=tend-mention-review \
-f "client_payload[kind]=${{ github.event_name }}" \
-f "client_payload[pr]=${{ github.event.pull_request.number }}" \
-f "client_payload[id]=${{ github.event.review.id || github.event.comment.id }}"
verify:
# 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') == false && contains(github.event.issue.labels.*.name, 'tend-rate-limit') == false)
runs-on: ubuntu-24.04
environment:
name: tend
deployment: false
outputs:
should_run: ${{ steps.check.outputs.should_run }}
reason: ${{ steps.check.outputs.reason }}
url: ${{ steps.check.outputs.url }}
ts: ${{ steps.check.outputs.ts }}
steps:
- name: Verify bot engagement
id: check
run: |
# shellcheck shell=bash
# Pre-check for tend-mention: decide whether the mention is addressed to the
# bot — by name or by engagement — and so whether the agent boots at all.
#
# 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, EVENT_NAME, COMMENT_BODY, COMMENT_AUTHOR, COMMENT_AUTHOR_TYPE,
# ISSUE_BODY, ISSUE_OR_PR_NUMBER, ISSUE_AUTHOR, PR_URL, PAYLOAD_KIND,
# PAYLOAD_PR, PAYLOAD_ID, GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN
# out: should_run, reason, url, ts
# A relayed review event arrives as identifiers only ({kind, pr, id}): resolve
# them against the API before judging anything, so the words weighed below are
# the ones GitHub holds rather than whatever the payload carried. Any
# write-scoped actor can POST a dispatch, so a forged payload faces the same
# checks a real event does — and fetching by PR and id binds the two, so a
# payload pairing a real review with some other PR dies here instead of
# steering the handle job. The ids are spliced into API paths here and into the
# prompt later, so reject anything but digits at this edge.
KIND="$EVENT_NAME"
if [ "$EVENT_NAME" = "repository_dispatch" ]; then
KIND="$PAYLOAD_KIND"
if ! [[ "$PAYLOAD_PR" =~ ^[0-9]+$ && "$PAYLOAD_ID" =~ ^[0-9]+$ ]]; then
echo "malformed dispatch payload — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
fi
case "$KIND" in
pull_request_review)
if ! REVIEW=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR/reviews/$PAYLOAD_ID"); then
echo "review $PAYLOAD_ID not found on PR $PAYLOAD_PR — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
fi
REVIEW_AUTHOR=$(echo "$REVIEW" | jq -r '.user.login')
# REST reports the state uppercase (a webhook payload's is lowercase);
# normalize so the terminal-approval gate below reads one shape.
REVIEW_STATE=$(echo "$REVIEW" | jq -r '.state | ascii_downcase')
COMMENT_BODY=$(echo "$REVIEW" | jq -r '.body // ""')
echo "url=$(echo "$REVIEW" | jq -r '.html_url')" >> "$GITHUB_OUTPUT"
# A review without `submitted_at` (a PENDING one, which only a forged
# dispatch can name) would otherwise write the string `null`, which
# handle's `date -d` rejects — failing the job red where the empty-value
# guard would have skipped it.
echo "ts=$(echo "$REVIEW" | jq -r '.submitted_at // empty')" >> "$GITHUB_OUTPUT"
;;
pull_request_review_comment)
if ! COMMENT=$(gh api "repos/$GITHUB_REPOSITORY/pulls/comments/$PAYLOAD_ID"); then
echo "comment $PAYLOAD_ID not found — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
fi
# Comments fetch by id alone, so bind the PR explicitly.
if [ "$(echo "$COMMENT" | jq -r '.pull_request_url')" != "https://api.github.com/repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR" ]; then
echo "comment $PAYLOAD_ID does not belong to PR $PAYLOAD_PR — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
fi
COMMENT_AUTHOR=$(echo "$COMMENT" | jq -r '.user.login')
COMMENT_BODY=$(echo "$COMMENT" | jq -r '.body // ""')
echo "url=$(echo "$COMMENT" | jq -r '.html_url')" >> "$GITHUB_OUTPUT"
echo "ts=$(echo "$COMMENT" | jq -r '.updated_at')" >> "$GITHUB_OUTPUT"
;;
*)
echo "unknown dispatch kind '$KIND' — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
;;
esac
fi
# Mentions always run
if [ "$KIND" = "issues" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# The bot's own comments never summon it (its PAT-based User account is
# invisible to the Bot-type skip below). Placed *before* the mention check,
# since a bot comment can quote a prior @-mention. Comments only: the bot's
# review *submissions* are judged with the review kind below — a review carries
# reviewer-role signal a comment can't.
if { [ "$KIND" = "issue_comment" ] || [ "$KIND" = "pull_request_review_comment" ]; } \
&& [ "$COMMENT_AUTHOR" = "$BOT_NAME" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -n "$COMMENT_BODY" ] && printf '%s\n' "$COMMENT_BODY" | grep -qF "@$BOT_NAME"; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "reason=mention" >> "$GITHUB_OUTPUT"
exit 0
fi
# Other bots' undirected comments (deploy notifications, CI status) summon by
# mention only, never by the engagement heuristics below — which would boot a
# no-op session per notification, twice when the source bot edits its comment.
if [ "$KIND" = "issue_comment" ] && [ "$COMMENT_AUTHOR_TYPE" = "Bot" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 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. 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 count every review as
# reply-only.
if [ "$KIND" = "pull_request_review" ]; 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 "@$BOT_NAME"; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "reason=mention" >> "$GITHUB_OUTPUT"
exit 0
fi
FRESH_INLINE=$(printf '%s\n' "$INLINE" | jq -s '[.[] | select(.in_reply_to_id == null)] | length')
# A contentless approval — no body, no inline comments — asks for nothing,
# whoever submitted it, and the bot cannot merge on its own. Without this it
# reads as engagement below and boots a session whose only outcome is a
# silent exit. `approved` and `$INLINE` empty are both load-bearing: a bare
# COMMENTED review is how GitHub wraps a human's inline reply (not terminal),
# and an approval whose nits live inline is a request to the PR's author — on
# a bot-authored PR, 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
if [ "$KIND" = "issue_comment" ]; then
ISSUE_NUMBER="$ISSUE_OR_PR_NUMBER"
if [ -z "$PR_URL" ]; then
if [ "$ISSUE_AUTHOR" = "$BOT_NAME" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0
fi
if printf '%s\n' "$ISSUE_BODY" | grep -qF "@$BOT_NAME"; then
echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0
fi
# Don't reduce inside jq: `gh api --paginate` applies `--jq` once per page,
# so `| length` emits one count per page rather than one overall. Past 100
# comments the variable holds e.g. `100\n7`, a numeric test on it errors
# with `integer expression expected`, and the failed test falls through to
# should_run=false — the bot goes quiet on its most-engaged threads.
# Capture the per-element stream and test it for emptiness — the bare
# substitution also keeps a failing `gh api` fatal under GHA's default
# `bash -e`.
BOT_COMMENTS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" \
--jq ".[] | select(.user.login == \"$BOT_NAME\") | .id")
if [ -n "$BOT_COMMENTS" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0
fi
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
fi
PR_NUMBER="$ISSUE_NUMBER"
else
PR_NUMBER="$PAYLOAD_PR"
fi
PR_AUTHOR=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json author --jq '.author.login')
# The bot's own review summons a session in exactly one shape: the reviewer
# role handing work to the author role — fresh content (a body, or an inline
# comment that isn't a reply) on a PR the bot authored. On another author's PR
# the review session already did whatever the review warranted; an empty-body
# reply-only review is the synthetic container GitHub wraps around an inline
# reply — the same comment the self-comment skip above drops on its other event
# path. Either would otherwise read as engagement below (the BOT_REVIEWS
# heuristic counts this very review) and boot a session that exits silently.
# Sits *after* the inline @-mention scan, so an explicit summons the bot quotes
# still wins.
if [ "$KIND" = "pull_request_review" ] && [ "$REVIEW_AUTHOR" = "$BOT_NAME" ]; then
if [ "$PR_AUTHOR" = "$BOT_NAME" ] \
&& { [ -n "$COMMENT_BODY" ] || [ "$FRESH_INLINE" -gt 0 ]; }; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0
fi
echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0
fi
if [ "$PR_AUTHOR" = "$BOT_NAME" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "reason=participation" >> "$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 == \"$BOT_NAME\") | .id")
if [ -n "$BOT_REVIEWS" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0
fi
BOT_COMMENTS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
--jq ".[] | select(.user.login == \"$BOT_NAME\") | .id")
if [ -n "$BOT_COMMENTS" ]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0
fi
echo "should_run=false" >> "$GITHUB_OUTPUT"
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
BOT_NAME: prql-bot
EVENT_NAME: ${{ github.event_name }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
COMMENT_AUTHOR_TYPE: ${{ github.event.comment.user.type }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_OR_PR_NUMBER: ${{ github.event.issue.number }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
PR_URL: ${{ github.event.issue.pull_request.url }}
PAYLOAD_KIND: ${{ github.event.client_payload.kind }}
PAYLOAD_PR: ${{ github.event.client_payload.pr }}
PAYLOAD_ID: ${{ github.event.client_payload.id }}
# The dispatch arm covers a relayed inline comment, whose id the check
# step verified belongs to this PR; a review *submission* has no single
# comment to react to, so it gets no eyes — same as when the events
# arrived directly.
- name: React with eyes
if: |
steps.check.outputs.should_run == 'true'
&& ((github.event.comment && contains(github.event.comment.body, '@prql-bot'))
|| (github.event.client_payload.kind == 'pull_request_review_comment'
&& steps.check.outputs.reason == 'mention'))
run: |
gh api "repos/$REPO/$TARGET/reactions" -f content=eyes --silent \
|| echo "::warning::could not add the eyes reaction"
env:
REPO: ${{ github.repository }}
TARGET: ${{ github.event_name == 'issue_comment'
&& format('issues/comments/{0}', github.event.comment.id)
|| format('pulls/comments/{0}', github.event.comment.id || github.event.client_payload.id) }}
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
handle:
needs: verify
if: needs.verify.outputs.should_run == 'true'
concurrency:
group: ${{ github.workflow }}-handle-${{ github.event.issue.number || github.event.client_payload.pr }}
cancel-in-progress: false
runs-on: ubuntu-24.04
environment:
name: tend
deployment: false
permissions:
contents: write
pull-requests: write
actions: read
issues: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.TEND_BOT_TOKEN }}
- uses: ./.github/actions/tend-setup
- name: Check out PR branch
if: |
(github.event_name == 'issue_comment' && github.event.issue.pull_request.url != '') ||
github.event_name == 'repository_dispatch'
run: |
PR_STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state')
if [ "$PR_STATE" = "OPEN" ]; then
gh pr checkout "$PR_NUMBER"
else
echo "::warning::PR is $PR_STATE — staying on default branch"
fi
env:
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}
PR_NUMBER: ${{ github.event_name == 'issue_comment' && github.event.issue.number || github.event.client_payload.pr }}
- name: Compute queue delay
id: delay
run: |
if [ -z "$EVENT_TS" ]; then
echo "seconds=" >> "$GITHUB_OUTPUT"
exit 0
fi
event_epoch=$(date -d "$EVENT_TS" +%s)
echo "seconds=$(( $(date +%s) - event_epoch ))" >> "$GITHUB_OUTPUT"
env:
# A relayed event's timestamp comes from verify, which read it off
# 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.17
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: >-
${{ steps.delay.outputs.seconds
&& format('This job started {0}s after the triggering event (over ~40s means it was queued). ',
steps.delay.outputs.seconds) || '' }}Before acting,
check recent comments: exit silently if the bot already responded
to the trigger; handle any other unaddressed comments too.
${{ github.event_name == 'issues'
&& format('An issue was updated with a mention of you ({0}). Read it and respond.', github.event.issue.html_url)
|| (github.event.client_payload.kind == 'pull_request_review_comment' && needs.verify.outputs.reason == 'mention'
&& format('You were mentioned in an inline review comment on PR #{0} ({1}, comment ID {2}). Read the full context, then respond. If changes are requested, make them, commit, and push.', github.event.client_payload.pr, needs.verify.outputs.url, github.event.client_payload.id))
|| (github.event.client_payload.kind == 'pull_request_review_comment'
&& format('An inline review comment was posted on a PR where you previously participated (PR #{0}, {1}, comment ID {2}). Read the full context. Only respond if the comment is directed at you or requests changes.', github.event.client_payload.pr, needs.verify.outputs.url, github.event.client_payload.id))
|| (github.event.client_payload.kind == 'pull_request_review' && needs.verify.outputs.reason == 'mention'
&& format('A review was submitted on PR #{0} that mentions you ({1}, review ID {2}). Read the review and full context, then respond. If changes were requested, make them, commit, and push.', github.event.client_payload.pr, needs.verify.outputs.url, github.event.client_payload.id))
|| (github.event.client_payload.kind == 'pull_request_review'
&& format('A review was submitted on a PR where you previously participated (PR #{0}, {1}, review ID {2}). Read the review and full context. If it requests changes or asks questions, respond appropriately — including when you authored the review: a review your review workflow left on your own PR is your reviewer role speaking, not a self-loop, so action it. Exit silently for a plain approval, a review with no actionable content, or one between other participants.', github.event.client_payload.pr, needs.verify.outputs.url, github.event.client_payload.id))
|| (contains(github.event.comment.body, '@prql-bot')
&& format('You were mentioned in a comment ({0}). Read the full context and respond. If changes are requested, make them, commit, and push.', github.event.comment.html_url))
|| 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 the eyes reaction
if: |
always()
&& ((github.event.comment && contains(github.event.comment.body, '@prql-bot'))
|| (github.event.client_payload.kind == 'pull_request_review_comment'
&& needs.verify.outputs.reason == 'mention'))
run: |
REACTION_ID=$(gh api "repos/$REPO/$TARGET/reactions?content=eyes" \
--jq ".[] | select(.user.login == \"$BOT_NAME\") | .id" | head -n1)
if [ -n "$REACTION_ID" ]; then
gh api -X DELETE "repos/$REPO/$TARGET/reactions/$REACTION_ID" --silent \
|| echo "::warning::could not remove the eyes reaction"
fi
env:
REPO: ${{ github.repository }}
TARGET: ${{ github.event_name == 'issue_comment'
&& format('issues/comments/{0}', github.event.comment.id)
|| format('pulls/comments/{0}', github.event.comment.id || github.event.client_payload.id) }}
BOT_NAME: prql-bot
GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}