From 9fda95eb8110ee2cd00a29bfe41eef7d23abd06f Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 24 Jul 2026 13:47:44 +0000 Subject: [PATCH 1/3] fix(ci): point rainix-sol-artifacts at RPC_URL_ETHEREUM_FORK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The artifacts job read ETH_RPC_URL from CI_DEPLOY_SEPOLIA_RPC_URL, a legacy org secret removed in the RPC-secret consolidation to the canonical RPC_URL_*_FORK scheme. With it empty, forge treated the empty --rpc-url as a local path and "connection refused" against the cwd (test/fixture/) — main went red with a misleading error (see #283). The job only SIMULATES script/Deploy.sol, so any eth fork RPC works; use the surviving RPC_URL_ETHEREUM_FORK. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b4553f..e34f4c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,6 +67,6 @@ jobs: - run: nix develop ../.. --command forge soldeer install - name: Run ${{ matrix.task }} env: - ETH_RPC_URL: ${{ secrets.CI_DEPLOY_SEPOLIA_RPC_URL || vars.CI_DEPLOY_SEPOLIA_RPC_URL }} + ETH_RPC_URL: ${{ secrets.RPC_URL_ETHEREUM_FORK || vars.RPC_URL_ETHEREUM_FORK }} ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY }} run: nix develop ../.. --command ${{ matrix.task }} From 0353d295253e3509da7c1e409cb92f0bd196a2d1 Mon Sep 17 00:00:00 2001 From: David Meister Date: Sun, 26 Jul 2026 06:12:21 +0000 Subject: [PATCH 2/3] make rainix-sol-artifacts hermetic: own anvil when no ETH_RPC_URL The CI artifacts job depended on a live external RPC via secrets that have been deleted (empty ETH_RPC_URL, connection refused on main) and no surviving secret can satisfy it (the retry --resume needs the recorded chain, and a mainnet fork URL yields 'Deployment not found for chain 1'). Instead of repointing at another external RPC, the task now runs against its own ephemeral anvil when no ETH_RPC_URL is provided: no secrets, no external network, no rate limits. An explicit ETH_RPC_URL (a real deploy) always wins. The fixture Deploy.sol now reads DEPLOYMENT_KEY like consumer deploy scripts do, so the fixture exercises the task exactly as consumers run it (broadcast included); the skip-simulation bats test supplies the key via env accordingly. CI drops the PRIVATE_KEY / RPC / etherscan secret plumbing from the job. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test.yml | 8 +++----- flake.nix | 17 +++++++++++++++++ test/bats/task/skip-simulation.test.bats | 6 ++++-- test/fixture/script/Deploy.sol | 8 +++++++- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e34f4c8..a965ce3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,8 +23,6 @@ jobs: task: rainix-rs-static fail-fast: false runs-on: ${{ matrix.os }} - env: - DEPLOYMENT_KEY: ${{ secrets.PRIVATE_KEY }} defaults: run: working-directory: test/fixture @@ -65,8 +63,8 @@ jobs: restore-keys: | foundry-full-${{ runner.os }}- - run: nix develop ../.. --command forge soldeer install + # rainix-sol-artifacts runs hermetically here: with no ETH_RPC_URL the + # task deploys against its own ephemeral anvil, so this job needs no RPC + # or key secrets. - name: Run ${{ matrix.task }} - env: - ETH_RPC_URL: ${{ secrets.RPC_URL_ETHEREUM_FORK || vars.RPC_URL_ETHEREUM_FORK }} - ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY }} run: nix develop ../.. --command ${{ matrix.task }} diff --git a/flake.nix b/flake.nix index 5486390..734a1d3 100644 --- a/flake.nix +++ b/flake.nix @@ -237,6 +237,23 @@ # Upload all function selectors to the registry. forge selectors up --all + # With no ETH_RPC_URL the task is hermetic: it runs against its own + # ephemeral anvil instance with anvil's first funded dev account as + # the deployment key, so it needs no secrets and no external RPC. + # An explicit ETH_RPC_URL (a real deploy) always wins and leaves + # DEPLOYMENT_KEY untouched. Port 18545 avoids clobbering a dev's + # own anvil on the default 8545. + if [[ -z "''${ETH_RPC_URL:-}" ]]; then + anvil --port 18545 --silent & + anvil_pid=$! + trap 'kill "''${anvil_pid}" 2>/dev/null' EXIT + export ETH_RPC_URL='http://127.0.0.1:18545' + export DEPLOYMENT_KEY='0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + until cast chain-id --rpc-url "''${ETH_RPC_URL}" >/dev/null 2>&1; do + sleep 0.2 + done + fi + # Deploy all contracts to testnet. # Assumes the existence of a `Deploy.sol` script in the `script` directory. # Echos the deploy pubkey to stdout to make it easy to add gas to the account. diff --git a/test/bats/task/skip-simulation.test.bats b/test/bats/task/skip-simulation.test.bats index d135ca7..dcdb0e1 100644 --- a/test/bats/task/skip-simulation.test.bats +++ b/test/bats/task/skip-simulation.test.bats @@ -13,12 +13,14 @@ teardown() { } forge_deploy() { - forge script script/Deploy.sol:Deploy \ + # The fixture Deploy.sol reads DEPLOYMENT_KEY itself (consumer convention); + # anvil's first funded dev account. + DEPLOYMENT_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + forge script script/Deploy.sol:Deploy \ -vvvvv \ --broadcast \ ${DEPLOY_SKIP_SIMULATION:+--skip-simulation} \ --rpc-url http://127.0.0.1:8545 \ - --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ 2>&1 } diff --git a/test/fixture/script/Deploy.sol b/test/fixture/script/Deploy.sol index de64852..3bb4374 100644 --- a/test/fixture/script/Deploy.sol +++ b/test/fixture/script/Deploy.sol @@ -8,8 +8,14 @@ import {Counter} from "../src/Counter.sol"; contract Deploy is Script { function setUp() public {} + /// Reads the deployer key from `DEPLOYMENT_KEY`, the same convention as + /// the consumer `Deploy.sol` scripts `rainix-sol-artifacts` runs, so the + /// fixture exercises the task exactly as consumers do (broadcast included + /// — a bare `vm.broadcast()` would hit foundry's default-sender refusal). function run() public { - vm.broadcast(); + uint256 deployerPrivateKey = vm.envUint("DEPLOYMENT_KEY"); + vm.startBroadcast(deployerPrivateKey); new Counter(); + vm.stopBroadcast(); } } From 5d8a665ce1a9d31e6011feca90dd089edbd2c53e Mon Sep 17 00:00:00 2001 From: David Meister Date: Sun, 26 Jul 2026 06:27:32 +0000 Subject: [PATCH 3/3] fail fast when the hermetic anvil cannot become ready The readiness wait was unbounded, so an anvil that exited during startup (or never bound the port) hung the task until the surrounding job timed out. Bound the wait to 20s and check the anvil process is still alive each iteration, so both cases exit non-zero with a clear message. Co-Authored-By: Claude Opus 4.8 --- flake.nix | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 734a1d3..348dd64 100644 --- a/flake.nix +++ b/flake.nix @@ -249,9 +249,25 @@ trap 'kill "''${anvil_pid}" 2>/dev/null' EXIT export ETH_RPC_URL='http://127.0.0.1:18545' export DEPLOYMENT_KEY='0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' - until cast chain-id --rpc-url "''${ETH_RPC_URL}" >/dev/null 2>&1; do + # Bounded readiness wait: an anvil that died or never bound the + # port must fail here with a clear message, not hang the task + # until the surrounding job's timeout. + anvil_ready="" + for _ in $(seq 1 100); do + if ! kill -0 "''${anvil_pid}" 2>/dev/null; then + echo 'rainix-sol-artifacts: anvil exited during startup' >&2 + exit 1 + fi + if cast chain-id --rpc-url "''${ETH_RPC_URL}" >/dev/null 2>&1; then + anvil_ready=1 + break + fi sleep 0.2 done + if [[ -z "''${anvil_ready}" ]]; then + echo "rainix-sol-artifacts: anvil not ready on ''${ETH_RPC_URL} after 20s" >&2 + exit 1 + fi fi # Deploy all contracts to testnet.