Skip to content

fix(cli,wizard): correctness follow-ups to the Drizzle EQL migration rewriter #2214

fix(cli,wizard): correctness follow-ups to the Drizzle EQL migration rewriter

fix(cli,wizard): correctness follow-ups to the Drizzle EQL migration rewriter #2214

Workflow file for this run

name: Test JS
on:
push:
branches:
- 'main'
pull_request:
branches:
- "**"
jobs:
# Biome format + lint. Its own job (no DB, no credentials, node-agnostic) so it
# surfaces as a distinct check and runs once rather than per Node matrix leg.
# Gates on ERRORS only — warnings are allowed, so the `no-type-erasing-assertions`
# plugin (at `warn`) surfaces new `as any`/`never`/`unknown` without blocking.
# Tightening to fail on warnings is tracked in #625.
lint:
name: Lint (Biome)
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6.0.9
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6.5.0
with:
# Node 22 is the supply-chain hardening baseline every pnpm-using job
# must pin (enforced by e2e/tests/supply-chain.e2e.test.ts). Lint is
# runtime-agnostic, so a single pinned version is fine.
node-version: 22
cache: 'pnpm'
# node-pty's install hook falls back to `node-gyp rebuild` when no
# linux-x64 prebuild matches; pnpm/action-setup v6 no longer ships it.
- name: Install node-gyp
run: npm install -g node-gyp
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Biome check (format + lint)
run: pnpm run code:check
run-tests:
name: Run Tests (Node ${{ matrix.node-version }})
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
matrix:
node-version: [22, 24]
# Postgres + EQL for the integration tests. Official EQL image —
# PostgreSQL 17 with EQL pre-installed via /docker-entrypoint-initdb.d.
# Pinned to eql-2.3.1 to match the EQL payload format the code emits
# (protect-ffi 0.23.x); bump in lockstep with the protect-ffi upgrade.
services:
postgres:
image: ghcr.io/cipherstash/postgres-eql:17-2.3.1
env:
POSTGRES_USER: cipherstash
POSTGRES_PASSWORD: password
POSTGRES_DB: cipherstash
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U cipherstash -d cipherstash"
--health-interval 2s
--health-timeout 5s
--health-retries 20
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6.0.9
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6.5.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
# node-pty's install hook falls back to `node-gyp rebuild` when no
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
# node-gyp on PATH, so install it explicitly.
- name: Install node-gyp
run: npm install -g node-gyp
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Fail loudly if any live-test credential is missing, so the v3 live
# matrix (and every other `describeLive` suite) can't silently skip.
- name: Require CipherStash secrets
uses: ./.github/actions/require-cs-secrets
with:
workspace-crn: ${{ vars.CS_WORKSPACE_CRN }}
client-id: ${{ vars.CS_CLIENT_ID }}
client-key: ${{ secrets.CS_CLIENT_KEY }}
client-access-key: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
- name: Type tests (stack)
run: pnpm --filter @cipherstash/stack run test:types
# The v3 domain catalog lives in the test kit, and its
# `satisfies Record<EqlV3TypeName, DomainSpec>` is what forces a new SDK
# domain to be covered. That check only fires under `tsc`, so without this
# step a domain added to stack would slip through untested.
- name: Type tests (test-kit — enforces v3 domain coverage)
run: pnpm --filter @cipherstash/test-kit run test:types
# The adapter packages carry their own `.test-d.ts` type-contract guards
# (the M1 client-surface / #622 erasure guards, the Supabase key-set
# gating). They moved here from `@cipherstash/stack` in the #627 split but
# were never wired into CI, so type-level regressions in them went
# undetected — run them explicitly.
- name: Type tests (stack-drizzle)
run: pnpm --filter @cipherstash/stack-drizzle run test:types
- name: Type tests (stack-supabase)
run: pnpm --filter @cipherstash/stack-supabase run test:types
# prisma-next's operator-capability gating is proven by `.test-d.ts`
# `@ts-expect-error` assertions (an unsupported operator on a column
# must be a compile error). Those only fire when tsc processes them,
# so the package `typecheck` must run — and be required — here (#684).
# Its tsconfig resolves stack subpaths to SOURCE, so no build step is
# needed first.
- name: Typecheck (prisma-next — enforces v3 operator-capability gating)
run: pnpm --filter @cipherstash/prisma-next run typecheck
# `packages/bench` is a live importer of `@cipherstash/stack` and
# `@cipherstash/stack-drizzle`, but it has no `test` script (its suites
# need a database), so `pnpm run test` never reaches it and an adapter
# rename could break it unnoticed. Its `build` is `tsc --noEmit`, and
# turbo's `^build` builds the two adapters first. This also runs in the
# Bun job's full `turbo build`, but that job swallows its own test
# failures — the importer guard should not depend on it.
- name: Typecheck (bench — guards the stack/stack-drizzle importers)
run: pnpm exec turbo run build --filter @cipherstash/bench
# The wizard is built by tsup, which transpiles without typechecking, so
# nothing here caught the three `auth.AutoStrategy` resolution errors that
# sat in `main` until #771. Gate it so they cannot come back silently.
- name: Typecheck (wizard)
run: pnpm --filter @cipherstash/wizard run typecheck
# `examples/*` are standalone apps outside the `./packages/*` filter that
# root `build`/`test` use, so nothing in CI compiled them. `examples/basic`
# had been broken since the v2 removal deleted `encryptedType` and the v2
# `encryptedSupabase` — on a fully green board. Gate it through turbo so
# `^build` builds stack/stack-drizzle/stash first.
- name: Typecheck (examples/basic — guards the v3 stack/stack-drizzle importers)
run: pnpm exec turbo run typecheck --filter @cipherstash/basic-example
# The rest of the surfaces that were compiling nowhere. Each of these has
# a `tsconfig.json` that covers its `src` and tests, and each was already
# clean — so they are gated now, before they drift. They resolve their
# workspace dependencies through `dist/*.d.ts`, hence the turbo filters
# (`^build` builds the dependencies first).
#
# NOT fully gated yet, and deliberately: `@cipherstash/stack` (147 errors
# under its own tsconfig), `stash` (21) and `@cipherstash/stack-supabase`
# (11). Their `test:types` scripts only cover `__tests__/**/*.test-d.ts`,
# so `src` and the runtime test suites compile nowhere.
# `@cipherstash/stack-drizzle` now type-tests every `integration/**`
# source as well; its remaining `src` / runtime-test errors share one
# root cause — `V3_MATRIX`'s `indexes` union in `@cipherstash/test-kit`
# — see #778.
- name: Typecheck (migrate)
run: pnpm exec turbo run typecheck --filter @cipherstash/migrate
- name: Typecheck (nextjs)
run: pnpm exec turbo run typecheck --filter @cipherstash/nextjs
- name: Typecheck (examples/prisma — guards the prisma-next importers)
run: pnpm exec turbo run typecheck --filter @cipherstash/prisma-next-example
- name: Typecheck (e2e)
run: pnpm exec turbo run typecheck --filter @cipherstash/e2e
# Everything else typechecks against SOURCE. This one reads the emitted
# `.d.ts`, which is what customers consume — and where typed `encryptQuery`
# sat broken for every column through the whole rc series, because `tsc`
# strips private member types and that was the only place the column's
# domain parameter appeared bare.
- name: Typecheck (stack — emitted declarations, not source)
run: pnpm exec turbo run test:types:dist --filter @cipherstash/stack
# `stash init` writes a client file into the user's project and tells them
# not to hand-edit it. Nothing compiled that file, so tightening
# `Encryption` to require a non-empty schema set left every `stash init`
# emitting a project that fails its first `tsc` — with CI green (#772
# review). The fixtures are pinned byte-for-byte to the generator by
# `placeholder-client-fixture.test.ts`.
# Through turbo, not `pnpm run` — the fixtures import `@cipherstash/stack/v3`,
# so this needs that package BUILT. Invoked directly it passed only because
# earlier steps in this job happen to build it via their own `^build`; drop
# or reorder those (they read as guards for other packages, so they look
# independently removable) and this fails `TS2307`, which reads as "the
# scaffold is broken" rather than "you forgot to build" (#787 review).
- name: Typecheck (stash init's scaffolded client)
run: pnpm exec turbo run typecheck:scaffold --filter stash
- name: Lint — no hardcoded package-manager runners
run: pnpm run lint:runners
# The gates above only mean something if they compile source. A tsconfig
# with no `include` globs `**/*`, which sweeps the package's own `dist/`
# into the program — and whether `dist/` exists during a gate depends on
# what an earlier step built transitively, so the gate compiles a
# different program in CI than it does locally.
- name: Lint — typecheck gates compile source, not build output
run: pnpm run lint:typecheck-scope
# Deleting or renaming a package leaves `packages/<name>` references
# dangling in docs and CI config. Nothing else catches it (#760 review).
- name: Lint — no references to deleted package directories
run: pnpm run lint:package-paths
- name: Test — lint script self-tests
run: pnpm run test:scripts
- name: Create .env file in ./packages/stack/
run: |
touch ./packages/stack/.env
echo "CS_WORKSPACE_CRN=${{ vars.CS_WORKSPACE_CRN }}" >> ./packages/stack/.env
echo "CS_CLIENT_ID=${{ vars.CS_CLIENT_ID }}" >> ./packages/stack/.env
echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/stack/.env
echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/stack/.env
echo "DATABASE_URL=postgres://cipherstash:password@localhost:5432/cipherstash" >> ./packages/stack/.env
# Run TurboRepo tests
- name: Run tests
run: pnpm run test
# CLI E2E tests drive the built `dist/bin/stash.js` through a real
# pseudo-terminal via node-pty. Run via turbo so the `^build` + `build`
# deps declared on the `test:e2e` task are honored.
- name: Run CLI E2E tests
run: pnpm exec turbo run test:e2e --filter stash
e2e-tests:
name: Run E2E Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
# Auth-dependent suites in `e2e/` skip themselves unless these env vars
# are set. We expose them at the job level so the wizard subprocess
# picks them up via `process.env`.
env:
CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }}
CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }}
CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }}
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
CS_ZEROKMS_HOST: https://ap-southeast-2.aws.zerokms.cipherstashmanaged.net
CS_CTS_HOST: https://ap-southeast-2.aws.cts.cipherstashmanaged.net
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6.0.9
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6.5.0
with:
node-version: 22
cache: 'pnpm'
# node-pty's install hook falls back to `node-gyp rebuild` when no
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
# node-gyp on PATH, so install it explicitly.
- name: Install node-gyp
run: npm install -g node-gyp
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Auth-dependent `e2e/` suites skip themselves without these vars — a
# silent skip would hide regressions behind a green job. Fail loudly.
- name: Require CipherStash secrets
uses: ./.github/actions/require-cs-secrets
with:
workspace-crn: ${{ vars.CS_WORKSPACE_CRN }}
client-id: ${{ vars.CS_CLIENT_ID }}
client-key: ${{ secrets.CS_CLIENT_KEY }}
client-access-key: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
# Run the standalone `e2e/` workspace via turbo so the `^build`
# dep on the `test:e2e` task builds cli + wizard first. CLI's own
# E2E (`packages/cli/tests/e2e/**`) is covered by the `run-tests`
# job above; we filter to the new workspace here to avoid duplication.
- name: Run E2E tests
run: pnpm exec turbo run test:e2e --filter @cipherstash/e2e
# Verifies @cipherstash/stack/wasm-inline works under Deno — i.e. the
# WASM build of protect-ffi 0.26+ and auth 0.40+ can round-trip an
# encryption against ZeroKMS / CTS in a runtime with no native
# bindings available. The deno.json deliberately omits --allow-ffi so
# a silent fallback to the NAPI module is impossible.
wasm-e2e-tests:
name: Run WASM E2E Tests (Deno)
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
# CS_WORKSPACE_CRN is the single source of truth for workspace
# identity and region — the stack /wasm-inline config requires it and
# derives the AccessKeyStrategy region from it.
env:
CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }}
CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }}
CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }}
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
steps:
- name: Checkout Repo
uses: actions/checkout@v6
with:
persist-credentials: false
- uses: pnpm/action-setup@v6.0.9
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6.5.0
with:
node-version: 22
cache: 'pnpm'
# node-pty (a dev-dep of @cipherstash/cli used by its E2E PTY
# tests) falls back to `node-gyp rebuild` when no prebuild matches
# the runner, and pnpm/action-setup v6 no longer ships node-gyp on
# PATH. The WASM smoke test itself uses no native modules — this
# install only exists so the workspace-wide `pnpm install` step
# below doesn't fail.
- name: Install node-gyp
run: npm install -g node-gyp
- name: Install Deno
uses: denoland/setup-deno@v2.0.5
with:
deno-version: v2.x
- name: Install dependencies
run: pnpm install --frozen-lockfile
# The Deno smoke test imports the locally-built dist/wasm-inline.js
# via a file URL in e2e/wasm/deno.json — it needs a fresh build.
- name: Build stack
run: pnpm exec turbo run build --filter @cipherstash/stack
# The e2e/wasm suites FAIL when any of the four CS_* env vars is
# missing (requireEnv throws — no skip gating), so a rotated / cleared
# secret can't hide a real WASM regression behind a green job. This
# preflight just fails faster, before the Deno module downloads.
- name: Require CipherStash secrets
uses: ./.github/actions/require-cs-secrets
with:
workspace-crn: ${{ vars.CS_WORKSPACE_CRN }}
client-id: ${{ vars.CS_CLIENT_ID }}
client-key: ${{ secrets.CS_CLIENT_KEY }}
client-access-key: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
- name: Run Deno WASM smoke test
working-directory: e2e/wasm
run: deno task test
run-tests-bun:
name: Run Tests (Bun)
runs-on: blacksmith-4vcpu-ubuntu-2404
continue-on-error: true
services:
postgres:
image: ghcr.io/cipherstash/postgres-eql:17-2.3.1
env:
POSTGRES_USER: cipherstash
POSTGRES_PASSWORD: password
POSTGRES_DB: cipherstash
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U cipherstash -d cipherstash"
--health-interval 2s
--health-timeout 5s
--health-retries 20
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- uses: pnpm/action-setup@v6.0.9
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6.5.0
with:
node-version: 22
cache: 'pnpm'
- name: Install node-gyp
run: npm install -g node-gyp
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create .env file in ./packages/stack/
run: |
touch ./packages/stack/.env
echo "CS_WORKSPACE_CRN=${{ vars.CS_WORKSPACE_CRN }}" >> ./packages/stack/.env
echo "CS_CLIENT_ID=${{ vars.CS_CLIENT_ID }}" >> ./packages/stack/.env
echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/stack/.env
echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/stack/.env
echo "DATABASE_URL=postgres://cipherstash:password@localhost:5432/cipherstash" >> ./packages/stack/.env
# Build with Node (turbo/tsup need Node), then run tests with Bun
- name: Build packages
run: pnpm turbo build --filter './packages/*'
- name: Run tests with Bun
run: |
for dir in packages/stack; do
if [ -f "$dir/vitest.config.ts" ] || [ -f "$dir/package.json" ]; then
echo "--- Testing $dir ---"
(cd "$dir" && bunx --bun vitest run) || true
fi
done