Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,14 +246,6 @@ jobs:
- name: Lint — no references to deleted package directories
run: pnpm run lint:package-paths

# TEMPORARY — delete with the script in the phase-4 cutover PR.
# The seven protect-ffi packages live here but still publish from
# cipherstash/protectjs-ffi. A changeset naming any of them bumps all
# seven through their fixed group and the next release tries a publish
# npm has not been repointed to trust.
- name: Lint — no protect-ffi changeset before the publishing cutover
run: pnpm run lint:ffi-changeset

- name: Test — lint script self-tests
run: pnpm run test:scripts

Expand Down
46 changes: 25 additions & 21 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,30 +134,34 @@ so that stays true for everyone else.
Rust. Everything else under `dist/` stays ignored. The re-inclusion chain
spans the root `.gitignore`, the package's own, and a `.gitignore` wasm-pack
generates — see the comments in each.
- **Publishing has not moved yet.** All seven packages are still published from
`cipherstash/protectjs-ffi` until npm trusted publishing is repointed, so a
changeset naming any of them fails CI (`scripts/lint-no-ffi-changeset.mjs`).
Change the package freely — but write the changeset and park it as
`.changeset/<name>.md.deferred`, don't skip it. Changesets and the guard both
select on `.endsWith('.md')`, so that extension is inert to
`changeset version`; the cutover PR renames **every** one of them back
(`for f in .changeset/*.md.deferred; do git mv "$f" "${f%.deferred}"; done`).
Check what is parked rather than assuming a single file — `ls
.changeset/*.md.deferred`. Two are waiting today: the lazy native load, and
the manifest repoint to `cipherstash/stack`.
- **The pipeline that will publish them is built and inert.** `release.yml`
asks `scripts/release-gate.mjs` which committed versions are missing from npm;
if any FFI one is, `_build-ffi-artifacts.yml` compiles the six platforms with
an explicit `CARGO_BUILD_TARGET` each, packs all seven tarballs, and
- **Publishing has moved here.** npm trusted publishing for all seven packages
is repointed at this repo, bound to `release.yml`, so write changesets for
them normally. Nothing has actually published from here yet — the first FFI
release is still ahead, and until it lands treat the path as configured rather
than proven. The remaining steps and what is still unverified live in
`docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md`, Phase 4.
- **A `.md.deferred` changeset is now inert, not a CI failure.** The parking
convention and the `lint-no-ffi-changeset` guard that enforced it are both
gone. If you find such a file, it was written on a branch cut before the
cutover: `git mv` it back to `.md`, or the change it describes ships with no
changelog entry. Nothing detects one for you.
- **The pipeline that publishes them.** `release.yml` asks
`scripts/release-gate.mjs` which committed versions are missing from npm; if
any FFI one is, `_build-ffi-artifacts.yml` compiles the six platforms with an
explicit `CARGO_BUILD_TARGET` each, packs all seven tarballs, and
`publish-ffi` publishes the six platform packages **before** the wrapper and
tags all seven — because `changeset publish` packs from the workspace, where
`index.node` does not exist, and tags only what it published itself. Nothing
fires until a version is unpublished, which the changeset guard above
prevents. `ffi-preflight.yml` is the dry run (`changeset publish` has no
`--dry-run`); dispatch it against the Version Packages branch before the
cutover. The seven manifests already name `cipherstash/stack`, which npm
requires of the publishing repository — so a publish attempted from the old
repository would now be rejected, and nothing publishes from there.
fires unless a committed version is absent from the registry, so a push that
bumps nothing is a no-op for all seven. `ffi-preflight.yml` is the dry run
(`changeset publish` has no `--dry-run`); dispatch it against the Version
Packages branch before merging a release that moves an FFI version.
- **Trusted publishing binds to (repository, workflow filename).** Keep
`release.yml` as the single npm entry point; a rename silently invalidates all
seven publisher configurations. Each one must also list `npm publish` under
**Allowed actions** — npm made that field required for configurations created
after 2026-05-20, and a stage-only setting reads as configured while failing
every `npm publish`. Check with `npm trust list <pkg>`.

