feat: Add lifecycle hook infrastructure for automated quality gates #17886
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: "Velocity Accelerator" | |
| # Detect development acceleration opportunities from repository events | |
| # ADR-006: Thin workflow, business logic in scripts/velocity_accelerator.py | |
| on: | |
| pull_request: | |
| types: [closed] | |
| issues: | |
| types: [opened, labeled] | |
| push: | |
| paths: | |
| - '.agents/**' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: velocity-accelerator-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number || github.sha }} | |
| cancel-in-progress: false | |
| env: | |
| GH_TOKEN: ${{ secrets.BOT_PAT }} | |
| jobs: | |
| detect-opportunities: | |
| name: Detect Velocity Opportunities | |
| # ADR-025: ARM runner for cost optimization | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 5 | |
| # Skip bot-generated events | |
| if: >- | |
| github.actor != 'dependabot[bot]' && | |
| github.actor != 'github-actions[bot]' | |
| outputs: | |
| opportunities: ${{ steps.detect.outputs.opportunities }} | |
| count: ${{ steps.detect.outputs.count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: '3.14' | |
| - name: Detect opportunities | |
| id: detect | |
| shell: pwsh -NoProfile -Command "& '{0}'" | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_ACTION: ${{ github.event.action }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_MERGED: ${{ github.event.pull_request.merged }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_TITLE: ${{ github.event.issue.title }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| AFTER_SHA: ${{ github.sha }} | |
| run: | | |
| $args = @('scripts/velocity_accelerator.py', '--event', $env:EVENT_NAME, '--output-format', 'json') | |
| if ($env:EVENT_ACTION) { $args += @('--action', $env:EVENT_ACTION) } | |
| if ($env:PR_NUMBER -and $env:PR_NUMBER -ne '') { $args += @('--pr-number', $env:PR_NUMBER) } | |
| if ($env:PR_MERGED -eq 'true') { $args += '--pr-merged' } | |
| if ($env:ISSUE_NUMBER -and $env:ISSUE_NUMBER -ne '') { $args += @('--issue-number', $env:ISSUE_NUMBER) } | |
| if ($env:ISSUE_TITLE) { $args += @('--issue-title', $env:ISSUE_TITLE) } | |
| if ($env:ISSUE_BODY) { $args += @('--issue-body', $env:ISSUE_BODY) } | |
| $env:GITHUB_EVENT_BEFORE = $env:BEFORE_SHA | |
| $env:GITHUB_SHA = $env:AFTER_SHA | |
| $output = python @args 2>&1 | |
| $exitCode = $LASTEXITCODE | |
| if ($exitCode -eq 2) { | |
| Write-Error "Configuration error running velocity accelerator" | |
| exit 1 | |
| } | |
| try { | |
| $opportunities = $output | ConvertFrom-Json | |
| $count = ($opportunities | Measure-Object).Count | |
| } catch { | |
| $opportunities = @() | |
| $count = 0 | |
| } | |
| $json = $opportunities | ConvertTo-Json -Compress -Depth 10 | |
| if (-not $json -or $json -eq 'null') { $json = '[]' } | |
| "opportunities=$json" >> $env:GITHUB_OUTPUT | |
| "count=$count" >> $env:GITHUB_OUTPUT | |
| Write-Host "Detected $count velocity opportunities" | |
| post-summary: | |
| name: Post Velocity Summary | |
| needs: detect-opportunities | |
| if: needs.detect-opportunities.outputs.count > 0 | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: '3.14' | |
| - name: Generate and post summary comment | |
| if: github.event_name == 'pull_request' || github.event_name == 'issues' | |
| shell: pwsh -NoProfile -Command "& '{0}'" | |
| env: | |
| OPPORTUNITIES_JSON: ${{ needs.detect-opportunities.outputs.opportunities }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| run: | | |
| # ADR-006: Format summary in Python script, post via gh CLI | |
| # All user-controlled inputs are in env vars, not interpolated in shell | |
| $tempFile = [System.IO.Path]::GetTempFileName() | |
| $env:OPPORTUNITIES_JSON | Set-Content -Path $tempFile -Encoding utf8 | |
| # Generate markdown summary from JSON using Python (keeps logic out of YAML) | |
| $summary = python -c @" | |
| import json, sys | |
| data = json.load(open(sys.argv[1])) | |
| if not data: | |
| sys.exit(0) | |
| lines = ['## Velocity Accelerator: {} Opportunities Detected\n'.format(len(data))] | |
| for opp in data: | |
| lines.append('### {}'.format(opp['title'])) | |
| lines.append('- **Type**: `{}`'.format(opp['opportunity_type'])) | |
| lines.append('- **Priority**: {}'.format(opp['priority'])) | |
| if opp.get('suggested_agent'): | |
| lines.append('- **Suggested Agent**: {}'.format(opp['suggested_agent'])) | |
| lines.append('- {}\n'.format(opp['description'])) | |
| lines.append('<!-- VELOCITY-ACCELERATOR -->') | |
| print('\n'.join(lines)) | |
| "@ $tempFile | |
| Remove-Item $tempFile -ErrorAction SilentlyContinue | |
| if (-not $summary) { | |
| Write-Host "No summary to post" | |
| exit 0 | |
| } | |
| if ($env:EVENT_NAME -eq 'pull_request') { | |
| $number = $env:PR_NUMBER | |
| } else { | |
| $number = $env:ISSUE_NUMBER | |
| } | |
| $marker = '<!-- VELOCITY-ACCELERATOR -->' | |
| # Find existing comment with marker using pagination to avoid duplicates | |
| $existingId = $null | |
| $page = 1 | |
| $repo = $env:GITHUB_REPOSITORY | |
| do { | |
| $response = gh api "repos/${repo}/issues/${number}/comments?per_page=100&page=${page}" 2>$null | |
| if (-not $response) { break } | |
| $comments = $response | ConvertFrom-Json | |
| if ($comments.Count -eq 0) { break } | |
| foreach ($comment in $comments) { | |
| if ($comment.body -and $comment.body.Contains($marker)) { | |
| $existingId = $comment.id | |
| break | |
| } | |
| } | |
| $page++ | |
| } while ($comments.Count -eq 100 -and -not $existingId) | |
| # Write summary to temp file for gh to read (avoids shell escaping issues) | |
| $bodyFile = [System.IO.Path]::GetTempFileName() | |
| $summary | Set-Content -Path $bodyFile -Encoding utf8 | |
| $body = Get-Content -Path $bodyFile -Raw | |
| if ($existingId) { | |
| gh api "repos/${repo}/issues/comments/${existingId}" -X PATCH -f body="$body" | |
| Write-Host "Updated existing velocity comment (id: $existingId)" | |
| } else { | |
| gh api "repos/${repo}/issues/${number}/comments" -f body="$body" | |
| Write-Host "Created new velocity comment on #${number}" | |
| } | |
| Remove-Item $bodyFile -ErrorAction SilentlyContinue | |
| - name: Log summary to workflow | |
| env: | |
| OPPORTUNITIES_COUNT: ${{ needs.detect-opportunities.outputs.count }} | |
| run: | | |
| echo "::notice::Velocity Accelerator detected $OPPORTUNITIES_COUNT opportunities" |