Skip to content

feat(ci): tests-python.yml for the Python SDK workspace - #1406

Open
yujiyokoo wants to merge 4 commits into
feat/python-sdk-skeletonfrom
feat/python-sdk-ci
Open

yujiyokoo wants to merge 4 commits into
feat/python-sdk-skeletonfrom
feat/python-sdk-ci

Conversation

@yujiyokoo

@yujiyokoo yujiyokoo commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Python SDK at languages/python had no CI. This adds tests-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. python runs fmt, clippy, cargo test --locked, and pytest on both interpreters. wheels builds six abi3-py39 release wheels, uploads them as artifacts, and writes the build time per platform to the job summary. No job receives a CS_* credential.
  • scripts/__tests__/python-ci.test.mjs: holds the workflow to the tree. The path filter must cover every path dependency in the workspace's Cargo.lock. The Python matrix must be the requires-python floor and the version in mise.toml. The wheel matrix must match the protect-ffi platform packages.
  • scripts/__tests__/eql-suite-ci.test.mjs: counts a mise run as an EQL task only from a step that runs under packages/eql. Before this, mise run test from languages/python was read as EQL's test task.
  • scripts/__tests__/lib/workflows.mjs: two shared helpers, triggers and runWorkingDirectory. Six existing guards switch to triggers.
  • languages/python/Cargo.lock: re-seeded from the root lock. The old one named crate versions the root no longer uses, so every --locked check failed.
  • tests.yml: excludes languages/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 plain cargo test.

Verification

  • scripts/__tests__ meta-tests: 83 files pass.
  • In languages/python: cargo fmt --all --check, cargo clippy --locked, mise run test:cargo, and mise run test pass locally.
  • shellcheck is clean on every run: block.
  • No CI run has happened yet. The six wheel legs are untested until the first run of this PR.

Related

Closes #1383. Stacked on #1405. The live test job is #1389.

Review notes

This PR adds no Python lockfile. A uv.lock comes 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 test extra. 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:

  • A new release of a test package can fail CI on a PR that did not change anything related.
  • Two runs of the same commit can install different versions.
  • The live job installs releases nobody has checked, while it has the CS_* secrets in its environment.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d0a8e3a

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

@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI; Enforced overrides: Organization
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c8071aba-4e79-4648-ab02-09f9459cc532

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

yujiyokoo and others added 3 commits October 28, 2026 16:23
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>
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>
@yujiyokoo
yujiyokoo added this pull request to stack #1520 November 4, 2026 02:58
@yujiyokoo
yujiyokoo marked this pull request as ready for review November 10, 2026 00:40
@yujiyokoo
yujiyokoo requested a review from a team as a code owner November 10, 2026 00:40

@cipherstash-agent cipherstash-agent 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.

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"

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.

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

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.

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]

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.

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 cipherstash-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 only secrets.CS_ and vars.CS_. The bracket form secrets['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.

Comment thread AGENTS.md
- `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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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/python

Found by 1 model: claude

const paths = on.pull_request?.paths ?? []
const wanted = [
PYTHON_ROOT,
...pathDependencyTrees(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants