feat(hogql): separate hogql from django app #45131
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| 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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 | |
| with: | |
| version: '0.11.14' # 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 }} | |
| 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 | |
| # A substantive non-APPROVED verdict (REFUSE/ESCALATE) | |
| # removes the label, breaking the auto-rerun loop until a | |
| # human re-applies it after addressing the feedback. ERROR | |
| # means the review agent couldn't reach its LLM backend | |
| # (auth/credit/outage) — that's not a verdict on the PR, so | |
| # keep the label and let it retry rather than silently | |
| # dropping it across every queued PR during an outage. | |
| if [ "$VERDICT" != "APPROVED" ] && [ "$VERDICT" != "ERROR" ]; 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 | |
| && 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 master (full history) | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| 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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 | |
| with: | |
| version: '0.11.14' # 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'. | |
| # 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 github-actions[bot] approvals, so it's a | |
| # no-op when there's nothing to dismiss. | |
| 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 | |
| # 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 |