Docs: architecture diagrams #167
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
| 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: Create feature branch | |
| id: create-branch | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue-details.outputs.issue_number }} | |
| ISSUE_TITLE: ${{ steps.issue-details.outputs.issue_title }} | |
| run: | | |
| # Improved sanitization (Issue #10) - handles special chars, spaces, consecutive dashes | |
| 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}" | |
| git config user.name "Amber Agent" | |
| git config user.email "[email protected]" | |
| git checkout -b "$BRANCH_NAME" | |
| echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT | |
| echo "Created 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 }} | |
| 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; | |
| // Safely get git diff (no shell injection risk with execFile) | |
| const { stdout: diff } = await execFileAsync('git', ['diff', 'HEAD~1', '--stat']); | |
| 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- 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\n\n---\n🔍 [View AI decision process](${serverUrl}/${repository}/actions/runs/${runId}) (logs available for 90 days)` | |
| }); | |
| - name: Create 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 }} | |
| 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; | |
| // 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'); | |
| } | |
| // Create PR with error handling (Issue #3) | |
| try { | |
| 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 | |
| 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] | |
| }); | |
| }); | |
| // Link PR back to issue | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `🤖 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)` | |
| }); | |
| console.log('Created PR:', pr.data.html_url); | |
| } catch (error) { | |
| console.error('Failed to create PR:', error); | |
| core.setFailed(`PR creation failed: ${error.message}`); | |
| // Notify on issue about failure | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `⚠️ 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.` | |
| }); | |
| } | |
| - 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; | |
| 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 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.` | |
| }); |