Skip to content

ci: port CI for the stack-* crates, bindings and Go module - #1003

Draft
auxesis wants to merge 4 commits into
build/import-stack-cratesfrom
ci/port-stack-crates
Draft

auxesis wants to merge 4 commits into
build/import-stack-cratesfrom
ci/port-stack-crates

Conversation

@auxesis

@auxesis auxesis commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds the CI workflows that build and test the Rust crates, bindings and Go module that PR B brings in. It ports them from cipherstash/cipherstash-suite, and adds tests that keep every check connected to a workflow that runs it.

This is PR C of 6 in the stack crates import, which moves this code here from the private suite repository, with its history. The stack crates import plan in Linear lists every step, and Linear issue CIP-4274 tracks the work.

This PR is stacked on PR B, #1001. Its base branch is PR B's branch, so the diff shows only this PR's own changes. It stays a draft until the freeze on Friday 2 October 2026, Pacific time. The freeze is the window in which the six PRs merge in order: #1000, #1001, #1003, #1002, #1009, then #1010. PR C is #1003, so it merges before PR D, #1002.

Read all four commits, starting with the seven workflows

Read each commit in full. Commit 1 is the largest, with 1,241 added lines in 17 files: the seven workflows, their check registrations and the docs. Commit 2 is one new test file of 673 lines. Commits 3 and 4 are small fixes that CI runs found.

Each commit makes one change

  1. 04fbaad4 ports seven workflows from the suite, and updates AGENTS.md and docs/fuzzing.md. It also adds the new jobs to the lists that the repository's workflow checks keep. Every job runs at the repository root, and tests the root Cargo workspace with nextest only. nextest is a Rust test runner that runs each test in its own process.
  2. a8ce61a1 adds crates-ci.test.mjs. It checks that a workflow runs every task the crates define, or that the test records why not. It also checks that no workflow runs plain cargo test over the workspace, and that @cipherstash/profile stays private.
  3. 897da0a2 sets MISE_ENV: test in each job that uses a cargo tool. mise is the tool that pins this repository's tool versions and runs its tasks. PR B moved the cargo tools into mise.test.toml, which mise reads only when MISE_ENV is test. This also fixes udeps.yml, which had built a cargo-udeps that needs a newer Rust than the runner has.
  4. c93a39b0 adds rustfmt and clippy after mise restores its cache. CI found this after the rebuild on the new main. On a cache hit, mise reports Rust as installed, but never adds those two components. So the first run on a cache key passes, and every later run fails. A guard test now works out which jobs need this step, and it failed on exactly the two that lacked it.

The seven workflows each cover one kind of check

  • tests-crates.yml runs formatting, clippy lints, nextest, doctests, rustdoc and the stack-encrypt examples. It also runs the binding tests, a check that the committed typings match a fresh build, and the WebAssembly tests.
  • tests-golang.yml builds and tests the two Go guests, the Rust crates that the Go module runs as WebAssembly. It runs the WASI checks, which confirm that the WASI builds link no JavaScript-host or native HTTP crate. WASI is the WebAssembly System Interface that the guests target. It also runs go test and golangci-lint, repeats the Go tests on macOS and Windows, and runs live tests that need credentials.
  • crap-crates.yml sets a CRAP score limit for stack-auth and stack-encrypt. CRAP (Change Risk Anti-Patterns) combines a function's complexity with its test coverage.
  • mutants.yml runs cargo mutants --in-diff, which changes the lines a PR touches on purpose and checks that a test fails. It posts the result as a comment on the PR.
  • fuzz.yml feeds generated input to the parsers. On each PR it replays the saved inputs, and every night it runs a longer campaign that searches for inputs that crash.
  • miri.yml runs stack-guest-abi under Miri, an interpreter that detects undefined behaviour in Rust code.
  • udeps.yml looks for unused Rust dependencies, with a pinned nightly toolchain.

A deliberate break proved that each check can fail

The plan treats a ported check as dead until someone watches it fail. So 21 runs tested the ported checks on throwaway branches. 20 of them pushed a deliberate fault, and 19 of those failed at the expected step. The fuzz campaign run had no fault, and passed as intended.

