cc-haha/.github/workflows/pr-quality.yml
程序员阿江(Relakkes) 435e4ccc9f ci: harden deterministic PR quality gates
Route required checks by changed surface, add offline provider and chat contracts, and keep fork PRs independent of live credentials. Layer agent guidance by subtree and enforce a compact instruction budget.

Tested: bun run check:policy
Confidence: high
Scope-risk: broad
2026-07-10 20:29:01 +08:00

332 lines
11 KiB
YAML

name: PR Quality
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
permissions:
contents: read
pull-requests: read
concurrency:
group: pr-quality-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
scope-plan:
name: scope-plan
runs-on: ubuntu-latest
outputs:
areas: ${{ steps.policy.outputs.areas }}
area_labels: ${{ steps.policy.outputs.area_labels }}
blocked: ${{ steps.policy.outputs.blocked }}
blocking_reasons: ${{ steps.policy.outputs.blocking_reasons }}
desktop_checks: ${{ steps.policy.outputs.desktop_checks }}
server_checks: ${{ steps.policy.outputs.server_checks }}
adapter_checks: ${{ steps.policy.outputs.adapter_checks }}
desktop_native_checks: ${{ steps.policy.outputs.desktop_native_checks }}
provider_contract_checks: ${{ steps.policy.outputs.provider_contract_checks }}
chat_contract_checks: ${{ steps.policy.outputs.chat_contract_checks }}
persistence_checks: ${{ steps.policy.outputs.persistence_checks }}
policy_checks: ${{ steps.policy.outputs.policy_checks }}
docs_checks: ${{ steps.policy.outputs.docs_checks }}
coverage_checks: ${{ steps.policy.outputs.coverage_checks }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Collect changed files
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPOSITORY: ${{ github.repository }}
run: |
gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename' > changed-files.txt
jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH" > labels.txt
- name: Build deterministic scope plan
id: policy
run: bun run scripts/pr/change-policy.ts --files changed-files.txt --labels-file labels.txt --plan-only
policy-enforcement:
name: policy-enforcement
needs: scope-plan
if: ${{ always() && needs.scope-plan.result == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout policy sources
if: needs.scope-plan.outputs.policy_checks == 'true'
uses: actions/checkout@v4
- name: Setup Bun
if: needs.scope-plan.outputs.policy_checks == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
if: needs.scope-plan.outputs.policy_checks == 'true'
run: bun install --frozen-lockfile
- name: Run policy regression tests
if: needs.scope-plan.outputs.policy_checks == 'true'
run: bun run check:policy
- name: Enforce change policy
env:
BLOCKED: ${{ needs.scope-plan.outputs.blocked }}
BLOCKING_REASONS: ${{ needs.scope-plan.outputs.blocking_reasons }}
run: |
if [ "$BLOCKED" = "true" ]; then
echo "::error::${BLOCKING_REASONS:-Change policy blocked this pull request}"
exit 1
fi
echo "Change policy passed"
desktop-checks:
name: desktop-checks
needs: scope-plan
if: needs.scope-plan.outputs.desktop_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Run desktop checks
run: bun run check:desktop
server-checks:
name: server-checks
needs: scope-plan
if: needs.scope-plan.outputs.server_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Run root runtime checks
run: bun run check:server
provider-contract-checks:
name: provider-contract-checks
needs: scope-plan
if: needs.scope-plan.outputs.provider_contract_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Run offline provider contracts
run: bun run check:provider-contract
chat-contract-checks:
name: chat-contract-checks
needs: scope-plan
if: needs.scope-plan.outputs.chat_contract_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Run desktop-server chat contracts
run: bun run check:chat-contract
adapter-checks:
name: adapter-checks
needs: scope-plan
if: needs.scope-plan.outputs.adapter_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install adapter dependencies
working-directory: adapters
run: bun install --frozen-lockfile
- name: Run adapter checks
run: bun run check:adapters
desktop-native-checks:
name: desktop-native-checks
needs: scope-plan
if: needs.scope-plan.outputs.desktop_native_checks == 'true'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential curl wget file \
libxdo-dev libssl-dev patchelf \
libfuse2
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Run desktop native checks
run: bun run check:native
persistence-checks:
name: persistence-checks
needs: scope-plan
if: needs.scope-plan.outputs.persistence_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Run persistence upgrade contracts
run: bun run check:persistence-upgrade
docs-checks:
name: docs-checks
needs: scope-plan
if: needs.scope-plan.outputs.docs_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Run docs checks
run: npm ci && npm run docs:build
coverage-checks:
name: coverage-checks
needs: scope-plan
if: needs.scope-plan.outputs.coverage_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Install adapter dependencies
working-directory: adapters
run: bun install --frozen-lockfile
- name: Run coverage checks
env:
COVERAGE_BASE_REF: origin/${{ github.base_ref }}
run: bun run check:coverage
- name: Summarize coverage report
if: always()
run: |
latest_report="$(find artifacts/coverage -name coverage-report.md -print | sort | tail -n 1)"
if [ -n "$latest_report" ]; then
cat "$latest_report" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: artifacts/coverage/
retention-days: 14
pr-quality-gate:
name: pr-quality-gate
needs:
- scope-plan
- policy-enforcement
- desktop-checks
- server-checks
- provider-contract-checks
- chat-contract-checks
- adapter-checks
- desktop-native-checks
- persistence-checks
- docs-checks
- coverage-checks
if: always()
runs-on: ubuntu-latest
steps:
- name: Require every selected quality job to pass
run: |
failures=0
require_success() {
local name="$1"
local result="$2"
if [ "$result" != "success" ]; then
echo "::error::$name ended with result: $result"
failures=$((failures + 1))
else
echo "$name: success"
fi
}
require_selected() {
local name="$1"
local expected="$2"
local result="$3"
if [ "$expected" = "true" ]; then
require_success "$name" "$result"
elif [ "$result" != "skipped" ]; then
echo "::error::$name was not selected but ended with result: $result"
failures=$((failures + 1))
else
echo "$name: skipped by scope plan"
fi
}
require_success "scope-plan" "${{ needs.scope-plan.result }}"
require_success "policy-enforcement" "${{ needs.policy-enforcement.result }}"
require_selected "desktop-checks" "${{ needs.scope-plan.outputs.desktop_checks }}" "${{ needs.desktop-checks.result }}"
require_selected "server-checks" "${{ needs.scope-plan.outputs.server_checks }}" "${{ needs.server-checks.result }}"
require_selected "provider-contract-checks" "${{ needs.scope-plan.outputs.provider_contract_checks }}" "${{ needs.provider-contract-checks.result }}"
require_selected "chat-contract-checks" "${{ needs.scope-plan.outputs.chat_contract_checks }}" "${{ needs.chat-contract-checks.result }}"
require_selected "adapter-checks" "${{ needs.scope-plan.outputs.adapter_checks }}" "${{ needs.adapter-checks.result }}"
require_selected "desktop-native-checks" "${{ needs.scope-plan.outputs.desktop_native_checks }}" "${{ needs.desktop-native-checks.result }}"
require_selected "persistence-checks" "${{ needs.scope-plan.outputs.persistence_checks }}" "${{ needs.persistence-checks.result }}"
require_selected "docs-checks" "${{ needs.scope-plan.outputs.docs_checks }}" "${{ needs.docs-checks.result }}"
require_selected "coverage-checks" "${{ needs.scope-plan.outputs.coverage_checks }}" "${{ needs.coverage-checks.result }}"
if [ "$failures" -gt 0 ]; then
exit 1
fi