Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/setup-target/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ inputs:
permission-pull-requests:
description: 'Token permission for pull-requests (omit to inherit all)'
default: ''
permission-issues:
description: 'Token permission for issues (omit to inherit all)'
default: ''
permission-vulnerability-alerts:
description: 'Token permission for vulnerability-alerts (omit to inherit all)'
default: ''
Expand Down Expand Up @@ -98,6 +101,7 @@ runs:
repositories: ${{ steps.parse.outputs.name }}
permission-contents: ${{ inputs.permission-contents || '' }}
permission-pull-requests: ${{ inputs.permission-pull-requests || '' }}
permission-issues: ${{ inputs.permission-issues || '' }}
permission-vulnerability-alerts: ${{ inputs.permission-vulnerability-alerts || '' }}
permission-security-events: ${{ inputs.permission-security-events || '' }}

Expand Down
335 changes: 335 additions & 0 deletions .github/workflows/auto-engineer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
---
name: BLEnder Auto-Engineer
run-name: >-
Auto-engineer ${{ inputs.target_repo }}
phase=${{ inputs.phase }} issue #${{ inputs.issue_number }}
on:
workflow_dispatch:
inputs:
target_repo:
description: 'Target repo (e.g. mozilla/fx-private-relay)'
required: true
phase:
description: 'Phase: plan, implement, or self-review'
required: true
issue_number:
description: 'Issue number (0 = let Claude pick)'
default: '0'
issue_title:
description: 'Issue title'
default: ''
pr_number:
description: 'Existing PR number (0 = no PR yet)'
default: '0'
trusted_author_associations:
description: 'Comma-separated trusted author associations'
default: 'OWNER'
forbidden_paths:
description: 'Space-separated forbidden paths for implement mode'
default: '.github/ .env .circleci/'
dry_run:
description: 'Dry run (true = no mutations)'
default: 'true'
verbose:
description: 'Print full Claude output'
default: 'false'

permissions:
contents: read

concurrency:
group: blender-auto-engineer-${{ inputs.target_repo }}
cancel-in-progress: false

jobs:
plan:
if: inputs.phase == 'plan'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Setup target repo
id: setup
uses: ./.github/actions/setup-target
with:
target-repo: ${{ inputs.target_repo }}
app-id: ${{ secrets.BLENDER_APP_ID }}
private-key: ${{ secrets.BLENDER_APP_PRIVATE_KEY }}
blender-workspace: ${{ github.workspace }}
permission-issues: read
permission-contents: write
permission-pull-requests: write
install-sandbox: 'true'
install-claude: 'true'

- name: Check for existing auto-engineer PR
id: existing-pr
run: |
if [ "$PR_NUMBER" != "0" ]; then
echo "pr_exists=true" >> "$GITHUB_OUTPUT"
else
echo "pr_exists=false" >> "$GITHUB_OUTPUT"
fi
env:
PR_NUMBER: ${{ inputs.pr_number }}

- name: Gather context (new plan)
if: steps.existing-pr.outputs.pr_exists != 'true'
working-directory: target
run: ${{ github.workspace }}/scripts/gather-issue-context.sh
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
ISSUE_NUMBER: ${{ inputs.issue_number }}
REPO: ${{ inputs.target_repo }}
PROMPT_TEMPLATE: ${{ github.workspace }}/prompts/auto-engineer-plan-prompt.md
ISSUE_TITLE: ${{ inputs.issue_title }}
TRUSTED_AUTHOR_ASSOCIATIONS: ${{ inputs.trusted_author_associations }}

- name: Gather context (plan feedback)
if: steps.existing-pr.outputs.pr_exists == 'true'
working-directory: target
run: ${{ github.workspace }}/scripts/gather-review-context.sh
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
PR_NUMBER: ${{ inputs.pr_number }}
ISSUE_NUMBER: ${{ inputs.issue_number }}
REPO: ${{ inputs.target_repo }}
PROMPT_TEMPLATE: ${{ github.workspace }}/prompts/auto-engineer-plan-prompt.md
ISSUE_TITLE: ${{ inputs.issue_title }}
TRUSTED_AUTHOR_ASSOCIATIONS: ${{ inputs.trusted_author_associations }}