The wasm:wasi-check run passed with its fault in place, because that check could never fail. PR B, #1001, now fixes it. The full table is in the deliberate-break results comment on this PR.

Two checks could not be watched in full. The nightly fuzz run starts only from main, so its first run comes after this PR merges. tests.yml cannot be started by hand, so its break run added a manual trigger on the throwaway branch.

CI passes, including the Go live test

  • CI: 30 checks pass and 12 are skipped.
  • The Go live test TestLiveTermOrderIsPlaintextOrder failed on an earlier run of this PR. It now passes, because the suite history that PR B imports now includes cipherstash/cipherstash-suite#2292, which fixed the test.

Merge this PR with a merge commit after PR B

Merge it with a merge commit, which keeps every commit, and never with a squash or a rebase. PR D is built on these exact commits. A squash or a rebase would leave PR D based on commits that are not in main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a

@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: c93a39b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Mutation testing (cargo-mutants, --in-diff, stack-auth + stack-encrypt)

No mutants were generated for the changed lines.

auxesis added a commit that referenced this pull request Oct 2, 2026
mise merges config down the directory tree, so every tool in the root
mise.toml is also part of the toolset in packages/eql and in
languages/typescript/packages/protect-ffi. Their CI jobs run
`mise install` (and `mise run`) in those folders, so each one now built
cargo-nextest, cargo-llvm-cov, cargo-crap, cargo-mutants, cargo-fuzz and
cargo-udeps from source, none of which they use.

mise's cargo backend compiles a `cargo:` tool with the runner's default
rustc (1.92 on the Ubuntu images), not the root's pinned 1.94.1. So a
`latest` that raises its minimum Rust fails the install: cargo-udeps
0.1.61 needs cargo@0.96, which needs rustc 1.93. That broke the
protect-ffi integration suite on #1001 and the new udeps job on #1003.

The cargo tools move to mise.test.toml, which mise loads only for the
`test` environment (`mise x --env test`, or MISE_ENV=test). Nested
workspaces no longer inherit them. Each is pinned to an exact version
that builds on the runners' rustc; cargo-udeps goes back to 0.1.60.
The tasks that call them (crap:*, mutants:*, fuzz:*, and nextest in
wasm:guest:test and wasm:auth-guest:test) now run them through
`mise x --env test --`, as test:doc and the crap coverage step already
did, so `mise run <task>` keeps working locally without MISE_ENV.

Rust, Go, golangci-lint and wasm-pack stay in the root mise.toml. They
are prebuilt downloads, and plan section 3.5 has protect-ffi inherit
the Rust pin. That section's intent holds: the repository pins one
version of each cargo tool, reached through mise. The pins now sit in
the test environment rather than the default one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
auxesis added a commit that referenced this pull request Oct 2, 2026
`pnpm run test` now reaches the @cipherstash/auth and
@cipherstash/profile vitest suites, which load the napi module. Their
`test` scripts do not build it, and nothing else in run-tests does, so
profile-store.test.ts failed with "Failed to load native binding for
linux-x64" on this PR (Run Tests, Node 22 and 24).

run-tests now builds both bindings with `build:debug` after the
protect-ffi binding, before the test steps. The previous commit has
`build:debug` write its typings to the committed native.d.ts, so the
build leaves the tree clean. This step and that fix came from the CI
port (#1003); they land here because this PR merges first and has to
pass on its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
@auxesis
auxesis force-pushed the ci/port-stack-crates branch from 662a4bd to 430e826 Compare October 2, 2026 00:41
@blacksmith-sh

This comment has been minimized.

@auxesis

auxesis commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Deliberate-break results for this PR

All 21 breaks from the plan's §1 rule ran on throwaway branches against 430e826b, on 2 October 2026. 19 failed at the expected step, the fuzz campaign passed as intended, and one check was dead.

Workflow and job Break Result
tests-crates / rust rustfmt, clippy, a UI .stderr line, a README doctest, a broken rustdoc link Each failed at its own step
tests-crates / rust A compile error in an example Failed at clippy first. Under default features only, it failed at "Build the stack-encrypt examples"
tests-crates / node-bindings A stale native.d.ts, a profile test, a stack-auth-wasm test Each failed at its own step
tests.yml / run-tests The binding build step deleted pnpm run test failed to load the profile binding
crap-crates, mutants, fuzz regression, miri, udeps One break each Each failed at its own step. The mutants comment showed 0 caught and 5 missed
fuzz campaign None: dispatched for 30 seconds Passed, and all 6 legs saved their corpus
tests-golang The guest import gate, golangci-lint, a bad checksum, an empty CS_CLIENT_KEY Each failed at its own step
tests-golang / wasi-check wasm-bindgen added to stack-kms Passed: the check was dead

The dead check is fixed in PR B

wasm:wasi-check greps cargo tree output. CI sets CARGO_TERM_COLOR=always, so cargo wraps the tree prefix in colour codes, and the grep could never match. PR B, #1001, now runs cargo tree --color never, and a script test guards it. With colour on, the fixed gate fails on the same break and passes on the clean tree. cipherstash-suite has the same task and setting, so its gate is dead too.

Two checks could not be fully watched

  • The fuzz cron only fires from the default branch, so it waits until this PR merges.
  • tests.yml has no workflow_dispatch, so its break added one on the throwaway branch.

Every throwaway branch, the throwaway draft PR and the caches they made have been deleted.

🤖 Generated with Claude Code

auxesis and others added 3 commits October 2, 2026 18:00
Adds the root workflows for the imported set (plan section 7.5):
tests-crates, crap-crates, mutants, fuzz, miri, tests-golang and udeps,
ported from the suite's test-unit (the set's share), test-stack-auth,
test-stack-profile, crap-stack-auth, crap-stack-encrypt, mutants, fuzz,
miri, test-wasi and test-no-unused-cargo-dependencies. Every job runs
mise at the root with `working_directory: .`. The root workspace is
tested with nextest only; plain `cargo test` races the stack-kms tests
that set environment variables.

tests-crates.yml rebuilds both node bindings and fails if that leaves
the auth or profile package changed, untracked files included: napi
must write native.d.ts and never the hand-written index.d.ts.
tests.yml's binding-build step, from the import PR, now points at
that check.

Registers the new jobs with the workflow guards: the four PR-only gates
in workflow-paths-filter-parity, the mutants comment job in
workflow-publish-permissions, and the Go live job in
ffi-binding-step-order. The EQL rust-cache workspace check now applies
to EQL's jobs only, with a non-empty floor. AGENTS.md and
docs/fuzzing.md describe the CI that now exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
crates-ci.test.mjs holds that a root workflow reaches every mise task
the set defines (or names why not, and fails on a stale exemption),
that the imported workflow directory stays gone, that the trybuild ui
binary runs somewhere, that NEXTEST_PROFILE names a defined profile,
that the Go jobs test the guests whose sha256 the Linux job recorded,
and that @cipherstash/profile stays private.

Two more, for this repository: every napi build in the auth and
profile bindings writes `--dts native.d.ts`, which is committed and
diffed by the tests-crates drift guard; and no root step runs plain
`cargo test` over the workspace, where the stack-kms env-var tests
race.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
The import PR moved the cargo tools (nextest, llvm-cov, crap, mutants,
fuzz, udeps) from the root mise.toml to mise.test.toml, pinned, so
that EQL and protect-ffi stop inheriting them. mise loads that file
only in the test environment, so every job here that uses one now sets
MISE_ENV: test. This also fixes udeps.yml, which built the floating
`latest` cargo-udeps (0.1.61, which needs rustc 1.93) with the
runner's rustc 1.92; the test environment pins 0.1.60.

`mise run` and `mise x` install whatever tool of the toolset is
missing, and mise-action caches only what its own install step
installed. So tests-crates (rust), tests-golang (wasi-check),
crap-crates and fuzz, whose steps go through `mise run` or `mise x`,
drop `install_args` and install the whole test toolset, which then
shares one cache. mutants and udeps call their tool directly, so they
keep a narrow install.

The workflows that read mise.test.toml now list it in their path
filters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
auxesis added a commit that referenced this pull request Oct 2, 2026
mise merges config down the directory tree, so every tool in the root
mise.toml is also part of the toolset in packages/eql and in
languages/typescript/packages/protect-ffi. Their CI jobs run
`mise install` (and `mise run`) in those folders, so each one now built
cargo-nextest, cargo-llvm-cov, cargo-crap, cargo-mutants, cargo-fuzz and
cargo-udeps from source, none of which they use.

mise's cargo backend compiles a `cargo:` tool with the runner's default
rustc (1.92 on the Ubuntu images), not the root's pinned 1.94.1. So a
`latest` that raises its minimum Rust fails the install: cargo-udeps
0.1.61 needs cargo@0.96, which needs rustc 1.93. That broke the
protect-ffi integration suite on #1001 and the new udeps job on #1003.

The cargo tools move to mise.test.toml, which mise loads only for the
`test` environment (`mise x --env test`, or MISE_ENV=test). Nested
workspaces no longer inherit them. Each is pinned to an exact version
that builds on the runners' rustc; cargo-udeps goes back to 0.1.60.
The tasks that call them (crap:*, mutants:*, fuzz:*, and nextest in
wasm:guest:test and wasm:auth-guest:test) now run them through
`mise x --env test --`, as test:doc and the crap coverage step already
did, so `mise run <task>` keeps working locally without MISE_ENV.

Rust, Go, golangci-lint and wasm-pack stay in the root mise.toml. They
are prebuilt downloads, and plan section 3.5 has protect-ffi inherit
the Rust pin. That section's intent holds: the repository pins one
version of each cargo tool, reached through mise. The pins now sit in
the test environment rather than the default one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
auxesis added a commit that referenced this pull request Oct 2, 2026
`pnpm run test` now reaches the @cipherstash/auth and
@cipherstash/profile vitest suites, which load the napi module. Their
`test` scripts do not build it, and nothing else in run-tests does, so
profile-store.test.ts failed with "Failed to load native binding for
linux-x64" on this PR (Run Tests, Node 22 and 24).

run-tests now builds both bindings with `build:debug` after the
protect-ffi binding, before the test steps. The previous commit has
`build:debug` write its typings to the committed native.d.ts, so the
build leaves the tree clean. This step and that fix came from the CI
port (#1003); they land here because this PR merges first and has to
pass on its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
@auxesis
auxesis force-pushed the build/import-stack-crates branch from bceb0bc to 1ad1055 Compare October 2, 2026 08:07
@auxesis
auxesis force-pushed the ci/port-stack-crates branch from c8343ba to 897da0a Compare October 2, 2026 08:07
The second run of tests-crates `rust` and tests-golang `wasi-check` on
a mise cache key failed with "'cargo-fmt' is not installed for the
toolchain '1.94.1-x86_64-unknown-linux-gnu'" (and the same for
clippy). mise installs the root's rust as a symlink into ~/.rustup,
and mise-action caches ~/.local/share/mise but not ~/.rustup. The
ubuntu-2204 runners already carry a 1.94.1 toolchain, so on a cache
hit the symlink resolves, mise reports rust as installed, and the
components mise.toml declares are never added. The first run on a key
misses the cache and installs them, which is why the drafts passed.

Both jobs now run `rustup component add rustfmt clippy` after
mise-action, as test-eql.yml and bench-eql.yml already do. A guard in
crates-ci.test.mjs derives which jobs need it: any job whose root
mise-action can restore a cache and that runs `cargo fmt` or `cargo
clippy`, directly or through the mise tasks it calls, must add those
components before the step. It failed on exactly the two jobs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a

@yujiyokoo yujiyokoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems mostly adding workflows for CI. Left one comment but otherwise LGTM

Reviewed with GPT-6-Luna medium


# Fast pre-flight: fail in seconds if a secret was rotated or cleared.
# Ordering is asserted by scripts/__tests__/ffi-binding-step-order.test.mjs.
- uses: ./.github/actions/require-cs-secrets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM review (GPT-6-Luna medium) says this requires credentials, which is not available to fork PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants