diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b5bf26a..3dba111b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,8 +50,6 @@ jobs: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache-v2- - name: Ensure dependencies run: bash scripts/ensure-deps.sh @@ -84,8 +82,6 @@ jobs: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache-v2- - name: Ensure dependencies run: bash scripts/ensure-deps.sh @@ -127,8 +123,6 @@ jobs: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache-v2- - name: Ensure dependencies run: bash scripts/ensure-deps.sh diff --git a/.github/workflows/push-ci.yml b/.github/workflows/push-ci.yml new file mode 100644 index 00000000..2aef1166 --- /dev/null +++ b/.github/workflows/push-ci.yml @@ -0,0 +1,134 @@ +name: Push CI + +on: + push: + branches: [dev] + +concurrency: + group: push-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: [self-hosted, linux, x64] + env: + BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache + strategy: + fail-fast: false + matrix: + check: + - { name: typecheck, cmd: 'bun run typecheck' } + - { name: lint, cmd: 'bun run lint' } + - { name: format, cmd: 'bun run format:check' } + name: ${{ matrix.check.name }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.9' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Restore bun package cache + uses: actions/cache@v4 + with: + path: | + ${{ env.BUN_INSTALL_CACHE_DIR }} + key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} + + - name: Ensure dependencies + run: bash scripts/ensure-deps.sh + + - name: Run ${{ matrix.check.name }} + run: ${{ matrix.check.cmd }} + + build: + runs-on: [self-hosted, linux, x64] + name: build + env: + BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.9' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Restore bun package cache + uses: actions/cache@v4 + with: + path: | + ${{ env.BUN_INSTALL_CACHE_DIR }} + key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} + + - name: Ensure dependencies + run: bash scripts/ensure-deps.sh + + - name: Build + run: bun run build:all + + - name: Upload dist artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 1 + if-no-files-found: error + + test: + runs-on: [self-hosted, linux, x64] + name: test + needs: [build] + env: + BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.9' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Restore bun package cache + uses: actions/cache@v4 + with: + path: | + ${{ env.BUN_INSTALL_CACHE_DIR }} + key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} + + - name: Ensure dependencies + run: bash scripts/ensure-deps.sh + + - name: Download dist artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Test + run: bun run test:all + + - name: Test CLI e2e + env: + CCS_E2E_SKIP_BUILD: '1' + run: bun run test:e2e diff --git a/.husky/pre-push b/.husky/pre-push index 07a426a0..b47b1dd4 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -45,9 +45,9 @@ echo " branch: $CURRENT_BRANCH" echo " base: $BASE_BRANCH" bun run typecheck -bun run lint:fix +bun run lint bun run format:check -bun run build:all +bun run test:fast git fetch origin "$BASE_BRANCH" --quiet || true DIFF_RANGE="HEAD" diff --git a/package.json b/package.json index d758342e..9ef64f27 100644 --- a/package.json +++ b/package.json @@ -70,12 +70,14 @@ "lint:fix": "eslint src/ --fix", "format": "prettier --write src/", "format:check": "prettier --check src/", - "validate": "bun run typecheck && bun run lint:fix && bun run format:check && bun run test:all", + "validate": "bun run typecheck && bun run lint && bun run format:check && bun run test:fast", "validate:ci-parity": "bash scripts/ci-parity-gate.sh", "verify:bundle": "node scripts/verify-bundle.js", "test": "bun run build && bun run test:all", "test:ci": "bun run test:all", - "test:all": "bun test --max-concurrency=1 tests/unit tests/integration tests/npm", + "test:fast": "node scripts/run-test-bucket.js fast", + "test:slow": "node scripts/run-test-bucket.js slow", + "test:all": "bun run test:fast && bun run test:slow", "test:unit": "bun test tests/unit", "test:npm": "bun test tests/npm/", "test:native": "bash tests/native/unix/edge-cases.sh", diff --git a/scripts/ci-parity-gate.sh b/scripts/ci-parity-gate.sh index 5e1c7a1f..c9b31a53 100755 --- a/scripts/ci-parity-gate.sh +++ b/scripts/ci-parity-gate.sh @@ -55,8 +55,12 @@ if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then fi fi -echo "[i] Running CI-equivalent local checks..." +echo "[i] Running CI-parity local checks..." +bun run typecheck +bun run lint +bun run format:check bun run build:all -bun run validate +bun run test:all +CCS_E2E_SKIP_BUILD=1 bun run test:e2e echo "[OK] CI parity gate passed." diff --git a/scripts/run-test-bucket.js b/scripts/run-test-bucket.js new file mode 100644 index 00000000..f17ed2f7 --- /dev/null +++ b/scripts/run-test-bucket.js @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const bucket = process.argv[2]; +const rootDir = path.resolve(__dirname, '..'); +const candidateRoots = ['tests/unit', 'tests/integration', 'tests/npm']; +const slowTests = [ + 'tests/integration/cursor-daemon-lifecycle.test.ts', + 'tests/integration/proxy/daemon-lifecycle.test.ts', + 'tests/unit/commands/persist-command-handler.test.ts', + 'tests/unit/hooks/ccs-browser-mcp-server.test.ts', + 'tests/unit/targets/codex-runtime-integration.test.ts', + 'tests/unit/targets/codex-settings-bridge-launch.test.ts', + 'tests/unit/targets/droid-command-routing-integration.test.ts', + 'tests/unit/targets/droid-config-manager.test.ts', + 'tests/unit/targets/settings-profile-browser-launch.test.ts', + 'tests/unit/targets/settings-profile-image-analysis-launch.test.ts', + 'tests/unit/targets/settings-profile-websearch-launch.test.ts', + 'tests/unit/web-server/cursor-routes.test.ts', + 'tests/unit/web-server/websearch-routes.test.ts', +]; + +if (bucket !== 'fast' && bucket !== 'slow') { + console.error('[X] Usage: node scripts/run-test-bucket.js '); + process.exit(1); +} + +const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/; + +function collectFiles(dir, files = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + collectFiles(fullPath, files); + continue; + } + + if (filePattern.test(entry.name)) { + files.push(path.relative(rootDir, fullPath).split(path.sep).join('/')); + } + } + + return files; +} + +function readsBuiltDist(relativePath) { + const source = fs.readFileSync(path.join(rootDir, relativePath), 'utf8'); + return source.includes('dist/'); +} + +const discovered = candidateRoots + .flatMap((relativeDir) => collectFiles(path.join(rootDir, relativeDir))) + .sort(); +const forceSlow = discovered.filter((file) => { + if (file.startsWith('tests/npm/')) { + return true; + } + + if (/\.(c|m)?js$/.test(file)) { + return true; + } + + return readsBuiltDist(file); +}); +const slowSet = new Set([...slowTests, ...forceSlow]); +const selected = + bucket === 'slow' + ? [...slowSet].sort() + : discovered.filter((file) => !slowSet.has(file)); + +if (selected.length === 0) { + console.error(`[X] No tests matched the '${bucket}' bucket.`); + process.exit(1); +} + +if (bucket === 'slow' && !fs.existsSync(path.join(rootDir, 'dist', 'ccs.js'))) { + const build = spawnSync('bun', ['run', 'build'], { + cwd: rootDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + + if (build.status !== 0) { + process.exit(build.status ?? 1); + } +} + +const result = spawnSync( + 'bun', + ['test', '--max-concurrency=1', ...selected], + { + cwd: rootDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }, +); + +process.exit(result.status ?? 1);