name: Backend Tests # Always triggers so it can be a required PR check; the `changes` job below # decides whether backend/extension paths actually changed (test-only changes # match the same globs, since tests are co-located with the code), and every # other job is skipped — not absent — when they didn't, which still satisfies # a required check. puter.js SDK changes are covered by puterjs-tests.yaml. on: pull_request: types: [opened, synchronize, reopened] permissions: contents: read pull-requests: write jobs: changes: runs-on: ubuntu-latest outputs: backend: ${{ steps.filter.outputs.backend }} steps: - uses: actions/checkout@v7 - uses: dorny/paths-filter@v3 id: filter with: filters: | backend: - 'src/backend/**' - 'extensions/**' - 'tools/**' - 'package.json' - 'package-lock.json' - 'tsconfig.json' - 'tsconfig.build.json' - '.github/workflows/backend-tests.yaml' # `tsconfig.build.json` builds with `noCheck: true`, so nothing else in CI # runs the type checker and a missing export compiles to `undefined`, # surfacing only when the call is finally reached at runtime. This diffs # against tools/typecheck-baseline.json and fails only on *new* errors. # # The consuming repo runs an equivalent gate, but only once someone bumps the # submodule pointer — too late to keep the error off this repo's default # branch. Hence a gate here, on this repo's own pull requests. # # Its own job rather than a step in `test`: that one runs a base/PR matrix for # coverage comparison, and the check only needs the PR ref, once. typecheck: needs: changes if: needs.changes.outputs.backend == 'true' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v7 - name: Set up Node.js uses: actions/setup-node@v7 with: node-version: '24' cache: 'npm' - name: Install dependencies run: npm ci - name: Type check (new errors only) run: npm run typecheck # `needs.changes` gates the steps below, not this job, on purpose: a # job-level `if` on a matrix job skips the whole job as one unexpanded # check ("test (${{ matrix.artifact }})", literally, since there's no # matrix context to fill it in) instead of the two real per-artifact # checks a required rule pins to ("test (base)" / "test (pr)") — so the # required check never gets reported and the PR blocks forever. Letting # the matrix always expand keeps the check names stable either way. test: needs: changes # Overrides the default matrix naming, which would otherwise bake the # base/head branch name (matrix.ref) into the check name — making it a # different string on every PR and impossible to pin as a required check. name: test (${{ matrix.artifact }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - ref: ${{ github.base_ref }} artifact: base - ref: ${{ github.head_ref }} artifact: pr steps: - name: Checkout ${{ matrix.ref }} if: needs.changes.outputs.backend == 'true' uses: actions/checkout@v7 with: ref: ${{ matrix.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Set up Node.js if: needs.changes.outputs.backend == 'true' uses: actions/setup-node@v7 with: node-version: '24' cache: 'npm' - name: Install dependencies if: needs.changes.outputs.backend == 'true' run: npm ci - name: Run backend tests with coverage if: needs.changes.outputs.backend == 'true' run: npm run test:backend -- --coverage env: CI: 'true' - name: Upload coverage artifact if: needs.changes.outputs.backend == 'true' && !cancelled() uses: actions/upload-artifact@v7 with: name: backend-coverage-${{ matrix.artifact }} path: src/backend/coverage/ retention-days: 14 report-coverage: needs: [changes, test] if: always() && needs.changes.outputs.backend == 'true' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v7 - name: Download PR coverage uses: actions/download-artifact@v8 with: name: backend-coverage-pr path: src/backend/coverage - name: Download base coverage uses: actions/download-artifact@v8 with: name: backend-coverage-base path: coverage-base - name: Report coverage on PR uses: davelosert/vitest-coverage-report-action@v2 with: json-summary-path: src/backend/coverage/coverage-summary.json json-final-path: src/backend/coverage/coverage-final.json json-summary-compare-path: coverage-base/coverage-summary.json