Skip to content

Epic A: Workflow & CI Governance (Sprint 1) #1621

Epic A: Workflow & CI Governance (Sprint 1)

Epic A: Workflow & CI Governance (Sprint 1) #1621

Workflow file for this run

# 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 }}