Skip to content

AI Orchestrator

AI Orchestrator #28

name: AI Orchestrator
# AI Orchestrator workflow for coordinated multi-phase fixes
# Centralized timeout configuration
env:
WORKFLOW_TIMEOUT_MINUTES: 30
CLI_INSTALL_TIMEOUT_MINUTES: 5
AI_EXECUTION_TIMEOUT_MINUTES: ${{ vars.AI_EXECUTION_TIMEOUT_MINUTES || '10' }}
DEBUG_MODE: ${{ vars.AI_DEBUG_MODE || 'false' }}
on:
pull_request:
types: [labeled]
workflow_run:
workflows: ['Quality Checks']
types: [completed]
# Security controls
permissions:
contents: write
issues: write
pull-requests: write
actions: read
# Concurrency controls
concurrency:
group: >
ai-orchestrator-${{ github.event.pull_request.number ||
(github.event.workflow_run.pull_requests[0] &&
github.event.workflow_run.pull_requests[0].number) || 'no-pr' }}
cancel-in-progress: false
jobs:
check-trigger:
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
should-orchestrate: ${{ steps.trigger_check.outputs.should_orchestrate }}
needs-analysis: ${{ steps.quality_check.outputs.needs_analysis }}
pr-number: ${{ steps.pr_number.outputs.pr_number }}
quality-checks-failed: ${{ steps.quality_check.outputs.quality_checks_failed }}
trigger-type: ${{ steps.trigger_check.outputs.trigger_type }}
steps:
- name: Debug event information
if: env.DEBUG_MODE == 'true'
run: |
echo "🐛 DEBUG: Event information"
echo "Event name: ${{ github.event_name }}"
echo "Event action: ${{ github.event.action }}"
echo "Label name: ${{ github.event.label.name }}"
echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}"
echo "Workflow run head branch: ${{ github.event.workflow_run.head_branch }}"
echo "PR number (workflow_run): ${{
github.event.workflow_run.pull_requests[0] &&
github.event.workflow_run.pull_requests[0].number || 'none' }}"
echo "PR number (pull_request): ${{ github.event.pull_request.number }}"
- name: Determine PR number
id: pr_number
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "🔍 Determining PR number from event: ${{ github.event_name }}"
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
PR_NUMBER="${{
github.event.workflow_run.pull_requests[0] &&
github.event.workflow_run.pull_requests[0].number || '' }}"
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
PR_NUMBER="${{ github.event.pull_request.number }}"
else
echo "❌ Unsupported event type: ${{ github.event_name }}"
exit 1
fi
if [[ -z "$PR_NUMBER" || "$PR_NUMBER" == "null" ]]; then
echo "❌ No PR number found in event"
exit 1
fi
echo "✅ Found PR number: $PR_NUMBER"
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
- name: Check orchestration trigger
id: trigger_check
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ steps.pr_number.outputs.pr_number }}"
echo "🔍 Checking orchestration trigger for PR #$PR_NUMBER"
# Get current PR labels
LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name' | tr '\n' ' ')
echo "Current labels: $LABELS"
# Check for orchestration labels
if echo "$LABELS" | grep -E "(ai-fix-all|ai-orchestrate)" > /dev/null; then
echo "✅ Orchestration trigger found"
echo "should_orchestrate=true" >> $GITHUB_OUTPUT
echo "trigger_type=orchestration" >> $GITHUB_OUTPUT
elif echo "$LABELS" | grep -E "ai-fix-" > /dev/null; then
echo "✅ Single AI fix label found"
echo "should_orchestrate=true" >> $GITHUB_OUTPUT
echo "trigger_type=single_fix" >> $GITHUB_OUTPUT
else
echo "❌ No orchestration triggers found"
echo "should_orchestrate=false" >> $GITHUB_OUTPUT
echo "trigger_type=none" >> $GITHUB_OUTPUT
fi
- name: Check quality check status
id: quality_check
if: steps.trigger_check.outputs.should_orchestrate == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ steps.pr_number.outputs.pr_number }}"
echo "🔍 Checking quality check status for PR #$PR_NUMBER"
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
# Quality check just completed
QUALITY_STATUS="${{ github.event.workflow_run.conclusion }}"
echo "Quality check conclusion: $QUALITY_STATUS"
if [[ "$QUALITY_STATUS" == "failure" ]]; then
echo "quality_checks_failed=true" >> $GITHUB_OUTPUT
echo "needs_analysis=true" >> $GITHUB_OUTPUT
else
echo "quality_checks_failed=false" >> $GITHUB_OUTPUT
echo "needs_analysis=false" >> $GITHUB_OUTPUT
fi
else
# Label was added - need to check if quality checks have run
echo "Label trigger - checking existing quality check status"
echo "quality_checks_failed=unknown" >> $GITHUB_OUTPUT
echo "needs_analysis=true" >> $GITHUB_OUTPUT
fi
analyze-needs:
needs: check-trigger
if: needs.check-trigger.outputs.should-orchestrate == 'true' && needs.check-trigger.outputs.needs-analysis == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
needs-lint: ${{ steps.analysis.outputs.needs_lint }}
needs-security: ${{ steps.analysis.outputs.needs_security }}
needs-tests: ${{ steps.analysis.outputs.needs_tests }}
needs-docs: ${{ steps.analysis.outputs.needs_docs }}
execution-plan: ${{ steps.analysis.outputs.execution_plan }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Analyze PR needs
id: analysis
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ needs.check-trigger.outputs.pr-number }}"
echo "🔍 Analyzing PR #$PR_NUMBER for fix requirements..."
# Get PR labels to see what's already requested
EXISTING_LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name' | tr '\n' ' ')
echo "Existing labels: $EXISTING_LABELS"
# Check for existing quality check failures
NEEDS_LINT="false"
NEEDS_SECURITY="false"
NEEDS_TESTS="false"
NEEDS_DOCS="false"
# Check if quality check workflows have run and failed
# This is a simplified check - in practice, you'd query the specific workflow runs
if echo "$EXISTING_LABELS" | grep -q "ai-fix-lint"; then
NEEDS_LINT="true"
fi
if echo "$EXISTING_LABELS" | grep -q "ai-fix-security"; then
NEEDS_SECURITY="true"
fi
if echo "$EXISTING_LABELS" | grep -q "ai-fix-tests"; then
NEEDS_TESTS="true"
fi
if echo "$EXISTING_LABELS" | grep -q "ai-fix-docs"; then
NEEDS_DOCS="true"
fi
# If no specific labels found, analyze the PR changes
if [[ "$NEEDS_LINT" == "false" && "$NEEDS_SECURITY" == "false" && \
"$NEEDS_TESTS" == "false" && "$NEEDS_DOCS" == "false" ]]; then
echo "No specific fix labels found, analyzing PR changes..."
# Get list of changed files
gh pr diff $PR_NUMBER --name-only > changed-files.txt
# Check if Python files are changed (need linting)
if grep -E '\.(py|pyi)$' changed-files.txt > /dev/null; then
NEEDS_LINT="true"
echo "Python files detected - enabling lint fixes"
fi
# Check if security-sensitive files are changed
if grep -E '\.(py|js|ts|yaml|yml|json)$' changed-files.txt > /dev/null; then
NEEDS_SECURITY="true"
echo "Security-sensitive files detected - enabling security fixes"
fi
# Check if test files are changed or new code needs tests
if grep -E 'test.*\.(py|js|ts)$|.*_test\.(py|js|ts)$|\.(spec|test)\.(js|ts)$' \
changed-files.txt > /dev/null; then
NEEDS_TESTS="true"
echo "Test files detected - enabling test fixes"
fi
# Check if documentation needs updates
if grep -E '\.(md|rst|txt)$|README|CHANGELOG' changed-files.txt > /dev/null; then
NEEDS_DOCS="true"
echo "Documentation files detected - enabling docs fixes"
fi
fi
# Create execution plan based on dependencies
EXECUTION_PLAN="[]"
if [[ "$NEEDS_LINT" == "true" ]]; then
EXECUTION_PLAN=$(echo "$EXECUTION_PLAN" | jq '. + [{"type": "lint", "priority": 1, "parallel": false}]')
fi
if [[ "$NEEDS_SECURITY" == "true" ]]; then
EXECUTION_PLAN=$(echo "$EXECUTION_PLAN" | jq '. + [{"type": "security", "priority": 2, "parallel": true}]')
fi
if [[ "$NEEDS_TESTS" == "true" ]]; then
EXECUTION_PLAN=$(echo "$EXECUTION_PLAN" | jq '. + [{"type": "tests", "priority": 3, "parallel": true}]')
fi
if [[ "$NEEDS_DOCS" == "true" ]]; then
EXECUTION_PLAN=$(echo "$EXECUTION_PLAN" | jq '. + [{"type": "docs", "priority": 4, "parallel": true}]')
fi
echo "Execution plan: $EXECUTION_PLAN"
# Set outputs
echo "needs_lint=$NEEDS_LINT" >> $GITHUB_OUTPUT
echo "needs_security=$NEEDS_SECURITY" >> $GITHUB_OUTPUT
echo "needs_tests=$NEEDS_TESTS" >> $GITHUB_OUTPUT
echo "needs_docs=$NEEDS_DOCS" >> $GITHUB_OUTPUT
echo "execution_plan=$EXECUTION_PLAN" >> $GITHUB_OUTPUT
# Phase 1: Lint fixes (must run first to clean up code style)
orchestrate-lint:
needs: [check-trigger, analyze-needs]
if: |
needs.check-trigger.outputs.should-orchestrate == 'true' &&
(needs.analyze-needs.outputs.needs-lint == 'true' || contains(github.event.label.name, 'lint'))
uses: ./.github/workflows/reusable-ai-fix.yml
with:
pr_number: ${{ fromJSON(needs.check-trigger.outputs.pr-number) }}
fix_type: lint
branch_name: >
${{ github.event.pull_request.head.ref ||
(github.event.workflow_run.pull_requests[0] &&
github.event.workflow_run.pull_requests[0].head.ref) ||
github.event.workflow_run.head_branch }}
debug_mode: false
secrets:
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
gh_pat: ${{ secrets.GH_PAT }}
# Phase 2: Security and Tests (can run in parallel after lint)
orchestrate-security:
needs: [check-trigger, analyze-needs, orchestrate-lint]
if: |
always() &&
needs.check-trigger.outputs.should-orchestrate == 'true' &&
(needs.analyze-needs.outputs.needs-security == 'true' || contains(github.event.label.name, 'security')) &&
(needs.orchestrate-lint.result == 'success' || needs.orchestrate-lint.result == 'skipped')
uses: ./.github/workflows/reusable-ai-fix.yml
with:
pr_number: ${{ fromJSON(needs.check-trigger.outputs.pr-number) }}
fix_type: security
branch_name: >
${{ github.event.pull_request.head.ref ||
(github.event.workflow_run.pull_requests[0] &&
github.event.workflow_run.pull_requests[0].head.ref) ||
github.event.workflow_run.head_branch }}
debug_mode: false
secrets:
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
gh_pat: ${{ secrets.GH_PAT }}
orchestrate-tests:
needs: [check-trigger, analyze-needs, orchestrate-lint]
if: |
always() &&
needs.check-trigger.outputs.should-orchestrate == 'true' &&
(needs.analyze-needs.outputs.needs-tests == 'true' || contains(github.event.label.name, 'tests')) &&
(needs.orchestrate-lint.result == 'success' || needs.orchestrate-lint.result == 'skipped')
uses: ./.github/workflows/reusable-ai-fix.yml
with:
pr_number: ${{ fromJSON(needs.check-trigger.outputs.pr-number) }}
fix_type: tests
branch_name: >
${{ github.event.pull_request.head.ref ||
(github.event.workflow_run.pull_requests[0] &&
github.event.workflow_run.pull_requests[0].head.ref) ||
github.event.workflow_run.head_branch }}
debug_mode: false
secrets:
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
gh_pat: ${{ secrets.GH_PAT }}
# Phase 3: Documentation (can run independently)
orchestrate-docs:
needs: [check-trigger, analyze-needs]
if: |
needs.check-trigger.outputs.should-orchestrate == 'true' &&
(needs.analyze-needs.outputs.needs-docs == 'true' || contains(github.event.label.name, 'docs'))
uses: ./.github/workflows/reusable-ai-fix.yml
with:
pr_number: ${{ fromJSON(needs.check-trigger.outputs.pr-number) }}
fix_type: docs
branch_name: >
${{ github.event.pull_request.head.ref ||
(github.event.workflow_run.pull_requests[0] &&
github.event.workflow_run.pull_requests[0].head.ref) ||
github.event.workflow_run.head_branch }}
debug_mode: false
secrets:
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
gh_pat: ${{ secrets.GH_PAT }}
# Final status report
orchestration-summary:
needs:
[
check-trigger,
analyze-needs,
orchestrate-lint,
orchestrate-security,
orchestrate-tests,
orchestrate-docs,
]
if: always() && needs.check-trigger.outputs.should-orchestrate == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Debug orchestration results
if: env.DEBUG_MODE == 'true'
run: |
echo "🐛 DEBUG: Orchestration results"
echo "Lint result: ${{ needs.orchestrate-lint.result }}"
echo "Security result: ${{ needs.orchestrate-security.result }}"
echo "Tests result: ${{ needs.orchestrate-tests.result }}"
echo "Docs result: ${{ needs.orchestrate-docs.result }}"
echo "Lint cost: ${{ needs.orchestrate-lint.outputs.cost_used }}"
echo "Security cost: ${{ needs.orchestrate-security.outputs.cost_used }}"
echo "Tests cost: ${{ needs.orchestrate-tests.outputs.cost_used }}"
echo "Docs cost: ${{ needs.orchestrate-docs.outputs.cost_used }}"
- name: Generate orchestration summary
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ needs.check-trigger.outputs.pr-number }}"
echo "🤖 **AI Orchestration Summary for PR #$PR_NUMBER**" > summary.md
echo "" >> summary.md
# Collect results from each phase
LINT_RESULT="${{ needs.orchestrate-lint.result }}"
SECURITY_RESULT="${{ needs.orchestrate-security.result }}"
TESTS_RESULT="${{ needs.orchestrate-tests.result }}"
DOCS_RESULT="${{ needs.orchestrate-docs.result }}"
# Calculate total cost
TOTAL_COST="0"
if [[ "$LINT_RESULT" == "success" ]]; then
TOTAL_COST=$(echo "$TOTAL_COST + ${{ needs.orchestrate-lint.outputs.cost_used || 0 }}" | bc -l)
fi
if [[ "$SECURITY_RESULT" == "success" ]]; then
TOTAL_COST=$(echo "$TOTAL_COST + ${{ needs.orchestrate-security.outputs.cost_used || 0 }}" | bc -l)
fi
if [[ "$TESTS_RESULT" == "success" ]]; then
TOTAL_COST=$(echo "$TOTAL_COST + ${{ needs.orchestrate-tests.outputs.cost_used || 0 }}" | bc -l)
fi
if [[ "$DOCS_RESULT" == "success" ]]; then
TOTAL_COST=$(echo "$TOTAL_COST + ${{ needs.orchestrate-docs.outputs.cost_used || 0 }}" | bc -l)
fi
echo "**Execution Results:**" >> summary.md
echo "" >> summary.md
# Format results with emojis
format_result() {
case "$1" in
"success") echo "✅ Success" ;;
"failure") echo "❌ Failed" ;;
"cancelled") echo "🚫 Cancelled" ;;
"skipped") echo "⏭️ Skipped" ;;
*) echo "❓ Unknown" ;;
esac
}
if [[ "${{ needs.analyze-needs.outputs.needs-lint }}" == "true" ]] || [[ "$LINT_RESULT" != "" ]]; then
echo "- **Lint Fixes:** $(format_result "$LINT_RESULT")" >> summary.md
fi
if [[ "${{ needs.analyze-needs.outputs.needs-security }}" == "true" ]] || [[ "$SECURITY_RESULT" != "" ]]; then
echo "- **Security Fixes:** $(format_result "$SECURITY_RESULT")" >> summary.md
fi
if [[ "${{ needs.analyze-needs.outputs.needs-tests }}" == "true" ]] || [[ "$TESTS_RESULT" != "" ]]; then
echo "- **Test Fixes:** $(format_result "$TESTS_RESULT")" >> summary.md
fi
if [[ "${{ needs.analyze-needs.outputs.needs-docs }}" == "true" ]] || [[ "$DOCS_RESULT" != "" ]]; then
echo "- **Documentation Fixes:** $(format_result "$DOCS_RESULT")" >> summary.md
fi
echo "" >> summary.md
echo "**Summary:**" >> summary.md
echo "- **Total Cost:** \$$TOTAL_COST" >> summary.md
echo "- **Circuit Breaker State:** ${{ needs.orchestrate-lint.outputs.circuit_state || 'Unknown' }}" >> summary.md
echo "- **Orchestration Trigger:** ${{ github.event.label.name || github.event.workflow_run.conclusion }}" >> summary.md
# Determine overall success
OVERALL_SUCCESS="true"
for result in "$LINT_RESULT" "$SECURITY_RESULT" "$TESTS_RESULT" "$DOCS_RESULT"; do
if [[ "$result" == "failure" ]]; then
OVERALL_SUCCESS="false"
break
fi
done
if [[ "$OVERALL_SUCCESS" == "true" ]]; then
echo "" >> summary.md
echo "🎉 **All requested AI fixes completed successfully!**" >> summary.md
else
echo "" >> summary.md
echo "⚠️ **Some AI fixes failed. Please review the workflow logs for details.**" >> summary.md
fi
# Add timestamp
echo "" >> summary.md
echo "_Orchestration completed at $(date -u '+%Y-%m-%d %H:%M:%S UTC')_" >> summary.md
- name: Post orchestration summary
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ needs.check-trigger.outputs.pr-number }}"
gh pr comment $PR_NUMBER --body-file summary.md
- name: Clean up orchestration labels
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ needs.check-trigger.outputs.pr-number }}"
# Remove orchestration trigger labels
gh pr edit $PR_NUMBER --remove-label "ai-fix-all" || true
gh pr edit $PR_NUMBER --remove-label "ai-orchestrate" || true
# Add completion label
OVERALL_SUCCESS="true"
for result in \
"${{ needs.orchestrate-lint.result }}" \
"${{ needs.orchestrate-security.result }}" \
"${{ needs.orchestrate-tests.result }}" \
"${{ needs.orchestrate-docs.result }}"; do
if [[ "$result" == "failure" ]]; then
OVERALL_SUCCESS="false"
break
fi
done
if [[ "$OVERALL_SUCCESS" == "true" ]]; then
gh pr edit $PR_NUMBER --add-label "ai-orchestration-complete" || true
else
gh pr edit $PR_NUMBER --add-label "ai-orchestration-partial" || true
fi