Skip to content

Commit 751c1f7

Browse files
committed
fix(eql): name stack-encrypt 0.2.0 and verify the feature from the packaged crate
The `stack-encrypt` feature depends on stack-encrypt by path and version. Every in-tree job builds the path; the published crate resolves the version from crates.io. The version said 0.1.0, which shipped without `Describe`, `Description` and `Label` — so the feature compiled here and could not compile for any consumer, or for docs.rs, which enables it. Nothing noticed, because `cargo publish` verifies the default feature set and this feature is off by default: the dry run in CI passed, and release-plz would have published it. Three changes close that. The requirement names 0.2.0, the version the descriptor rework releases as (ad62087 on the base branch; cargo refuses a requirement the in-tree path dependency does not satisfy, so the bump had to land there first). The dry-run step and release-plz both pass `--all-features`, so the verify builds the feature from the PACKAGED manifest, where there is no path and stack-encrypt comes from the registry — the build a consumer gets. And `release-plz.yml`'s eql-bindings job now runs after the stack-* crates job: on a push that releases both, the crate eql-bindings resolves exists by the time it looks. The dry-run step is red until stack-encrypt 0.2.0 is on crates.io. That is the true state of this branch, and it clears when #1050 merges and the crates publish. Found by the Codex review of #971 (the path dependency hid a registry mismatch). Refs #1049.
1 parent 787d4c2 commit 751c1f7

9 files changed

Lines changed: 102 additions & 19 deletions

File tree

‎.github/workflows/release-plz.yml‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ name: "Release crates (crates.io)"
22

33
# Publishes two crates.io release lines via release-plz (crates.io Trusted
44
# Publishing over OIDC — no CARGO_REGISTRY_TOKEN): `eql-bindings` from
5-
# packages/eql (`release`), and `stack-auth` + `stack-profile` from the root
6-
# workspace (`release-crates`, described above its own jobs at the end).
5+
# packages/eql (`release`), and the stack-* crates from the root workspace
6+
# (`release-crates`, described above its own jobs at the end). `release` runs
7+
# after `release-crates`: see the comment on its `needs:`.
78
# Everything from here to `permissions:` is about the EQL line.
89
#
910
# THE FILENAME CANNOT CHANGE: crates.io binds the publisher to it.
@@ -85,8 +86,20 @@ jobs:
8586

8687
release:
8788
name: "Release"
88-
needs: [eql-armed]
89-
if: needs.eql-armed.outputs.armed == 'true'
89+
# AFTER `release-crates`, not beside it. eql-bindings' `stack-encrypt`
90+
# feature names a stack-encrypt VERSION, and `cargo publish` resolves it
91+
# from crates.io, so on a push that releases both, publishing in parallel
92+
# races this job's resolution against the other job's upload. Ordered,
93+
# the stack-* crates land first and this job's verify finds them.
94+
# `always()` because `release-crates` is skipped on every push that moves
95+
# no stack-* crate, and a skipped dependency would otherwise skip this job
96+
# too; its FAILURE still stops this one, since the crates it needed did
97+
# not publish.
98+
needs: [eql-armed, release-crates]
99+
if: >-
100+
always()
101+
&& needs.eql-armed.outputs.armed == 'true'
102+
&& (needs.release-crates.result == 'success' || needs.release-crates.result == 'skipped')
90103
# GitHub-hosted, matching this repo's other publishing jobs.
91104
runs-on: ubuntu-latest
92105
timeout-minutes: 30

‎.github/workflows/test-eql.yml‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,9 +604,21 @@ jobs:
604604
# a real path dependency without a version — on the PR rather than at
605605
# release time. No token needed. `--allow-dirty` tolerates any files the
606606
# preceding regenerate-and-diff steps leave in the working tree.
607+
#
608+
# `--all-features`: the verify step compiles the DEFAULT feature set
609+
# unless told otherwise, and the `stack-encrypt` feature is off by
610+
# default — so this step passed while that feature did not build against
611+
# the crates.io stack-encrypt the manifest names. Every other job here
612+
# compiles stack-encrypt by PATH, from the tree, and cannot notice; the
613+
# packaged manifest has no `path`, so this verify resolves stack-encrypt
614+
# from the REGISTRY, which is the build a consumer and docs.rs get.
615+
# release-plz does the same at publish time (`publish_all_features` in
616+
# packages/eql/release-plz.toml). The step is RED while the stack-encrypt
617+
# version eql-bindings names is not yet on crates.io: that is the true
618+
# state of the tree, and it clears when that crate ships.
607619
- name: Verify eql-bindings packages cleanly for crates.io
608620
run: |
609-
cargo publish -p eql-bindings --dry-run --allow-dirty
621+
cargo publish -p eql-bindings --dry-run --allow-dirty --all-features
610622
611623
codegen:
612624
name: "Encrypted-domain codegen"

‎AGENTS.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,27 @@ monorepo, which is where the silent failures are.
504504
changes what the whole EQL CI surface compiles against, and with the skip in
505505
place the release-stopper is closed without it. If wanted, the pin belongs
506506
upstream and arrives by subtree pull.
507+
- **`eql-bindings`' `stack-encrypt` feature names a stack-encrypt VERSION, and
508+
the published crate is built against that version, not the tree.** The
509+
dependency is `path` + `version`: the path is what every in-tree job
510+
compiles, the version is what crates.io resolves once `cargo publish` strips
511+
the path. The two agree only if the stack-encrypt on crates.io at that
512+
version has the API `src/encryption.rs` uses — and nothing in a path build
513+
can tell. stack-encrypt 0.1.0 shipped without `Describe`, and the feature
514+
would have published against it while every test passed. Two guards compile
515+
the feature FROM THE PACKAGED CRATE against the registry: `cargo publish
516+
--dry-run --all-features` in `test-eql.yml`'s `rust-crates` job, and
517+
`publish_all_features = true` in `packages/eql/release-plz.toml`. Without
518+
`--all-features` both verify the default feature set, which is the feature
519+
off. Consequences: the stack-encrypt a bump of that requirement names must
520+
reach crates.io before eql-bindings does (the root release-plz line is
521+
publish-only — a stack-* version moves by a hand-edited `Cargo.toml`, and
522+
cargo refuses a requirement the in-tree path dependency does not satisfy, so
523+
the bump lands in the stack-encrypt PR first); the dry-run step is RED on a
524+
PR that names an unpublished stack-encrypt, correctly, until it ships; and
525+
`release-plz.yml`'s `release` job runs after `release-crates` so that on a
526+
push releasing both, the crate eql-bindings resolves exists by the time it
527+
looks.
507528
- **`eql-bindings` resolves by path from `languages/typescript/packages/protect-ffi`, never from
508529
crates.io**, and `scripts/lint-no-eql-registry-pins.mjs` (`pnpm run
509530
lint:eql-pins`) is what keeps it that way. The two halves of EQL are the Rust

‎docs/plans/2026-09-12-eql-stack-encrypt-derives.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ Validation completed:
4141
- Root workflow wiring guards pass. Encryption integration dependencies live in
4242
the separate `eql-encryption-tests` crate, preserving lean default tests.
4343

44-
Release prerequisites remain: publish the suite crates, regenerate both EQL and
45-
FFI lockfiles against registry dependencies, run the clean-package verification,
46-
and perform live ZeroKMS authorization checks. Local tests use real cryptography
44+
Release prerequisites remain: publish `stack-encrypt` 0.2.0 (the version the
45+
feature names; 0.1.0 shipped without `Describe`, and the packaged-crate
46+
verification below is red until 0.2.0 is on crates.io), and perform live
47+
ZeroKMS authorization checks. The clean-package verification is now in CI:
48+
`cargo publish --dry-run --all-features` on every PR, and `publish_all_features`
49+
in release-plz at publish time. Local tests use real cryptography
4750
with the suite's test key source, which cannot establish authorization behavior.
4851
No existing-column or mixed JS/Rust producer compatibility is claimed.
4952

@@ -347,8 +350,10 @@ See Cargo's [dependency publication rules](https://doc.rust-lang.org/cargo/refer
347350
Coordinate suite publication prerequisites first, then the EQL release using
348351
this repository's existing lockstep process. The existing EQL CI job already
349352
runs `cargo publish -p eql-bindings --dry-run --allow-dirty`; preserve that gate
350-
and also verify the new feature from the packaged artifact. Do not alter release
351-
arming/trusted-publishing configuration as part of adding derives.
353+
and also verify the new feature from the packaged artifact (done: the step and
354+
the release-plz publish both pass `--all-features`, which is what makes the
355+
verify compile the feature against the registry `stack-encrypt`). Do not alter
356+
release arming/trusted-publishing configuration as part of adding derives.
352357

