diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e332e79c..846de4735 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: name: Classify changes runs-on: ubuntu-latest outputs: + code: ${{ steps.package.outputs.required }} package: ${{ steps.package.outputs.required }} steps: - uses: actions/checkout@v4 @@ -60,7 +61,34 @@ jobs: static: name: Static checks + needs: changes + if: ${{ always() }} runs-on: ubuntu-latest + steps: + - name: Require successful change classification + env: + CHANGES_RESULT: ${{ needs.changes.result }} + run: test "${CHANGES_RESULT}" = "success" + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - if: github.event_name == 'push' || needs.changes.outputs.code == 'true' + run: npm run check:ci + - if: github.event_name == 'pull_request' && needs.changes.outputs.code != 'true' + run: npm run format:check + + test-shard: + name: Test shard (${{ matrix.shard }}) + needs: changes + if: github.event_name == 'push' || needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1/2, 2/2] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -68,24 +96,54 @@ jobs: node-version-file: .nvmrc cache: npm - run: npm ci - - run: npm run check:ci + - run: npm run test:ci:shard -- --shard=${{ matrix.shard }} + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: vitest-report-${{ strategy.job-index }} + path: .vitest-reports/* + include-hidden-files: true + if-no-files-found: error + overwrite: true + retention-days: 1 test: name: Test (coverage + timing) + needs: [changes, test-shard] + if: ${{ always() }} runs-on: ubuntu-latest steps: + - name: Require successful test shards + env: + CHANGES_RESULT: ${{ needs.changes.result }} + CODE_REQUIRED: ${{ needs.changes.outputs.code }} + SHARD_RESULT: ${{ needs.test-shard.result }} + run: | + test "${CHANGES_RESULT}" = "success" + test "${CODE_REQUIRED}" != "true" || test "${SHARD_RESULT}" = "success" - uses: actions/checkout@v4 + if: needs.changes.outputs.code == 'true' && success() - uses: actions/setup-node@v4 + if: needs.changes.outputs.code == 'true' && success() with: node-version-file: .nvmrc cache: npm - - run: npm ci - - run: npm run test:ci + - if: needs.changes.outputs.code == 'true' && success() + run: npm ci + - uses: actions/download-artifact@v4 + if: needs.changes.outputs.code == 'true' && success() + with: + path: .vitest-reports + pattern: vitest-report-* + merge-multiple: true + - if: needs.changes.outputs.code == 'true' && success() + run: npm run test:ci:merge - uses: actions/upload-artifact@v4 - if: always() + if: needs.changes.outputs.code == 'true' && always() with: name: test-results path: test-results.xml + if-no-files-found: error retention-days: 30 package-e2e: diff --git a/.gitignore b/.gitignore index 0ff55fdcf..114a6eb6f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ codedb.snapshot htmlcov/ .coverage coverage/ +.vitest-reports/ +test-results.xml .turbo/ # OS diff --git a/AGENTS.md b/AGENTS.md index 7b0ec76d0..6a143b947 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec - `npm run test:fast`: run the parallel test project without the serial integration group. - `npm run test:integration`: run the serial filesystem, process, and CLI integration group. - `npm run test:watch`: watch tests affected by working-tree changes in the parallel project, without coverage. -- `npm run test:coverage`: run the complete suite with coverage; CI owns verbose and JUnit reporting. +- `npm run test:coverage`: run the complete suite with coverage; CI shards the suite and owns JUnit reporting. - `npm run typecheck`: run strict TypeScript checks without emitting files. - `npm run lint`: apply oxlint rules (complexity, max-lines, unused vars, and TypeScript-specific checks). - `npm run lint:fix`: auto-fix oxlint violations where possible. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8673efacb..6e92be536 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,14 +40,15 @@ artifact; the generated HTML is not committed. Local `npm test` runs without coverage, retries, or verbose output. Use `npm run test:fast` for the parallel group, `npm run test:integration` for serial filesystem/process/CLI cases, and `npm run test:watch` to watch tests affected by -working-tree changes. CI runs `npm run test:ci`, which adds coverage, retries, -verbose output, and JUnit results. Coverage thresholds remain in +working-tree changes. CI splits the complete suite across two native Vitest +shards, then merges their coverage and JUnit reports. `npm run test:ci` runs the +equivalent unsharded gate locally. Coverage thresholds remain in `vitest.config.ts`. CI installs dependencies once for all static checks, cancels superseded PR -runs, and skips package and Windows lanes when a PR changes only files outside -the package/runtime/test boundary. TypeDoc renders only in pull-request CI; it -does not run in local commit or pre-push hooks, or in post-merge `main` CI. +runs, and skips package, Windows, and full test lanes for documentation-only +pull requests. TypeDoc renders only in pull-request CI; it does not run in local +commit or pre-push hooks, or in post-merge `main` CI. Tests that need a temporary directory must use `createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`. The diff --git a/package.json b/package.json index ff610b2c0..5e23ef1c8 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,10 @@ "test:integration": "vitest run --project serial-integration", "pretest:coverage": "npm run build:cached", "test:coverage": "vitest run --coverage.enabled --coverage.provider=v8", - "test:ci": "npm run test:coverage -- --reporter=verbose --reporter=junit --outputFile.junit=test-results.xml", + "test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml", + "pretest:ci:shard": "npm run build:cached", + "test:ci:shard": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob", + "test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8 --reporter=default --reporter=junit --outputFile.junit=test-results.xml", "pretest:verbose": "npm run build:cached", "test:verbose": "vitest run --reporter=verbose", "pretest:watch": "npm run build:cached", diff --git a/tests/packageInstallWorkflow.test.ts b/tests/packageInstallWorkflow.test.ts index b1ed2a5a1..9b48d3303 100644 --- a/tests/packageInstallWorkflow.test.ts +++ b/tests/packageInstallWorkflow.test.ts @@ -20,6 +20,18 @@ describe("package installation workflows", () => { expect(continuousIntegration).toContain("npm run verify:package"); expect(continuousIntegration).toContain("name: Static checks"); expect(continuousIntegration).toContain("npm run check:ci"); + expect(continuousIntegration).toContain("shard: [1/2, 2/2]"); + expect(continuousIntegration).toContain("needs: [changes, test-shard]"); + expect(continuousIntegration).toContain("if-no-files-found: error"); + expect(continuousIntegration).toContain("overwrite: true"); + expect(continuousIntegration).toContain( + "code: ${{ steps.package.outputs.required }}", + ); + expect(continuousIntegration).toContain("CODE_REQUIRED:"); + expect(continuousIntegration).toContain( + 'test "${CODE_REQUIRED}" != "true" || test "${SHARD_RESULT}" = "success"', + ); + expect(continuousIntegration).toContain("npm run test:ci:merge"); expect(continuousIntegration).toContain("needs: changes"); expect(continuousIntegration).toContain( "cancel-in-progress: ${{ github.event_name == 'pull_request' }}",