feat: Add lifecycle hook infrastructure for automated quality gates #22004
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
| # ============================================================================= | |
| # Copilot Context Synthesis Workflow | |
| # ============================================================================= | |
| # | |
| # PURPOSE: | |
| # Automatically synthesizes context from issue comments and assigns GitHub | |
| # Copilot when the 'copilot-ready' label is added to an issue. | |
| # | |
| # TRIGGERS: | |
| # 1. issues:labeled - Immediate processing when label is added | |
| # 2. schedule - Hourly sweep to catch any missed issues | |
| # 3. workflow_dispatch - Manual trigger for testing | |
| # | |
| # HOW IT WORKS: | |
| # 1. Maintainer reviews issue and adds 'copilot-ready' label | |
| # 2. This workflow triggers on the 'labeled' event | |
| # 3. Fetches issue context (AI Triage, CodeRabbit, maintainer comments) | |
| # 4. Generates synthesis comment with @copilot mention | |
| # 5. Creates or updates synthesis comment (idempotent - ONE comment only) | |
| # 6. Assigns copilot-swe-agent to the issue | |
| # 7. Removes the copilot-ready label to indicate completion | |
| # 8. Copilot creates PR with full context | |
| # | |
| # EVENTUAL CONSISTENCY: | |
| # The scheduled sweep job runs hourly to process any issues that might have | |
| # been missed due to workflow failures, race conditions, or API issues. | |
| # This ensures all copilot-ready labeled issues are eventually processed. | |
| # | |
| # IDEMPOTENCY: | |
| # Re-processing an issue updates the existing synthesis comment rather than | |
| # creating duplicates. Detection uses HTML marker: <!-- COPILOT-CONTEXT-SYNTHESIS --> | |
| # | |
| # RELATED: | |
| # - Issue #92: https://github.com/rjmurillo/ai-agents/issues/92 | |
| # - Script: .claude/skills/github/scripts/issue/Invoke-CopilotAssignment.ps1 | |
| # - Config: .claude/skills/github/copilot-synthesis.yml | |
| # | |
| # ============================================================================= | |
| name: Copilot Context Synthesis | |
| on: | |
| issues: | |
| types: [labeled] | |
| # Scheduled sweep for eventual consistency - runs hourly | |
| schedule: | |
| - cron: "0 * * * *" | |
| # Manual trigger for testing or re-running synthesis | |
| workflow_dispatch: | |
| inputs: | |
| issue_number: | |
| description: "Issue number to synthesize context for (leave empty for sweep mode)" | |
| required: false | |
| type: number | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| # =========================================================================== | |
| # Job 1: Process Single Issue (label trigger or manual with issue number) | |
| # =========================================================================== | |
| synthesize-single: | |
| name: Synthesize Context and Assign Copilot | |
| # ADR-025: ARM runner for cost optimization (37.5% savings vs x64) | |
| runs-on: ubuntu-24.04-arm | |
| # Run for: | |
| # - Label trigger with copilot-ready label | |
| # - Manual trigger with issue_number provided | |
| if: | | |
| (github.event_name == 'issues' && github.event.label.name == 'copilot-ready') || | |
| (github.event_name == 'workflow_dispatch' && inputs.issue_number != '') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 | |
| - name: Determine issue number | |
| id: issue | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| echo "number=${{ inputs.issue_number }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Synthesize context and assign Copilot | |
| shell: pwsh -NoProfile -Command "& '{0}'" | |
| env: | |
| ISSUE_NUMBER: ${{ steps.issue.outputs.number }} | |
| run: | | |
| Write-Host "Starting context synthesis for issue #$env:ISSUE_NUMBER" -ForegroundColor Cyan | |
| # Run the synthesis script | |
| $result = & "./.claude/skills/github/scripts/issue/Invoke-CopilotAssignment.ps1" ` | |
| -IssueNumber $env:ISSUE_NUMBER | |
| # Output result summary | |
| if ($result.Success) { | |
| Write-Host "::notice::$($result.Action) synthesis comment: $($result.CommentUrl)" | |
| if ($result.Assigned) { | |
| Write-Host "::notice::Assigned copilot-swe-agent to issue #$env:ISSUE_NUMBER" | |
| } | |
| } else { | |
| Write-Host "::error::Failed to synthesize context for issue #$env:ISSUE_NUMBER" | |
| exit 1 | |
| } | |
| - name: Remove copilot-ready label | |
| if: success() | |
| run: | | |
| echo "Removing copilot-ready label to indicate successful processing..." | |
| gh issue edit ${{ steps.issue.outputs.number }} --remove-label "copilot-ready" | |
| echo "::notice::Removed copilot-ready label from issue #${{ steps.issue.outputs.number }}" | |
| - name: Summary | |
| if: success() | |
| run: | | |
| echo "## Copilot Context Synthesis Complete :robot:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Issue**: #${{ steps.issue.outputs.number }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Actions Taken" >> $GITHUB_STEP_SUMMARY | |
| echo "- Synthesized context from trusted sources" >> $GITHUB_STEP_SUMMARY | |
| echo "- Posted/updated synthesis comment with @copilot mention" >> $GITHUB_STEP_SUMMARY | |
| echo "- Assigned copilot-swe-agent to the issue" >> $GITHUB_STEP_SUMMARY | |
| echo "- Removed copilot-ready label (processing complete)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Copilot will now create a PR based on the synthesized context." >> $GITHUB_STEP_SUMMARY | |
| # =========================================================================== | |
| # Job 2: Sweep for Missed Issues (scheduled or manual without issue number) | |
| # =========================================================================== | |
| sweep-missed: | |
| name: Sweep Missed Issues | |
| # ADR-025: ARM runner for cost optimization (37.5% savings vs x64) | |
| runs-on: ubuntu-24.04-arm | |
| # Run for: | |
| # - Scheduled trigger (cron) | |
| # - Manual trigger WITHOUT issue_number (sweep mode) | |
| if: | | |
| github.event_name == 'schedule' || | |
| (github.event_name == 'workflow_dispatch' && inputs.issue_number == '') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 | |
| - name: Find issues with copilot-ready label | |
| id: find-issues | |
| run: | | |
| echo "Searching for issues with copilot-ready label..." | |
| ISSUES=$(gh issue list --label "copilot-ready" --state open --json number --jq '.[].number' | tr '\n' ' ') | |
| echo "issues=$ISSUES" >> $GITHUB_OUTPUT | |
| if [ -z "$ISSUES" ]; then | |
| echo "No issues found with copilot-ready label" | |
| echo "count=0" >> $GITHUB_OUTPUT | |
| else | |
| COUNT=$(echo $ISSUES | wc -w) | |
| echo "Found $COUNT issue(s) to process: $ISSUES" | |
| echo "count=$COUNT" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Process each issue | |
| if: steps.find-issues.outputs.count != '0' | |
| shell: pwsh -NoProfile -Command "& '{0}'" | |
| env: | |
| ISSUES: ${{ steps.find-issues.outputs.issues }} | |
| run: | | |
| $issues = $env:ISSUES.Trim() -split '\s+' | |
| $results = @() | |
| $failed = @() | |
| Write-Host "Processing $($issues.Count) issue(s)..." -ForegroundColor Cyan | |
| foreach ($issueNumber in $issues) { | |
| if ([string]::IsNullOrWhiteSpace($issueNumber)) { continue } | |
| Write-Host "`n=== Processing Issue #$issueNumber ===" -ForegroundColor Yellow | |
| try { | |
| # Run the synthesis script (same script as single-issue job - DRY!) | |
| $result = & "./.claude/skills/github/scripts/issue/Invoke-CopilotAssignment.ps1" ` | |
| -IssueNumber $issueNumber | |
| if ($result.Success) { | |
| Write-Host "::notice::Issue #$issueNumber - $($result.Action) synthesis comment" | |
| # Remove the label to indicate successful processing | |
| gh issue edit $issueNumber --remove-label "copilot-ready" | |
| Write-Host "::notice::Issue #$issueNumber - Removed copilot-ready label" | |
| $results += [PSCustomObject]@{ | |
| Issue = $issueNumber | |
| Status = "Success" | |
| Action = $result.Action | |
| } | |
| } else { | |
| Write-Host "::warning::Issue #$issueNumber - Synthesis failed" | |
| $failed += $issueNumber | |
| } | |
| } | |
| catch { | |
| Write-Host "::error::Issue #$issueNumber - Error: $_" | |
| $failed += $issueNumber | |
| } | |
| } | |
| # Summary | |
| Write-Host "`n=== Sweep Complete ===" -ForegroundColor Cyan | |
| Write-Host "Processed: $($results.Count) issue(s)" | |
| if ($failed.Count -gt 0) { | |
| Write-Host "Failed: $($failed.Count) issue(s): $($failed -join ', ')" -ForegroundColor Red | |
| # Don't fail the job - we want to process as many as possible | |
| } | |
| - name: Summary | |
| if: always() | |
| run: | | |
| echo "## Copilot Context Synthesis Sweep :broom:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Trigger**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Issues Found**: ${{ steps.find-issues.outputs.count }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.find-issues.outputs.count }}" == "0" ]; then | |
| echo "No issues with \`copilot-ready\` label found. All caught up! :white_check_mark:" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "### Issues Processed" >> $GITHUB_STEP_SUMMARY | |
| echo "Issues: ${{ steps.find-issues.outputs.issues }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Check the job logs for individual issue processing results." >> $GITHUB_STEP_SUMMARY | |
| fi |