ci: run typecheck, build & tests on PRs and main (#6)
* ci: run typecheck, build & tests on PRs and main Add a GitHub Actions workflow (.github/workflows/ci.yml) that installs on Node 20 (the pinned runtime), builds shared/server/web, typechecks desktop, and runs the full vitest suite across all packages on every pull request and push to main. The repo already had 500+ tests but only ever ran them locally. Also wires up the missing test/typecheck plumbing this exposed: - web package had 55 test files but no test script — add test/test:watch/typecheck - add root-level test and typecheck aggregate scripts - fix a latent web type error the new typecheck surfaced: reference vite-plugin-pwa/react types so virtual:pwa-register/react is typed (useRegisterSW callback params were implicit any; the tsc half of the web build script has been failing, masked by vite build ignoring type errors) * test: fix two latent failures surfaced by CI on Node 20 - server: exclude dist/** from vitest. Vitest 4's default exclude dropped dist/, so after `pnpm build` emits compiled .test.js files, vitest ran those stale copies alongside src/*.test.ts and they failed (compiled vi.mock paths resolve differently). - web: polyfill Blob.prototype.stream in the jsdom test setup. jsdom's Blob has no .stream() on Node 20 (Node 25 provided one, masking this locally); undici's Response constructor calls blob.stream(), so new Response(blob) threw 'object.stream is not a function'.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
name: CI
|
||||
|
||||
# Runs the workspace's typecheck, production build, and full test suite on every
|
||||
# pull request and on pushes to main. Purpose: catch compile errors and test
|
||||
# regressions before merge instead of relying on each contributor running tests
|
||||
# locally. Once this check is green on a PR, enable branch protection on `main`
|
||||
# ("Require status checks to pass" → select "Build & test") to make it blocking.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# A newer commit on the same branch/PR supersedes in-flight runs — cancel the
|
||||
# stale one so a rapid push sequence doesn't queue redundant CI.
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build & test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.34.3
|
||||
|
||||
# Node 20 is the project's supported runtime (package.json engines pins
|
||||
# >=20 <21, .nvmrc says 20). Running the matrix on 20 also means the fresh
|
||||
# install below pulls better-sqlite3's prebuilt binary for the correct ABI,
|
||||
# which is what makes the server suite runnable in CI.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
# The desktop postinstall tries to rebuild the native uiohook-napi module.
|
||||
# It fails gracefully (|| warn) without X11 dev headers, and nothing in CI
|
||||
# needs the native binary — the desktop tests and TS compile are pure JS —
|
||||
# so we intentionally skip installing those headers to keep CI fast.
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Compiles shared → server → web (each runs tsc; web also runs the Vite
|
||||
# production build). This is the typecheck + build gate for those three.
|
||||
- name: Build shared, server & web
|
||||
run: pnpm build
|
||||
|
||||
# Desktop is not part of `pnpm build` (that produces an Electron installer,
|
||||
# which release.yml owns). Type-check its source here so desktop TS errors
|
||||
# surface on PRs rather than only at release-tag time.
|
||||
- name: Typecheck desktop
|
||||
run: pnpm --filter @backspace/desktop build:ts
|
||||
|
||||
# Runs every package's `test` script (server, web, desktop) via vitest.
|
||||
- name: Test
|
||||
run: pnpm -r test
|
||||
@@ -18,6 +18,8 @@
|
||||
"build:server": "pnpm --filter @backspace/server build",
|
||||
"build:web": "pnpm --filter @backspace/web build",
|
||||
"build": "pnpm --filter @backspace/shared build && pnpm --filter @backspace/server build && pnpm --filter @backspace/web build",
|
||||
"typecheck": "pnpm --filter @backspace/shared build && pnpm -r typecheck",
|
||||
"test": "pnpm -r test",
|
||||
"dev:desktop": "pnpm --filter @backspace/desktop dev",
|
||||
"build:desktop": "pnpm --filter @backspace/desktop build",
|
||||
"gen-icons": "node scripts/gen-icons.mjs"
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig, configDefaults } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
setupFiles: ['./test/setup-env.ts'],
|
||||
// Vitest 4's default `exclude` is only node_modules/.git — it no longer
|
||||
// ignores dist/. Once `pnpm build` (tsc) has emitted the compiled test files
|
||||
// into dist/, vitest would otherwise run those stale .js copies alongside the
|
||||
// real src/*.test.ts — and they fail, because compiled vi.mock() paths
|
||||
// resolve differently than the source. Never run build output as tests.
|
||||
exclude: [...configDefaults.exclude, 'dist/**'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"preview": "vite preview",
|
||||
"e2e:identity-deletion": "playwright test e2e/identity-deletion.spec.ts"
|
||||
},
|
||||
|
||||
@@ -70,6 +70,25 @@ if (typeof ClipboardItem === 'undefined') {
|
||||
};
|
||||
}
|
||||
|
||||
// jsdom's Blob implementation has no `.stream()` method on Node 20 (Node 25+
|
||||
// happens to provide one, which is why this only surfaces on the pinned target
|
||||
// runtime). undici's `Response` constructor extracts a Blob body by calling
|
||||
// `blob.stream()`, so `new Response(blob)` throws "object.stream is not a
|
||||
// function" without this. Back it with the blob's own arrayBuffer().
|
||||
if (typeof Blob !== 'undefined' && typeof Blob.prototype.stream !== 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Blob.prototype as any).stream = function stream(this: Blob): ReadableStream<Uint8Array> {
|
||||
const blob = this;
|
||||
return new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
const buffer = await blob.arrayBuffer();
|
||||
controller.enqueue(new Uint8Array(buffer));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Patch globalThis.Response to preserve Blob content-type in jsdom.
|
||||
// jsdom's fetch Response.blob() drops the Blob's MIME type; this shim
|
||||
// restores it so tests that construct `new Response(blob)` behave correctly.
|
||||
|
||||
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-pwa/react" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_FORCE_BOOT_STALL?: string;
|
||||
|
||||
Reference in New Issue
Block a user