Skip to content

Generate the network config from the roster instead of comparing it - #237

Open
thedavidmeister wants to merge 3 commits into
mainfrom
fix-233
Open

thedavidmeister wants to merge 3 commits into
mainfrom
fix-233

Conversation

@thedavidmeister

Copy link
Copy Markdown
Contributor

Closes #233. Closes #194.

LibRainDeploy.supportedNetworkConfigs() becomes the single statement of the
supported network set — name, chain id, explorer url and default endpoint.
LibRainDeployConfig emits [rpc_endpoints], [etherscan] and the
.env.example endpoint variables from it and splices each between its markers;
BuildScript.run() generates both files. supportedNetworks() is now the
roster's names.

That closes #194 by removing its subject rather than answering it. #194 said
nothing in the suite could make the foundry.toml config assertions fail: they
compared two hand-maintained statements of one set, and no test could drive
either side. With both sides written from one list there is nothing left to
compare, so testSupportedNetworksAreFullyConfigured is deleted — and with it
checkNetworksConfigured, checkEtherscanEntriesResolvable and
EtherscanEntryUnresolvable, which #229 had just moved the comparison's body
into.

Enforcement moves to Git is clean, the mechanism already holding
src/generated/: a tree whose config has drifted from the roster it pins fails
the job every push runs.

foundry refuses to write its own config

forge script ./script/Build.sol cannot write the project root's
foundry.toml — "access to foundry.toml is not allowed", a guard on the path
that no fs_permissions grant and no spelling of the path gets past, refusing
writeFile, writeLine and copyFile alike. So the fs_permissions change
#233 asked for (read → read-write on ./foundry.toml) is not the one that
works.

Reads are allowed, which is what makes the splice possible. run() reads each
file, splices its blocks and writes the result to .staged-config/;
script/build.sh installs each staged file onto the file of that name at the
root. That hook is rainix's own rainix-copy-artifacts consumer hook, which
runs outside any devshell, after the regeneration and before the git diff
that fails a stale tree. It needs no forge, no nix and no --ffi; granting
--ffi is the alternative and is not taken, because it would be granted to
every consumer's build rather than to this one step.

A repo with no script/build.sh is refused — BuildHookMissing — because
nothing else installs a staged file, and generating for such a repo would write
the roster where nothing reads it while the config went on saying whatever it
said, green.

Staging also retires the race #233 flagged as a requirement: nothing under
forge test can race a rewrite of the config every other test reads, because
nothing rewrites it.

what generation cannot settle

Whether a declared chain id is the one the bound endpoint reports is a claim
about the world rather than about the text, and chain is what --verify
submits. So RainDeployVerifyChain.testSupportedNetworkChainIdsAreBound forks
every supported network and compares block.chainid against the roster.

merging #229

main's #229 strengthened the reads this deletes, so the merge decides between
them everywhere the two meet, and generation wins: prose and assertions that
read foundry.toml back are the generator reading its own output. Kept from
#229: its reasoning about what a wrong chain costs, and its property that
every entry is checked and not only the first — ported to the roster as
testChainIdChecksEveryEntry. testRunCallsEveryHookThatRegenerates becomes
testRunCallsEveryGenerator: run() now also calls regenerateConfig, which
is deliberately not a hook, so the set it enumerates is every internal
function that can write rather than the internal virtual ones.

consumer impact

A repo inheriting BuildScript has to:

  • copy script/build.sh, and gitignore .staged-config;
  • carry the markers in its own foundry.toml and .env.example, once each,
    begin before end — everything outside them stays the consumer's, and the
    build neither reads nor moves it;
  • keep fs_permissions on ./foundry.toml and ./.env.example at read, and
    add read-write on ./.staged-config and read on ./script/build.sh.

A consumer that called checkNetworksConfigured or
checkEtherscanEntriesResolvable by hand loses them. What replaces the
assertion is that the sections are written rather than checked. The roster is
deliberately not overridable: a repo able to narrow it would deploy to and
verify fewer chains with nothing red, so a network arrives in a consumer's
config by a version bump and by nothing else.

QA

  • Discriminating tests: testRpcEndpointsSectionIsTheRoster,
    testEtherscanSectionStatesChainOnEveryEntry,
    testEtherscanSectionEntriesAreResolvable,
    testEtherscanSectionOfTheSupportedNetworksIsResolvable,
    testEtherscanSectionZeroChainIdReverts,
    testEnvExampleSectionIsTheRosterDefaults, testVariableNamesAreUppercased,
    testEmptyRosterReverts, the ten testSplice* / testWrite* cases over the
    marker splice and the staged write, testStagedPathsAreNamedAsTheFilesTheyInstallOver,
    testWriteStagedConfigWithoutBuildHookReverts, testRunStagesTheNetworkConfig,
    testRunCallsEveryGenerator, testCutReleaseLeavesTheConfigAlone,
    testChainIdChecksEveryEntry, testChainIdEmptyRosterReverts,
    testSupportedNetworksAreTheRosterNames, testSupportedNetworkChainIds,
    testSupportedNetworkExplorerUrls. None of these can be run against base to
    fail there: LibRainDeployConfig, BuildScript.regenerateConfig and the
    SupportedNetwork roster they bind are added by this PR, so the suite does not
    compile on origin/main at all. Discrimination is shown by mutation instead —
    each line below is reverted to a shape base behaves as, and a named test fails.
  • Mutations applied: mutation-probe over
    /home/thedavidmeister/code/scratch/fix-233-mutants.toml, baseline green at
    585 passed / 0 failed, 7 applied, 7 KILLED, 0 survived, 0 no-run, 0 harness
    errors.
    • RainDeployVerifyChain.checkChainIds: if (declared != reported) ->
      if (false) (the mismatch never reverts) -> testChainIdChecksEveryEntry,
      testChainIdIsReadFromTheForkedEndpoint, testChainIdMismatchReverts,
      testZoltuFactoryCodehash.
    • RainDeployVerifyChain.checkChainIds: if (declared != reported) ->
      if (true) (every match reverts) -> testChainIdMatchPasses.
    • RainDeployVerifyChain.checkChainIds: i < networks.length -> i < 1
      (only the first roster entry is checked) -> testChainIdChecksEveryEntry.
    • RainDeployVerifyChain.checkChainIds: if (networks.length == 0) ->
      if (false) (an empty roster passes having forked nothing) ->
      testChainIdEmptyRosterReverts.
    • BuildScript.run(): regenerateConfig(); deleted (run stops generating the
      config) -> testEveryHookIsReachedFromAnEntryPoint,
      testRunCallsEveryGenerator, testRunStagesTheNetworkConfig.
    • BuildScript.run(): recordRoot(); added beside it (run holds a call that
      generates nothing) -> testRunCallsEveryGenerator.
    • LibRainDeployConfig.etherscanSection: '}", chain = ', vm.toString(networks[i].chainId) -> '}"' (the generated entry states no
      chain, which is The config check passes an [etherscan] entry that takes verification down for every network #192's bug re-introduced on the generating side) ->
      testEtherscanSectionEntriesAreResolvable,
      testEtherscanSectionOfTheSupportedNetworksIsResolvable. The first pass
      scored this KILLED but named no killer, because its fail-pattern did not
      match forge's failure line for these two; re-probing this mutant alone with
      a widened pattern named them, same KILLED verdict.
  • Oracle: for the generator, string literals written out in
    test/src/lib/LibRainDeployConfig.t.sol rather than concatenated the way the
    source concatenates — an expectation built by the source's own spelling would
    pass for any spelling, including a broken one — over a fixture roster
    alpha/beta/gamma that names no real network, so nothing passes against
    this repo's own config by accident. For run()'s wiring, the compiler's AST
    for the base contract, enumerating the generators the base declares rather
    than a hand-kept list, so a generator added and left uncalled fails without
    the test being touched. For the chain ids, the forked endpoint's own
    block.chainid, which is the world rather than the text. The intent oracle is
    Generate the network config sections from supportedNetworks() instead of comparing them #233's: RainDeployVerifySnapshot's NatSpec that the config sections "MUST be
    EXACTLY supportedNetworks(), which makes the three lists one".
  • Category check: Generate the network config sections from supportedNetworks() instead of comparing them #233 asks for (a) [rpc_endpoints], (b) [etherscan] with
    each chain id, and (c) .env.example generated from the roster, (d) delimited
    so hand-written config around them survives, (e) from a hook in run(),
    (f) enforced by Git is clean, (g) the membership assertions removed, (h) a
    fork test on block.chainid with a real subject, (i) fs_permissions on
    ./foundry.toml moved read -> read-write, and (j) no test racing the rewrite.
    Covered: a, b, c, d, e, f, g, h, j. NOT covered, deliberately: (i) — forge
    refuses to write the project root's foundry.toml whatever fs_permissions
    says, so the grant stays read and script/build.sh installs from
    .staged-config/; staging is also what settles (j), since nothing rewrites
    the config under forge test at all. Nothing in the suite can make the foundry.toml config assertions fail #194 asks that the config assertions be
    capable of failing; covered by removing them, their subject being the
    comparison this generates away.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN

baku-ccron and others added 3 commits September 15, 2026 19:50
`LibRainDeploy.supportedNetworkConfigs()` becomes the single statement of the
supported network set — name, chain id, explorer url and default endpoint.
`LibRainDeployConfig` emits `[rpc_endpoints]`, `[etherscan]` and the
`.env.example` endpoint variables from it and splices each between its markers,
and `BuildScript.run()` writes both files. `supportedNetworks()` is now the
roster's names.

`testSupportedNetworksAreFullyConfigured` goes: with both sides written from
one list there is nothing left for it to compare. What generation cannot settle
is whether a declared chain id is the one the bound endpoint reports, so
`RainDeployVerifyChain` gains `testSupportedNetworkChainIdsAreBound`, which
forks every supported network and checks `block.chainid`.

Every generated `[etherscan]` entry states `chain`, which carries #229's
requirement across as a property of the generator rather than an assertion
about a hand-written file.

KNOWN BLOCKER, unresolved: foundry refuses every fs cheatcode write to the
project-root `foundry.toml` — `ensure_not_foundry_toml`, "access to
`foundry.toml` is not allowed" — regardless of `fs_permissions`. `writeFile`,
`writeLine` and `copyFile` are all refused, under every path spelling
(`foundry.toml`, `./foundry.toml`, `src/../foundry.toml`, absolute, absolute
with `..`). So `forge script ./script/Build.sol` reverts, and the `Git is
clean` job that runs it goes red. The `.env.example` half writes fine. The
suite does not see this because `BuildScriptHarness` points `configPath()` at a
fixture root, where the guard does not apply.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
Foundry refuses every filesystem cheatcode write to the project root's own
`foundry.toml` — "access to `foundry.toml` is not allowed", a guard on the path
that no `fs_permissions` grant and no spelling of the path gets past, and that
refuses `writeFile`, `writeLine` and `copyFile` alike. So `forge script
./script/Build.sol` reverted, and the `Git is clean` job that runs it went red,
while the suite stayed green because the harness pointed the writer at a fixture
root the guard does not apply to.

Reads are allowed, which is what makes this possible. `run()` now reads each
file, splices its blocks and writes the result to `.staged-config/` under the
same name; `script/build.sh` copies each staged file onto the file of that name
at the root and removes the directory. That hook is rainix's own consumer hook:
`rainix-copy-artifacts` runs it outside any devshell, after the regeneration and
before the `git diff` that fails a stale tree. It needs no forge, no nix and no
`--ffi` — the alternative, and not taken, because the invocation that matters
passes no `--ffi` and granting it there would hand FFI to every consumer's
build.

`.env.example` is staged too, though foundry would allow that one written
directly, so which file foundry happens to guard is not something the design
depends on. Staging also retires the hazard the direct write carried: nothing
under `forge test` can race a rewrite of the config every other test reads.

A repo with no `script/build.sh` is refused — `BuildHookMissing` — because
nothing else installs a staged file, and generating for such a repo would write
the roster where nothing reads it while the config went on saying whatever it
said, green. Presence is the same condition `rainix-copy-artifacts` runs the
hook on.

`configPath()` and `envExamplePath()` collapse into one `configRoot()` hook, and
`fs_permissions` on both generated files goes to READ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
Six conflicts, five of them the same decision: #233 GENERATES the network
config from `LibRainDeploy.supportedNetworkConfigs()`, so a check that reads
`foundry.toml` back is the generator reading its own output. #229 landed on
main in the other direction — it strengthened those reads. Generation wins
everywhere the two meet.

- `README.md`, `foundry.toml`: the branch's text and the generated blocks. The
  `[etherscan]` block is emitted, so main's prose inside it and its
  hand-maintained comment cannot survive there; the rationale for stating
  `chain` on every entry lives in `LibRainDeployConfig` instead.
- `RainDeployVerifySnapshot.sol`: `testSupportedNetworksAreFullyConfigured` is
  gone. With both sides written from one list there is nothing to compare.
- `RainDeployVerifyChain.sol`, `RainDeployVerifyChain.t.sol`: the branch's
  roster-based `checkNetworkChainIds` is kept and main's `declaredChainIds`,
  `DeclaredChainId` and `NoDeclaredChainIds` are deleted, for the same reason.
  Main's prose about what a wrong `chain` costs is kept; so is its property
  that every entry is checked and not only the first, ported to the roster as
  `testChainIdChecksEveryEntry`.
- `BuildScript.t.sol`: both constant sets, which do not overlap.

Two things followed from those resolutions rather than being conflicts.

`checkNetworksConfigured` and `checkEtherscanEntriesResolvable` are deleted
from `RainDeployVerifySnapshotBase`, with `EtherscanEntryUnresolvable` and
their tests. #229 moved the comparison's body there from the test; deleting
the test leaves it dead, and it IS the comparison — keeping it would leave a
tested, consumer-callable assertion about a file this package now writes.

`testRunCallsEveryHookThatRegenerates` becomes `testRunCallsEveryGenerator`.
It required `run()`'s calls to be exactly the `internal virtual` hooks that
write, and `run()` now also calls `regenerateConfig`, which is deliberately
NOT a hook: the roster is this package's own, and a repo able to override the
emission would deploy to and verify fewer chains with nothing red. The set it
enumerates is now every `internal` function that can write, which holds the
same two claims over a strictly larger set and no longer turns on `virtual`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 58d44ef9-e928-4b86-b5bb-5cd5d0f3acca

📥 Commits

Reviewing files that changed from the base of the PR and between 2e21422 and 80d1295.

📒 Files selected for processing (20)
  • .env.example
  • .gitignore
  • .soldeerignore
  • CLAUDE.md
  • README.md
  • foundry.toml
  • script/build.sh
  • src/abstract/BuildScript.sol
  • src/abstract/RainDeployVerifyChain.sol
  • src/abstract/RainDeployVerifySnapshot.sol
  • src/abstract/RainDeployVerifySnapshotBase.sol
  • src/lib/LibRainDeploy.sol
  • src/lib/LibRainDeployConfig.sol
  • test/concrete/BuildScriptHarness.sol
  • test/script/Deploy.t.sol
  • test/src/abstract/BuildScript.t.sol
  • test/src/abstract/RainDeployVerifyChain.t.sol
  • test/src/abstract/RainDeployVerifySnapshotBase.t.sol
  • test/src/lib/LibRainDeploy.t.sol
  • test/src/lib/LibRainDeployConfig.t.sol

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Labels

None yet

Projects

None yet

1 participant