AI Orchestrator #28
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: 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 |