name: BM Bossbot "on": workflow_run: workflows: - Tests types: - completed workflow_dispatch: inputs: pr_number: description: Pull request number to review required: true # Review-thread activity re-evaluates the approval status without re-running # the full LLM review: new feedback flips the gate to failure, resolving the # last thread restores a previously earned approval for the same head SHA. pull_request_review: types: - submitted pull_request_review_comment: types: - created pull_request_review_thread: types: - resolved - unresolved permissions: contents: read pull-requests: write statuses: write issues: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" BM_BOSSBOT_STATUS_CONTEXT: "BM Bossbot Approval" jobs: review: name: BM Bossbot Review # Job-level concurrency (not workflow-level): thread-recheck events for the # same PR must never cancel an in-flight review run, and vice versa. concurrency: group: bm-bossbot-review-${{ github.event.workflow_run.pull_requests[0].number || inputs.pr_number }} cancel-in-progress: true if: | github.event_name == 'workflow_dispatch' || ( github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].number != '' ) runs-on: ubuntu-latest outputs: pr_number: ${{ steps.pr.outputs.pr_number }} should_review: ${{ steps.pr.outputs.should_review }} steps: - name: Checkout trusted base ref uses: actions/checkout@v6 with: ref: ${{ github.event.repository.default_branch }} fetch-depth: 1 - name: Set up uv uses: astral-sh/setup-uv@v3 - name: Normalize PR event id: pr env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail event_file="${RUNNER_TEMP}/bm-bossbot-event.json" should_review=true if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then pr_number="${{ inputs.pr_number }}" tested_sha="" else pr_number="$(jq -r '.workflow_run.pull_requests[0].number // ""' "${GITHUB_EVENT_PATH}")" tested_sha="$(jq -r '.workflow_run.head_sha // ""' "${GITHUB_EVENT_PATH}")" fi gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" > "${RUNNER_TEMP}/pull.json" current_head_sha="$(jq -r '.head.sha' "${RUNNER_TEMP}/pull.json")" draft="$(jq -r '.draft' "${RUNNER_TEMP}/pull.json")" if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then tests_run_id="$( gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/test.yml/runs" \ -f event=push \ -f head_sha="${current_head_sha}" \ -f status=completed \ --jq '[.workflow_runs[] | select(.conclusion == "success")][0].id // ""' )" if [ -z "${tests_run_id}" ]; then should_review=false echo "BM Bossbot skipped PR ${pr_number}: no successful Tests workflow for ${current_head_sha}." fi fi if [ "${draft}" = "true" ]; then should_review=false echo "BM Bossbot skipped PR ${pr_number}: draft pull request." fi if [ -n "${tested_sha}" ] && [ "${tested_sha}" != "${current_head_sha}" ]; then should_review=false echo "BM Bossbot skipped PR ${pr_number}: Tests passed for ${tested_sha}, but current head is ${current_head_sha}." fi jq --arg repo "${GITHUB_REPOSITORY}" \ '{repository:{full_name:$repo}, pull_request:{number:.number,title:.title,body:(.body // ""),html_url:.html_url,head:{sha:.head.sha,ref:.head.ref},base:{ref:.base.ref,sha:.base.sha},author_association:.author_association,draft:.draft}}' \ "${RUNNER_TEMP}/pull.json" > "${event_file}" echo "event_file=${event_file}" >> "${GITHUB_OUTPUT}" echo "pr_number=$(jq -r '.pull_request.number' "${event_file}")" >> "${GITHUB_OUTPUT}" echo "head_sha=$(jq -r '.pull_request.head.sha' "${event_file}")" >> "${GITHUB_OUTPUT}" echo "head_ref=$(jq -r '.pull_request.head.ref' "${event_file}")" >> "${GITHUB_OUTPUT}" echo "author_association=$(jq -r '.pull_request.author_association // ""' "${event_file}")" >> "${GITHUB_OUTPUT}" echo "tested_sha=${tested_sha}" >> "${GITHUB_OUTPUT}" echo "should_review=${should_review}" >> "${GITHUB_OUTPUT}" - name: Classify PR author id: trust if: steps.pr.outputs.should_review == 'true' env: AUTHOR_ASSOCIATION: ${{ steps.pr.outputs.author_association }} run: | set -euo pipefail case "${AUTHOR_ASSOCIATION}" in OWNER|MEMBER|COLLABORATOR) trusted_author=true ;; *) trusted_author=false ;; esac echo "trusted_author=${trusted_author}" >> "${GITHUB_OUTPUT}" echo "author_association=${AUTHOR_ASSOCIATION}" >> "${GITHUB_OUTPUT}" - name: Mark BM Bossbot approval pending if: steps.pr.outputs.should_review == 'true' env: GITHUB_TOKEN: ${{ github.token }} run: | uv run --script scripts/bm_bossbot_status.py pending \ --event "${{ steps.pr.outputs.event_file }}" \ --repo "${GITHUB_REPOSITORY}" \ --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" - name: Finalize BM Bossbot approval if: always() && steps.pr.outputs.should_review == 'true' env: GITHUB_TOKEN: ${{ github.token }} run: | uv run --script scripts/bm_bossbot_status.py finalize \ --event "${{ steps.pr.outputs.event_file }}" \ --trusted "${{ steps.trust.outputs.trusted_author }}" \ --repo "${GITHUB_REPOSITORY}" \ --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" recheck: name: BM Bossbot Thread Recheck if: | github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review_thread' runs-on: ubuntu-latest # Job-level concurrency: collapse bursts of thread events for one PR while # staying isolated from the review job's group so neither cancels the other. concurrency: group: bm-bossbot-recheck-${{ github.event.pull_request.number }} cancel-in-progress: true steps: - name: Checkout trusted base ref uses: actions/checkout@v6 with: ref: ${{ github.event.repository.default_branch }} fetch-depth: 1 - name: Set up uv uses: astral-sh/setup-uv@v3 - name: Re-evaluate approval from review-thread state env: GITHUB_TOKEN: ${{ github.token }} run: | uv run --script scripts/bm_bossbot_status.py recheck \ --pr-number "${{ github.event.pull_request.number }}" \ --repo "${GITHUB_REPOSITORY}" \ --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"