- name: Run Claude (plan)
working-directory: target
run: ${{ github.workspace }}/scripts/run-claude.sh
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
REPO: ${{ inputs.target_repo }}
REPO_NAME: ${{ steps.setup.outputs.repo_name }}
BLENDER_DIR: ${{ github.workspace }}
BLENDER_MODE: plan
CLAUDE_VERBOSE: ${{ inputs.verbose }}

- name: Create or update plan PR
if: inputs.dry_run != 'true'
working-directory: target
run: |
if [ ! -f .blender-plan.md ]; then
echo "No plan file produced. Skipping PR creation."
exit 0
fi

SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | head -c 40)
BRANCH="blender/auto-engineer/${ISSUE_NUMBER}-${SLUG}"

mkdir -p .blender/plans
cp .blender-plan.md ".blender/plans/${ISSUE_NUMBER}.md"

git config user.name "mozilla-blender[bot]"
git config user.email "mozilla-blender[bot]@users.noreply.github.com"

if [ "${PR_EXISTS}" = "true" ]; then
git fetch "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "${BRANCH}"
git checkout "${BRANCH}"
git add ".blender/plans/${ISSUE_NUMBER}.md"
git commit -m "BLEnder plan(#${ISSUE_NUMBER}): update plan based on feedback"
Comment thread
groovecoder marked this conversation as resolved.
else
git checkout -b "${BRANCH}"
git add ".blender/plans/${ISSUE_NUMBER}.md"
git commit -m "BLEnder plan(#${ISSUE_NUMBER}): ${ISSUE_TITLE}"
fi

git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "${BRANCH}" --force-with-lease

if [ "${PR_EXISTS}" != "true" ]; then
PR_BODY="## Auto-Engineer Plan for #${ISSUE_NUMBER}

This PR contains a plan for implementing #${ISSUE_NUMBER}.

