Skip to content

chore(release): retire the FFI publishing guard and activate its parked changesets #2455

chore(release): retire the FFI publishing guard and activate its parked changesets

chore(release): retire the FFI publishing guard and activate its parked changesets #2455

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.10
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.10
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.
# Before the binding build below, not after: this costs seconds and that
# costs minutes on a cold cache.
- 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 }}
# `pnpm run test` below runs the `packages/stack` suites, and only 8 of
# those 120 files mock `@cipherstash/protect-ffi` — the rest load
# `index.node`, which nothing else in this job produces.
#
# NOT for protect-ffi's own suite, though vendoring it did add
# `@cipherstash/protect-ffi#test` to `turbo test --filter './packages/*'`.
# That suite must pass with no binding at all (its `build` is `tsc` and
# its `test` is deliberately cargo-free), and `src/lintWiring.test.ts`
# re-runs it here with the artifact hidden to keep that true — this job
# having a binding is exactly why that nested run is the only place the
# artifact-free path gets exercised in CI.
#
# The type steps below need `lib/`: they call package scripts directly,
# so turbo's `^build` never runs and protect-ffi's declarations resolve
# to nothing. No `wasm: true` — every `wasm-inline` unit test either
# mocks the module or asserts on the bundle graph.
- name: Build the protect-ffi binding
uses: ./.github/actions/build-ffi-binding
- 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/stack-prisma 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-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.10
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 }}
# `e2e/vitest.config.ts` includes `tests/**/*.e2e.test.ts` and the step
# below applies no file filter, so this job runs
# `tests/prisma-example-readme.e2e.test.ts` — whose
# `describe.skipIf(!authConfigured)` un-skips the moment CS_CLIENT_ID and
# CS_CLIENT_KEY are set, which the job env above does. That walkthrough's
# `pnpm start` step encrypts against the live service through
# `@cipherstash/stack`, so it needs `index.node`; turbo's `^build` only
# gets as far as protect-ffi's `build` (tsc → `lib/`), which is the half
# that does not encrypt anything.
#
# After the pre-flight above, per
# scripts/__tests__/ffi-binding-step-order.test.mjs. No `wasm: true` —
# nothing in `e2e/tests/**` loads the WASM build (the Deno smoke test that
# does lives in `e2e/wasm/` and runs in `wasm-e2e-tests`).
- name: Build the protect-ffi binding
uses: ./.github/actions/build-ffi-binding
# 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.10
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 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 two builds below and the Deno
# module downloads.
#
# It is the preflight that moves above the binding build here, not the
# binding build that moves below it: `Build stack` consumes
# protect-ffi's dist/wasm output, so the two build steps have to stay in
# this order.
- 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 }}
# `wasm: true` — this is the job the WASM output exists for. stack's
# dist/wasm-inline.js imports protect-ffi's dist/wasm/protect_ffi_inline.js
# by relative path, and only the three .d.ts under dist/wasm are tracked;
# the .js and .wasm come from wasm-pack and nothing else in CI runs it.
- name: Build the protect-ffi binding
uses: ./.github/actions/build-ffi-binding
with:
wasm: 'true'
# The only job that can run this: the type tests read the GENERATED
# dist/wasm/*.d.ts (wasm-bindgen emits them from the `typescript_type`
# attributes in crates/protect-ffi/src/wasm.rs) and those declarations
# import ../../lib/types.js, so it needs both halves of the step above.
# That is why it is exempt from protect-ffi's `test` entry point, which
# must pass in a fresh clone with no dist/ — and the exemption in
# src/lintWiring.test.ts asserts this step exists, because between the
# absorption and this line the check ran nowhere at all: the jobs its
# exemption named were the upstream copies under
# packages/protect-ffi/.github/, which GitHub never executes.
- name: Typecheck the generated WASM declarations
run: pnpm --filter @cipherstash/protect-ffi run test:typecheck:wasm
# 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
- 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.10
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
# Fail loudly if any live-test credential is missing. The `.env` written
# below feeds the same live suites `run-tests` runs, and this job hides
# its own failures twice over (`continue-on-error: true` on the job, `||
# true` around the vitest call), so a rotated secret would otherwise show
# up as nothing at all.
# Before the binding build, not after: this costs seconds and that costs
# minutes on a cold cache.
- 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 }}
# The `packages/stack` suites below are the live ones — 120 files, of
# which only 8 mock `@cipherstash/protect-ffi` — so they load
# `index.node`, and the `Build packages` step below does not produce it
# (protect-ffi's `build` is `tsc`, deliberately cargo-free). No
# `wasm: true`: every `wasm-inline` unit test resolves the stubs in
# `vitest.shared.ts` or asserts on the bundle graph, same as `run-tests`.
#
# It shares the native cache key with `run-tests`, so in the normal case
# this is a restore rather than a compile.
- name: Build the protect-ffi binding
uses: ./.github/actions/build-ffi-binding
- 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