Skip to content

perf(agent): skip session JSONL sanitize when file is unchanged #5920

perf(agent): skip session JSONL sanitize when file is unchanged

perf(agent): skip session JSONL sanitize when file is unchanged #5920

name: PR Approval Agent
on:
pull_request:
types: [labeled, ready_for_review, synchronize]
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-approval-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
# Write access is required to apply the Stamphog label, so no
# additional author_association check is needed. Bot-authored PRs are
# hard-excluded regardless of who applies the label — see the
# bot-author-skip job, which strips the label and explains why.
# Triggers: explicit `Stamphog` label, ready_for_review with the
# label already present, or `synchronize` where decide-delta
# asked for re-review (or itself failed — fail closed for safety).
needs: [decide-delta, dismiss]
if: >-
always()
&& !github.event.pull_request.draft
&& github.event.pull_request.user.type != 'Bot'
&& !contains(github.event.pull_request.user.login, '[bot]')
&& github.event.pull_request.user.login != 'posthog-bot'
&& (
github.event.label.name == 'Stamphog'
|| (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'Stamphog'))
|| needs.decide-delta.outputs.run_review == 'true'
|| needs.decide-delta.result == 'failure'
)
runs-on: ubuntu-latest
# Budget for the in-flight-bot-review wait (5 min of sleeps plus the
# per-poll refetches, so wall-clock exceeds it) plus the LLM review —
# see BOT_REVIEW_WAIT_BUDGET_SECONDS in review_pr.py. The cap costs
# nothing unless a run hangs; runners bill actual minutes.
timeout-minutes: 20
steps:
- name: Get app token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_APP_ID }}
private-key: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_PRIVATE_KEY }}
# Always run the approval script from main — hardcoded so a PR
# targeting a non-main branch can't supply a tampered script.
- name: Checkout main (blobless, full history)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.app-token.outputs.token }}
ref: main
filter: blob:none
fetch-depth: 0
- name: Fetch PR head ref
run: git fetch --filter=blob:none origin pull/${{ github.event.pull_request.number }}/head
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: '0.11.28' # pinned: unpinned setup-uv calls GH API on every job, exhausts rate limit
enable-cache: false
- name: Run review
env:
# Dedicated key so a rotation of the shared ANTHROPIC_API_KEY
# doesn't break stamphog (and vice versa). The env var name
# stays ANTHROPIC_API_KEY — that's what the Claude Agent SDK reads.
ANTHROPIC_API_KEY: ${{ secrets.STAMPHOG_ANTHROPIC_API_KEY }}
# ai-gateway cutover: when both are set, the review routes through
# the Go ai-gateway (phs_ secret) instead of Anthropic directly.
# Unset = direct path, so setting them is the cutover.
AI_GATEWAY_URL: ${{ secrets.STAMPHOG_AI_GATEWAY_URL }}
AI_GATEWAY_API_KEY: ${{ secrets.STAMPHOG_AI_GATEWAY_API_KEY }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
uv run tools/pr-approval-agent/review_pr.py \
${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--output-json /tmp/review.json
- name: Post review
if: always()
env:
# Local difference from upstream: approvals post with
# GITHUB_TOKEN so github-actions[bot] is the reviewer — its
# approvals are confirmed to count toward this repo's branch
# ruleset. Upstream posthog switched to a single app approval
# after confirming their app unblocks PRs; flip this once the
# same is confirmed here. Everything else (sticky comment,
# label strip) posts as the app (GH_TOKEN).
GH_TOKEN_APPROVE: ${{ github.token }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
# Derived from the token step so the sticky-comment author
# filter tracks an app rename instead of failing silent.
BOT_LOGIN: ${{ steps.app-token.outputs.app-slug }}[bot]
run: |
PR=${{ github.event.pull_request.number }}
REPO=${{ github.repository }}
VERDICT=$(jq -r '.final_verdict // ""' /tmp/review.json 2>/dev/null || echo "")
# Prefer the structured comment body (reasoning + judgment
# bullets + folded gate mechanics); fall back to the bare
# reasoning when the script predates the field.
REASONING=$(jq -r '.review_body // .reviewer.reasoning // ""' /tmp/review.json 2>/dev/null || echo "")
REVIEWED_SHA=$(jq -r '.head_sha // ""' /tmp/review.json 2>/dev/null || echo "")
# Lock the review to the sha the LLM actually saw — `gh pr
# review` records against the head at API-call time, which
# drifts mid-LLM-roundtrip if the author force-pushes.
SHA_ARGS=()
if [ -n "$REVIEWED_SHA" ]; then
SHA_ARGS=(-f "commit_id=$REVIEWED_SHA")
fi
if [ "$VERDICT" = "APPROVED" ]; then
# Single github-actions[bot] approval, carrying the review
# body. Fatal on failure (the step runs under `set -e`):
# this is the only approval, so if it doesn't post the PR
# stays blocked and that must surface as a red run.
GH_TOKEN="$GH_TOKEN_APPROVE" gh api \
-X POST "repos/$REPO/pulls/$PR/reviews" \
"${SHA_ARGS[@]}" \
-f event=APPROVE \
-f body="$REASONING"
else
# Non-approve verdicts share ONE sticky comment, updated in
# place, instead of a fresh COMMENT review per run. Review
# bodies can't be minimized or deleted, so when an automation
# loop keeps re-applying the label against a deterministic
# denial (e.g. the size gate) the refusals pile up — one PR
# collected 60+ near-identical stamphog reviews.
MARKER='<!-- stamphog-sticky-review -->'
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# Author + leading-marker filter: on a public repo anyone can
# post a comment containing the marker; without the filter the
# bot would PATCH (clobber) a third-party comment. Slurp so the
# jq runs once over all pages (per-page filtering could emit
# one object per page); gh rejects --slurp combined with --jq,
# so pipe to standalone jq. The || guard keeps a transient
# lookup failure from aborting the step before the label strip
# below (it degrades to posting a fresh comment instead).
EXISTING=$(gh api "repos/$REPO/issues/$PR/comments" --paginate --slurp \
| jq "[.[][] | select(.user.login == \"$BOT_LOGIN\" and (.body | startswith(\"$MARKER\")))][0] // empty" \
|| echo "")
COMMENT_ID=""
EXISTING_BODY=""
PREV_COUNT=0
if [ -n "$EXISTING" ]; then
COMMENT_ID=$(jq -r '.id' <<<"$EXISTING")
EXISTING_BODY=$(jq -r '.body' <<<"$EXISTING")
PREV_COUNT=$(jq -r '.body | (match("stamphog-review-count:([0-9]+)").captures[0].string // "0")' <<<"$EXISTING")
fi
# The append path requires an actual verdict in the prior body —
# a crash-only prior comment goes through the rebuild branch so
# it can't end up claiming "the verdict above" over no verdict.
if [ -z "$REASONING" ] && [ -n "$COMMENT_ID" ] \
&& grep -q 'verdict: \*\*' <<<"$EXISTING_BODY"; then
# Verdictless crash with a prior verdict on record: append the
# failure note below it rather than clobbering the reasoning
# the author still needs to act on. The anchored grep dedups
# only the note itself, never verdict reasoning that happens
# to mention review failures.
{ grep -v '^> ⚠️ A later review run failed' <<<"$EXISTING_BODY" || true; } > /tmp/stamphog-comment.md
{
echo
echo "> ⚠️ A later review run failed before producing a verdict — check the [workflow run]($RUN_URL). The verdict above is from an earlier run."
} >> /tmp/stamphog-comment.md
else
# The counter tracks verdicts only — a crash rebuild keeps
# the previous count so repeated failures don't inflate it.
if [ -n "$REASONING" ]; then
COUNT=$((PREV_COUNT + 1))
HEADLINE="stamphog reviewed \`${REVIEWED_SHA:-unknown}\` — verdict: **${VERDICT:-unknown}**"
DETAIL="$REASONING"
else
COUNT=$PREV_COUNT
HEADLINE="stamphog review failed before producing a verdict"
DETAIL="Check the [workflow run]($RUN_URL) and re-apply the label to retry."
fi
{
echo "$MARKER"
echo "<!-- stamphog-review-count:$COUNT -->"
echo "> [!NOTE]"
echo "> 🤖 $HEADLINE"
echo
echo "$DETAIL"
if [ -n "$COMMENT_ID" ]; then
echo
[ "$PREV_COUNT" -gt 0 ] \
&& echo "_Updated in place — this replaces $PREV_COUNT earlier stamphog review(s) on this PR._" \
|| echo "_Updated in place from an earlier stamphog review._"
fi
} > /tmp/stamphog-comment.md
fi
# A failed PATCH (comment deleted, transient API error) falls
# back to posting fresh, so the only unguarded API call left
# ahead of the label strip is the same single POST the old
# COMMENT-review path had.
if [ -n "$COMMENT_ID" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$COMMENT_ID" \
-F "body=@/tmp/stamphog-comment.md" || COMMENT_ID=""
fi
if [ -z "$COMMENT_ID" ]; then
gh api -X POST "repos/$REPO/issues/$PR/comments" \
-F "body=@/tmp/stamphog-comment.md"
fi
fi
# Only the two substantive verdicts strip the label — a
# REFUSE/ESCALATE means the author must address feedback and
# re-apply. Everything else fails toward retention: ERROR
# (LLM backend down), WAIT (reviewer bot mid-review), and an
# empty verdict (script crash / job cancelled before
# review.json was written) are not verdicts on the PR, and
# retention just means the next push retries. An explicit
# strip-list also means future verdicts keep the label by
# default instead of everyone remembering to extend a
# keep-list.
if [ "$VERDICT" = "REFUSED" ] || [ "$VERDICT" = "ESCALATE" ]; then
gh pr edit "$PR" --remove-label Stamphog \
--repo "$REPO"
fi
- name: Upload evidence
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: review-${{ github.event.pull_request.number }}
path: /tmp/review.json
retention-days: 30
# Defense-in-depth: main ruleset has dismiss_stale_reviews_on_push=false
# and require_last_push_approval=false, so a stale bot approval could
# otherwise inherit malicious commits. Two-step gate: decide-delta
# classifies the new commits since the last bot approval, dismiss only
# runs when the delta is non-trivial. Trivial deltas (test/docs/lockfile
# /generated paths and clean merges from the base branch) retain the
# prior approval — a comment on the PR records the reason. The stamphog
# label stays sticky across pushes; the review job's existing
# non-APPROVED label-strip is the auto-loop's escape hatch.
decide-delta:
if: >-
github.event.action == 'synchronize'
&& !github.event.pull_request.draft
&& github.event.pull_request.user.type != 'Bot'
&& !contains(github.event.pull_request.user.login, '[bot]')
&& github.event.pull_request.user.login != 'posthog-bot'
&& contains(github.event.pull_request.labels.*.name, 'Stamphog')
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
dismiss_approval: ${{ steps.decide.outputs.dismiss_approval }}
run_review: ${{ steps.decide.outputs.run_review }}
reason: ${{ steps.decide.outputs.reason }}
last_approved_sha: ${{ steps.decide.outputs.last_approved_sha }}
steps:
- name: Get app token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_APP_ID }}
private-key: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_PRIVATE_KEY }}
- name: Checkout main (full history)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.app-token.outputs.token }}
ref: main
filter: blob:none
fetch-depth: 0
- name: Fetch PR head
run: git fetch --filter=blob:none origin pull/${{ github.event.pull_request.number }}/head
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: '0.11.28' # pinned: unpinned setup-uv calls GH API on every job, exhausts rate limit
enable-cache: false
- name: Decide retain vs dismiss
id: decide
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_REF: origin/${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
decision=$(uv run tools/pr-approval-agent/dismiss_check.py)
echo "$decision"
echo "dismiss_approval=$(echo "$decision" | jq -r .dismiss_approval)" >> "$GITHUB_OUTPUT"
echo "run_review=$(echo "$decision" | jq -r .run_review)" >> "$GITHUB_OUTPUT"
echo "reason=$(echo "$decision" | jq -r .reason)" >> "$GITHUB_OUTPUT"
echo "last_approved_sha=$(echo "$decision" | jq -r '.last_approved_sha // ""')" >> "$GITHUB_OUTPUT"
# Only post the comment on actual retention reasons — not on
# no_prior_approval (nothing to retain) or empty_delta (HEAD
# didn't move, comment would be noise).
- name: Note retained approval
if: contains(fromJSON('["trivial_paths", "merge_only", "mixed_trivial"]'), steps.decide.outputs.reason)
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
REASON: ${{ steps.decide.outputs.reason }}
run: |
gh pr comment "$PR" --repo "$REPO" \
--body "Retaining stamphog approval — delta since last review classified as \`$REASON\`."
dismiss:
needs: decide-delta
# Fail closed on three cases:
# - decide-delta said dismiss (smart path)
# - decide-delta failed (uv install / checkout / fetch timeout)
# - decide-delta was skipped (label removed out-of-band) — mirrors
# the pre-PR unconditional dismiss-on-push behavior so a stale
# bot approval can't outlive the label under main ruleset's
# dismiss_stale_reviews_on_push=false / require_last_push_approval=false
# Explicit synchronize + draft gates stop spurious dismissal on
# labeled / ready_for_review events where decide-delta's result is
# also 'skipped'.
# Bot authors are deliberately NOT excluded here: a bot-authored PR may
# still carry a stale github-actions[bot] approval from before the
# bot-author gate landed, and it must get dismissed on push. decide-delta
# is skipped for bots, which routes here via the 'skipped' fail-closed
# path. This only ever touches stamphog bot approvals, so it's a
# no-op when there's nothing to dismiss.
# Same-repo only: forked / Dependabot PRs don't receive the app secrets,
# so the app-token step below would fail on every such synchronize. There
# is nothing to dismiss there anyway, since stamphog can never have
# approved a fork PR (the review job's app-token step is equally starved
# of secrets on forks), so gate the whole job to head branches in this repo.
if: >-
always()
&& github.event.action == 'synchronize'
&& !github.event.pull_request.draft
&& github.event.pull_request.head.repo.full_name == github.repository
&& (
needs.decide-delta.outputs.dismiss_approval == 'true'
|| needs.decide-delta.result == 'failure'
|| needs.decide-delta.result == 'skipped'
)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Get app token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_APP_ID }}
private-key: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_PRIVATE_KEY }}
- name: Dismiss stale bot approvals
env:
# Act as the Stamphog app for consistency with every other
# write. pull-requests:write can dismiss any review regardless
# of its author, so the app dismisses both its own approvals
# and any legacy github-actions[bot] ones.
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
REASON: ${{ needs.decide-delta.outputs.reason || (needs.decide-delta.result == 'skipped' && 'label_absent') || 'decide_delta_failed' }}
run: |
set -euo pipefail
# Dismiss APPROVED reviews from either Stamphog identity —
# github-actions[bot] (this repo's approval identity) and
# stamphog[bot] (the app, in case approvals move there).
# Human reviews and non-approval reviews are untouched.
mapfile -t REVIEW_IDS < <(
gh api "repos/$REPO/pulls/$PR/reviews" --paginate \
--jq '.[] | select((.user.login == "github-actions[bot]" or .user.login == "stamphog[bot]") and .state == "APPROVED") | .id'
)
for id in "${REVIEW_IDS[@]}"; do
[ -z "$id" ] && continue
gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$id/dismissals" \
-f message="New commits pushed (delta classified \`$REASON\`) — stamphog approval dismissed; re-review running automatically." \
-f event=DISMISS
done
# stamphog never reviews bot-authored PRs (dependabot, mendral, other
# agents). A human applying the label can't override this — bot output
# isn't a trusted basis for an auto-approval. review / decide-delta are
# gated to skip bot authors; this job runs instead to strip the label so
# it never lingers. Same bot definition as those job gates, and as the
# review script's defense-in-depth REFUSE.
# Fires on labeled / ready_for_review (the apply paths — comment + strip)
# and on synchronize when the label is still present (a leftover from before
# this gate landed, or a failed earlier strip — clean it up, no comment).
bot-author-skip:
if: >-
!github.event.pull_request.draft
&& (
github.event.pull_request.user.type == 'Bot'
|| contains(github.event.pull_request.user.login, '[bot]')
|| github.event.pull_request.user.login == 'posthog-bot'
)
&& (
github.event.label.name == 'Stamphog'
|| (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'Stamphog'))
)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Get app token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_APP_ID }}
private-key: ${{ secrets.GH_APP_PR_APPROVAL_AGENT_PRIVATE_KEY }}
- name: Comment and strip label
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
ACTION: ${{ github.event.action }}
run: |
set -euo pipefail
# Explain only on the apply paths (a human just labeled it, or
# marked the PR ready). On synchronize the note was already
# posted — or the label predates this gate — so just clean up.
if [ "$ACTION" != "synchronize" ]; then
gh pr comment "$PR" --repo "$REPO" \
--body "stamphog does not review bot-authored PRs — removing the \`Stamphog\` label. This change needs a human reviewer."
fi
gh pr edit "$PR" --repo "$REPO" --remove-label Stamphog