### The `integration-tests/` suite

Expand Down
55 changes: 38 additions & 17 deletions docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,19 @@

**Working-tree state is not part of this plan's guarantees.** An earlier revision claimed "working tree clean"; that was true when written and false shortly after. A prior rewrite of this document was lost by being left uncommitted across a branch switch — **commit plan edits in the session that makes them.**

**Phase 3 is built; phases 4 and 5 remain.** Phase 4 contains the only
irreversible steps and requires seven manual npmjs.com changes. Phase 5 is
blocked until phase 4 publishes.

The pipeline is inert until a version is unpublished, and
`scripts/lint-no-ffi-changeset.mjs` is what keeps that from happening early: an
FFI changeset stays parked as `.changeset/<name>.md.deferred` until the cutover
PR renames it. Two are waiting — `protect-ffi-lazy-load.md.deferred` and
`protect-ffi-repository-url.md.deferred`.
**Phases 3 and 5 are built; phase 4 is in flight.** Phase 5 (`stash doctor`)
landed ahead of phase 4 rather than behind it — see #883; the "blocked until
phase 4 publishes" sequencing an earlier revision asserted did not hold, because
the diagnostics subpath probes the binding in the workspace and needs no
release.

Phase 4 contains the only irreversible steps. Its guard —
`scripts/lint-no-ffi-changeset.mjs`, which held FFI changesets parked as
`.changeset/<name>.md.deferred` — is **deleted** as of the cutover PR, and the
two files it was holding (`protect-ffi-lazy-load`, `protect-ffi-repository-url`)
are renamed back and live. What remains is the release itself: pre-flight
against the versioned ref, merge Version Packages, verify, archive the old
repository.

### Phase 3 progress

