ci(nightly): package the two publishable crates and check what actually ships (#179 E1) - #292
Merged
mario4tier merged 2 commits intoAug 30, 2026
Conversation
…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
force-pushed
the
issue-179-e1-cargo-package-gate
branch
from
August 30, 2026 14:51
bd2c893 to
046535b
Compare
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 isreachable whether or not it would ship. The
.cratetarball is a differentartifact: cargo builds its file list from git, filtered by the manifest's
include/excludeand by every.gitignoreabove it. So a file can bepresent, 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
clippyjob, drivingscripts/rust_package_check.py.What it checks
1. It packages the pair.
cargo package -p ta-lib-dispatch -p ta-lib, notthe 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
.cratemust beexactly that crate's git-tracked files plus the three cargo generates
(
Cargo.lock,Cargo.toml.orig,.cargo_vcs_info.json). Missing entries meanthe 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 isthe 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 themsrc/ta_func/*.rs) and 4 forta-lib-dispatch— 194 and 7 tarball entries oncethe generated three are counted.
3. Anti-vacuity. Check 2 passes for the wrong reason on an empty expectation:
git ls-filesoutside a checkout returns nothing, and the empty set is a subsetof anything. So each crate's tracked set must be non-empty,
ta-libmust carryCargo.toml,LICENSE,README.mdandsrc/lib.rs, and its packagedsrc/ta_func/*.rscount must clear a floor of 100. The floor is deliberatelycrude 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
cargo package--testsexclude = ["LICENSE"]exclude = ["src/ta_func/sma.rs"]src/ta_func/zz_untracked_probe.rsThe first row is the one that carries the claim. With
LICENSEexcluded from themanifest, both verification builds still compile and cargo exits 0; clippy stays
green, rustdoc with
-D warningsstays green, all 534 doctests pass, the--testssuites pass. Only this step goes red: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 packageis perfectly green about it. Untracked scratch leaking into apublished 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 stepruns 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.ymlis untouched, so this does not collide withthe two gate PRs in flight.
No
setup-pythonstep: the script is stdlib-only andubuntu-latestshipspython3, so theclippyjob stays the self-contained Rust-only job itadvertises itself as. No
--allow-dirtyin CI either — it runs on a freshcheckout, 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
generateis a no-op by construction — andscripts/is outsidecalculate_sources_digest()'sfile_patterns, soTA_LIB_SOURCES_DIGESTdoesnot move.
I did not run:
generate, the C suites, or the cross-language legs — nothingthis 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
--testsor doctest steps under the second and third controls, only under thefirst.
Unresolved
This does not run
cargo publish --dry-run. On the pinned-but-unpublisheddispatch version that would need
--no-verifyor a registry override to get pastresolution, and it would add nothing over what
cargo packagealready does here.Worth revisiting once dispatch 0.1.2 is on crates.io — that is the point at which
-p ta-libalone starts working too, and this script's two-crate invocationcould be simplified.