Skip to content

Commit 1935265

Browse files
authored
chore(release): sync v1.8.0-rc.4 to main (#1450)
Promotes dev/v1.8 at 00fab7f (tree e195658) for v1.8.0-rc.4.
1 parent 9fed1bd commit 1935265

154 files changed

Lines changed: 2819 additions & 1335 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/tests/ci-verify-workflow.test.ts‎

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,17 @@ function loadLefthook(): LefthookDefinition {
4444
return yaml.parse(readFileSync(lefthookPath, 'utf8')) as LefthookDefinition;
4545
}
4646

47+
// CI-5: the old single test job is split into shard jobs plus the aggregating
48+
// `test` job that keeps the required check name.
49+
const testShardJobIds = ['test-repo', 'test-app', 'test-ui', 'test'];
50+
4751
function getTestJobStep(name: string): WorkflowStep | undefined {
4852
const workflow = loadWorkflow();
49-
return workflow.jobs?.test?.steps?.find((step) => step.name === name);
53+
for (const jobId of testShardJobIds) {
54+
const step = workflow.jobs?.[jobId]?.steps?.find((candidate) => candidate.name === name);
55+
if (step) return step;
56+
}
57+
return undefined;
5058
}
5159

5260
test('UI coverage captures bounded resource evidence without changing the gate', () => {
@@ -389,9 +397,20 @@ test('ci-verify can dispatch the complete release-candidate matrix manually', ()
389397
expect(workflow.jobs?.[jobId]?.if).toContain("github.event_name == 'workflow_dispatch'");
390398
}
391399

392-
for (const jobId of ['security-actions', 'changes', 'lint', 'test', 'build']) {
400+
for (const jobId of [
401+
'security-actions',
402+
'changes',
403+
'lint',
404+
'test-repo',
405+
'test-app',
406+
'test-ui',
407+
'build',
408+
]) {
393409
expect(workflow.jobs?.[jobId]?.if).toBeUndefined();
394410
}
411+
// The aggregating required check must run after a failed shard so it can
412+
// report failure itself; a skipped required check counts as passing.
413+
expect(workflow.jobs?.test?.if).toBe('always()');
395414

396415
expect(workflow.jobs?.codeql?.needs).toStrictEqual(['security-actions']);
397416
expect(workflow.jobs?.fuzz?.needs).toStrictEqual(['security-actions']);
@@ -682,3 +701,95 @@ test('the advisory ci-timing job cannot fail the run or gate a release', () => {
682701
);
683702
expect(releaseCut).not.toMatch(/ci-timing|CI timing/);
684703
});
704+
705+
test('test suites are sharded and a final job keeps the required check name', () => {
706+
const workflow = loadWorkflow();
707+
708+
expect(workflow.jobs?.test?.name).toBe('Quality: Test & Coverage');
709+
expect(workflow.jobs?.test?.needs).toStrictEqual(['test-repo', 'test-app', 'test-ui']);
710+
for (const jobId of ['test-repo', 'test-app', 'test-ui']) {
711+
expect(workflow.jobs?.[jobId]?.needs).toBeUndefined();
712+
expect(workflow.jobs?.[jobId]?.name).not.toBe('Quality: Test & Coverage');
713+
}
714+
715+
// Shards run the same commands the pre-push coverage gate runs, so the
716+
// per-workspace 100% thresholds stay enforced where the tests execute.
717+
expect(getWorkflowStep('test-app', 'Run app tests')).toMatchObject({
718+
run: 'npm test',
719+
'working-directory': 'app',
720+
});
721+
expect(getWorkflowStep('test-ui', 'Run ui tests')?.run).toContain(
722+
'npm run test:unit -- --maxWorkers=1',
723+
);
724+
expect(getWorkflowStep('test-ui', 'Typecheck ui')).toMatchObject({
725+
run: 'npm run typecheck',
726+
'working-directory': 'ui',
727+
});
728+
729+
// build gates on the aggregate, not on individual shards.
730+
expect(workflow.jobs?.build?.needs).toStrictEqual(['lint', 'test']);
731+
});
732+
733+
test('coverage shards hand lcov and gap reports to the aggregating job', () => {
734+
for (const [jobId, workspace] of [
735+
['test-app', 'app'],
736+
['test-ui', 'ui'],
737+
]) {
738+
const lcov = getWorkflowStep(jobId, 'Upload coverage report');
739+
expect(lcov?.if).toBe('always()');
740+
expect(lcov?.with).toMatchObject({
741+
name: `coverage-shard-${workspace}`,
742+
path: `${workspace}/coverage/lcov.info`,
743+
});
744+
745+
const gaps = getWorkflowStep(jobId, 'Write coverage gap report');
746+
expect(gaps?.if).toBe('failure()');
747+
expect(gaps?.run).toContain(`--workspace ${workspace}`);
748+
expect(gaps?.run).toContain('|| true');
749+
expect(getWorkflowStep(jobId, 'Upload coverage gap report')?.with).toMatchObject({
750+
name: `coverage-gaps-${workspace}`,
751+
path: `.coverage-gaps.${workspace}.json`,
752+
});
753+
// Gap reports are dotfiles; upload-artifact skips hidden files by default.
754+
expect(getWorkflowStep(jobId, 'Upload coverage gap report')?.with).toMatchObject({
755+
'include-hidden-files': true,
756+
});
757+
}
758+
});
759+
760+
test('the aggregating job uploads the merged gap report including hidden files', () => {
761+
expect(getWorkflowStep('test', 'Upload coverage gap report')?.with).toMatchObject({
762+
name: 'coverage-gaps',
763+
path: '.coverage-gaps.json',
764+
'include-hidden-files': true,
765+
});
766+
});
767+
768+
test('the aggregating job merges shard coverage and fails closed on any shard result', () => {
769+
const merge = getWorkflowStep('test', 'Merge coverage gap reports');
770+
expect(merge?.if).toBe('always()');
771+
expect(merge?.run).toContain('scripts/merge-coverage-gaps.mjs');
772+
expect(merge?.run).toContain('.coverage-gaps.app.json');
773+
expect(merge?.run).toContain('.coverage-gaps.ui.json');
774+
775+
const normalize = getWorkflowStep('test', 'Normalize coverage paths');
776+
expect(normalize?.run).toBe('node scripts/prepare-codecov-reports.mjs');
777+
expect(normalize?.if).toContain("needs.test-app.result == 'success'");
778+
expect(normalize?.if).toContain("needs.test-ui.result == 'success'");
779+
780+
const gate = getWorkflowStep('test', 'Enforce shard results');
781+
expect(gate?.if).toBe('always()');
782+
for (const shard of ['test-repo', 'test-app', 'test-ui']) {
783+
expect(gate?.env).toHaveProperty(
784+
`RESULT_${shard.replace('-', '_').toUpperCase()}`,
785+
`\${{ needs.${shard}.result }}`,
786+
);
787+
}
788+
expect(gate?.run).toContain('success');
789+
expect(gate?.run).toContain('exit 1');
790+
791+
// Codecov upload stays advisory.
792+
for (const name of ['Upload app coverage to Codecov', 'Upload ui coverage to Codecov']) {
793+
expect(getWorkflowStep('test', name)?.['continue-on-error']).toBe(true);
794+
}
795+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { fileURLToPath } from 'node:url';
2+
3+
import { loadWorkflow } from './workflow-test-utils';
4+
5+
const workflowPath = fileURLToPath(
6+
new URL('../workflows/closing-keywords-guard.yml', import.meta.url),
7+
);
8+
9+
test('closing keywords guard runs on PRs into main and re-runs when the body is edited', () => {
10+
const workflow = loadWorkflow(workflowPath);
11+
const pullRequest = (workflow.on as Record<string, { branches?: string[]; types?: string[] }>)
12+
.pull_request;
13+
expect(pullRequest.branches).toStrictEqual(['main']);
14+
expect(pullRequest.types).toContain('edited');
15+
});
16+
17+
test('closing keywords guard passes the PR text through env, not interpolation', () => {
18+
const steps = loadWorkflow(workflowPath).jobs?.['closing-keywords']?.steps ?? [];
19+
const guard = steps.find((step) => step.run?.includes('scripts/closing-keywords.mjs'));
20+
expect(guard).toBeDefined();
21+
expect(guard?.env?.PR_TITLE).toBe('${{ github.event.pull_request.title }}');
22+
expect(guard?.env?.PR_BODY).toBe('${{ github.event.pull_request.body }}');
23+
expect(guard?.run).not.toContain('${{');
24+
});
25+
26+
test('closing keywords guard also scans the commit messages that build the squash message', () => {
27+
const steps = loadWorkflow(workflowPath).jobs?.['closing-keywords']?.steps ?? [];
28+
const checkout = steps.find((step) => step.uses?.startsWith('actions/checkout@'));
29+
expect(checkout?.with?.['fetch-depth']).toBe(0);
30+
const guard = steps.find((step) => step.run?.includes('scripts/closing-keywords.mjs'));
31+
expect(guard?.env?.BASE_SHA).toBe('${{ github.event.pull_request.base.sha }}');
32+
expect(guard?.env?.HEAD_SHA).toBe('${{ github.event.pull_request.head.sha }}');
33+
expect(guard?.run).toContain('git log --format=%B "${BASE_SHA}..${HEAD_SHA}"');
34+
expect(guard?.run).toContain('PR commits=');
35+
expect(guard?.run).not.toContain('${{');
36+
});

‎.github/tests/harden-runner-workflow.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const expectedPolicy: Record<string, EgressPolicy> = {
2121
'ci-verify.yml/secrets': 'block',
2222
'ci-verify.yml/fuzz': 'block',
2323
'ci-verify.yml/test': 'block',
24+
'ci-verify.yml/test-repo': 'block',
25+
'ci-verify.yml/test-app': 'block',
26+
'ci-verify.yml/test-ui': 'block',
2427
'ci-verify.yml/web': 'block',
2528
'ci-verify.yml/grype-image': 'block',
2629
// Required-context rename mirrors. They run one `test` against a `needs`

‎.github/tests/readme-translations.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe.each(translatedReadmes)('%s', (readme) => {
103103

104104
test('carries the security and SQLite RC migration warnings', () => {
105105
expect(content.match(/^> \[!WARNING\]$/gm)).toHaveLength(2);
106-
expect(content).toContain('1.8.0-rc.3');
106+
expect(content).toContain('1.8.0-rc.4');
107107
expect(content).toContain('SQLite');
108108
expect(content).toContain('`401`');
109109
expect(content).toContain('`503`');

0 commit comments

Comments
 (0)