Expand Down Expand Up @@ -149,7 +153,7 @@ error The package "@cipherstash/stack" depends on the skipped package
Please add "@cipherstash/stack" to the `ignore` option.
```

An ignored package's dependents must also be ignored, cascading through the Stack fixed group to a total release freeze — the alternative this plan rejected. Replaced by `scripts/lint-no-ffi-changeset.mjs`. All seven packages are already on npm at `0.31.0`, and `changeset publish` only publishes versions absent from the registry, so a release is *already* a no-op for them. Full analysis: `.work/2026-08-04-protect-ffi-changesets-ignore-analysis.md`.
An ignored package's dependents must also be ignored, cascading through the Stack fixed group to a total release freeze — the alternative this plan rejected. Replaced for the duration of the cutover window by `scripts/lint-no-ffi-changeset.mjs`, deleted in Phase 4. All seven packages were already on npm at `0.31.0`, and `changeset publish` only publishes versions absent from the registry, so a release was *already* a no-op for them. Full analysis: `.work/2026-08-04-protect-ffi-changesets-ignore-analysis.md`.

### `optionalDependencies` were never tracked

Expand Down Expand Up @@ -1861,26 +1865,43 @@ git commit -m "ci: run the Rust checks from a root path-filtered workflow"

The only irreversible steps.

- [ ] Merge a cutover PR that deletes `scripts/lint-no-ffi-changeset.mjs`, its self-test, its fixtures, the `lint:ffi-changeset` script and the `tests.yml` step; **and** activates the deferred `@cipherstash/protect-ffi` **minor** changeset for the laziness change and `assertNativeBindingAvailable()` — it is already written and parked, so this half is a rename, not composition:
- [x] Merge a cutover PR that deletes `scripts/lint-no-ffi-changeset.mjs`, its self-test, its fixtures, the `lint:ffi-changeset` script and the `tests.yml` step; **and** activates the deferred `@cipherstash/protect-ffi` **minor** changeset for the laziness change and `assertNativeBindingAvailable()` — it is already written and parked, so this half is a rename, not composition:

```bash
for f in .changeset/*.md.deferred; do git mv "$f" "${f%.deferred}"; done
```

Both halves in one PR — the guard exists to stop that changeset landing early. The `.md.deferred` extension is what makes parking safe: `@changesets/read` and the guard both select on `.endsWith('.md')`, so the file is inert to `changeset version`/`publish` until renamed. Check for more than one parked file — any protect-ffi change landing during the window parks its changeset the same way.
- [ ] Let the Version Packages job create the release PR. Verify it bumps all seven FFI packages to `0.32.0`, rewrites the wrapper's six `optionalDependencies`, and patch-bumps the six Stack packages (expected — see "Release lines are coupled by pinning").

Done in the PR that ticks this box. Two parked files were renamed, not one: `protect-ffi-lazy-load.md` (minor) and `protect-ffi-repository-url.md` (patch).

Six fixtures under `scripts/__tests__/fixtures/lint-no-ffi-changeset/` were deleted with the self-test. An earlier revision of this line claimed there were none, on the strength of a check against `scripts/fixtures/` — the wrong directory. The self-test used a tmpdir for two cases only (CRLF, and the parked-file case); the rest loaded these on-disk fixtures through ``resolve(…, `../fixtures/lint-no-ffi-changeset/${name}`)``. Deleting the test without them leaves six files no referent points at, and `pnpm run test:scripts` stays green either way — which is exactly why it needed checking rather than inferring.

**A third changeset is parked on the branch of #905** (the `jsonwebtoken` CVE bump); once this lands, nothing reads or warns about that extension, so #905 must rename its own file before merging or the fix ships with an empty changelog.
- [ ] Let the Version Packages job create the release PR. Verify it bumps all seven FFI packages to `0.32.0` and rewrites the wrapper's six `optionalDependencies`.

**Do not expect a Stack patch.** This criterion originally read "patch-bumps the six Stack packages", which follows from the pinning analysis and is the right prediction for an FFI bump in isolation. It is not what will happen: `.changeset/prisma-next-0-17.md` carries `'@cipherstash/stack-prisma': major`, which propagates through the Stack fixed group, so `changeset status` reports all six Stack packages at **major** — and does so on `origin/main` too, with no FFI changeset in play. The Stack major is unrelated to this phase and must not be read as evidence the FFI bump misbehaved.
- [ ] Run `ffi-preflight.yml` against that **versioned release-PR ref**.
- [ ] **Repoint npm trusted publishing for all seven packages**: `cipherstash/protectjs-ffi` → `cipherstash/stack`, workflow `release.yml`. For each publisher, **explicitly select `npm publish` under "Allowed actions"** — npm made that field required for configurations created after 2026-05-20, and these are new configurations. Confirm `repository.url` already reads `cipherstash/stack` (Task 2) or the publish is rejected. Only after the versioned pre-flight is green.
- [x] **Repoint npm trusted publishing for all seven packages**: `cipherstash/protectjs-ffi` → `cipherstash/stack`, workflow `release.yml`. For each publisher, **explicitly select `npm publish` under "Allowed actions"** — npm made that field required for configurations created after 2026-05-20, and these are new configurations. Confirm `repository.url` already reads `cipherstash/stack` (Task 2) or the publish is rejected. Only after the versioned pre-flight is green.

Done ahead of the pre-flight rather than after it, which inverts the "only after the versioned pre-flight is green" sequencing above. That ordering was about not repointing until the pipeline was known good; the pipeline is built and its jobs have run, so the residual risk is a misconfigured publisher rather than a broken workflow.

**The Allowed-actions setting is not verified.** npm accepts a publisher scoped to `npm stage publish` only — a configuration that reads as enabled in the UI and fails every `npm publish`. Check all seven before the release, six platform packages included, since `publish-ffi` publishes those first and a stage-only setting on one of six fails a release halfway through:

```bash
npm trust list @cipherstash/protect-ffi --json
# …and the six @cipherstash/protect-ffi-<platform> packages
```
- [ ] Merge the Version Packages PR. The gate returns `ffi=true js=true`; artifacts build, six platform packages then the wrapper publish, tags and the GitHub release are created, and `changeset publish` skips the seven and publishes the JS packages.
- [ ] Verify npm provenance on all seven, the seven git tags, the `protect-ffi-v0.32.0` release, and the Stack tags from changesets. Smoke-test a fresh install.
- [ ] Archive `cipherstash/protectjs-ffi`.

## Phase 5 — wire `stash doctor`

- [ ] Add a `@cipherstash/stack/diagnostics` subpath (both `import` and `require`). It must import protect-ffi **without** importing `@cipherstash/auth`, be pure, and let the loader error propagate unwrapped.
- [ ] Rework `packages/cli/src/commands/doctor/index.ts` to probe it, keeping the separate `@cipherstash/auth` probe. This fixes a pre-existing bug: `dist/index.js` statically imports `@cipherstash/auth`, which is eager on two counts (top-level `require`, plus `module.exports = { ...native }` — a spread forces any loader), so today's stack probe silently duplicates the auth probe while rendering two green rows.
- [ ] Add the missing-binary e2e fixture.
- [ ] Add changesets for the new Stack subpath and the CLI diagnostic behaviour.
- [x] Add a `@cipherstash/stack/diagnostics` subpath (both `import` and `require`). It must import protect-ffi **without** importing `@cipherstash/auth`, be pure, and let the loader error propagate unwrapped.
- [x] Rework `packages/cli/src/commands/doctor/index.ts` to probe it, keeping the separate `@cipherstash/auth` probe. This fixes a pre-existing bug: `dist/index.js` statically imports `@cipherstash/auth`, which is eager on two counts (top-level `require`, plus `module.exports = { ...native }` — a spread forces any loader), so today's stack probe silently duplicates the auth probe while rendering two green rows.
- [x] Add the missing-binary e2e fixture.
- [x] Add changesets for the new Stack subpath and the CLI diagnostic behaviour.

---

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"clean": "rimraf --glob **/.next **/.turbo **/dist **/node_modules",
"code:fix": "biome check --write",
"code:check": "biome check",
"lint:ffi-changeset": "node scripts/lint-no-ffi-changeset.mjs",
"lint:package-paths": "node scripts/lint-no-dead-package-paths.mjs",
"lint:runners": "node scripts/lint-no-hardcoded-runners.mjs",
"lint:typecheck-scope": "node scripts/lint-typecheck-scope.mjs",
Expand Down
19 changes: 7 additions & 12 deletions packages/protect-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,18 +379,13 @@ packages. The changeset body becomes the `CHANGELOG.md` entry — Changesets own
that file now, so the `[Unreleased]` heading and the `version` npm lifecycle
hook that used to promote it are gone.

**During the publishing cutover, a protect-ffi changeset is blocked.** All seven
packages are already on npm at the version this workspace carries, and npm
trusted publishing still names `cipherstash/protectjs-ffi` as the publisher. A
changeset naming any of them would bump all seven and send a publish npm rejects
or misattributes, so `scripts/lint-no-ffi-changeset.mjs` fails CI on one. Change
this package freely — only the changeset waits.

Write it anyway, and park it as `.changeset/<name>.md.deferred`. Changesets
selects changesets by `.endsWith('.md')`, so that extension is invisible to
`changeset version` and `changeset publish` (and to the guard), and the cutover
PR that repoints trusted publishing renames it back rather than reconstructing
it from the git log.
npm trusted publishing for all seven packages names `cipherstash/stack` and the
repository-root `release.yml`, so a changeset here releases like any other
package. The temporary guard that blocked one during the cutover window, and the
`.md.deferred` convention for parking one behind it, are both gone.

No FFI release has run from this repository yet — 0.31.0 was published from the
old one. Until the first one lands, the path is configured rather than proven.

The previous repository's GitHub Actions workflows were deposited under
`.github/` in this directory by the subtree import and kept as the reference for
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading