feat: make quality gates observable and enforceable

The repository now has a measurable PR quality path instead of a loose set of
manual checks. Coverage, quarantine governance, provider smoke, desktop smoke,
and workflow wiring all produce durable reports that contributors and maintainers
can inspect without reconstructing terminal output.

This also fixes the desktop smoke current-runtime path so browser-driven smoke
runs use the desktop default active provider instead of forcing the official
current model, and records that runtime decision as an artifact.

Constraint: Default PR gates must remain non-live and contributor-safe while live model checks stay explicit.
Constraint: Release packaging is still GitHub Actions based, so release preflight must run before the build matrix.
Rejected: Make live provider or desktop smoke mandatory on every PR | secrets, quotas, and model availability are maintainer-controlled.
Rejected: Let PRs lower coverage baselines in the same change | base-branch ratchet comparison must remain authoritative.
Confidence: high
Scope-risk: moderate
Directive: Do not relax coverage or quarantine policy without a maintainer approval label and a fresh quality report.
Tested: ALLOW_CLI_CORE_CHANGE=1 ALLOW_COVERAGE_BASELINE_CHANGE=1 bun run quality:gate --mode pr
Tested: bun run quality:gate --mode baseline --allow-live --only provider-smoke:* --provider-model nvidia-custom:main:nvidia-custom-main --artifacts-dir /tmp/quality-gate-live-smoke
Tested: bun run quality:gate --mode baseline --allow-live --only desktop-smoke:* --provider-model current:current:current-runtime --artifacts-dir /tmp/quality-gate-desktop-smoke-fixed
Tested: git diff --check
Not-tested: Full live release mode with multiple providers in hosted CI; provider credentials and quota remain maintainer-controlled.
This commit is contained in:
程序员阿江(Relakkes)
2026-05-06 16:25:10 +08:00
parent d1f3b1f91b
commit 9719726cd2
40 changed files with 2157 additions and 87 deletions
+53
View File
@@ -25,9 +25,12 @@ jobs:
adapter_checks: ${{ steps.policy.outputs.adapter_checks }}
desktop_native_checks: ${{ steps.policy.outputs.desktop_native_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
@@ -58,6 +61,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
@@ -179,3 +184,51 @@ jobs:
- name: Run docs checks
run: npm ci && npm run docs:build
coverage-checks:
name: coverage-checks
needs: change-policy
if: needs.change-policy.outputs.coverage_checks == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install root dependencies
run: bun install
- name: Install desktop dependencies
working-directory: desktop
run: bun install
- name: Install adapter dependencies
working-directory: adapters
run: bun install
- 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
+57 -10
View File
@@ -35,6 +35,8 @@ jobs:
'area:cli-core': 'b60205',
'needs-maintainer-approval': 'b60205',
'allow-cli-core-change': 'c2e0c6',
'allow-missing-tests': 'c2e0c6',
'allow-coverage-baseline-change': 'c2e0c6',
}
const files = await github.paginate(github.rest.pulls.listFiles, {
@@ -107,6 +109,32 @@ jobs:
file.startsWith('src/tools/') ||
file.startsWith('src/utils/')
)
const missingTestSignals = []
if (desktopProduct.length > 0 && !hasTest('desktop/src/')) {
missingTestSignals.push('Desktop product files changed without a desktop test file in the PR.')
}
if (serverProduct.length > 0 && !hasTest('src/server/')) {
missingTestSignals.push('Server product files changed without a server test file in the PR.')
}
if (adapterProduct.length > 0 && !hasTest('adapters/')) {
missingTestSignals.push('Adapter product files changed without an adapter test file in the PR.')
}
if (agentRuntimeProduct.length > 0 && !filenames.some((file) =>
(file.startsWith('src/tools/') || file.startsWith('src/utils/')) &&
(/\.test\.[cm]?[jt]sx?$/.test(file) || file.includes('/__tests__/'))
)) {
missingTestSignals.push('Agent/runtime product files changed without a tools/utils test file in the PR.')
}
if (missingTestSignals.length > 0 && !currentLabels.has('allow-missing-tests')) {
areas.add('needs-maintainer-approval')
}
const coveragePolicyFiles = filenames.filter((file) =>
file === 'scripts/quality-gate/coverage-baseline.json' ||
file === 'scripts/quality-gate/coverage-thresholds.json'
)
if (coveragePolicyFiles.length > 0 && !currentLabels.has('allow-coverage-baseline-change')) {
areas.add('needs-maintainer-approval')
}
const requiredChecks = ['change-policy']
if (areas.has('area:desktop') || areas.has('area:server')) requiredChecks.push('desktop-checks')
@@ -121,17 +149,20 @@ jobs:
)
) requiredChecks.push('desktop-native-checks')
if (areas.has('area:docs')) requiredChecks.push('docs-checks')
if (
filenames.some((file) =>
file.startsWith('desktop/src/') ||
file.startsWith('src/server/') ||
file.startsWith('src/tools/') ||
file.startsWith('src/utils/') ||
file.startsWith('adapters/') ||
file.startsWith('scripts/quality-gate/') ||
['package.json', 'desktop/package.json', 'desktop/bun.lock'].includes(file)
)
) requiredChecks.push('coverage-checks')
const testSignals = []
if (desktopProduct.length > 0 && !hasTest('desktop/src/')) {
testSignals.push('Desktop product files changed without a desktop test file in the PR.')
}
if (serverProduct.length > 0 && !hasTest('src/server/')) {
testSignals.push('Server product files changed without a server test file in the PR.')
}
if (adapterProduct.length > 0 && !hasTest('adapters/')) {
testSignals.push('Adapter product files changed without an adapter test file in the PR.')
}
testSignals.push(...missingTestSignals.map((signal) => `BLOCKING unless \`allow-missing-tests\` is applied: ${signal}`))
if (agentRuntimeProduct.length > 0) {
testSignals.push('Agent/model runtime path changed: use mock/request-shape tests in PR and maintainer live-model smoke before release.')
}
@@ -183,7 +214,7 @@ jobs:
}
for (const label of Object.keys(managedLabels)) {
if (label === 'allow-cli-core-change') continue
if (label === 'allow-cli-core-change' || label === 'allow-missing-tests' || label === 'allow-coverage-baseline-change') continue
if (!areas.has(label) && currentLabels.has(label)) {
try {
await github.rest.issues.removeLabel({
@@ -203,9 +234,18 @@ jobs:
const approvalText = cliCoreFiles.length > 0 && !currentLabels.has('allow-cli-core-change')
? 'Blocked by policy until a maintainer applies `allow-cli-core-change` and approves the PR.'
: 'No CLI-core policy block detected.'
const missingTestText = missingTestSignals.length > 0 && !currentLabels.has('allow-missing-tests')
? 'Blocked by policy until a maintainer applies `allow-missing-tests` or matching tests are added.'
: 'No missing-test policy block detected.'
const coveragePolicyText = coveragePolicyFiles.length > 0 && !currentLabels.has('allow-coverage-baseline-change')
? 'Blocked by policy until a maintainer applies `allow-coverage-baseline-change` after reviewing the baseline or threshold change.'
: 'No coverage-baseline policy block detected.'
const cliFilesText = cliCoreFiles.length
? cliCoreFiles.slice(0, 20).map((file) => `- \`${file}\``).join('\n')
: '- none'
const coveragePolicyFilesText = coveragePolicyFiles.length
? coveragePolicyFiles.slice(0, 20).map((file) => `- \`${file}\``).join('\n')
: '- none'
const body = [
marker,
@@ -215,9 +255,16 @@ jobs:
'',
`**CLI core policy:** ${approvalText}`,
'',
`**Missing-test policy:** ${missingTestText}`,
'',
`**Coverage baseline policy:** ${coveragePolicyText}`,
'',
'**CLI core files:**',
cliFilesText,
'',
'**Coverage policy files:**',
coveragePolicyFilesText,
'',
'**Expected checks:**',
requiredChecks.map((check) => `- \`${check}\``).join('\n'),
'',
+66
View File
@@ -19,7 +19,73 @@ concurrency:
cancel-in-progress: true
jobs:
quality-preflight:
name: Quality preflight
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential curl wget file \
libxdo-dev libssl-dev \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev patchelf \
libfuse2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install root dependencies
run: bun install
- name: Install desktop dependencies
working-directory: desktop
run: bun install
- name: Install adapter dependencies
working-directory: adapters
run: bun install
- name: Run release quality preflight
run: bun run quality:gate --mode pr --artifacts-dir artifacts/quality-runs
- name: Summarize quality report
if: always()
shell: bash
run: |
latest_report="$(find artifacts/quality-runs -name report.md -print | sort | tail -n 1)"
if [ -n "$latest_report" ]; then
cat "$latest_report" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload quality report
if: always()
uses: actions/upload-artifact@v4
with:
name: release-quality-gate
path: artifacts/quality-runs/
retention-days: 14
build:
needs: quality-preflight
strategy:
fail-fast: false
matrix: