mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
20b6e6dc9e
4f9fec876 added this workflow with `cron: '0 18 * * *'`. That was the wrong call
to make unilaterally: the repository had no scheduled workflow at all before it,
so this was not one more cron among several but the introduction of recurring CI
spend — about ninety minutes per run — on a schedule nobody asked for.
The reasoning for the sweep still holds: a per-PR gate only covers what the diff
reaches, so it is blind to checks no recent PR selected and to failures that only
appear when the whole suite runs together. Keeping the workflow on
`workflow_dispatch` keeps that one click away without deciding for the maintainer
when to spend the time.
pr-quality-workflow.test.ts now asserts the absence of `schedule:` and `cron:`
rather than their presence, so a schedule cannot drift back in unnoticed —
verified by adding the cron back and watching the test go red. The docs' four-tier
table renames the tier accordingly; calling it "Nightly" when nothing runs nightly
is exactly the kind of comment that outlives its code.
195 lines
8.8 KiB
TypeScript
195 lines
8.8 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import { readFileSync } from 'node:fs'
|
|
import { parse } from 'yaml'
|
|
|
|
type WorkflowJob = {
|
|
needs?: string | string[]
|
|
steps?: Array<{ name?: string; run?: string }>
|
|
}
|
|
|
|
function workflowJobs(workflow: string) {
|
|
return (parse(workflow) as { jobs: Record<string, WorkflowJob> }).jobs
|
|
}
|
|
|
|
describe('PR quality workflow', () => {
|
|
test('uses packageManager as the single pinned Bun version source', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as {
|
|
packageManager?: string
|
|
}
|
|
const setupBunStepCount = workflow.match(/uses: oven-sh\/setup-bun@v2/g)?.length ?? 0
|
|
const versionFileCount = workflow.match(/bun-version-file: package\.json/g)?.length ?? 0
|
|
|
|
expect(packageJson.packageManager).toBe('bun@1.3.14')
|
|
expect(setupBunStepCount).toBeGreaterThan(0)
|
|
expect(versionFileCount).toBe(setupBunStepCount)
|
|
expect(workflow).not.toContain('bun-version:')
|
|
expect(workflow).not.toContain('1.3.12')
|
|
expect(workflow).not.toContain('bun-version: latest')
|
|
})
|
|
|
|
test('builds scope before routing independent quality jobs', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
|
|
expect(workflow).toContain('scope-plan:')
|
|
expect(workflow).toContain('--plan-only')
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.desktop_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.server_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.provider_contract_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.chat_contract_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.persistence_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.adapter_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.desktop_native_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.docs_checks == 'true'")
|
|
expect(workflow).toContain("if: needs.scope-plan.outputs.coverage_checks == 'true'")
|
|
})
|
|
|
|
test('installs frozen dependencies before policy regressions without blocking product routing', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
const jobs = workflowJobs(workflow)
|
|
const policySteps = jobs['policy-enforcement'].steps ?? []
|
|
const installIndex = policySteps.findIndex((step) => step.name === 'Install root dependencies')
|
|
const regressionIndex = policySteps.findIndex((step) => step.name === 'Run policy regression tests')
|
|
|
|
expect(jobs['policy-enforcement'].needs).toBe('scope-plan')
|
|
expect(installIndex).toBeGreaterThanOrEqual(0)
|
|
expect(installIndex).toBeLessThan(regressionIndex)
|
|
for (const jobId of [
|
|
'desktop-checks',
|
|
'server-checks',
|
|
'provider-contract-checks',
|
|
'chat-contract-checks',
|
|
'agent-flow-checks',
|
|
'adapter-checks',
|
|
'desktop-native-checks',
|
|
'persistence-checks',
|
|
'docs-checks',
|
|
'coverage-checks',
|
|
]) {
|
|
expect(jobs[jobId].needs).toBe('scope-plan')
|
|
}
|
|
})
|
|
|
|
test('keeps coverage artifacts observable in CI', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
|
|
expect(workflow).toContain('COVERAGE_BASE_REF: origin/${{ github.base_ref }}')
|
|
expect(workflow).toContain('cat "$latest_report" >> "$GITHUB_STEP_SUMMARY"')
|
|
expect(workflow).toContain('uses: actions/upload-artifact@v4')
|
|
expect(workflow).toContain('path: artifacts/coverage/')
|
|
expect(workflow).toContain('retention-days: 14')
|
|
})
|
|
|
|
test('installs and validates the isolated React site for docs changes', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
const jobs = workflowJobs(workflow)
|
|
const docsSteps = jobs['docs-checks'].steps ?? []
|
|
const runDocs = docsSteps.find((step) => step.name === 'Run docs checks')
|
|
|
|
expect(workflow).toContain('cache-dependency-path: site/package-lock.json')
|
|
expect(runDocs?.run).toBe(
|
|
'npm --prefix site ci && npm --prefix site run build && npm --prefix site run check',
|
|
)
|
|
expect(workflow).not.toContain('vitepress')
|
|
})
|
|
|
|
test('keeps required PR checks deterministic and secret-free', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
|
|
expect(workflow).not.toContain('--allow-live')
|
|
expect(workflow).not.toContain('QUALITY_GATE_PROVIDER_API_KEY')
|
|
expect(workflow).not.toContain('secrets.')
|
|
expect(workflow).not.toContain('pull_request_target')
|
|
expect(workflow.match(/uses: actions\/checkout@v4/g)?.length).toBeGreaterThan(0)
|
|
expect(workflow.match(/persist-credentials: false/g)?.length).toBe(
|
|
workflow.match(/uses: actions\/checkout@v4/g)?.length,
|
|
)
|
|
})
|
|
|
|
test('exposes a single required gate job for branch protection', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
|
|
expect(workflow).toContain('pr-quality-gate:')
|
|
expect(workflow).toContain('name: pr-quality-gate')
|
|
expect(workflow).toContain('if: always()')
|
|
expect(workflow).toContain('require_success "scope-plan" "${{ needs.scope-plan.result }}"')
|
|
expect(workflow).toContain('require_success "policy-enforcement" "${{ needs.policy-enforcement.result }}"')
|
|
expect(workflow).toContain('require_selected "provider-contract-checks"')
|
|
expect(workflow).toContain('require_selected "chat-contract-checks"')
|
|
expect(workflow).toContain('require_selected "agent-flow-checks"')
|
|
expect(workflow).toContain('require_selected "coverage-checks"')
|
|
})
|
|
|
|
test('routes the deterministic agent flow through the scope plan and keeps its evidence', () => {
|
|
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
|
|
const jobs = workflowJobs(workflow)
|
|
const steps = jobs['agent-flow-checks'].steps ?? []
|
|
|
|
expect(workflow).toContain('agent_flow_checks: ${{ steps.policy.outputs.agent_flow_checks }}')
|
|
expect(steps.some((step) => step.run === 'bun run check:agent-flow')).toBe(true)
|
|
expect(workflow).toContain('path: artifacts/agent-flow/')
|
|
// The lane must stay runnable on an untrusted fork: no desktop install, no
|
|
// secrets, no live provider.
|
|
expect(steps.some((step) => String(step.run ?? '').includes('secrets'))).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('full quality workflow', () => {
|
|
test('runs every deterministic lane on demand without secrets or live providers', () => {
|
|
const workflow = readFileSync('.github/workflows/nightly-quality.yml', 'utf8')
|
|
const jobs = workflowJobs(workflow)
|
|
const runs = (jobs['full-deterministic'].steps ?? []).map((step) => step.run ?? '')
|
|
|
|
expect(workflow).toContain('workflow_dispatch:')
|
|
// Manual only, and it stays that way. This sweep costs about ninety minutes of
|
|
// CI; when to spend that is the maintainer's decision, so a schedule must not
|
|
// reappear here without one.
|
|
expect(workflow).not.toContain('schedule:')
|
|
expect(workflow).not.toContain('cron:')
|
|
for (const command of [
|
|
'bun run check:policy',
|
|
'bun run check:agent-flow',
|
|
'bun run check:server',
|
|
'bun run check:provider-contract',
|
|
'bun run check:chat-contract',
|
|
'bun run check:adapters',
|
|
'bun run check:desktop',
|
|
'bun run check:electron',
|
|
'bun run check:persistence-upgrade',
|
|
'bun run check:quarantine',
|
|
'bun run check:coverage',
|
|
]) {
|
|
expect(runs, `full sweep must run ${command}`).toContain(command)
|
|
}
|
|
|
|
expect(workflow).not.toContain('--allow-live')
|
|
expect(workflow).not.toContain('secrets.')
|
|
expect(workflow.match(/persist-credentials: false/g)?.length).toBe(
|
|
workflow.match(/uses: actions\/checkout@v4/g)?.length,
|
|
)
|
|
})
|
|
|
|
test('re-proves the dependency graph so PR selection cannot silently degrade', () => {
|
|
const workflow = readFileSync('.github/workflows/nightly-quality.yml', 'utf8')
|
|
const jobs = workflowJobs(workflow)
|
|
const runs = (jobs['selection-drift'].steps ?? []).map((step) => step.run ?? '')
|
|
|
|
expect(runs.some((run) => run.includes('module-graph.test.ts'))).toBe(true)
|
|
expect(runs.some((run) => run.includes('check:impact'))).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('docs deployment workflow', () => {
|
|
test('builds and uploads the isolated React site', () => {
|
|
const workflow = readFileSync('.github/workflows/deploy-docs.yml', 'utf8')
|
|
|
|
expect(workflow).toContain(" - 'site/**'")
|
|
expect(workflow).toContain('cache-dependency-path: site/package-lock.json')
|
|
expect(workflow).toContain('run: npm --prefix site ci')
|
|
expect(workflow).toContain('run: npm --prefix site run build')
|
|
expect(workflow).toContain('path: site/dist')
|
|
expect(workflow).not.toContain('vitepress')
|
|
expect(workflow).not.toContain('docs/.vitepress/dist')
|
|
})
|
|
})
|