ci(eql): port and harden the inert EQL release pipeline - #941
Merged
Conversation
The EQL subtree arrived with eleven files under `packages/eql/.github/`, a directory GitHub never reads. Seven of them were workflows, and between them they are the entire publishing pipeline for `@cipherstash/eql`: the npm package, the `eql-bindings` crate, the SQL bundle, the docs bundle and the `postgres-eql` image, all of which ship at one version. None of it executed. This ports the lot and keeps it inert, so it can be reviewed and dry-run before trusted publishing is repointed at this repository. Inertness is a derived switch rather than a flag. `scripts/eql-pipeline-armed.mjs` reads `FROZEN_PUBLISHERS` in `scripts/release-gate.mjs` — the map that already records "this package lives here and is published elsewhere" — and every job that publishes an EQL artefact is gated on its answer. The cutover has to delete that entry (the release gate refuses every release until it does), so deleting it is what arms the pipeline; there is no second flag to forget, and forgetting one would fail silently in the direction that publishes an npm package with no SQL release, no docs and no crate. Three paths had to be rewritten because the subtree root is not the package root here: the npm manifest is two levels down at `packages/eql/packages/eql`, mise config is only found from `packages/eql`, and `release-plz/action` needs explicit `manifest_path` / `config` inputs because a composite action's steps do not inherit the caller's `defaults.run.working-directory`. Three of this repo's guards had to grow rather than be exempted, and two real defects came out of them: the prerelease publish job was missing `node-gyp` before its install, and built with `pnpm --filter` rather than through turbo. Claude-Session: https://claude.ai/code/session_01T26DhFPkLfN3qPFDq2ttKw
|
Seven findings from the review of the ported pipeline, all confirmed before acting. Three were guards that could not fire, which is the failure mode this repository cares most about — a check that reads as protection and is incapable of reporting anything. `rebuild-docs.yml` was the worst of them, and worse than reported: it has never run, not once, against `@cipherstash/stack@1.0.0`, `1.1.0` and `1.1.1`. A ref created with GITHUB_TOKEN starts no workflow run, and changesets creates those tags with that token — the same rule this pipeline already works around for the image build. Porting EQL's `eql-*` tag trigger into it would have added a second dead trigger. The EQL docs rebuild is now a job in `release.yml` that fires the webhook from inside the publishing run; the `@cipherstash/stack@*` half is left alone as a pre-existing bug, recorded in that file's header so the next person does not repeat the mistake. `release-plz.yml`'s "refusing to publish the DEV placeholder" guard grepped for `eql_v3`, which appears 23,723 times in every build of that bundle including a DEV one. It now matches the schema version stamp against the crate version — the only line in the bundle that records which build produced it. `classify` accepted any version containing a hyphen where `prepare-bindings-assets.sh` requires `X.Y.Z-(alpha|beta|rc).N`. Under the loose check, `3.0.6-beta` got a public tag and GitHub release before the npm job died on it. Also: `PRE_GA_LATEST` was still `true` in the EQL publish script, so the first prerelease cut through the new job would have moved the `latest` dist-tag off the GA release. The file's own comment said to flip it once 3.0.0 GA shipped; npm's `latest` is 3.0.5. Two documentation defects of my own (a paragraph duplicated into AGENTS.md, and a SECURITY.md sentence contradicting the permissions split this PR introduces), one stale plan checkbox, an over-deep checkout, and a module-scope `execFileSync` whose failure took 40 unrelated assertions with it. Comment volume across everything this branch added is cut by roughly half. Claude-Session: https://claude.ai/code/session_01T26DhFPkLfN3qPFDq2ttKw
tobyhede
force-pushed
the
toby/cip-3742-eql-release-pipeline
branch
from
August 24, 2026 04:46
463f8b4 to
ab4c4f3
Compare
tobyhede
marked this pull request as ready for review
August 25, 2026 00:53
freshtonic
approved these changes
Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EQL is the PostgreSQL layer that stores and queries encrypted data. Its subtree import brought eleven files under
packages/eql/.github/, including the release pipeline, but GitHub only reads workflows from the repository root. The npm package, Rust crate, SQL bundle, docs bundle, and Postgres image therefore had no executable release path in this repository.This PR ports and hardens the viable release pipeline at the repository root while keeping it inert. It does not arm EQL publishing.
Inertness is derived from
FROZEN_PUBLISHERSinscripts/release-gate.mjs, not from a second release flag:scripts/eql-pipeline-armed.mjsgates the EQL SQL, docs, crate, image, and prerelease npm jobs.release-gate.mjs, which fails the shared release gate while EQL remains frozen.Both paths read the same map entry. Removing
@cipherstash/eqlfrom that map during cutover arms the complete pipeline; there is no independent switch to forget.Changes
Release workflows
_build-sql.yml.github/workflows/_build-eql-sql.yml_build-docs.yml.github/workflows/_build-eql-docs.ymlrelease.yml.github/workflows/release.ymlbecause npm trusted publishing binds to the workflow filenamerelease-plz.ymlrelease-postgres-eql-image.ymllint-release.ymlrebuild-docs.ymlDOCS_WEBHOOK_URLactionlint.yaml,ISSUE_TEMPLATE/docs-feedback.yml.github/release.ymlworkflows/README.mdThe imported
packages/eql/.github/directory is deleted.scripts/__tests__/eql-suite-ci.test.mjsasserts that it stays absent and that no root workflow consumes the retired docs webhook secret.Monorepo path corrections
packages/eql/packages/eql, two levels below the subtree root.packages/eql, where mise can discover the subtree configuration.release-plz/actionreceives explicitmanifest_pathandconfiginputs; composite-action steps do not inheritdefaults.run.working-directory.Release correctness
X.Y.Z-(alpha|beta|rc).Nprerelease identity before any tag or release is created.mise run --force build --version ....--versionis not a declared mise source, so an unforced build can reuse SQL stamped for an earlier release when the SQL sources are unchanged.verify-release-assets.mjsvalidates the SQL schema-version stamp against the package version. Its formereql_v3substring check also passed placeholder bundles.latesttag after GA.Supply-chain hardening
release-plz.ymlruns only for EQL paths and checks crates.io before importing the signing key.cipherstash/stack, preventing a future push from relinking the GHCR package to the old repository.Guards and audit fixes
pnpm-workspace.yaml, including nested EQL and per-platform FFI packages..claude/worktreescheckouts.pnpm devreaches nested workspace packages with./packages/**.Verification
pnpm run test:scripts— 49 files, 816 passed, 1 skipped.actionlintpasses for the changed release workflows.scripts/lint-no-workflow-caching.mjsexits 0.scripts/lint-no-eql-registry-pins.mjsexits 0.git diff --checkpasses.A temporary EQL patch bump was used to exercise the lockstep version hook. It moved the npm package,
eql-bindingscrate, SQL schema stamps, release manifests, and generated TypeScript manifest to one version. In that bumped state,release-gate.mjsrefused publishing while EQL remained frozen.The publish-time SQL verifier was also driven against a
DEV-stamped bundle with an otherwise agreeing manifest: it exits non-zero and names the incorrect schema stamp.The docs workflow still needs its first execution in this repository when the pipeline is armed. It builds and attaches the versioned HTML/XML/Markdown artifact; there is intentionally no external docs-site webhook.
Cutover notes
This PR builds the pipeline but does not perform the cutover. Before removing the EQL entry from
FROZEN_PUBLISHERS, verify the external publisher configuration required by npm and crates.io and the GHCR release path. Thepostgres-eqlimage is also currently a major version behind EQL, so moving its floating tags should be deliberate.Related
Refs #885
Review notes
Start with
scripts/eql-pipeline-armed.mjsandscripts/release-gate.mjs, then review the production and prerelease EQL paths in.github/workflows/release.yml.workflow_dispatchis the notable behavior change to the existing shared release workflow. Production jobs are explicitly gated so a dispatch from a feature branch cannot publish JS or FFI packages.No changeset: this PR changes repository tooling, tests, and documentation. The modified EQL
prepublishOnlyverifier is excluded from the published package by itsfilesallowlist.