Files
basicmachines-co-basic-memory/.github/workflows/bm-bossbot.yml
T
Paul Hernandez 93494b8c13 ci(ci): gate bossbot on passing tests (#937)
Signed-off-by: phernandez <paul@basicmachines.co>
2026-06-09 20:47:34 -05:00

378 lines
15 KiB
YAML

name: BM Bossbot
"on":
workflow_run:
workflows:
- Tests
types:
- completed
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to review
required: true
permissions:
contents: read
pull-requests: write
statuses: write
issues: read
concurrency:
group: bm-bossbot-${{ github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
BM_BOSSBOT_STATUS_CONTEXT: "BM Bossbot Approval"
jobs:
review:
name: BM Bossbot Review
if: |
github.event_name == 'workflow_dispatch' ||
(
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 }}
head_ref: ${{ steps.pr.outputs.head_ref }}
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: Decline outside contributor PRs
id: outside
if: steps.pr.outputs.should_review == 'true' && steps.trust.outputs.trusted_author != 'true'
env:
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
AUTHOR_ASSOCIATION: ${{ steps.trust.outputs.author_association }}
run: |
set -euo pipefail
review_file="${RUNNER_TEMP}/bm-bossbot-review.json"
jq -n \
--arg sha "${HEAD_SHA}" \
--arg association "${AUTHOR_ASSOCIATION}" \
'{
reviewed_head_sha: $sha,
review_complete: false,
verdict: "needs_human",
blocking_findings: [
{
title: "BM Bossbot does not run for outside contributors",
body: "This PR author association is \($association). BM Bossbot only runs for OWNER, MEMBER, and COLLABORATOR pull requests, so this PR requires a maintainer path outside the automatic merge gate."
}
],
nonblocking_findings: [],
summary: "BM Bossbot intentionally did not run Codex because this PR was not opened by an owner, member, or collaborator."
}' > "${review_file}"
echo "review_file=${review_file}" >> "${GITHUB_OUTPUT}"
- name: Collect sanitized PR context
id: context
if: steps.pr.outputs.should_review == 'true' && steps.trust.outputs.trusted_author == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
run: |
set -euo pipefail
metadata="${RUNNER_TEMP}/bm-bossbot-pr.json"
diff_file="${RUNNER_TEMP}/bm-bossbot-pr.diff"
prompt_file="${RUNNER_TEMP}/bm-bossbot-prompt.md"
review_file="${RUNNER_TEMP}/bm-bossbot-review.json"
max_diff_bytes=120000
gh pr view "${PR_NUMBER}" \
--repo "${GITHUB_REPOSITORY}" \
--json number,title,body,author,headRefName,headRefOid,baseRefName,labels,files,commits,reviewDecision,mergeStateStatus,isDraft \
> "${metadata}"
gh pr diff "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --patch > "${diff_file}"
diff_bytes="$(wc -c < "${diff_file}" | tr -d '[:space:]')"
diff_truncated=false
if [ "${diff_bytes}" -gt "${max_diff_bytes}" ]; then
diff_truncated=true
fi
cat .github/basic-memory/bm-bossbot-review.md > "${prompt_file}"
{
echo ""
echo "## Pull Request Context"
echo ""
echo "Head SHA to review: ${HEAD_SHA}"
echo ""
echo "### Metadata JSON"
jq . "${metadata}"
echo ""
echo "### Diff"
echo ""
echo '```diff'
if [ "${diff_truncated}" = "true" ]; then
echo "[Diff omitted: ${diff_bytes} bytes exceeds BM Bossbot's ${max_diff_bytes} byte review limit.]"
else
cat "${diff_file}"
fi
echo ""
echo '```'
} >> "${prompt_file}"
if [ "${diff_truncated}" = "true" ]; then
jq -n \
--arg sha "${HEAD_SHA}" \
--argjson bytes "${diff_bytes}" \
--argjson max_bytes "${max_diff_bytes}" \
'{
reviewed_head_sha: $sha,
review_complete: false,
verdict: "needs_human",
blocking_findings: [
{
title: "Diff exceeds BM Bossbot review limit",
body: "The PR diff is \($bytes) bytes, exceeding the deterministic \($max_bytes) byte review limit. A human review is required or the PR must be split before BM Bossbot can approve."
}
],
nonblocking_findings: [],
summary: "BM Bossbot did not approve because the PR diff exceeded the deterministic review limit."
}' > "${review_file}"
fi
echo "prompt_file=${prompt_file}" >> "${GITHUB_OUTPUT}"
echo "review_file=${review_file}" >> "${GITHUB_OUTPUT}"
echo "diff_truncated=${diff_truncated}" >> "${GITHUB_OUTPUT}"
- name: Run BM Bossbot review with Codex
id: codex
if: steps.pr.outputs.should_review == 'true' && steps.trust.outputs.trusted_author == 'true' && steps.context.outputs.diff_truncated != 'true'
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt-file: ${{ steps.context.outputs.prompt_file }}
output-file: ${{ steps.context.outputs.review_file }}
codex-args: --output-schema ${{ github.workspace }}/.github/basic-memory/bm-bossbot-review.schema.json
sandbox: read-only
safety-strategy: drop-sudo
- name: Select BM Bossbot review output
id: review_output
if: always() && steps.pr.outputs.should_review == 'true'
env:
OUTSIDE_REVIEW_FILE: ${{ steps.outside.outputs.review_file }}
CONTEXT_REVIEW_FILE: ${{ steps.context.outputs.review_file }}
run: |
set -euo pipefail
review_file="${OUTSIDE_REVIEW_FILE:-${CONTEXT_REVIEW_FILE:-${RUNNER_TEMP}/missing-bm-bossbot-review.json}}"
echo "review_file=${review_file}" >> "${GITHUB_OUTPUT}"
- 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 }}" \
--review "${{ steps.review_output.outputs.review_file }}" \
--repo "${GITHUB_REPOSITORY}" \
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
assets:
name: BM Bossbot Assets
needs: review
if: needs.review.result == 'success' && needs.review.outputs.should_review == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
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: Generate non-gating PR image
continue-on-error: true
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.review.outputs.pr_number }}
run: |
set -euo pipefail
gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json body --jq '.body // ""' > "${RUNNER_TEMP}/bm-bossbot-pr-body.md"
uv run --script scripts/generate_pr_infographic.py \
--pr-number "${PR_NUMBER}" \
--pr-body-file "${RUNNER_TEMP}/bm-bossbot-pr-body.md" \
--provenance-output "${RUNNER_TEMP}/bm-bossbot-image-provenance.md" \
--output "docs/assets/infographics/pr-${PR_NUMBER}.webp"
- name: Publish non-gating PR image
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.review.outputs.pr_number }}
HEAD_REF: ${{ needs.review.outputs.head_ref }}
run: |
set -euo pipefail
asset_path="docs/assets/infographics/pr-${PR_NUMBER}.webp"
provenance_file="${RUNNER_TEMP}/bm-bossbot-image-provenance.md"
test -f "${asset_path}"
test -f "${provenance_file}"
safe_ref="$(printf '%s' "${HEAD_REF}" | tr -c 'A-Za-z0-9._-' '-')"
asset_branch="pr-assets/${safe_ref}"
tmp_asset="$(mktemp)"
cp "${asset_path}" "${tmp_asset}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch --orphan "${asset_branch}"
git rm -rf --ignore-unmatch .
mkdir -p "$(dirname "${asset_path}")"
cp "${tmp_asset}" "${asset_path}"
git add "${asset_path}"
git commit -m "chore: publish PR ${PR_NUMBER} image"
git push --force origin "HEAD:${asset_branch}"
asset_url="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${asset_branch}/${asset_path}"
body_file="${RUNNER_TEMP}/bm-bossbot-pr-body.md"
updated_body="${RUNNER_TEMP}/bm-bossbot-pr-body-updated.md"
gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json body --jq '.body // ""' > "${body_file}"
python3 - "${body_file}" "${updated_body}" "${asset_url}" "${PR_NUMBER}" "${provenance_file}" <<'PY'
import re
import sys
from pathlib import Path
body_path, output_path, asset_url, pr_number, provenance_path = sys.argv[1:]
body = Path(body_path).read_text(encoding="utf-8")
def upsert_block(body: str, block: str, start: str, end: str) -> str:
pattern = re.compile(rf"{re.escape(start)}.*?{re.escape(end)}", flags=re.DOTALL)
if pattern.search(body):
return pattern.sub(block, body, count=1)
if body.strip():
return f"{body.rstrip()}\n\n{block}\n"
return f"{block}\n"
image_block = "\n".join(
[
"<!-- pr-infographic:start -->",
f"![BM Bossbot image for PR #{pr_number}]({asset_url})",
"<!-- pr-infographic:end -->",
]
)
provenance_block = Path(provenance_path).read_text(encoding="utf-8")
body = upsert_block(
body,
image_block,
"<!-- pr-infographic:start -->",
"<!-- pr-infographic:end -->",
)
body = upsert_block(
body,
provenance_block,
"<!-- BM_INFOGRAPHIC_PROVENANCE:start -->",
"<!-- BM_INFOGRAPHIC_PROVENANCE:end -->",
)
Path(output_path).write_text(body, encoding="utf-8")
PY
gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body-file "${updated_body}"