diff --git a/.github/workflows/free-tests.yml b/.github/workflows/free-tests.yml new file mode 100644 index 000000000..89e1ee5c4 --- /dev/null +++ b/.github/workflows/free-tests.yml @@ -0,0 +1,139 @@ +name: Free Tests +# The full free suite (`bun test`: browse/test/ + test/ + make-pdf/test/ minus +# paid evals) previously ran in NO CI job — only Windows curated shards, paid +# evals, and doc-freshness gates existed. Two test files crashed at module load +# for 48 versions without any signal. This job closes that hole. +on: + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: free-tests-${{ github.head_ref }} + cancel-in-progress: true + +env: + IMAGE: ghcr.io/${{ github.repository }}/ci + +jobs: + # Same cached pre-baked toolchain image as evals.yml (only rebuilds on + # Dockerfile/lockfile change). + build-image: + runs-on: ubicloud-standard-8 + permissions: + contents: read + packages: write + outputs: + image-tag: ${{ steps.meta.outputs.tag }} + steps: + - uses: actions/checkout@v4 + + - id: meta + run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'package.json', 'bun.lock') }}" >> "$GITHUB_OUTPUT" + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if image exists + id: check + run: | + if docker manifest inspect ${{ steps.meta.outputs.tag }} > /dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - if: steps.check.outputs.exists == 'false' + run: cp package.json bun.lock .github/docker/ + + - if: steps.check.outputs.exists == 'false' + uses: docker/build-push-action@v6 + with: + context: .github/docker + file: .github/docker/Dockerfile.ci + push: true + tags: | + ${{ steps.meta.outputs.tag }} + ${{ env.IMAGE }}:latest + + free-tests: + runs-on: ubicloud-standard-8 + needs: build-image + container: + image: ${{ needs.build-image.outputs.image-tag }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --user runner + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + # Bun creates root-owned temp dirs during Docker build. GH Actions runs as + # runner user with HOME=/github/home. Redirect bun's cache to a writable dir. + - name: Fix bun temp + run: | + mkdir -p /home/runner/.cache/bun + { + echo "BUN_INSTALL_CACHE_DIR=/home/runner/.cache/bun" + echo "BUN_TMPDIR=/home/runner/.cache/bun" + echo "TMPDIR=/home/runner/.cache" + } >> "$GITHUB_ENV" + + # Same restore rationale as evals.yml: recursive copy beats symlink + # (realpath escapes workspace) and hardlink (cross-device overlay-fs). + - name: Restore deps + run: | + if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then + cp -r /opt/node_modules_cache node_modules + else + bun install + fi + + - run: bun run build + + # Fail fast if the container can't launch Chromium — the browse + # integration tests need it. + - name: Verify Chromium + run: | + echo "whoami=$(whoami) HOME=$HOME TMPDIR=${TMPDIR:-unset}" + bun -e "import {chromium} from 'playwright';const b=await chromium.launch({args:['--no-sandbox']});console.log('Chromium OK');await b.close()" + + # ONE BUN PROCESS PER FILE, on purpose. A single multi-file `bun test` + # run of this suite is structurally unreliable here — observed twice + # while building this job: + # 1. Silent truncation: server-lifecycle tests stub process.exit, and + # shutdown's async timers can hit the REAL exit after restore, + # killing the whole bun process mid-suite with exit 0 and NO + # summary (died at file 47, then file 51, of 358). + # 2. Co-run state bleed: files green in isolation failed under + # multi-file module sharing. + # Per-file spawning makes truncation impossible by construction (the + # census drives the loop; a killed child is a recorded failure, not a + # vanished suite) and also covers the old exit-0-on-module-load-error + # Bun behavior. Same isolation model as scripts/test-paid-shards.ts. + - name: Run free suite (per-file isolation) + run: | + set -o pipefail + FILES=$(bun run scripts/test-free-shards.ts --list | grep -E '^ (browse/|test/|make-pdf/)' | sed 's/^ //') + TOTAL=$(echo "$FILES" | wc -l | tr -d ' ') + echo "Enumerated $TOTAL free test files" + FAILED="" + N=0 + for f in $FILES; do + N=$((N+1)) + if ! bun test "$f" > /tmp/one.log 2>&1; then + echo "FAIL [$N/$TOTAL] $f" + tail -30 /tmp/one.log + FAILED="$FAILED $f" + fi + done + if [ -n "$FAILED" ]; then + echo "" + echo "Failed files:$FAILED" + exit 1 + fi + echo "All $TOTAL files green."