mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
93494b8c13
Signed-off-by: phernandez <paul@basicmachines.co>
378 lines
15 KiB
YAML
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"",
|
|
"<!-- 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}"
|