feat(workspace): link protect-ffi and build it in the jobs that need it - #863
feat(workspace): link protect-ffi and build it in the jobs that need it#863tobyhede wants to merge 2 commits into
Conversation
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
`@cipherstash/stack` and the two adapters move from the exact pin `0.31.0` to `workspace:*`, so they resolve the in-tree copy rather than the npm tarball. This is the step the previous three set up, and it is deliberately atomic. **Why it cannot be split further.** The tarball ships `lib/`, `index.node` and `dist/wasm/**` as CONTENTS. The workspace copy ships them as BUILD OUTPUTS — `lib/` is the package `main` and is generated, and nothing produces `index.node` without cargo. So the moment this line changes, every CI job that encrypts, decrypts, or typechecks against the package needs the binding built first, and the workflow steps below have to land in the same commit. Split them and the failure is `Cannot find module '.../index.node'`, reported once per test rather than once per job. `minimumReleaseAgeExclude` drops its `@cipherstash/protect-ffi*` entry in the same commit and for the same reason: the cooldown applies to registry dependencies, and as of this line these are not registry dependencies. Leaving it would be a stale exemption for packages nothing fetches. The published surface does not change — the workspace copy is the 0.31.0 release, tree-identical to the tag. The `major` changeset for adopting 0.31.0 is in the first PR of this stack, where the breaking change actually is.
The other half of the workspace link. `.github/actions/build-ffi-binding` compiles the binding and proves it loads, and every job that reaches encryption now runs it before its tests: `tests.yml`'s unit, e2e and bun jobs, the three integration suites, and both Prisma e2e workflows. The action caches `index.node` on a content hash of the Rust inputs, so a PR touching no Rust pays a restore rather than a compile. `wasm: 'true'` is passed by exactly two jobs — the ones that load the real WASM. 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. **Ordering is load-bearing in two places, and neither looks it.** `require-cs-secrets` must precede `build-ffi-binding`. The secrets check reads four inputs and fails in seconds; a cold binding build costs minutes, and more with `wasm: 'true'`. Put the build first and a job with a rotated credential pays the whole compile before learning it could never have encrypted anything — which is the same as having no pre-flight. Both steps are self-contained `uses:` blocks and swapping them changes nothing about whether the job passes, so `ffi-binding-step-order.test.mjs` checks it by position across every workflow, discovered rather than listed. Note the direction of the fix when it fails: in `wasm-e2e-tests` a `Build stack` step sits between them and consumes `dist/wasm`, so there the pre-flight moves UP, not the build DOWN. In `wasm-e2e-tests`, `Build stack` must follow the binding build, because it now consumes protect-ffi's `dist/wasm` output from the workspace copy rather than from a tarball. `test:typecheck:wasm` returns here, with the job that runs it. It needs `dist/wasm`, so it cannot hang off the default `test`, and `lintWiring`'s exemption list only accepts a carve-out whose name appears in a root workflow — which is why the script was held back until this commit rather than shipped with the package and a promise. The no-caching gate gains a live citation rather than a fixture: `build-ffi-binding` restores two GitHub Actions caches, and its header says publishing workflows must not use it. The test asserts the caching is really there before asserting the gate flags it, so removing the caching fails the test — which is the prompt to fix the header too, rather than leaving a check that passes because it now proves nothing. No changeset: CI wiring, no published surface. A changeset naming any protect-ffi package would fail `scripts/lint-no-ffi-changeset.mjs` regardless — publishing does not move until the trusted-publishing cutover.
e0d242e to
98aee99
Compare
freshtonic
left a comment
There was a problem hiding this comment.
Review: APPROVE
Purely CI-wiring + internal dependency linking (stack 4 of 4). Consumers move @cipherstash/protect-ffi from the 0.31.0 pin to workspace:*, and every job that encrypts/decrypts/typechecks against it gains build-ffi-binding. Reviewed in context against the base branch; ran everything runnable locally.
Verification performed
pnpm install --frozen-lockfile— clean, lockfile in sync.pnpm run test:scripts— 295 passing, incl. newffi-binding-step-order.test.mjs(9) and the updatedlint-no-workflow-cachingsuite.- protect-ffi
lintWiring.test.ts— 12 passing;pnpm run lint:ffi-changeset— passes (no protect-ffi changeset in the PR). pnpm run code:check(biome) — clean for the PR's files.EXPECTED_PAIRED_JOBSmatches ground truth exactly (7 files, 8 paired jobs).wasm:flags cross-checked against each job'sCS_IT_SUITE: drizzlewasm:'true'(includesintegration/wasm/**) ✓, supabase native-only ✓, prisma-next native-only ✓.wasm-e2e-testsreorder verified:require-cs-secrets→build-ffi-binding(wasm)→test:typecheck:wasm→Build stack→ Deno. Preflight correctly moves up above the builds.
Blocking
None.
Nits (non-blocking)
prisma-example-readme-e2e.ymlcomment says the binding build "is only ever exercised by the push-to-main run", but the protect-ffi source paths were added to bothpushandpull_requestfilters, so a PR touchingpackages/protect-ffi/**triggers it too. Behaviour is correct/more coverage;prisma-next-e2e.yml's wording ("on push to main and on any PR touching its paths") is the accurate version — consider aligning.EXPECTED_PAIRED_JOBSis a hand-maintained list. Deliberate and well-documented ("guard on the scan"), just flagging that future job renames must touch it.
Highlights
ffi-binding-step-order.test.mjsis exemplary — enforces an invisible ordering invariant, discovers paired jobs rather than listing them, guards the scan against vacuous-pass with a mutation-tested floor, and its failure messages teach the correct direction of the fix.- The caching-gate test uses a live citation (
build-ffi-binding/action.yml) and asserts the caching exists before asserting the gate flags it, so it can't rot into proving nothing. build-ffi-bindingverifies the binding actually loads (assertNativeBindingAvailable()+ wasm-inline probe), failing at the named artifact instead of as scattered downstream encryption errors.workspace:*resolves to the same0.31.0the consumers were pinned to, so the "no published surface / no changeset" reasoning holds. No skills impact — no public API/CLI/workflow surface changed.
Solid, exceptionally well-documented work. Approving.
Stack 4 of 4 — splitting #858. Base: #862.
packages/protect-ffiand wire its own CIWhat
@cipherstash/stackand the two adapters move from the exact pin0.31.0toworkspace:*, and the CI jobs that reach encryption gain.github/actions/build-ffi-binding. Review found two that did not — see Known gap below.The same commit also wires
lint:ffi-changesetintotests.yml. That guard has existed since #862 and nothing ran it, which is the failure mode the absorption keeps tripping over: a check nothing invokes reads exactly like a check that passes.Why the two halves cannot be split
The npm tarball ships
lib/,index.nodeanddist/wasm/**as contents. The workspace copy ships them as build outputs —lib/is the packagemainand is generated, and nothing producesindex.nodewithout cargo. The moment the dependency line changes, every job that encrypts, decrypts or typechecks against the package needs the binding built first. Split them and the failure isCannot find module '.../index.node', reported once per test rather than once per job.minimumReleaseAgeExcludedrops its@cipherstash/protect-ffi*entry in the same commit and for the same reason: the cooldown applies to registry dependencies, and as of this line these are not registry dependencies.Ordering is load-bearing in two places, and neither looks it
require-cs-secretsmust precedebuild-ffi-binding. The secrets check costs seconds; a cold binding build costs minutes, more withwasm: 'true'. Build first and a job with a rotated credential pays the whole compile before learning it could never have encrypted anything. Both are self-containeduses:blocks and swapping them changes nothing about whether the job passes — henceffi-binding-step-order.test.mjs, checking by position across every workflow, discovered rather than listed. Note the direction of the fix if it fails: inwasm-e2e-testsaBuild stackstep sits between them and consumesdist/wasm, so there the pre-flight moves up, not the build down.Build stackmust follow the binding build inwasm-e2e-tests, because it now consumes protect-ffi'sdist/wasmfrom the workspace copy rather than from a tarball.test:typecheck:wasmreturns here with the job that runs it. It needsdist/wasmso it cannot hang off the defaulttest, andlintWiring's exemption list only accepts a carve-out whose name appears in a root workflow — so the script was held back rather than shipped with a promise.The no-caching gate gains a live citation rather than a fixture:
build-ffi-bindingrestores two Actions caches and its header says publishing workflows must not use it. The test asserts the caching is really there before asserting the gate flags it, so removing the caching fails the test — the prompt to fix the header too, rather than leaving a check that proves nothing.wasm: 'true'is the minority caseAcross the repo three jobs pass it —
integration-drizzle,integration-protect-ffi(from #862), andtests.yml'swasm-e2e-tests— because those are the ones that load the real WASM build; it costs a second cargo build against wasm32.98aee99a's commit message says "exactly two", counting only the jobs that commit touched.AGENTS.mdnow states the rule rather than a number, since the number drifts and the rule does not.Known gap
Review found two credentialed jobs in
tests.ymlthat reach live encryption without building the binding:e2e-tests— runsturbo run test:e2ewith no file filter, soe2e/vitest.config.ts'stests/**/*.e2e.test.tspicks upprisma-example-readme.e2e.test.ts. That suite isdescribe.skipIf(!authConfigured)and un-skips wheneverCS_CLIENT_IDandCS_CLIENT_KEYare set, which this job sets at job level. It drives the README'spnpm startthroughnewClient, and turbo's^buildreaches protect-ffi'stscbut neverindex.node.run-tests-bun— runspackages/stackvitest against live suites;continue-on-error: trueplus|| truemeans it stays green, so it cannot report the breakage it hits.Fix in progress, not in this push: a coverage assertion beside the ordering one in
ffi-binding-step-order.test.mjs, discovering credentialed jobs by scanning the workflow directory rather than by list —run-tests-bunis only visible that way, since its credentials appear solely inside arun:heredoc.Verification
The head of this stack is byte-identical to #858 across all 311 files the absorption touched (verified per-file by blob hash). Main has moved 8 commits ahead since #858 branched; those 29 files are disjoint from the absorption's 311, so nothing of main's newer work is reverted.
Scripts suite 295 passing; protect-ffi's JS chain passing; supply-chain e2e 21 passing; biome 0 errors; lockfile in sync.
No changeset: CI wiring, no published surface — and a changeset naming any protect-ffi package would fail
lint-no-ffi-changeset.mjsregardless.