Epic A: Workflow & CI Governance (Sprint 1) #1616
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
| # owner: @arii | |
| # purpose: Deterministic PR quality gate and conditional AI review | |
| # metrics: | |
| # - median_duration | |
| # - failure_rate | |
| # - ai_invocation_rate | |
| # - deterministic_fail_rate | |
| name: PR Quality | |
| on: | |
| pull_request: | |
| types: [opened, reopened, ready_for_review, synchronize, labeled] | |
| workflow_dispatch: | |
| inputs: | |
| force_ai: | |
| description: 'Force Gemini review' | |
| required: false | |
| default: 'false' | |
| pr_number: | |
| description: 'PR number for dispatch-triggered runs' | |
| required: false | |
| default: '' | |
| head_sha: | |
| description: 'Optional head SHA override for dispatch-triggered runs' | |
| required: false | |
| default: '' | |
| base_sha: | |
| description: 'Optional base SHA override for dispatch-triggered runs' | |
| required: false | |
| default: '' | |
| workflow_call: | |
| inputs: | |
| force_ai: | |
| type: string | |
| default: 'false' | |
| pr_number: | |
| type: string | |
| required: false | |
| default: '' | |
| head_sha: | |
| type: string | |
| required: false | |
| default: '' | |
| base_sha: | |
| type: string | |
| required: false | |
| default: '' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| checks: write | |
| actions: read | |
| concurrency: | |
| group: pr-quality-${{ github.event.pull_request.number || inputs.pr_number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| welcome: | |
| name: 👋 PR Welcome | |
| if: github.event_name == 'pull_request' && github.event.action == 'opened' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Post Welcome Comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const body = '## 👋 Welcome to HRM!\n\n' + | |
| 'Thanks for your contribution. This repository uses Gemini AI for automated triage, code review, and generation.\n\n' + | |
| '#### 🤖 Gemini Manual Trigger Quick Reference\n' + | |
| '| Command | Action |\n' + | |
| '| :--- | :--- |\n' + | |
| '| `@gemini-bot` | Run AI Code Review (PR only) |\n\n' + | |
| 'For more details, see the [Manual Trigger Guide](' + context.payload.repository.html_url + '/blob/leader/docs/workflows/MANUAL_TRIGGERS.md).'; | |
| github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.pull_request.number, | |
| body: body | |
| }); | |
| deterministic: | |
| name: Deterministic checks | |
| runs-on: ubuntu-latest | |
| outputs: | |
| passed: ${{ steps.set_result.outputs.passed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ inputs.head_sha || github.event.pull_request.head.sha || github.sha }} | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'pnpm' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: ESLint | |
| id: eslint | |
| run: pnpm run lint 2>&1 | tee eslint-output.txt | |
| continue-on-error: true | |
| - name: Typecheck | |
| id: tsc | |
| if: steps.eslint.outcome == 'success' | |
| run: pnpm run type-check 2>&1 | tee tsc-output.txt | |
| continue-on-error: true | |
| - name: Knip | |
| id: knip | |
| if: steps.eslint.outcome == 'success' && steps.tsc.outcome == 'success' | |
| run: pnpm run knip 2>&1 | tee knip-output.txt | |
| continue-on-error: true | |
| - name: Set deterministic result | |
| id: set_result | |
| shell: bash | |
| run: | | |
| if [[ "${{ steps.eslint.outcome }}" == "success" && "${{ steps.tsc.outcome }}" == "success" && "${{ steps.knip.outcome }}" == "success" ]]; then | |
| echo "passed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "passed=false" >> "$GITHUB_OUTPUT" | |
| if [[ "${{ steps.eslint.outcome }}" == "failure" ]]; then | |
| echo "FAILED_STEP=ESLint" >> $GITHUB_ENV | |
| echo "FAILED_COMMAND=pnpm run lint" >> $GITHUB_ENV | |
| echo "LOG_FILE=eslint-output.txt" >> $GITHUB_ENV | |
| echo "REMEDIATION=Run \`pnpm lint:fix\` locally to resolve auto-fixable issues." >> $GITHUB_ENV | |
| elif [[ "${{ steps.tsc.outcome }}" == "failure" ]]; then | |
| echo "FAILED_STEP=Typecheck" >> $GITHUB_ENV | |
| echo "FAILED_COMMAND=pnpm run type-check" >> $GITHUB_ENV | |
| echo "LOG_FILE=tsc-output.txt" >> $GITHUB_ENV | |
| echo "REMEDIATION=Fix TypeScript errors in the reported files." >> $GITHUB_ENV | |
| elif [[ "${{ steps.knip.outcome }}" == "failure" ]]; then | |
| echo "FAILED_STEP=Knip" >> $GITHUB_ENV | |
| echo "FAILED_COMMAND=pnpm run knip" >> $GITHUB_ENV | |
| echo "LOG_FILE=knip-output.txt" >> $GITHUB_ENV | |
| echo "REMEDIATION=Remove unused exports, files, or dependencies reported by Knip." >> $GITHUB_ENV | |
| fi | |
| fi | |
| - name: Post diagnostic PR comment | |
| if: steps.set_result.outputs.passed == 'false' && (github.event_name == 'pull_request' || inputs.pr_number != '') | |
| uses: actions/github-script@v7 | |
| env: | |
| FAILED_STEP: ${{ env.FAILED_STEP }} | |
| FAILED_COMMAND: ${{ env.FAILED_COMMAND }} | |
| REMEDIATION: ${{ env.REMEDIATION }} | |
| LOG_FILE: ${{ env.LOG_FILE }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const { FAILED_STEP, FAILED_COMMAND, REMEDIATION, LOG_FILE, PR_NUMBER } = process.env; | |
| let logs = "No logs available."; | |
| if (LOG_FILE && fs.existsSync(LOG_FILE)) { | |
| logs = fs.readFileSync(LOG_FILE, 'utf8').split('\n').slice(0, 20).join('\n'); | |
| } | |
| const body = `### ❌ Deterministic Quality Gate Failed\n\n` + | |
| `- **Failed Step:** ${FAILED_STEP}\n` + | |
| `- **Command:** \`${FAILED_COMMAND}\`\n\n` + | |
| `#### 📝 Top 20 Errors (truncated)\n\`\`\`text\n${logs}\n\`\`\`\n\n` + | |
| `#### 💡 Remediation\n${REMEDIATION}\n\n` + | |
| `> [!IMPORTANT]\n` + | |
| `> Gemini AI review is blocked until deterministic checks pass.`; | |
| github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: parseInt(PR_NUMBER), | |
| body | |
| }); | |
| - name: Fail the job if checks failed | |
| if: steps.set_result.outputs.passed == 'false' | |
| run: exit 1 | |
| ai_gate_decision: | |
| name: AI gate decision | |
| runs-on: ubuntu-latest | |
| needs: deterministic | |
| if: always() | |
| outputs: | |
| run_ai: ${{ steps.decide.outputs.run_ai }} | |
| reason: ${{ steps.decide.outputs.reason }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ inputs.head_sha || github.event.pull_request.head.sha || github.sha }} | |
| - name: Gather changed files | |
| id: changed | |
| uses: tj-actions/changed-files@v45 | |
| with: | |
| base_sha: ${{ inputs.base_sha || github.event.pull_request.base.sha }} | |
| files_yaml: | | |
| risk: | |
| - server.ts | |
| - middleware.ts | |
| - context/WebSocketContext.tsx | |
| - context/webSocketReducer.ts | |
| - hooks/useBluetoothHRM.ts | |
| - .github/workflows/** | |
| - package.json | |
| - pnpm-lock.yaml | |
| docs_only: | |
| - "**/*.md" | |
| - "**/*.mdx" | |
| - "docs/**" | |
| - name: Decide whether to run AI | |
| id: decide | |
| env: | |
| DETERMINISTIC_PASSED: ${{ needs.deterministic.outputs.passed }} | |
| FORCE_AI: ${{ inputs.force_ai || 'false' }} | |
| PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name || '[]') }} | |
| ANY_CHANGED: ${{ steps.changed.outputs.any_changed }} | |
| RISK_CHANGED: ${{ steps.changed.outputs.risk_any_changed }} | |
| DOCS_ONLY: ${{ steps.changed.outputs.only_changed == 'true' && steps.changed.outputs.docs_only_any_changed == 'true' }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_ACTION: ${{ github.event.action }} | |
| IS_DRAFT: ${{ github.event.pull_request.draft || 'false' }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| run_ai=false | |
| reason="" | |
| # Hard stop: deterministic fail | |
| if [[ "$DETERMINISTIC_PASSED" != "true" ]]; then | |
| echo "run_ai=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=deterministic_failed" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Force override | |
| if [[ "$FORCE_AI" == "true" ]]; then | |
| echo "run_ai=true" >> "$GITHUB_OUTPUT" | |
| echo "reason=manual_force" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Label-driven allowlist (High Priority) | |
| if echo "$PR_LABELS" | grep -Eiq '"ai:required"|"risk:high"|"security:review"'; then | |
| echo "run_ai=true" >> "$GITHUB_OUTPUT" | |
| echo "reason=label_trigger" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Risk file trigger (High Priority) | |
| if [[ "$RISK_CHANGED" == "true" ]]; then | |
| echo "run_ai=true" >> "$GITHUB_OUTPUT" | |
| echo "reason=risk_file_changed" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Skip draft updates | |
| if [[ "$IS_DRAFT" == "true" ]]; then | |
| echo "run_ai=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=draft_skip" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Docs-only skip | |
| if [[ "$DOCS_ONLY" == "true" ]]; then | |
| echo "run_ai=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=docs_only" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Reduce synchronize events | |
| if [[ "$EVENT_ACTION" == "synchronize" ]]; then | |
| echo "run_ai=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=sync_skip" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Check AI invocation count (count actual reviews, not gate decisions) | |
| if [[ -n "$PR_NUMBER" ]]; then | |
| INVOCATION_COUNT=$(gh pr view "$PR_NUMBER" --json comments --jq '[.comments | .[] | select(.body | contains("Reviewed commit:"))] | length' 2>/dev/null || echo 0) | |
| MAX_INVOCATIONS=1 | |
| if echo "$PR_LABELS" | grep -iq "ai:required"; then | |
| MAX_INVOCATIONS=2 | |
| fi | |
| if [[ "$INVOCATION_COUNT" -ge "$MAX_INVOCATIONS" ]]; then | |
| echo "run_ai=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=max_invocations_reached" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| # Default: skip if no risk or label trigger hit | |
| echo "run_ai=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=low_risk_skip" >> "$GITHUB_OUTPUT" | |
| - name: Post gate summary | |
| if: (github.event_name == 'pull_request' || inputs.pr_number != '') && (steps.decide.outputs.run_ai == 'true' || github.event.action == 'opened') | |
| uses: actions/github-script@v7 | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} | |
| with: | |
| script: | | |
| const runAi = "${{ steps.decide.outputs.run_ai }}"; | |
| const reason = "${{ steps.decide.outputs.reason }}"; | |
| const prNumber = process.env.PR_NUMBER; | |
| if (!prNumber) return; | |
| const body = `### AI Gate Decision\n- run_ai: **${runAi}**\n- reason: \`${reason}\`\n\n#### Telemetry\n- deterministic_fail: **${{ needs.deterministic.outputs.passed != 'true' }}**\n- ai_skipped: **${runAi != 'true'}**\n- ai_invoked: **${runAi == 'true'}**\n- tokens_used: \`N/A\``; | |
| github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: parseInt(prNumber), | |
| body | |
| }); | |
| gemini_review: | |
| name: Gemini review | |
| needs: [deterministic, ai_gate_decision] | |
| if: ${{ needs.ai_gate_decision.outputs.run_ai == 'true' }} | |
| uses: ./.github/workflows/reusable-gemini-review.yml | |
| with: | |
| pr_quality_result: ${{ needs.deterministic.outputs.passed == 'true' && 'success' || 'failure' }} | |
| trigger_event: ${{ github.event_name }} | |
| pr_number: ${{ github.event.pull_request.number || inputs.pr_number }} | |
| base_sha: ${{ github.event.pull_request.base.sha || inputs.base_sha }} | |
| head_sha: ${{ github.event.pull_request.head.sha || inputs.head_sha }} | |
| last_non_empty_commit: ${{ github.event.pull_request.head.sha || inputs.head_sha }} | |
| secrets: inherit | |
| create_review_issues: | |
| name: Create review issues | |
| needs: [gemini_review] | |
| if: | | |
| always() && | |
| needs.gemini_review.result == 'success' && | |
| needs.gemini_review.outputs.review_performed == 'true' | |
| uses: ./.github/workflows/reusable-create-review-issues.yml | |
| with: | |
| pr_number: ${{ github.event.pull_request.number || inputs.pr_number }} | |
| base_sha: ${{ github.event.pull_request.base.sha || inputs.base_sha }} | |
| run_id: ${{ github.run_id }} | |
| secrets: | |
| gh_token: ${{ secrets.GITHUB_TOKEN }} |