Skip to content

feat(replay-vision): API validation + lens_result row column #16516

feat(replay-vision): API validation + lens_result row column

feat(replay-vision): API validation + lens_result row column #16516

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.
# 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.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
timeout-minutes: 10
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 master — hardcoded so a PR
# targeting a non-master branch can't supply a tampered script.
- name: Checkout master (blobless, full history)
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
ref: master
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@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
version: '0.10.2' # pinned: unpinned setup-uv calls GH API on every job, exhausts rate limit
enable-cache: false
- name: Run review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_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:
# Use GITHUB_TOKEN for approvals so github-actions[bot] is the
# reviewer — its approvals count toward branch protection rules,
# unlike GitHub App bot approvals which show author_association NONE.
GH_TOKEN_APPROVE: ${{ github.token }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
PR=${{ github.event.pull_request.number }}
REPO=${{ github.repository }}
VERDICT=$(jq -r '.final_verdict // ""' /tmp/review.json 2>/dev/null || echo "")
REASONING=$(jq -r '.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
GH_TOKEN="$GH_TOKEN_APPROVE" gh api \
-X POST "repos/$REPO/pulls/$PR/reviews" \
"${SHA_ARGS[@]}" \
-f event=APPROVE \
-f body="$REASONING"
elif [ -n "$REASONING" ]; then
gh api \
-X POST "repos/$REPO/pulls/$PR/reviews" \
"${SHA_ARGS[@]}" \
-f event=COMMENT \
-f body="$REASONING"
else
gh pr comment "$PR" \
--body "Review agent failed — check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) and re-apply the label to retry." \
--repo "$REPO"
fi
# Non-APPROVED verdict removes the label, breaking the
# auto-rerun loop until a human re-applies it after
# addressing the feedback.
if [ "$VERDICT" != "APPROVED" ]; 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: master 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
&& 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 master (full history)
uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
ref: master
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@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
version: '0.10.2' # 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 master 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'.
if: >-
always()
&& github.event.action == 'synchronize'
&& !github.event.pull_request.draft
&& (
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: Dismiss stale bot approvals
env:
# Same identity (github-actions[bot]) that posted the approval.
GH_TOKEN: ${{ github.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
# Only dismiss APPROVED reviews made by github-actions[bot] —
# 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]" 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