From e77bfcec036964fde53e7dcbfa9ef86827007570 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 19 Aug 2026 08:39:48 +1000 Subject: [PATCH 1/3] chore(release): retire the FFI publishing guard and activate its parked changesets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm trusted publishing for all seven `@cipherstash/protect-ffi` packages now names `cipherstash/stack` and `release.yml`, so the window the guard existed to protect is closed. `scripts/lint-no-ffi-changeset.mjs` failed CI on any changeset naming an FFI package, because such a changeset bumps all seven through their fixed group and the next release would then attempt a publish npm still attributed to `cipherstash/protectjs-ffi`. Delete it, its self-test, the `lint:ffi-changeset` script and the `tests.yml` step that ran it. There were no fixtures to delete — the self-test wrote its own to a tmpdir, so the plan's "its fixtures" was always wrong. The other half is the point of the guard: rename the two parked changesets back, so the behaviour they describe ships with a changelog entry instead of being reconstructed from the git log months later. - `protect-ffi-lazy-load.md` (minor) — the native binding loads lazily, plus `assertNativeBindingAvailable()` - `protect-ffi-repository-url.md` (patch) — published metadata names `cipherstash/stack` `changeset status` now reports all seven FFI packages at minor, as CIP-3719 step 2 expects. A third changeset is parked on the branch of #905, the `jsonwebtoken` CVE bump. Once this lands nothing reads `.md.deferred` and nothing warns about it, so #905 must rename its own file before merging or the fix ships with an empty changelog. CIP-3719 --- ...d.md.deferred => protect-ffi-lazy-load.md} | 0 ...deferred => protect-ffi-repository-url.md} | 0 .github/workflows/tests.yml | 8 - AGENTS.md | 42 ++-- ...6-08-04-protect-ffi-monorepo-absorption.md | 26 +- package.json | 1 - packages/protect-ffi/README.md | 16 +- .../__tests__/lint-no-ffi-changeset.test.mjs | 235 ------------------ scripts/lint-no-ffi-changeset.mjs | 145 ----------- 9 files changed, 41 insertions(+), 432 deletions(-) rename .changeset/{protect-ffi-lazy-load.md.deferred => protect-ffi-lazy-load.md} (100%) rename .changeset/{protect-ffi-repository-url.md.deferred => protect-ffi-repository-url.md} (100%) delete mode 100644 scripts/__tests__/lint-no-ffi-changeset.test.mjs delete mode 100644 scripts/lint-no-ffi-changeset.mjs diff --git a/.changeset/protect-ffi-lazy-load.md.deferred b/.changeset/protect-ffi-lazy-load.md similarity index 100% rename from .changeset/protect-ffi-lazy-load.md.deferred rename to .changeset/protect-ffi-lazy-load.md diff --git a/.changeset/protect-ffi-repository-url.md.deferred b/.changeset/protect-ffi-repository-url.md similarity index 100% rename from .changeset/protect-ffi-repository-url.md.deferred rename to .changeset/protect-ffi-repository-url.md diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 435c113ef..5d94ad044 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index c040b78bb..13cf6834d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,30 +134,30 @@ 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/.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.** All seven packages publish from this repo, via + npm trusted publishing bound to `release.yml`. Write changesets for them + normally. The `.md.deferred` parking convention and the + `lint-no-ffi-changeset` guard that enforced it are both gone — if you find a + `.md.deferred` file anywhere, it is a leftover from a branch cut before this + PR and needs renaming back, because nothing reads that extension and nothing + warns about it any more. +- **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 `. ### The `integration-tests/` suite diff --git a/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md b/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md index 003637198..50d573c35 100644 --- a/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md +++ b/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md @@ -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/.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/.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 @@ -1861,13 +1865,15 @@ 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, 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. + + 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). There were no fixtures to delete — the self-test wrote its own to a tmpdir, so the "its fixtures" in the line above was always wrong. **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`, rewrites the wrapper's six `optionalDependencies`, and patch-bumps the six Stack packages (expected — see "Release lines are coupled by pinning"). - [ ] 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. diff --git a/package.json b/package.json index 214dde32f..f6de0a5ef 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/protect-ffi/README.md b/packages/protect-ffi/README.md index 021f4f8b4..696043e10 100644 --- a/packages/protect-ffi/README.md +++ b/packages/protect-ffi/README.md @@ -379,18 +379,10 @@ 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/.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. The previous repository's GitHub Actions workflows were deposited under `.github/` in this directory by the subtree import and kept as the reference for diff --git a/scripts/__tests__/lint-no-ffi-changeset.test.mjs b/scripts/__tests__/lint-no-ffi-changeset.test.mjs deleted file mode 100644 index 149d0d89b..000000000 --- a/scripts/__tests__/lint-no-ffi-changeset.test.mjs +++ /dev/null @@ -1,235 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { - mkdtempSync, - readdirSync, - readFileSync, - rmSync, - writeFileSync, -} from 'node:fs' -import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' -import { afterAll, describe, expect, it } from 'vitest' -import { REPO_ROOT } from './lib/repo-root.mjs' - -const SCRIPT = resolve( - fileURLToPath(import.meta.url), - '../../lint-no-ffi-changeset.mjs', -) -function run(dir) { - try { - const stdout = execFileSync('node', dir ? [SCRIPT, dir] : [SCRIPT], { - encoding: 'utf8', - }) - return { exitCode: 0, output: stdout } - } catch (err) { - return { - exitCode: err.status, - output: String(err.stdout) + String(err.stderr), - } - } -} - -const fx = (name) => - resolve( - fileURLToPath(import.meta.url), - `../fixtures/lint-no-ffi-changeset/${name}`, - ) - -const tempDirs = [] -afterAll(() => { - for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true }) -}) - -describe('lint-no-ffi-changeset', () => { - it('passes against the real .changeset directory', () => { - // The whole point of the guard: until the phase-4 cutover, no pending - // changeset may name an FFI package. If this fails on `main`, the window - // invariant has already been broken. - expect(run().exitCode).toBe(0) - }) - - it('passes on changesets that name no FFI package', () => { - expect(run(fx('clean')).exitCode).toBe(0) - }) - - it('does not parse README.md as a changeset', () => { - // `.changeset/README.md` ships with changesets itself and is not a - // changeset; parsing it would be a false positive waiting to happen. - // - // The fixture README carries guarded frontmatter deliberately. Without it - // this assertion held whether or not the skip existed — a README with no - // frontmatter yields no package names either way, so the test passed by - // describing the fixture rather than the behaviour. - const { exitCode, output } = run(fx('clean')) - expect(exitCode).toBe(0) - expect(output).not.toMatch(/README/) - }) - - it('fails when a changeset names the wrapper', () => { - const { exitCode, output } = run(fx('offending')) - expect(exitCode).toBe(1) - expect(output).toMatch('@cipherstash/protect-ffi') - expect(output).toMatch('brave-lion-jump.md') - }) - - it('fails when a changeset names a platform package', () => { - // Named directly rather than via the wrapper — the fixed group means one - // is as publishing as the other. - expect(run(fx('offending')).output).toMatch( - '@cipherstash/protect-ffi-linux-x64-musl', - ) - }) - - it('reports every offending file, not just the first', () => { - const { output } = run(fx('offending')) - expect(output).toMatch('brave-lion-jump.md') - expect(output).toMatch('quiet-moth-wait.md') - }) - - it('catches an FFI package named on any frontmatter line, not just the first', () => { - // The likeliest real offender by some distance: one `pnpm changeset` run - // that selects the package you changed AND protect-ffi, which writes both - // into a single block. Every other fixture here names exactly one package - // on line one, so a parser that read only the first line of frontmatter - // passed this whole suite — mutation-tested by appending `.slice(0, 1)` - // to the frontmatter split: 10/10 still green. `darwin-arm64` appears in - // no other fixture, so matching it proves the second line was read. - const { exitCode, output } = run(fx('offending')) - expect(exitCode).toBe(1) - expect(output).toMatch('@cipherstash/protect-ffi-darwin-arm64') - expect(output).toMatch('wise-crane-list.md') - }) - - it('parses a changeset checked out with CRLF line endings', () => { - // `packagesIn` spells its line breaks `\r?\n` in both the frontmatter - // regex and the split — deliberate, because a Windows checkout with - // `core.autocrlf=true` yields CRLF, and this repo has no `.gitattributes` - // forcing otherwise. Nothing exercised it: dropping both `\r?` left the - // suite green, and the guard would then wave through every changeset - // written on Windows. - // - // Generated rather than committed for the same reason it needs testing — - // a CRLF fixture in git is one `autocrlf=true` commit away from being - // silently normalised to LF, which would disarm this test without a diff. - const dir = mkdtempSync(join(tmpdir(), 'ffi-changeset-crlf-')) - tempDirs.push(dir) - writeFileSync( - join(dir, 'tidy-vole-climb.md'), - "---\r\n'@cipherstash/stack': patch\r\n'@cipherstash/protect-ffi-linux-arm64-gnu': patch\r\n---\r\n\r\nWritten on Windows.\r\n", - ) - - const { exitCode, output } = run(dir) - expect(exitCode).toBe(1) - expect(output).toMatch('@cipherstash/protect-ffi-linux-arm64-gnu') - }) - - it('ignores an FFI package named only in the prose body', () => { - // A Stack changeset describing the 0.31 adoption necessarily mentions - // protect-ffi in its text, and may quote frontmatter to show a shape. - // Only the first fenced block is frontmatter. - expect(run(fx('prose-mention')).exitCode).toBe(0) - }) - - it('explains that the changeset should wait, not the change', () => { - // The failure a reader hits is "I changed Rust and CI went red". The - // message has to distinguish those two things or it reads as a ban on - // touching the package. - const { output } = run(fx('offending')) - expect(output).toMatch(/cutover PR/) - expect(output).toMatch(/Change protect-ffi freely/) - }) - - it('skips a parked `.md.deferred` changeset naming an FFI package', () => { - // The window has to leave protect-ffi's changeset somewhere, or the prose - // is reconstructed from the git log at cutover time. `.md.deferred` is - // that parking spot, and this is the behaviour that makes it one. - // - // Generated rather than committed so the assertion is about the SKIP. Run - // against the real .changeset directory it would also pass if the parked - // file simply named nothing guarded. - const dir = mkdtempSync(join(tmpdir(), 'ffi-changeset-deferred-')) - tempDirs.push(dir) - writeFileSync( - join(dir, 'parked.md.deferred'), - "---\n'@cipherstash/protect-ffi': minor\n---\n\nWaiting for the cutover.\n", - ) - - expect(run(dir).exitCode).toBe(0) - }) - - it('has the deferred laziness changeset already written and parked', () => { - // Why the guard can be deleted safely at cutover: the changeset it defers - // is not a thing someone has to remember to write, it is a `git mv`. The - // phase-4 checklist says so; this is what stops the file being deleted, - // emptied or renamed out from under that instruction in the meantime. - const changesetDir = resolve(REPO_ROOT, '.changeset') - const parked = readdirSync(changesetDir).filter((f) => - f.endsWith('.md.deferred'), - ) - expect( - parked, - 'no parked changeset in .changeset/ — the phase-2 laziness changeset must be written before the cutover, not during it', - ).not.toHaveLength(0) - - // `@changesets/read` selects changeset files with - // !file.startsWith('.') && file.endsWith('.md') && !/^README\.md$/i - // (@changesets/read/dist/changesets-read.esm.js). Reproduced rather than - // imported — it is a transitive dependency, not a declared one — because - // the failure worth catching is a well-meaning rename to - // `.deferred.md`, which changesets WOULD read, publishing all seven - // FFI packages into a trusted-publishing configuration that still names - // the old repository. Suffix order is the whole safety property. - for (const file of parked) { - expect( - !file.startsWith('.') && - file.endsWith('.md') && - !/^README\.md$/i.test(file), - `${file} would be read by changesets as a live changeset`, - ).toBe(false) - } - - const parkedPackages = parked.flatMap((file) => - [ - ...readFileSync(join(changesetDir, file), 'utf8').matchAll( - /^\s*['"]?(@?[^'":\n]+?)['"]?\s*:\s*(?:major|minor|patch)\s*$/gm, - ), - ].map(([, name]) => name), - ) - expect( - parkedPackages, - 'a parked changeset must name @cipherstash/protect-ffi — that is the only reason to park one', - ).toContain('@cipherstash/protect-ffi') - }) - - it('names its own removal condition in the source', () => { - // A temporary guard with no stated expiry becomes permanent. The cutover - // PR must be able to find this file from the plan and delete it. - const source = readFileSync(SCRIPT, 'utf8') - expect(source).toMatch(/TEMPORARY/) - expect(source).toMatch(/trusted publishing/) - }) - - it('guards exactly the seven packages in the FFI fixed group', () => { - // Drift between the guard list and the changesets fixed group would let a - // platform package through while the group still bumps it. - const config = JSON.parse( - readFileSync(resolve(REPO_ROOT, '.changeset/config.json'), 'utf8'), - ) - const ffiGroup = config.fixed.find((group) => - group.includes('@cipherstash/protect-ffi'), - ) - // Both directions. Asserting only that each configured name appears in the - // script catches a package dropped from the guard, but not one dropped - // from the fixed group or added to only one of the two — and it is the - // guard falling behind a NEW platform package that publishes something. - const guardedPackages = [ - ...readFileSync(SCRIPT, 'utf8').matchAll( - /'(@cipherstash\/protect-ffi(?:-[a-z0-9-]+)?)'/g, - ), - ].map(([, name]) => name) - - expect(ffiGroup).toHaveLength(7) - expect([...new Set(guardedPackages)].sort()).toEqual([...ffiGroup].sort()) - }) -}) diff --git a/scripts/lint-no-ffi-changeset.mjs b/scripts/lint-no-ffi-changeset.mjs deleted file mode 100644 index e8ba9938b..000000000 --- a/scripts/lint-no-ffi-changeset.mjs +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Fail if any pending changeset names one of the seven `@cipherstash/protect-ffi` - * packages. - * - * TEMPORARY. Delete this script, its self-test, its fixtures, and its - * `lint:ffi-changeset` entry in the phase-4 cutover PR, at the same moment npm - * trusted publishing is repointed from `cipherstash/protectjs-ffi` to - * `cipherstash/stack`. In the same PR, `git mv` every - * `.changeset/*.md.deferred` to `*.md` — see "Where a deferred changeset - * waits" below. - * - * ## Why this exists rather than the changesets `ignore` list - * - * The absorption plan specified `ignore` as the publication guard. Changesets - * rejects that configuration outright: - * - * The package "@cipherstash/stack" depends on the skipped package - * "@cipherstash/protect-ffi", but "@cipherstash/stack" is not being - * skipped. Please add "@cipherstash/stack" to the `ignore` option. - * - * An ignored package's dependents must themselves be ignored, and all three - * consumers now depend on the wrapper at `workspace:*`. Honouring that would - * mean ignoring `@cipherstash/stack`, `stack-drizzle` and `stack-supabase` — - * and, through the Stack fixed group, `stash`, `stack-prisma` and `wizard` - * with them. That is a total release freeze, which the plan considered and - * rejected for exactly the right reason: this repo releases actively and a - * freeze of unknown duration is a real operational cost. - * - * ## What actually guards the window - * - * Two things, and neither is configuration: - * - * 1. All seven packages are already on npm at 0.31.0 — the same version the - * workspace carries. `changeset publish` publishes only versions absent - * from the registry, so with no changeset naming them, a release is a - * no-op for all seven no matter how often `main` publishes. - * 2. This check, which stops the one thing that would change that. - * - * The hazard is narrow and specific: a changeset naming any FFI package makes - * `changeset version` bump all seven to 0.32.0 (they share a fixed group), and - * the next release then tries to publish seven packages whose npm trusted - * publisher still names `cipherstash/protectjs-ffi`. That is a failed or - * misattributed publish of the encryption core. - * - * This is *not* a rule against changing protect-ffi. Rust and TypeScript - * changes are fine and land normally; the constraint is only that their - * changeset waits for the cutover PR. - * - * ## Where a deferred changeset waits - * - * `.changeset/.md.deferred`. Write the changeset now, under that - * extension, and the cutover PR is a `git mv` instead of an act of memory — - * which matters most for the phase-2 laziness change and - * `assertNativeBindingAvailable()`, already parked as - * `protect-ffi-lazy-load.md.deferred`. Reconstructing that prose from the git - * log months later is how a user-visible behaviour change ships with an empty - * changelog. - * - * The suffix is load-bearing, and it is `.md.deferred` rather than - * `.deferred.md` for a reason: `@changesets/read` selects changesets with - * `!file.startsWith('.') && file.endsWith('.md') && !/^README\.md$/i`, and the - * loop below filters on `.endsWith('.md')` too. A name ending in `.md` - * publishes; a name ending in `.deferred` is invisible to both. Nothing else - * in changesets touches it either — `getOldChangesets` only descends into - * DIRECTORIES, `removeEmptyFolders` swallows the `ENOTDIR` a file raises, and - * `applyReleasePlan` deletes strictly `${changeset.id}.md` for ids in the - * release plan. Verified against the installed 2.31.0, and by - * `changeset status` reporting no FFI package with the file in place. - * - * The self-test pins all of this: that a parked file exists, that it names a - * guarded package, and that its name would NOT be read as a changeset. - */ -import { readdirSync, readFileSync } from 'node:fs' -import { join, relative, resolve } from 'node:path' - -const REPO_ROOT = resolve(import.meta.dirname, '..') - -const GUARDED = new Set([ - '@cipherstash/protect-ffi', - '@cipherstash/protect-ffi-darwin-x64', - '@cipherstash/protect-ffi-darwin-arm64', - '@cipherstash/protect-ffi-win32-x64-msvc', - '@cipherstash/protect-ffi-linux-x64-gnu', - '@cipherstash/protect-ffi-linux-arm64-gnu', - '@cipherstash/protect-ffi-linux-x64-musl', -]) - -const changesetDir = process.argv[2] - ? resolve(process.argv[2]) - : join(REPO_ROOT, '.changeset') - -/** - * Pull the package names out of a changeset's frontmatter. - * - * A changeset is `---\n'pkg': minor\n---\n\nprose`. Only the first fenced - * block counts: prose below it may quote a package name in a sentence, and - * `--- ` rules inside markdown would otherwise reopen the block. - */ -function packagesIn(source) { - const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(source) - if (!match) return [] - return match[1] - .split(/\r?\n/) - .map((line) => /^\s*['"]?(@?[^'":]+?)['"]?\s*:/.exec(line)) - .filter(Boolean) - .map((m) => m[1].trim()) -} - -const offenders = [] -for (const entry of readdirSync(changesetDir)) { - if (!entry.endsWith('.md') || entry === 'README.md') continue - const named = packagesIn(readFileSync(join(changesetDir, entry), 'utf8')) - const guarded = named.filter((name) => GUARDED.has(name)) - if (guarded.length) offenders.push({ file: entry, packages: guarded }) -} - -if (offenders.length === 0) { - console.log('No pending changeset names a protect-ffi package.') - process.exit(0) -} - -console.error( - '\nA pending changeset names a @cipherstash/protect-ffi package:\n', -) -for (const { file, packages } of offenders) { - // Relative to the repo root, not a literal `.changeset/` — the directory is - // overridable by argv for the self-tests, and a hardcoded prefix prints a - // path that does not exist whenever it is. - console.error(` ${relative(REPO_ROOT, join(changesetDir, file))}`) - for (const name of packages) console.error(` ${name}`) -} -console.error( - '\nThese seven packages live in this repo but are still PUBLISHED from\n' + - 'cipherstash/protectjs-ffi — npm trusted publishing has not been\n' + - 'repointed yet. A changeset naming any one of them bumps all seven to\n' + - '0.32.0 through their fixed group, and the next release then attempts a\n' + - 'publish npm will reject or misattribute.\n\n' + - 'Change protect-ffi freely — only its changeset has to wait, and it has\n' + - 'somewhere to wait. Rename the file to end in `.md.deferred`:\n\n' + - ' git mv .changeset/.md .changeset/.md.deferred\n\n' + - 'Changesets does not read that extension and neither does this check, so\n' + - 'the prose is written now and the cutover PR — the one that repoints\n' + - 'trusted publishing and deletes this script — renames it back.\n', -) -process.exit(1) From 24f4117ae7e0ff67d6bdc079bb919af00f688cd5 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 19 Aug 2026 09:03:56 +1000 Subject: [PATCH 2/3] fix: delete the guard's fixtures, and reconcile the docs this PR contradicted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on the cutover PR, all four in its own additions. **Six orphaned fixtures.** CIP-3719 said to delete "its self-test, fixtures"; the first pass deleted the self-test and left `scripts/__tests__/fixtures/lint-no-ffi-changeset/` behind, then recorded in the plan that no fixtures existed. That conclusion came from checking `scripts/fixtures/` — the wrong directory. The self-test used a tmpdir for two cases only; the rest resolved these six files. Nothing referenced them once the test was gone, and `test:scripts` passes either way, which is why the claim needed checking rather than inferring. Deleted, and the plan's correction reversed rather than left to mislead the next reader. **Prose contradicted its own checklist, twice.** AGENTS.md and the protect-ffi README asserted publishing had moved while the same commit left the plan's "Repoint npm trusted publishing" box unticked; and the plan's summary claimed "Phases 3 and 5 are built" over four unticked Phase 5 boxes. Both reconciled in the direction of what is true: the repoint is done, and all four Phase 5 items verified against the tree — the `./diagnostics` subpath with `import` and `require` conditions, the doctor probe that keeps the separate auth probe, the missing-binary e2e, and both changesets. The repoint box carries what is genuinely still open: npm accepts a publisher scoped to `npm stage publish` alone, which reads as enabled and fails every `npm publish`, and that setting is unverified on all seven. **Step 2's verification criterion was stale.** It told a reviewer to expect a patch bump of the six Stack packages. `.changeset/prisma-next-0-17.md` takes them to major through the fixed group, on `origin/main` already and with no FFI changeset in play. Left as-is it invites reading an unrelated major as evidence the FFI bump misbehaved. AGENTS.md also now names #905 as carrying a parked changeset, since AGENTS.md is the file agents actually read and a `.md.deferred` is inert rather than loud now that the guard is gone. CIP-3719 --- AGENTS.md | 10 +++--- ...6-08-04-protect-ffi-monorepo-absorption.md | 31 ++++++++++++++----- .../lint-no-ffi-changeset/clean/README.md | 10 ------ .../clean/happy-otter-sing.md | 6 ---- .../offending/brave-lion-jump.md | 5 --- .../offending/quiet-moth-wait.md | 5 --- .../offending/wise-crane-list.md | 9 ------ .../prose-mention/gentle-fox-run.md | 12 ------- 8 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/README.md delete mode 100644 scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/happy-otter-sing.md delete mode 100644 scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/brave-lion-jump.md delete mode 100644 scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/quiet-moth-wait.md delete mode 100644 scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/wise-crane-list.md delete mode 100644 scripts/__tests__/fixtures/lint-no-ffi-changeset/prose-mention/gentle-fox-run.md diff --git a/AGENTS.md b/AGENTS.md index 13cf6834d..72f1e3b41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,10 +137,12 @@ so that stays true for everyone else. - **Publishing has moved here.** All seven packages publish from this repo, via npm trusted publishing bound to `release.yml`. Write changesets for them normally. The `.md.deferred` parking convention and the - `lint-no-ffi-changeset` guard that enforced it are both gone — if you find a - `.md.deferred` file anywhere, it is a leftover from a branch cut before this - PR and needs renaming back, because nothing reads that extension and nothing - warns about it any more. + `lint-no-ffi-changeset` guard that enforced it are both gone — so a + `.md.deferred` file is now silently inert rather than a CI failure. If you + find one, it was written on a branch cut before the cutover and needs + `git mv`-ing back to `.md`, or the change it describes ships with no + changelog entry. **#905 carries one** (`protect-ffi-jsonwebtoken-cve`); rename + it on that branch before merging. - **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 diff --git a/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md b/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md index 50d573c35..595905eb3 100644 --- a/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md +++ b/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md @@ -1865,7 +1865,7 @@ git commit -m "ci: run the Rust checks from a root path-filtered workflow" The only irreversible steps. -- [x] Merge a cutover PR that deletes `scripts/lint-no-ffi-changeset.mjs`, its self-test, 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 @@ -1873,20 +1873,35 @@ The only irreversible steps. 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. - 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). There were no fixtures to delete — the self-test wrote its own to a tmpdir, so the "its fixtures" in the line above was always wrong. **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`, 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. + + 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- 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. --- diff --git a/scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/README.md b/scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/README.md deleted file mode 100644 index 4326a3ba9..000000000 --- a/scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/README.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@cipherstash/protect-ffi': minor ---- - -# Changesets - -This README must never be parsed as a changeset. The frontmatter above is bait: -`.changeset/README.md` ships with changesets itself, and a guard that read it -would fail every repo that has one. If the skip regresses, this fixture turns -the `clean` directory red and names README in the output. diff --git a/scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/happy-otter-sing.md b/scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/happy-otter-sing.md deleted file mode 100644 index d3cfa3cde..000000000 --- a/scripts/__tests__/fixtures/lint-no-ffi-changeset/clean/happy-otter-sing.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@cipherstash/stack': patch -'stash': minor ---- - -An ordinary Stack changeset. Names no FFI package. diff --git a/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/brave-lion-jump.md b/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/brave-lion-jump.md deleted file mode 100644 index 783c74f76..000000000 --- a/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/brave-lion-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@cipherstash/protect-ffi': minor ---- - -Make the native binding load lazily. diff --git a/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/quiet-moth-wait.md b/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/quiet-moth-wait.md deleted file mode 100644 index 6d0c0e7a0..000000000 --- a/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/quiet-moth-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@cipherstash/protect-ffi-linux-x64-musl": patch ---- - -A platform package named directly, with double quotes and no leading quote style match. diff --git a/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/wise-crane-list.md b/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/wise-crane-list.md deleted file mode 100644 index 07a2bb65f..000000000 --- a/scripts/__tests__/fixtures/lint-no-ffi-changeset/offending/wise-crane-list.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@cipherstash/stack': patch -'@cipherstash/protect-ffi-darwin-arm64': patch ---- - -Two packages in one block — what `pnpm changeset` writes when you select more -than one. The FFI package is deliberately NOT the first line: the package you -set out to change gets picked first, so this is the ordinary shape of the -mistake this guard exists to catch, not an exotic one. diff --git a/scripts/__tests__/fixtures/lint-no-ffi-changeset/prose-mention/gentle-fox-run.md b/scripts/__tests__/fixtures/lint-no-ffi-changeset/prose-mention/gentle-fox-run.md deleted file mode 100644 index cbda784b5..000000000 --- a/scripts/__tests__/fixtures/lint-no-ffi-changeset/prose-mention/gentle-fox-run.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@cipherstash/stack': patch ---- - -Adopt @cipherstash/protect-ffi 0.31.0. The body mentions the package by name, -and even shows frontmatter-looking text: - ---- -'@cipherstash/protect-ffi': major ---- - -That must not count — only the first fenced block is frontmatter. From 4d6dca9a0030d4e8b66bb4a078f277f4c6c8b046 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 19 Aug 2026 09:15:34 +1000 Subject: [PATCH 3/3] docs: stop claiming the cutover is finished when step 1 of 7 is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review findings, all in this PR's own prose. Both axes reached the same place from different directions: the docs were written as though the cutover had completed, when what has completed is the first of seven steps. - AGENTS.md and the protect-ffi README asserted publishing works here in unqualified present tense. Nothing has published from this repository yet — 0.31.0 came from the old one, and the release, the verification and the archive are all still ahead. Both now say the path is configured rather than proven, and point at the plan for what is open. - AGENTS.md named #905 as carrying a parked changeset. That is transient state in durable doctrine: the line goes false the day #905 merges, and the fact is already recorded in the plan where such things belong. The general rule stays, the PR number goes. - The Phase 4 step-4 criterion had lost its "only after the versioned pre-flight is green" sentence when the box was ticked, so a reader skimming boxes no longer saw the ordering rule at all. Restored; the note below it still records that the repoint in fact ran ahead of the pre-flight, and why that was judged acceptable. - The changesets-`ignore` rationale still described the guard in the present tense, in a section the earlier reconciliation walked past. Left open deliberately: three independent reviews now suggest replacing the deleted guard with its inverse — fail CI on any `.changeset/*.md.deferred`, since such a file is inert to changesets and, as of this PR, inert to CI too. That is a design decision against CIP-3719's explicit instruction to delete the script, not a defect fix, so it is not taken here. CIP-3719 --- AGENTS.md | 20 ++++++++++--------- ...6-08-04-protect-ffi-monorepo-absorption.md | 4 ++-- packages/protect-ffi/README.md | 3 +++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 72f1e3b41..c95d1f04b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,15 +134,17 @@ 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 moved here.** All seven packages publish from this repo, via - npm trusted publishing bound to `release.yml`. Write changesets for them - normally. The `.md.deferred` parking convention and the - `lint-no-ffi-changeset` guard that enforced it are both gone — so a - `.md.deferred` file is now silently inert rather than a CI failure. If you - find one, it was written on a branch cut before the cutover and needs - `git mv`-ing back to `.md`, or the change it describes ships with no - changelog entry. **#905 carries one** (`protect-ffi-jsonwebtoken-cve`); rename - it on that branch before merging. +- **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 diff --git a/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md b/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md index 595905eb3..3aea12a27 100644 --- a/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md +++ b/docs/plans/2026-08-04-protect-ffi-monorepo-absorption.md @@ -153,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 @@ -1882,7 +1882,7 @@ The only irreversible steps. **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**. -- [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. +- [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. diff --git a/packages/protect-ffi/README.md b/packages/protect-ffi/README.md index 696043e10..8cbad5960 100644 --- a/packages/protect-ffi/README.md +++ b/packages/protect-ffi/README.md @@ -384,6 +384,9 @@ 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 that port. They are gone: the six-platform build matrix now lives in the