name: Claude AI Review with inline comments # Instead of running the ai-reviewer GitHub Action inline, this workflow acts as # a thin, VPN-side relay to the Serge GitHub App hosted at # https://serge.huggingface.tech/. The App's /webhook endpoint sits behind a VPN # that GitHub's own webhook delivery cannot reach, so a runner inside the VPN # (group: aws-general-8-plus) re-delivers the triggering comment event to the App. # # The relay reproduces a genuine GitHub App webhook delivery: # - body: the original event payload with `installation.id` injected (the App # needs it to mint an installation token; Actions payloads omit it) # - X-Hub-Signature-256: HMAC-SHA256 of that exact body using the App's # webhook secret (verified at webapp.py:_verify_webhook_signature) # - X-GitHub-Event: the original event name (issue_comment / pull_request_review_comment) # # All reviewing, diff fetching and comment posting happens server-side under the # App identity, so this job needs no checkout and no write permissions. on: issue_comment: types: [created] pull_request_review_comment: types: [created] permissions: contents: read jobs: forward-to-serge-app: if: | ( github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.issue.state == 'open' && contains(github.event.comment.body, '@askserge') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR') ) || ( github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@askserge') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR') ) concurrency: group: claude-ai-review-${{ github.event.issue.number || github.event.pull_request.number }} cancel-in-progress: false # Must run inside the VPN so https://serge.huggingface.tech/ is reachable. runs-on: group: aws-general-8-plus steps: - name: Relay event to the Serge GitHub App env: WEBHOOK_URL: https://serge.huggingface.tech/webhook # App webhook secret — must match the App's GITHUB_WEBHOOK_SECRET. WEBHOOK_SECRET: ${{ secrets.SERGE_WEBHOOK_SECRET }} # Installation id of the Serge App on this repo. Not sensitive, but the # App requires it in the payload to obtain an installation token. INSTALLATION_ID: ${{ secrets.SERGE_INSTALLATION_ID }} EVENT_NAME: ${{ github.event_name }} DELIVERY_ID: ${{ github.run_id }}-${{ github.run_attempt }} run: | set -euo pipefail if [ -z "${WEBHOOK_SECRET}" ]; then echo "::error::SERGE_WEBHOOK_SECRET secret is not set" >&2 exit 1 fi if [ -z "${INSTALLATION_ID}" ]; then echo "::error::SERGE_INSTALLATION_ID secret is not set" >&2 exit 1 fi # Inject installation.id into the original event payload, compact form. # The signed bytes and the POSTed bytes must be byte-identical, so we # write the body to a file and reuse it for both the HMAC and the POST. jq -c --argjson iid "${INSTALLATION_ID}" \ '. + {installation: {id: $iid}}' \ "${GITHUB_EVENT_PATH}" > payload.json SIG="sha256=$(openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" payload.json | awk '{print $NF}')" HTTP_CODE=$(curl --silent --show-error --fail-with-body \ --output response.txt --write-out '%{http_code}' \ --request POST "${WEBHOOK_URL}" \ --header "Content-Type: application/json" \ --header "X-GitHub-Event: ${EVENT_NAME}" \ --header "X-GitHub-Delivery: ${DELIVERY_ID}" \ --header "X-Hub-Signature-256: ${SIG}" \ --data-binary @payload.json) || { echo "::error::Failed to deliver event to Serge App (HTTP ${HTTP_CODE:-000})" >&2 cat response.txt >&2 || true exit 1 } echo "Serge App responded with HTTP ${HTTP_CODE}" cat response.txt