Skip to content

Docs: architecture diagrams #167

Docs: architecture diagrams

Docs: architecture diagrams #167

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.`
});