353358
Add rustdoc/examples, a compatibility/profile note, and a root
354359
`.changeset/` entry for `@cipherstash/eql` (minor if additive; reassess if phase 1

‎packages/eql/Cargo.lock‎

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/eql/crates/eql-bindings/Cargo.toml‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,20 @@ serde_json = "1"
3434
ts-rs = { version = "10", features = ["no-serde-warnings"] }
3535
schemars = "1"
3636
# stack-encrypt lives in this repository (packages/stack-encrypt). The path
37-
# builds against it in-tree; the version is what crates.io resolves for the
38-
# published eql-bindings, so stack-encrypt must be released first.
39-
stack-encrypt = { path = "../../../stack-encrypt", version = "0.1.0", optional = true, default-features = false }
40-
# The vitaminc line stack-encrypt is built on: its PRF traits are re-exported
41-
# through stack-encrypt, and two vitaminc lines in one graph do not interoperate.
37+
# builds against it in-tree; `version` is what crates.io resolves for the
38+
# PUBLISHED eql-bindings. So the stack-encrypt this names must be on crates.io
39+
# before eql-bindings is, and must carry the API `src/encryption.rs` compiles
40+
# against (`Describe`, `Description`, `Label`, `target::transcode`). 0.1.0 did
41+
# not: it shipped before they landed, and the path build cannot notice,
42+
# because it compiles whatever the tree holds. Two guards compile this feature
43+
# from the PACKAGED crate against the registry stack-encrypt instead:
44+
# `cargo publish --dry-run --all-features` in test-eql.yml at PR time, and
45+
# `publish_all_features` in ../../release-plz.toml at release time.
46+
stack-encrypt = { path = "../../../stack-encrypt", version = "0.2.0", optional = true, default-features = false }
47+
# The vitaminc line stack-encrypt is built on. `PrfValue` is named from this
48+
# crate directly (stack-encrypt re-exports only `IntoPrfContext` and
49+
# `PrfContext`), and two vitaminc lines in one graph do not interoperate, so
50+
# the requirement is stack-encrypt's.
4251
vitaminc-prf = { version = "0.5.0", optional = true }
4352
base64 = { version = "0.22", optional = true }
4453
hex = { version = "0.4", optional = true }

‎packages/eql/crates/eql-bindings/README.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,15 @@ column, and record which.
176176

177177
Stack Encrypt lives in this repository (`packages/stack-encrypt`), and the
178178
feature depends on it by path and version: in-tree builds use the path, and the
179-
published `eql-bindings` resolves the version from crates.io. Publish the
180-
matching `stack-encrypt` before releasing an `eql-bindings` that needs it.
179+
published `eql-bindings` resolves the version from crates.io. The two agree only
180+
when the `stack-encrypt` on crates.io at that version has the API this feature
181+
uses, which a path build cannot check — 0.1.0 shipped without `Describe`, and
182+
every in-tree test passed against the newer tree. So the feature is also built
183+
from the packaged crate against the registry: `cargo publish -p eql-bindings
184+
--dry-run --all-features` in CI, and `publish_all_features` in
185+
`packages/eql/release-plz.toml` at release time. Bump the requirement in
186+
`Cargo.toml` only together with (or after) the `stack-encrypt` release that
187+
carries what the feature needs; the dry run is red until that release exists.
181188

182189
```bash
183190
# From packages/eql. The mise tasks are what CI runs (test-eql.yml, `rust-crates`).

‎packages/eql/release-plz.toml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,15 @@ publish = true
3737
# semver_check is noisy for a pre-1.0 crate and duplicated by the PR review;
3838
# disabled to match cipherstash-suite.
3939
semver_check = false
40+
# Verify the packaged crate with EVERY feature on. `cargo publish` verifies the
41+
# default feature set, and eql-bindings' `stack-encrypt` feature is off by
42+
# default, so a plain publish compiles none of the code that depends on
43+
# stack-encrypt. It would ship a feature that does not build against the
44+
# crates.io stack-encrypt its `version` names: docs.rs enables the feature and
45+
# goes red, and a consumer enabling it fails to compile. The in-tree path
46+
# dependency cannot catch this — it builds against whatever
47+
# packages/stack-encrypt holds, released or not. With `--all-features` the
48+
# verify step resolves stack-encrypt from the REGISTRY and refuses to publish
49+
# an eql-bindings whose feature does not build there. The PR-time twin is the
50+
# `cargo publish --dry-run --all-features` step in test-eql.yml.
51+
publish_all_features = true

‎scripts/__tests__/workflow-dispatch-job-conditions.test.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ const PERMISSIVE_NEEDS = {
188188
gate: { result: 'success', outputs: { ffi: 'true', auth: 'true' } },
189189
'publish-ffi': { result: 'success' },
190190
'publish-auth': { result: 'success' },
191+
// release-plz.yml's `release` runs after the stack-* crates job and accepts
192+
// a skipped one; held to success here, like the other upstream results.
193+
'release-crates': { result: 'success' },
191194
release: { result: 'success', outputs: { published_packages: '' } },
192195
'eql-assets': {
193196
result: 'success',

0 commit comments

Comments
 (0)