**Please review the plan** in \`.blender/plans/${ISSUE_NUMBER}.md\`.
Once approved, BLEnder will implement it.

---
🤖 Generated by [BLEnder](https://github.com/mozilla/blender)"

PR_URL=$(gh pr create \
--repo "${REPO}" \
--head "${BRANCH}" \
--title "BLEnder: ${ISSUE_TITLE}" \
--label "blender:auto-engineer" \
--body "${PR_BODY}")

if [ "${ISSUE_NUMBER}" != "0" ]; then
gh issue comment "${ISSUE_NUMBER}" \
--repo "${REPO}" \
--body "BLEnder created a plan for this issue: ${PR_URL}

Please review the plan and approve the PR when ready for implementation."
fi
fi
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
REPO: ${{ inputs.target_repo }}
ISSUE_NUMBER: ${{ inputs.issue_number }}
ISSUE_TITLE: ${{ inputs.issue_title }}
PR_EXISTS: ${{ steps.existing-pr.outputs.pr_exists }}

implement:
if: inputs.phase == 'implement'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Setup target repo
id: setup
uses: ./.github/actions/setup-target
with:
target-repo: ${{ inputs.target_repo }}
app-id: ${{ secrets.BLENDER_APP_ID }}
private-key: ${{ secrets.BLENDER_APP_PRIVATE_KEY }}
blender-workspace: ${{ github.workspace }}
permission-issues: read
permission-contents: write
permission-pull-requests: write
install-sandbox: 'true'
install-claude: 'true'

- name: Checkout PR branch
working-directory: target
run: |
PR_JSON=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}")
BRANCH=$(echo "$PR_JSON" | jq -r '.head.ref')
git fetch "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "${BRANCH}"
git checkout "${BRANCH}"
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
REPO: ${{ inputs.target_repo }}
PR_NUMBER: ${{ inputs.pr_number }}

- name: Check if addressing code review
id: check-review
run: |
IMPL_COMMITS=$(git log --oneline | grep -cv "BLEnder plan(" || true)
if [ "$IMPL_COMMITS" -gt 1 ]; then
echo "has_impl=true" >> "$GITHUB_OUTPUT"
else
echo "has_impl=false" >> "$GITHUB_OUTPUT"
fi

- name: Gather context (first implementation)
if: steps.check-review.outputs.has_impl != 'true'
working-directory: target
run: ${{ github.workspace }}/scripts/gather-implement-context.sh
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
ISSUE_NUMBER: ${{ inputs.issue_number }}
REPO: ${{ inputs.target_repo }}
PROMPT_TEMPLATE: ${{ github.workspace }}/prompts/auto-engineer-implement-prompt.md
ISSUE_TITLE: ${{ inputs.issue_title }}
TRUSTED_AUTHOR_ASSOCIATIONS: ${{ inputs.trusted_author_associations }}

- name: Gather context (code review feedback)
if: steps.check-review.outputs.has_impl == 'true'
working-directory: target
run: ${{ github.workspace }}/scripts/gather-review-context.sh
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
PR_NUMBER: ${{ inputs.pr_number }}
ISSUE_NUMBER: ${{ inputs.issue_number }}
REPO: ${{ inputs.target_repo }}
PROMPT_TEMPLATE: ${{ github.workspace }}/prompts/auto-engineer-implement-prompt.md
ISSUE_TITLE: ${{ inputs.issue_title }}
TRUSTED_AUTHOR_ASSOCIATIONS: ${{ inputs.trusted_author_associations }}

- name: Run Claude (implement)
working-directory: target
run: ${{ github.workspace }}/scripts/run-claude.sh
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
REPO: ${{ inputs.target_repo }}
REPO_NAME: ${{ steps.setup.outputs.repo_name }}
BLENDER_DIR: ${{ github.workspace }}
BLENDER_MODE: implement
BLENDER_FORBIDDEN_PATHS: ${{ inputs.forbidden_paths }}
CLAUDE_VERBOSE: ${{ inputs.verbose }}

- name: Commit and push
if: inputs.dry_run != 'true'
working-directory: target
run: ${{ github.workspace }}/scripts/commit.sh
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
REPO: ${{ inputs.target_repo }}

self-review:
if: inputs.phase == 'self-review'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Setup target repo
id: setup
uses: ./.github/actions/setup-target
with:
target-repo: ${{ inputs.target_repo }}
app-id: ${{ secrets.BLENDER_APP_ID }}
private-key: ${{ secrets.BLENDER_APP_PRIVATE_KEY }}
blender-workspace: ${{ github.workspace }}
permission-issues: read
permission-contents: read
permission-pull-requests: write
install-sandbox: 'true'
install-claude: 'true'

- name: Gather self-review context
working-directory: target
run: ${{ github.workspace }}/scripts/gather-self-review-context.sh
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
PR_NUMBER: ${{ inputs.pr_number }}
ISSUE_NUMBER: ${{ inputs.issue_number }}
REPO: ${{ inputs.target_repo }}
PROMPT_TEMPLATE: ${{ github.workspace }}/prompts/auto-engineer-self-review-prompt.md

- name: Run Claude (self-review)
working-directory: target
run: ${{ github.workspace }}/scripts/run-claude.sh
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
REPO: ${{ inputs.target_repo }}
REPO_NAME: ${{ steps.setup.outputs.repo_name }}
BLENDER_DIR: ${{ github.workspace }}
BLENDER_MODE: self-review
CLAUDE_VERBOSE: ${{ inputs.verbose }}

- name: Post self-review comment
if: inputs.dry_run != 'true'
working-directory: target
run: |
if [ ! -f .blender-self-review.md ]; then
echo "No self-review file produced. Posting fallback comment."
BODY="## Self-Review

BLEnder could not produce a self-review summary for this PR."
else
BODY=$(cat .blender-self-review.md)
fi

gh pr comment "${PR_NUMBER}" \
--repo "${REPO}" \
--body "${BODY}"
env:
GH_TOKEN: ${{ steps.setup.outputs.token }}
REPO: ${{ inputs.target_repo }}
PR_NUMBER: ${{ inputs.pr_number }}
13 changes: 13 additions & 0 deletions config/defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,16 @@ investigate:
run_tests: true
severity_threshold: ""
dismiss_unaffected: false

auto_engineer:
enabled: false
dry_run: true
issue_label: "auto-engineer"
trusted_author_associations: "OWNER"
forbidden_paths: ".github/ .env .circleci/"
max_plan_turns: 20
max_plan_budget_usd: 1.50
max_implement_turns: 40
max_implement_budget_usd: 4.00
max_self_review_turns: 15
max_self_review_budget_usd: 1.00
Loading
Loading