ci(sol): build the tree .soldeerignore publishes, not the repo tree - #316
ci(sol): build the tree .soldeerignore publishes, not the repo tree#316thedavidmeister wants to merge 5 commits into
Conversation
`.soldeerignore` is a second, hand-maintained definition of what a library is, disjoint from the source graph `forge build` walks in the repo. The repo tree is always complete, so a source the filter drops — or a shipped file whose import the filter drops — resolves in every check rainix runs today and only fails in a consumer's build after `soldeer install`. Add `rainix-static soldeer-package-build`: unpack what `forge soldeer push --dry-run` would upload into a scratch project, scaffold the build config, remappings and lockfile a consumer supplies, resolve declared dependencies, and `forge build` it. Wire it into `rainix-sol-static.yaml` ahead of slither. A repo whose foundry.toml declares no `[package]` name and version publishes nothing and is skipped. `soldeer_gate`'s zip and foundry.toml helpers are generalised from the cwd and the `version` key to an arbitrary directory and key, and shared; behaviour is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The skip case only had a foundry.toml declaring neither `name` nor `version`, so a build that accepted either half alone passed the suite. Assert both half-declared shapes skip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 10 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The step resolved `rainix-static` out of the sol-shell pinned by `env.RAINIX_SHA`, which is an older main commit whose binary has no `soldeer-package-build` — measured against the real pin (`53e96a7d`): `unknown subcommand "soldeer-package-build" … (available: no-submodules, soldeer-gate)`, exit 2. Every consumer's static job would have gone red on the first push after this landed. Move the step into a composite, the way `no-submodules` already resolves its check: `forge` still comes from the pinned sol-shell, so the package builds on the same toolchain as the rest of the job, while the check itself runs from the composite's own checkout through a `path:` ref, which tracks the action version and makes no api.github.com HEAD lookup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Any repo can call this composite, so its input is untrusted text. Bind it to an env var the way `rainix-autopublish.yaml` already binds `soldeer-package`, instead of interpolating it into the shell command. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`slither .` failed inside `nix-quick-install-action`, fetching `nix-2.24.12-x86_64-linux.tar.zstd`: `tar: Child returned status 1 / Error is not recoverable`, 200ms in, before any repo code ran. The same step succeeded in the twelve sibling jobs of that run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CI half of rainlanguage/rain.sol.codegen#76. The repo half — removing the
dangling import that finding is about — is
rainlanguage/rain.sol.codegen#129.
These two must land together. The consumer PR carries
Closes #76, somerging it alone closes the issue and silently drops the gate that keeps the
finding from coming back.
Why this is in rainix and not in the consumer repo
.soldeerignoreis a second, hand-maintained definition of what a library is,disjoint from the source graph
forge buildwalks. The repo tree is alwayscomplete, so a source the filter drops — or a shipped file whose import the
filter drops — resolves in every check that runs against the repo and fails only
in a consumer's
forge buildaftersoldeer install. Nothing in the org checksthe two definitions agree.
That hazard is a property of publishing to soldeer, not of
rain.sol.codegen.Every soldeer-publishing sol repo in the org has a
.soldeerignoreand the sameblind spot, and every one of them already gets its whole sol CI from
rainix-sol.yaml—rain.sol.codegen/.github/workflows/is three files with nosteps in them at all. Putting the job in the consumer would be the first inline
CI in that repo, would protect exactly one repo, and would have to be copied by
hand into the next fifteen. So:
rainix-sol-static.yaml, on every push, noconsumer wiring.
The logic is a
rainix-staticsubcommand rather than inline bash, per CLAUDE.md.It is also not optional here:
sol-shellships nounzip, so unpacking thepackage has to happen in-process, and
rainix-staticalready carries thezipcrate for
soldeer-gate.Why the step is a composite and not a
run:env.RAINIX_SHApins the sol-shell to an older main commit(
53e96a7d), and therainix-staticinside it is whatever that commit built —no-submodulesandsoldeer-gateonly. Arun: … #sol-shell -c rainix-static soldeer-package-buildtherefore exits 2 withunknown subcommandon everyconsumer, measured, not reasoned:
.github/actions/no-submodulesalready answers this, and its comment says why:run the check from the action's own checkout via a
path:ref so its versiontracks the action version "regardless of any RAINIX_SHA the caller pins". This
step does the same, with one addition —
forgestill comes from the pinnedsol-shell, which the job has already realised for its other steps, so the
package is built on the same toolchain as the repo tree rather than a second
one. A
path:ref makes no api.github.com HEAD lookup, so the unpinned-flake-refban in CLAUDE.md is respected. The workflow and the composite land in the same
commit range on
main, so a consumer pinning the reusable@mainnever seesone without the other.
What
rainix-static soldeer-package-builddoes[package] name/versionfromfoundry.toml. A repo that does notdeclare both publishes nothing and is skipped — so this is a no-op for every
non-publishing consumer and needs no input.
forge soldeer push <name>~<version> --dry-run— the same artifactsoldeer-gatealready hashes, i.e. exactly what would be uploaded.if they are absolute or contain
...foundry.toml,remappings.txtandsoldeer.lockonly when thepackage does not ship them —
.soldeerignoreusually excludes all threeand a consumer supplies them. A file the package ships is left alone.
forge soldeer installwhen the scratchfoundry.tomlopens a[dependencies]table, in either the[dependencies]or[dependencies.<name>]form.forge build. Non-zero exit fails the job and leaves the unpacked tree ondisk, named in the error, for inspection. A clean run removes it.
The published tree keeps its own directory names, so the copied
foundry.toml'ssrc/script/testpaths apply unchanged andforge buildcompiles every.solin the package rather than only what something imports.Shared with
soldeer-gateread_zip,run_cmdand the zip-in-a-directory helpers are nowpub(crate)and take the directory as an argument instead of assuming the cwd;
is_version_line/read_local_versiongeneralise tois_key_line/read_local_fieldsonamereads the same wayversionalready did.soldeer_gate's behaviour is unchanged and its tests are untouched.QA
The subcommand is new, so "fails on base" is the real consumer, not a stub:
built at this branch and run against unmodified
rain.sol.codegenmain(
935c725) it exits 1 on a genuinely broken published package, and exits 0once that repo's import is fixed. Output for both below. Bats 1 and 2 also
failed for the wrong reason on their first run (
new Helper()in apurefunction) — the fixture was fixed, not the assertion.
baseline row and the per-mutant killing test named. R6 survived the first
pass and a discriminating test was added for it; that history is in the matrix.
.soldeerignoresays/testdoes not publish, so a shippedscript/Build.solimporting
../test/concrete/Helper.solmust be unresolvable aftersoldeer install, and must resolve once the imported file moves undersrc/.The real-world confirmation is
rain-sol-codegen~0.1.13, a published packagethat is broken in exactly this way today, green on every other check.
package as published, in CI, on every push; (2) remove the current
inconsistency. This PR is (1); rain.sol.codegen#129 is (2). Both covered, and
they land together.
Failing first, on a real consumer. Built at this branch, run against an
unmodified
rain.sol.codegenmain(935c725) — itsscript/Build.solshipsand imports
../test/concrete/CodeGennable.sol, which.soldeerignoreexcludes:forge test,forge fmt --checkandslither .are all green on that sametree. This is the only check that sees it.
Passing after the consumer-side fix, same command, same scratch dir:
The wiring, run as CI will run it. Both forms invoked against a real
consumer through the real pin (
53e96a7d), the sha bound to a shell variableexactly as the composite binds it to
env.RAINIX_SHA. Tails only:A is what a
run:step would have done to every consumer's static job. C showsthe composite form still discriminates.
Suites.
cargo test→test result: ok. 113 passed; 0 failed.cargo clippy --all-targets -- -D warnings→ clean.bats test/bats/task/soldeer-package-build.test.bats→ 5/5, hermetic (thefixture package declares no dependencies and pins
solcto this shell'ssolc-0.8.25, so no network):Mutation matrix. Every mutant applied alone to the committed tree, both
oracles re-run, mutant reverted, tree confirmed clean at the end. The baseline
row proves the harness runs and that a green reading is reachable — a mutated
build that failed to compile would show up as an
errorline, not as a kill.cargo test1..5, 5 oksafe_entry_pathaccepts..escaping_entry_paths_are_rejectedFAILEDscaffold_missingoverwrites files the package shipsscaffolding_takes_only_what_the_package_omitsFAILEDdeclares_dependenciesstops matching[dependencies.x]dependencies_table_detectionFAILEDwrite_entrycounts.mdinstead of.sola_solidity_entry_is_counted_and_written_with_its_parentsFAILEDnot ok 2forge buildis ignorednot ok 1,not ok 3versionis defaulted, not skippednot ok 5SCAFFOLD_FILESdropsfoundry.tomlnot ok 1,not ok 2,not ok 3nameis defaulted, not skippednot ok 5R6 survived the first pass: the only skip test declared neither
[package]field, so a build that accepted either half alone was invisible. Test 5 (
a half-declared package is skipped rather than built) was added for it, and R8was then written for the symmetric half to prove test 5 covers the property and
not just the one mutant. Both die on it.
R5, R6, R7 and R8 are all blind to
cargo test— they live inrun(), whichspawns
forge, so the bats layer over the built binary is the only oracle thatcan see them. That is what the bats file is for.
Blast radius on landing
rainix-sol-static.yamlis pinned@mainby every sol consumer, so this gategoes live everywhere on merge. Reading
foundry.tomlfrom every non-archivedrepo in
rainlanguage,cyclofinanceandS01-Issuer: 30 declare[package]and will actually build (rain.deploy,raindex,rain.math.float,rain.metadata,st0x.deploy, …); 14 declare none and area no-op.
Any of those 30 whose published tree is already broken reds on its next push.
That is the check working — each such red is a package that does not compile
for a consumer today — but it is a real landing cost. Only
rain.sol.codegenwas measured (red before rain.sol.codegen#129, green after); the other 29 were
not swept, so the count of new reds is unknown. Sweeping them before merge is a
job on its own and was not in scope here; say the word and it can be run.
Found, not touched
rainix-autopublish.yaml:198runsrainix-static soldeer-gatethrough theRAINIX_SHA-pinned sol-shell, the same shape this PR just moved away from. Itis not broken —
soldeer-gateexists at the pin — but consumers run the gatelogic frozen at
53e96a7d, so a change to it onmainreaches nobody until thepin is bumped, silently and with no red. Same class as what broke here; left
alone as out of scope.
pre-commit run --all-filesat this repo's root reportsrustfmt ... Failedwithcargo metadata … could not find Cargo.toml. It isnot this branch: the same command on unmodified
main(7f223b4, cleanworktree) fails identically. The
rustfmt-conditionalhook guards on[ -f Cargo.toml ] || [ -f */Cargo.toml ], which is true here becauserainix-static/Cargo.tomlexists, then runscargo-fmt fmtin the cwd — theroot, which has no manifest and no workspace. Nothing red today: this repo's own
CI runs everything under
working-directory: test/fixture(which has aCargo.toml) and uses therainix-rs-statictask, not the hook bundle, whilethe
pre-commit run --all-filesstep is in the reusable consumers call, andconsumers have a root manifest. Reported rather than fixed — out of scope for
this PR.
What this does not do
It does not check that everything a consumer should get is in the package —
only that what is in the package builds. A source excluded by
.soldeerignoreand imported by nothing shipped is still silent. Catching thatneeds a positive statement of what the library is, which is a different change.