RFE: Make agent pluggable #199
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Amber Issue-to-PR Handler | |
| # | |
| # This workflow automates issue resolution via the Amber background agent. | |
| # | |
| # TRIGGERS: | |
| # - Issue labeled with: amber:auto-fix, amber:refactor, amber:test-coverage | |
| # - Issue comment containing: /amber execute or @amber | |
| # | |
| # BEHAVIOR: | |
| # - Checks for existing open PR for the issue (prevents duplicate PRs) | |
| # - Creates or updates feature branch: amber/issue-{number}-{sanitized-title} | |
| # - Runs Claude Code to implement changes | |
| # - Creates PR or pushes to existing PR | |
| # | |
| # SECURITY: | |
| # - Validates branch names against injection attacks | |
| # - Uses strict regex matching for PR lookup | |
| # - Handles race conditions when PRs are closed during execution | |
| name: Amber Issue-to-PR Handler | |
| on: | |
| issues: | |
| types: [labeled, opened] | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| id-token: write # Required for OIDC token (Bedrock/Vertex/Foundry/OAuth) | |
| jobs: | |
| amber-handler: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 # Issue #7: Prevent runaway jobs | |
| # Only run for specific labels, commands, or @amber mentions | |
| if: | | |
| (github.event.label.name == 'amber:auto-fix' || | |
| github.event.label.name == 'amber:refactor' || | |
| github.event.label.name == 'amber:test-coverage' || | |
| contains(github.event.comment.body, '/amber execute') || | |
| contains(github.event.comment.body, '@amber')) | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Determine Amber action type | |
| id: action-type | |
| env: | |
| LABEL_NAME: ${{ github.event.label.name }} | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| run: | | |
| # Parse label or comment to determine action | |
| if [[ "$LABEL_NAME" == "amber:auto-fix" ]]; then | |
| echo "type=auto-fix" >> $GITHUB_OUTPUT | |
| echo "severity=low" >> $GITHUB_OUTPUT | |
| elif [[ "$LABEL_NAME" == "amber:refactor" ]]; then | |
| echo "type=refactor" >> $GITHUB_OUTPUT | |
| echo "severity=medium" >> $GITHUB_OUTPUT | |
| elif [[ "$LABEL_NAME" == "amber:test-coverage" ]]; then | |
| echo "type=test-coverage" >> $GITHUB_OUTPUT | |
| echo "severity=medium" >> $GITHUB_OUTPUT | |
| elif [[ "$COMMENT_BODY" == *"/amber execute"* ]] || [[ "$COMMENT_BODY" == *"@amber"* ]]; then | |
| # Treat @amber mentions same as /amber execute - let Claude figure out the intent | |
| echo "type=execute-proposal" >> $GITHUB_OUTPUT | |
| echo "severity=medium" >> $GITHUB_OUTPUT | |
| else | |
| echo "type=unknown" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Extract issue details | |
| id: issue-details | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| // Parse issue body for Amber-compatible context | |
| const body = issue.body || ''; | |
| // Extract file paths mentioned in issue | |
| const filePattern = /(?:File|Path):\s*`?([^\s`]+)`?/gi; | |
| const files = [...body.matchAll(filePattern)].map(m => m[1]); | |
| // Extract specific instructions | |
| const instructionPattern = /(?:Instructions?|Task):\s*\n([\s\S]*?)(?:\n#{2,}|\n---|\n\*\*|$)/i; | |
| const instructionMatch = body.match(instructionPattern); | |
| const instructions = instructionMatch ? instructionMatch[1].trim() : ''; | |
| // Set outputs | |
| core.setOutput('issue_number', issue.number); | |
| core.setOutput('issue_title', issue.title); | |
| core.setOutput('issue_body', body); | |
| core.setOutput('files', JSON.stringify(files)); | |
| core.setOutput('instructions', instructions || issue.title); | |
| console.log('Parsed issue:', { | |
| number: issue.number, | |
| title: issue.title, | |
| files: files, | |
| instructions: instructions || issue.title | |
| }); | |
| - name: Create Amber agent prompt | |
| id: create-prompt | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| ISSUE_TITLE: ${{ steps.issue-details.outputs.issue_title }} | |
| ISSUE_INSTRUCTIONS: ${{ steps.issue-details.outputs.instructions }} | |
| ISSUE_FILES: ${{ steps.issue-details.outputs.files }} | |
| ACTION_TYPE: ${{ steps.action-type.outputs.type }} | |
| ACTION_SEVERITY: ${{ steps.action-type.outputs.severity }} | |
| run: | | |
| cat > /tmp/amber-prompt.md <<'EOF' | |
| # Amber Agent Task: Issue #${ISSUE_NUMBER} | |
| **Action Type:** ${ACTION_TYPE} | |
| **Severity:** ${ACTION_SEVERITY} | |
| ## Issue Details | |
| **Title:** ${ISSUE_TITLE} | |
| **Instructions:** | |
| ${ISSUE_INSTRUCTIONS} | |
| **Files to modify (if specified):** | |
| ${ISSUE_FILES} | |
| ## Your Mission | |
| Based on the action type, perform the following: | |
| ### For `auto-fix` type: | |
| 1. Identify the specific linting/formatting issues mentioned | |
| 2. Run appropriate formatters (gofmt, black, prettier, etc.) | |
| 3. Fix any trivial issues (unused imports, spacing, etc.) | |
| 4. Ensure all changes pass existing tests | |
| 5. Create a clean commit with conventional format | |
| ### For `refactor` type: | |
| 1. Analyze the current code structure | |
| 2. Implement the refactoring as described in the issue | |
| 3. Ensure backward compatibility (no breaking changes) | |
| 4. Add/update tests to cover refactored code | |
| 5. Verify all existing tests still pass | |
| ### For `test-coverage` type: | |
| 1. Analyze current test coverage for specified files | |
| 2. Identify untested code paths | |
| 3. Write contract tests following project standards (see CLAUDE.md) | |
| 4. Ensure tests follow table-driven test pattern (Go) or pytest patterns (Python) | |
| 5. Verify all new tests pass | |
| ### For `execute-proposal` type: | |
| 1. Read the full issue body for the proposed implementation | |
| 2. Execute the changes as specified in the proposal | |
| 3. Follow the risk assessment and rollback plan provided | |
| 4. Ensure all testing strategies are implemented | |
| ## Requirements | |
| - Follow all standards in `CLAUDE.md` | |
| - Use conventional commit format: `type(scope): message` | |
| - Run all linters BEFORE committing: | |
| - Go: `gofmt -w .`, `golangci-lint run` | |
| - Python: `black .`, `isort .`, `flake8` | |
| - TypeScript: `npm run lint` | |
| - Ensure ALL tests pass: `make test` | |
| - Create branch following pattern: `amber/issue-${ISSUE_NUMBER}-{description}` | |
| ## Success Criteria | |
| - All linters pass with 0 warnings | |
| - All existing tests pass | |
| - New code follows project conventions | |
| - Commit message is clear and follows conventional format | |
| - Changes are focused on issue scope (no scope creep) | |
| ## Output Format | |
| After completing the work, provide: | |
| 1. **Summary of changes** (2-3 sentences) | |
| 2. **Files modified** (list with line count changes) | |
| 3. **Test results** (pass/fail for each test suite) | |
| 4. **Linting results** (confirm all pass) | |
| 5. **Commit SHA** | |
| Ready to execute! | |
| EOF | |
| # Substitute environment variables | |
| envsubst < /tmp/amber-prompt.md > amber-prompt.md | |
| echo "prompt_file=amber-prompt.md" >> $GITHUB_OUTPUT | |
| - name: Check for existing PR | |
| id: check-existing-pr | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Validate issue number is numeric to prevent injection | |
| if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then | |
| echo "Error: Invalid issue number format" | |
| exit 1 | |
| fi | |
| # Check if there's already an open PR for this issue using stricter matching | |
| # Search for PRs that reference this issue and filter by body containing exact "Closes #N" pattern | |
| EXISTING_PR=$(gh pr list --state open --json number,headRefName,body --jq \ | |
| ".[] | select(.body | test(\"Closes #${ISSUE_NUMBER}($|[^0-9])\")) | {number, headRefName}" \ | |
| 2>/dev/null | head -1 || echo "") | |
| if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ] && [ "$EXISTING_PR" != "{}" ]; then | |
| PR_NUMBER=$(echo "$EXISTING_PR" | jq -r '.number') | |
| EXISTING_BRANCH=$(echo "$EXISTING_PR" | jq -r '.headRefName') | |
| # Validate branch name format to prevent command injection | |
| if ! [[ "$EXISTING_BRANCH" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then | |
| echo "Error: Invalid branch name format in existing PR" | |
| echo "existing_pr=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| echo "existing_pr=true" >> $GITHUB_OUTPUT | |
| echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT | |
| echo "existing_branch=$EXISTING_BRANCH" >> $GITHUB_OUTPUT | |
| echo "Found existing PR #$PR_NUMBER on branch $EXISTING_BRANCH" | |
| else | |
| echo "existing_pr=false" >> $GITHUB_OUTPUT | |
| echo "No existing PR found for issue #${ISSUE_NUMBER}" | |
| fi | |
| - name: Create or checkout feature branch | |
| id: create-branch | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| ISSUE_TITLE: ${{ steps.issue-details.outputs.issue_title }} | |
| EXISTING_PR: ${{ steps.check-existing-pr.outputs.existing_pr }} | |
| EXISTING_BRANCH: ${{ steps.check-existing-pr.outputs.existing_branch }} | |
| run: | | |
| git config user.name "Amber Agent" | |
| git config user.email "amber@ambient-code.ai" | |
| # Validate issue number format | |
| if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then | |
| echo "Error: Invalid issue number format" | |
| exit 1 | |
| fi | |
| checkout_branch() { | |
| local branch="$1" | |
| local is_existing="$2" | |
| # Validate branch name format (alphanumeric, slashes, dashes, dots, underscores only) | |
| if ! [[ "$branch" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then | |
| echo "Error: Invalid branch name format: $branch" | |
| return 1 | |
| fi | |
| echo "Attempting to checkout branch: $branch" | |
| if git fetch origin "$branch" 2>/dev/null; then | |
| git checkout -B "$branch" "origin/$branch" | |
| echo "Checked out existing remote branch: $branch" | |
| elif [ "$is_existing" == "true" ]; then | |
| # Race condition: PR existed but branch was deleted | |
| echo "Warning: Branch $branch no longer exists on remote (PR may have been closed)" | |
| return 1 | |
| else | |
| echo "Creating new branch: $branch" | |
| git checkout -b "$branch" | |
| fi | |
| return 0 | |
| } | |
| if [ "$EXISTING_PR" == "true" ] && [ -n "$EXISTING_BRANCH" ]; then | |
| # Try to checkout existing PR branch with race condition handling | |
| if ! checkout_branch "$EXISTING_BRANCH" "true"; then | |
| echo "Existing PR branch unavailable, falling back to new branch creation" | |
| # Fall through to create new branch | |
| EXISTING_PR="false" | |
| else | |
| BRANCH_NAME="$EXISTING_BRANCH" | |
| fi | |
| fi | |
| if [ "$EXISTING_PR" != "true" ]; then | |
| # Create new branch with sanitized title | |
| # Sanitization: lowercase, replace non-alphanumeric with dash, collapse dashes, trim | |
| SANITIZED_TITLE=$(echo "$ISSUE_TITLE" \ | |
| | tr '[:upper:]' '[:lower:]' \ | |
| | sed 's/[^a-z0-9-]/-/g' \ | |
| | sed 's/--*/-/g' \ | |
| | sed 's/^-//' \ | |
| | sed 's/-$//' \ | |
| | cut -c1-50) | |
| BRANCH_NAME="amber/issue-${ISSUE_NUMBER}-${SANITIZED_TITLE}" | |
| # Validate the generated branch name | |
| if ! [[ "$BRANCH_NAME" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then | |
| echo "Error: Generated branch name is invalid: $BRANCH_NAME" | |
| exit 1 | |
| fi | |
| checkout_branch "$BRANCH_NAME" "false" || exit 1 | |
| fi | |
| echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT | |
| echo "Using branch: $BRANCH_NAME" | |
| - name: Read prompt file | |
| id: read-prompt | |
| run: | | |
| PROMPT_CONTENT=$(cat amber-prompt.md) | |
| # Use heredoc to safely handle multiline content | |
| echo "prompt<<EOF" >> $GITHUB_OUTPUT | |
| cat amber-prompt.md >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Install Claude Code CLI | |
| run: | | |
| npm install -g @anthropic-ai/claude-code | |
| - name: Execute Amber agent via Claude Code | |
| id: amber-execute | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| run: | | |
| # Run Claude Code with full tool access (you make the rules!) | |
| cat amber-prompt.md | claude --print --dangerously-skip-permissions || true | |
| echo "Claude Code execution completed" | |
| - name: Check if changes were made | |
| id: check-changes | |
| run: | | |
| # Check if there are any new commits on this branch vs main | |
| CURRENT_BRANCH=$(git branch --show-current) | |
| COMMITS_AHEAD=$(git rev-list --count origin/main.."$CURRENT_BRANCH" 2>/dev/null || echo "0") | |
| if [ "$COMMITS_AHEAD" -eq 0 ]; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| echo "No changes made by Amber (no new commits)" | |
| else | |
| COMMIT_SHA=$(git rev-parse HEAD) | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| echo "branch_name=$CURRENT_BRANCH" >> $GITHUB_OUTPUT | |
| echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT | |
| echo "Changes committed on branch $CURRENT_BRANCH (commit: ${COMMIT_SHA:0:7})" | |
| echo "Commits ahead of main: $COMMITS_AHEAD" | |
| fi | |
| - name: Report no changes | |
| if: steps.check-changes.outputs.has_changes == 'false' | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| ACTION_TYPE: ${{ steps.action-type.outputs.type }} | |
| RUN_ID: ${{ github.run_id }} | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const issueNumber = parseInt(process.env.ISSUE_NUMBER); | |
| const actionType = process.env.ACTION_TYPE; | |
| const runId = process.env.RUN_ID; | |
| const serverUrl = process.env.GITHUB_SERVER_URL; | |
| const repository = process.env.GITHUB_REPOSITORY; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `✅ Amber reviewed this issue but found no changes were needed. | |
| **Action Type:** ${actionType} | |
| **Possible reasons:** | |
| - Files are already properly formatted | |
| - No linting issues found | |
| - The requested changes may have already been applied | |
| If you believe changes are still needed, please provide more specific instructions or file paths in the issue description. | |
| --- | |
| 🔍 [View AI decision process](${serverUrl}/${repository}/actions/runs/${runId}) (logs available for 90 days)` | |
| }); | |
| - name: Push branch to remote | |
| if: steps.check-changes.outputs.has_changes == 'true' | |
| env: | |
| BRANCH_NAME: ${{ steps.check-changes.outputs.branch_name }} | |
| run: | | |
| git push -u origin "$BRANCH_NAME" | |
| echo "Pushed branch $BRANCH_NAME to remote" | |
| - name: Validate changes align with issue intent | |
| if: steps.check-changes.outputs.has_changes == 'true' | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| RUN_ID: ${{ github.run_id }} | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| EXISTING_PR: ${{ steps.check-existing-pr.outputs.existing_pr }} | |
| EXISTING_PR_NUMBER: ${{ steps.check-existing-pr.outputs.pr_number }} | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { execFile } = require('child_process'); | |
| const { promisify } = require('util'); | |
| const execFileAsync = promisify(execFile); | |
| const issueNumber = parseInt(process.env.ISSUE_NUMBER); | |
| const runId = process.env.RUN_ID; | |
| const serverUrl = process.env.GITHUB_SERVER_URL; | |
| const repository = process.env.GITHUB_REPOSITORY; | |
| const existingPr = process.env.EXISTING_PR === 'true'; | |
| const existingPrNumber = process.env.EXISTING_PR_NUMBER; | |
| // Safely get git diff (no shell injection risk with execFile) | |
| const { stdout: diff } = await execFileAsync('git', ['diff', 'HEAD~1', '--stat']); | |
| const nextSteps = existingPr | |
| ? `- Review that changes match the issue description\n- Verify no scope creep or unintended modifications\n- Changes pushed to existing PR #${existingPrNumber}` | |
| : `- Review that changes match the issue description\n- Verify no scope creep or unintended modifications\n- A PR will be created shortly for formal review`; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `## Amber Change Summary\n\nThe following files were modified:\n\n\`\`\`\n${diff}\n\`\`\`\n\n**Next Steps:**\n${nextSteps}\n\n---\n🔍 [View AI decision process](${serverUrl}/${repository}/actions/runs/${runId}) (logs available for 90 days)` | |
| }); | |
| - name: Create or Update Pull Request | |
| if: steps.check-changes.outputs.has_changes == 'true' | |
| env: | |
| BRANCH_NAME: ${{ steps.check-changes.outputs.branch_name }} | |
| COMMIT_SHA: ${{ steps.check-changes.outputs.commit_sha }} | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| ISSUE_TITLE: ${{ steps.issue-details.outputs.issue_title }} | |
| ACTION_TYPE: ${{ steps.action-type.outputs.type }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| EXISTING_PR: ${{ steps.check-existing-pr.outputs.existing_pr }} | |
| EXISTING_PR_NUMBER: ${{ steps.check-existing-pr.outputs.pr_number }} | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const branchName = process.env.BRANCH_NAME; | |
| const commitSha = process.env.COMMIT_SHA; | |
| const issueNumber = parseInt(process.env.ISSUE_NUMBER); | |
| const issueTitle = process.env.ISSUE_TITLE; | |
| const actionType = process.env.ACTION_TYPE; | |
| const repository = process.env.GITHUB_REPOSITORY; | |
| const runId = process.env.RUN_ID; | |
| const serverUrl = process.env.GITHUB_SERVER_URL; | |
| const existingPr = process.env.EXISTING_PR === 'true'; | |
| const existingPrNumber = process.env.EXISTING_PR_NUMBER ? parseInt(process.env.EXISTING_PR_NUMBER) : null; | |
| // Helper function for retrying API calls with exponential backoff | |
| // Retries on: 5xx errors, network errors (no status), JSON parse errors | |
| async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) { | |
| for (let i = 0; i < maxRetries; i++) { | |
| try { | |
| return await fn(); | |
| } catch (error) { | |
| const isLastAttempt = i === maxRetries - 1; | |
| // Retry on: network errors (undefined status), 5xx errors, or specific error patterns | |
| const isRetriable = !error.status || error.status >= 500; | |
| if (isLastAttempt || !isRetriable) { | |
| throw error; | |
| } | |
| const delay = initialDelay * Math.pow(2, i); | |
| const errorMsg = error.message || 'Unknown error'; | |
| const errorStatus = error.status || 'network error'; | |
| console.log(`Attempt ${i + 1} failed (${errorStatus}: ${errorMsg}), retrying in ${delay}ms...`); | |
| await new Promise(resolve => setTimeout(resolve, delay)); | |
| } | |
| } | |
| // Defensive: Should never reach here due to throw in loop, but explicit for clarity | |
| throw new Error('retryWithBackoff: max retries exceeded'); | |
| } | |
| // Helper function to safely add a comment with fallback logging | |
| async function safeComment(issueNum, body, description) { | |
| try { | |
| await retryWithBackoff(async () => { | |
| return await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNum, | |
| body: body | |
| }); | |
| }); | |
| console.log(`Successfully added comment: ${description}`); | |
| } catch (commentError) { | |
| // Log but don't fail the workflow for comment failures | |
| console.log(`Warning: Failed to add comment (${description}): ${commentError.message}`); | |
| console.log(`Comment body was: ${body.substring(0, 200)}...`); | |
| } | |
| } | |
| try { | |
| // If PR already exists, just add a comment about the new push | |
| if (existingPr && existingPrNumber) { | |
| console.log(`PR #${existingPrNumber} already exists, adding update comment`); | |
| // Add comment to PR about the new commit (with fallback) | |
| await safeComment( | |
| existingPrNumber, | |
| `🤖 **Amber pushed additional changes** | |
| - **Commit:** ${commitSha.substring(0, 7)} | |
| - **Action Type:** ${actionType} | |
| New changes have been pushed to this PR. Please review the updated code. | |
| --- | |
| 🔍 [View AI decision process](${serverUrl}/${repository}/actions/runs/${runId}) (logs available for 90 days)`, | |
| `PR #${existingPrNumber} update notification` | |
| ); | |
| // Also notify on the issue (with fallback) | |
| await safeComment( | |
| issueNumber, | |
| `🤖 Amber pushed additional changes to the existing PR #${existingPrNumber}.\n\n---\n🔍 [View AI decision process](${serverUrl}/${repository}/actions/runs/${runId}) (logs available for 90 days)`, | |
| `Issue #${issueNumber} update notification` | |
| ); | |
| console.log(`Updated existing PR #${existingPrNumber}`); | |
| return; | |
| } | |
| // Create new PR | |
| const pr = await github.rest.pulls.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `[Amber] Fix: ${issueTitle}`, | |
| head: branchName, | |
| base: 'main', | |
| body: `## Automated Fix by Amber Agent | |
| This PR addresses issue #${issueNumber} using the Amber background agent. | |
| ### Changes Summary | |
| - **Action Type:** ${actionType} | |
| - **Commit:** ${commitSha.substring(0, 7)} | |
| - **Triggered by:** Issue label/command | |
| ### Pre-merge Checklist | |
| - [ ] All linters pass | |
| - [ ] All tests pass | |
| - [ ] Changes follow project conventions (CLAUDE.md) | |
| - [ ] No scope creep beyond issue description | |
| ### Reviewer Notes | |
| This PR was automatically generated. Please review: | |
| 1. Code quality and adherence to standards | |
| 2. Test coverage for changes | |
| 3. No unintended side effects | |
| --- | |
| 🤖 Generated with [Amber Background Agent](https://github.com/${repository}/blob/main/docs/amber-automation.md) | |
| Closes #${issueNumber}` | |
| }); | |
| // Add labels with retry logic for transient API failures (non-critical) | |
| try { | |
| await retryWithBackoff(async () => { | |
| return await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.data.number, | |
| labels: ['amber-generated', 'auto-fix', actionType] | |
| }); | |
| }); | |
| } catch (labelError) { | |
| console.log(`Warning: Failed to add labels to PR #${pr.data.number}: ${labelError.message}`); | |
| } | |
| // Link PR back to issue (with fallback) | |
| await safeComment( | |
| issueNumber, | |
| `🤖 Amber has created a pull request to address this issue: #${pr.data.number}\n\nThe changes are ready for review. All automated checks will run on the PR.\n\n---\n🔍 [View AI decision process](${serverUrl}/${repository}/actions/runs/${runId}) (logs available for 90 days)`, | |
| `Issue #${issueNumber} PR link notification` | |
| ); | |
| console.log('Created PR:', pr.data.html_url); | |
| } catch (error) { | |
| console.error('Failed to create/update PR:', error); | |
| core.setFailed(`PR creation/update failed: ${error.message}`); | |
| // Notify on issue about failure (with fallback - best effort) | |
| await safeComment( | |
| issueNumber, | |
| `⚠️ Amber completed changes but failed to create a pull request.\n\n**Error:** ${error.message}\n\nChanges committed to \`${branchName}\`. A maintainer can manually create the PR.`, | |
| `Issue #${issueNumber} PR failure notification` | |
| ); | |
| } | |
| - name: Report failure | |
| if: failure() | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| ACTION_TYPE: ${{ steps.action-type.outputs.type }} | |
| RUN_ID: ${{ github.run_id }} | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const issueNumber = parseInt(process.env.ISSUE_NUMBER); | |
| const actionType = process.env.ACTION_TYPE || 'unknown'; | |
| const runId = process.env.RUN_ID; | |
| const serverUrl = process.env.GITHUB_SERVER_URL; | |
| const repository = process.env.GITHUB_REPOSITORY; | |
| // Validate issue number before attempting comment | |
| if (!issueNumber || isNaN(issueNumber)) { | |
| console.log('Error: Invalid issue number, cannot post failure comment'); | |
| return; | |
| } | |
| try { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `⚠️ Amber encountered an error while processing this issue. | |
| **Action Type:** ${actionType} | |
| **Workflow Run:** ${serverUrl}/${repository}/actions/runs/${runId} | |
| Please review the workflow logs for details. You may need to: | |
| 1. Check if the issue description provides sufficient context | |
| 2. Verify the specified files exist | |
| 3. Ensure the changes are feasible for automation | |
| Manual intervention may be required for complex changes.` | |
| }); | |
| console.log(`Posted failure comment to issue #${issueNumber}`); | |
| } catch (commentError) { | |
| console.log(`Warning: Failed to post failure comment to issue #${issueNumber}: ${commentError.message}`); | |
| } |