* ci: build and check packages in parallel The Vue SDK's declarations used the tsc resolver, which took 21 of the 32 seconds a local package build takes; tsdown's default oxc resolver writes byte-identical output in 4 seconds. Packages now build level by level, each level's packages together, with their output printed whole. Package checks run npm and Bun packing side by side and ATTW on every core instead of two. * ci: skip the merge queue's suites for a tree its PR already passed The merge queue reran every check even when master had not moved, so the queued commit had exactly the tree the pull request's CI had just passed. A passing PR run now records that tree as a commit status on the PR head, and the queue's classification compares its own tree with it: a match runs only the always-on checks, anything else the full suites. Fork PRs cannot write the status and keep the full run. * ci: accept a verified tree only from its pull request's passing CI run Any writer can post a commit status, and another pull request's CI could post one on this head, so a status alone could skip the queue's suites. The record now links the run that wrote it, and the queue accepts it only when GitHub shows Actions created it and the run is this repository's CI workflow on pull_request, passed, and ran on this exact head. Recording no longer fails the gate when the status cannot be written. Parallel packs and builds now all settle before a failure is reported, so none writes into a directory that is being removed or rebuilt. * refactor(ci): group the verified-tree lookup and recorder in one folder
130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
import { expect, test } from 'bun:test'
|
|
|
|
import {
|
|
classifyPaths,
|
|
ALWAYS_JOBS,
|
|
CODE_JOBS,
|
|
DOCS_JOB,
|
|
gateErrors,
|
|
type ChangeScope,
|
|
type JobStatus
|
|
} from '#ci/policy'
|
|
|
|
const documentation = [
|
|
'README.md',
|
|
'AGENTS.md',
|
|
'CHANGELOG.md',
|
|
'packages/vue/README.md',
|
|
'packages/core/README.md',
|
|
'packages/core/AGENTS.md',
|
|
'src/AGENTS.md',
|
|
'tests/AGENTS.md',
|
|
'tools/AGENTS.md',
|
|
'packages/docs/programmable/sdk/api/components/bindable-value.md',
|
|
'packages/docs/public/logo.svg',
|
|
'skills/open-pencil/SKILL.md',
|
|
'skills/open-pencil/references/design-authoring.md',
|
|
'openspec/proposal.md'
|
|
]
|
|
const code = [
|
|
'src/app/ai/chat/system-prompt.md',
|
|
'packages/design-jsx/src/reference/authoring.md',
|
|
'packages/core/src/README.md',
|
|
'packages/core/README.md.ts',
|
|
'packages/core/instructions.md',
|
|
'src/app/agents.md',
|
|
'packages/core/src/AGENTS.md.ts',
|
|
'packages/docs/.vitepress/config.ts',
|
|
'packages/docs/demo.vue',
|
|
'skills/open-pencil/scripts/create.ts',
|
|
'package.json',
|
|
'bun.lock',
|
|
'.github/workflows/ci.yml',
|
|
'new-domain/instructions.md',
|
|
'src/editor.ts'
|
|
]
|
|
|
|
test.each(documentation)('docs-only path: %s', (path) => {
|
|
expect(classifyPaths([path])).toBe('docs')
|
|
})
|
|
test.each(code)('code or unknown path: %s', (path) => {
|
|
expect(classifyPaths([path])).toBe('code')
|
|
expect(classifyPaths([...documentation, path])).toBe('code')
|
|
})
|
|
test('package READMEs and public guides share the docs-only route', () => {
|
|
expect(
|
|
classifyPaths(['packages/vue/README.md', 'packages/docs/programmable/sdk/getting-started.md'])
|
|
).toBe('docs')
|
|
})
|
|
test('empty diffs fail safe to code checks', () => {
|
|
expect(classifyPaths([])).toBe('code')
|
|
})
|
|
test('both sides of a rename affect classification', () => {
|
|
expect(classifyPaths(['src/prompt.md', 'packages/docs/prompt.md'])).toBe('code')
|
|
expect(classifyPaths(['packages/docs/old.md', 'packages/docs/new.md'])).toBe('docs')
|
|
})
|
|
|
|
function results(scope: ChangeScope): Record<string, JobStatus> {
|
|
return {
|
|
changes: { result: 'success', outputs: { scope } },
|
|
...Object.fromEntries(ALWAYS_JOBS.map((job) => [job, { result: 'success' }])),
|
|
[DOCS_JOB]: { result: scope === 'docs' ? 'success' : 'skipped' },
|
|
...Object.fromEntries(
|
|
CODE_JOBS.map((job) => [job, { result: scope === 'code' ? 'success' : 'skipped' }])
|
|
)
|
|
}
|
|
}
|
|
|
|
test.each(['docs', 'code', 'verified'] as const)(
|
|
'accepts only appropriate successful checks for %s',
|
|
(scope) => {
|
|
expect(gateErrors(results(scope))).toEqual([])
|
|
}
|
|
)
|
|
|
|
test.each(['failure', 'cancelled', 'skipped'] as const)(
|
|
'rejects %s detection and required checks',
|
|
(result) => {
|
|
for (const scope of ['docs', 'code'] as const) {
|
|
expect(gateErrors({ ...results(scope), changes: { result } })).not.toEqual([])
|
|
for (const job of [...ALWAYS_JOBS, ...(scope === 'docs' ? [DOCS_JOB] : CODE_JOBS)]) {
|
|
expect(gateErrors({ ...results(scope), [job]: { result } })).not.toEqual([])
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
test('missing jobs or outputs cannot pass', () => {
|
|
expect(gateErrors({})).not.toEqual([])
|
|
expect(gateErrors({ ...results('docs'), changes: { result: 'success' } })).not.toEqual([])
|
|
expect(
|
|
gateErrors({
|
|
...results('docs'),
|
|
changes: { result: 'success', outputs: { scope: 'unknown' } }
|
|
})
|
|
).not.toEqual([])
|
|
for (const scope of ['docs', 'code'] as const) {
|
|
for (const job of [...ALWAYS_JOBS, ...(scope === 'docs' ? [DOCS_JOB] : CODE_JOBS)]) {
|
|
const incomplete = Object.fromEntries(
|
|
Object.entries(results(scope)).filter(([name]) => name !== job)
|
|
)
|
|
expect(gateErrors(incomplete)).not.toEqual([])
|
|
}
|
|
}
|
|
})
|
|
|
|
test('docs routing rejects unexpected execution of test suites', () => {
|
|
for (const job of CODE_JOBS) {
|
|
expect(gateErrors({ ...results('docs'), [job]: { result: 'success' } })).not.toEqual([])
|
|
}
|
|
})
|
|
|
|
test('a verified tree runs only the checks every change runs', () => {
|
|
for (const job of [...CODE_JOBS, DOCS_JOB]) {
|
|
expect(gateErrors({ ...results('verified'), [job]: { result: 'success' } })).not.toEqual([])
|
|
}
|
|
for (const job of ALWAYS_JOBS) {
|
|
expect(gateErrors({ ...results('verified'), [job]: { result: 'skipped' } })).not.toEqual([])
|
|
}
|
|
})
|