Skip to content

ci(nightly): package the two publishable crates and check what actually ships (#179 E1) - #292

Merged
mario4tier merged 2 commits into
TA-Lib:devfrom
kevinlincg:issue-179-e1-cargo-package-gate
Aug 30, 2026
Merged

ci(nightly): package the two publishable crates and check what actually ships (#179 E1)#292
mario4tier merged 2 commits into
TA-Lib:devfrom
kevinlincg:issue-179-e1-cargo-package-gate

Conversation

@kevinlincg

Copy link
Copy Markdown
Collaborator

Nothing in CI had ever run cargo package. Every Rust step in the dev nightly —
clippy on both crates, rustdoc with -D warnings, the 534 doctests, cargo test --tests — compiles the crate in the workspace, where every file on disk is
reachable whether or not it would ship. The .crate tarball is a different
artifact: cargo builds its file list from git, filtered by the manifest's
include/exclude and by every .gitignore above it. So a file can be
present, compiled, linted, documented and tested here and still be absent from
what crates.io receives, and the first place that shows up is a published
release that does not build. #179 E1 asks for exactly this step, and with A3
(LICENSE in the crate) and A5 already landed there is now content in the tarball
that only the tarball can check.

One step at the end of the existing clippy job, driving
scripts/rust_package_check.py.

What it checks

1. It packages the pair. cargo package -p ta-lib-dispatch -p ta-lib, not
the library alone: the library pins ta-lib-dispatch = { version = "=0.1.2" },
which is not on crates.io yet, so on its own it cannot resolve — packaging both
makes cargo build a temporary registry from the sibling. The manifest comment on
that dependency already said so; this is the first thing that runs it. Cargo's
verification pass then compiles each crate from its own unpacked tarball, and
that is the leg that catches a source file which did not ship.

2. Contents, against git. For each crate, the entries in the .crate must be
exactly that crate's git-tracked files plus the three cargo generates
(Cargo.lock, Cargo.toml.orig, .cargo_vcs_info.json). Missing entries mean
the release is short a file; unexpected extras mean something untracked is being
shipped.

The expectation is derived from git ls-files, not recorded as a count. That is
the point: it needs no edit when a function is added, and it still fails when one
goes missing. Today that is 191 tracked files for ta-lib (183 of them
src/ta_func/*.rs) and 4 for ta-lib-dispatch — 194 and 7 tarball entries once
the generated three are counted.

3. Anti-vacuity. Check 2 passes for the wrong reason on an empty expectation:
git ls-files outside a checkout returns nothing, and the empty set is a subset
of anything. So each crate's tracked set must be non-empty, ta-lib must carry
Cargo.toml, LICENSE, README.md and src/lib.rs, and its packaged
src/ta_func/*.rs count must clear a floor of 100. The floor is deliberately
crude and is not the count check — check 2 is what makes the count exact; this
only refuses to run on nothing.

Controls — broken three ways, and watched

break cargo package clippy / rustdoc / doctests / --tests this step
exclude = ["LICENSE"] green all green red
exclude = ["src/ta_func/sma.rs"] red (E0583) not run red
untracked src/ta_func/zz_untracked_probe.rs green not run red

The first row is the one that carries the claim. With LICENSE excluded from the
manifest, both verification builds still compile and cargo exits 0; clippy stays
green, rustdoc with -D warnings stays green, all 534 doctests pass, the
--tests suites pass. Only this step goes red:

=== PACKAGE GATE FAILED ===
  ta-lib: 1 git-tracked file(s) did not make it into the .crate: LICENSE
  ta-lib: the .crate is missing LICENSE

exit 1. That is the gap, and nothing else in the nightly can see it.

Second row: excluding an indicator source makes cargo's verification build fail
to compile the tarball (rustc E0583, 14 errors, cargo exit 101). The script
reports that as its own failure rather than as a Python traceback.

Third row is a finding, not just a control: with an untracked, unignored file in
the crate directory, cargo ships it — 194 entries become 195, and
cargo package is perfectly green about it. Untracked scratch leaking into a
published release is a second thing this closes, and it is not the one E1 asked
about.

Cost

~3s here with the workspace target/ warm, which is the CI shape — this step
runs after the four that already built the crate. The first run in a cold
container took 17s, essentially all of it the two verification builds. Nightly
only, per E1; pr-codegen-gate.yml is untouched, so this does not collide with
the two gate PRs in flight.

No setup-python step: the script is stdlib-only and ubuntu-latest ships
python3, so the clippy job stays the self-contained Rust-only job it
advertises itself as. No --allow-dirty in CI either — it runs on a fresh
checkout, so an unexpectedly dirty tree is itself worth hearing about; the flag
exists on the script for local runs over an edited tree.

Scope and what was not run

Two files, neither generated. The generator and all four backends are unchanged,
so generate is a no-op by construction — and scripts/ is outside
calculate_sources_digest()'s file_patterns, so TA_LIB_SOURCES_DIGEST does
not move.

I did not run: generate, the C suites, or the cross-language legs — nothing
this touches is compiled into a language server. I did not measure the step on a
GitHub runner; the two timings above are from this container. I did not run the
--tests or doctest steps under the second and third controls, only under the
first.

Unresolved

This does not run cargo publish --dry-run. On the pinned-but-unpublished
dispatch version that would need --no-verify or a registry override to get past
resolution, and it would add nothing over what cargo package already does here.
Worth revisiting once dispatch 0.1.2 is on crates.io — that is the point at which
-p ta-lib alone starts working too, and this script's two-crate invocation
could be simplified.

…ly ships (TA-Lib#179 E1)

Nothing in CI had ever run `cargo package`. Every Rust step in the nightly --
clippy, rustdoc, the doctests, `cargo test --tests` -- compiles the crate IN the
workspace, where every file on disk is reachable whether or not it would ship.
The `.crate` tarball is a different artifact: cargo builds its file list from
git, filtered by the manifest's include/exclude and by every .gitignore above
it. So a file can be present, compiled, linted, documented and tested here and
still be absent from what crates.io receives, and the first place that shows is
a published release that does not build.

scripts/rust_package_check.py, one step at the end of the existing `clippy` job:

  1. `cargo package -p ta-lib-dispatch -p ta-lib`. The pair, not the library
     alone: it pins `ta-lib-dispatch = "=0.1.2"`, which is not on crates.io yet,
     so packaged alone it cannot resolve -- packaging both makes cargo build a
     temporary registry from the sibling, as the manifest comment on that
     dependency already says. Cargo's verification pass then COMPILES each crate
     from its own unpacked tarball, which is the leg that catches a source file
     that did not ship.

  2. Contents: the entries in each `.crate` must be exactly that crate's
     git-tracked files, plus the three cargo generates (Cargo.lock,
     Cargo.toml.orig, .cargo_vcs_info.json). Derived from `git ls-files`, not
     from a recorded count, so adding a function needs no edit here and losing
     one still fails. Today: 191 tracked files for ta-lib (183 of them
     src/ta_func/*.rs) and 4 for ta-lib-dispatch; 194 and 7 tarball entries.

  3. Anti-vacuity, because check 2 passes for the wrong reason on an empty
     expectation (`git ls-files` outside a checkout returns nothing, and the
     empty set is a subset of anything): each tracked set must be non-empty,
     ta-lib must carry Cargo.toml / LICENSE / README.md / src/lib.rs, and its
     packaged src/ta_func/*.rs count must clear a floor of 100. The floor is
     deliberately crude -- check 2 is what makes the count exact; this only
     refuses to run on nothing.

Measured, on this branch, by breaking it three ways:

  exclude = ["LICENSE"]  -- cargo package itself stays GREEN (both verification
    builds compile), and so do all four existing Rust steps: clippy, rustdoc
    with -D warnings, 534 doctests, the --tests suites. Only this step goes red:
    "1 git-tracked file(s) did not make it into the .crate: LICENSE" plus the
    required-file line, exit 1. That is the gap, and nothing else in the nightly
    can see it.

  exclude = ["src/ta_func/sma.rs"] -- cargo's verification build fails to
    compile the tarball (rustc E0583, 14 errors), cargo exits 101, the step
    reports it as this gate's failure rather than a traceback.

  an untracked src/ta_func/zz_untracked_probe.rs -- cargo SHIPS it (194 -> 195
    entries) and cargo package is green; the contents check names it. Untracked
    scratch files leaking into a release is the second thing this closes, and it
    was not the one TA-Lib#179 E1 asked about.

Cost: ~3s here with the workspace target dir warm, which is the CI shape (this
step runs after the four that already built the crate); the first run in a cold
container took 17s, all of it the two verification builds. Not measured on a
GitHub runner. Nightly only, per E1 -- the PR gate is untouched.

Two files, neither generated: the generator and every backend are unchanged, so
`generate` is a no-op by construction. NOT run here: `generate`, the C suites,
and the cross-language legs -- nothing this touches is compiled into a server.
scripts/ is outside calculate_sources_digest()'s file_patterns, so
TA_LIB_SOURCES_DIGEST does not move.
`<workspace>/target/package` is only where cargo puts the `.crate` tarballs
when nothing has moved the target directory. CARGO_TARGET_DIR and
`build.target-dir` both move it, and sharing one target directory across
worktrees is the ordinary reason to set either.

With CARGO_TARGET_DIR set, the gate packaged both crates correctly and then
reported "expected exactly one ta-lib-dispatch-<version>.crate ... found 0",
naming a directory cargo had never written to. It also aimed the stale-tarball
sweep at that same wrong directory, so the run could not clean up after itself.

Ask cargo instead of assuming: `cargo metadata` reports the target directory it
will actually use, whatever moved it.

CI sets neither variable, so the nightly step is unaffected -- this is the
local-reproduction path the gate's own error message points contributors at.

Measured, same commit, one-line manifest sabotage (exclude LICENSE):

  CARGO_TARGET_DIR set,   healthy tree   before: FAIL (found 0)   after: PASS
  CARGO_TARGET_DIR unset, healthy tree   before: PASS             after: PASS
  CARGO_TARGET_DIR set,   LICENSE gone   after: FAIL, naming LICENSE

The third leg is the one that matters: before this change that leg died on the
glob, before opening a tarball, so it could not have told a real packaging
defect from a moved target directory.
@kevinlincg
kevinlincg force-pushed the issue-179-e1-cargo-package-gate branch from bd2c893 to 046535b Compare August 30, 2026 14:51
@mario4tier
mario4tier merged commit c82450e into TA-Lib:dev Aug 30, 2026
4 checks passed
@kevinlincg
kevinlincg deleted the issue-179-e1-cargo-package-gate branch August 31, 2026 05:55
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.

2 participants