Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 55 additions & 25 deletions .github/workflows/rainix-copy-artifacts.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
name: rainix-copy-artifacts
on:
workflow_call:
inputs:
# One pass propagates one level of the generated-source dependency chain,
# so this bounds that depth rather than counting retries. Raising it is
# only ever correct for a repo whose generated sources genuinely nest
# deeper than the default.
max-codegen-passes:
description: Passes of the regeneration pipeline allowed before the repo is declared non-converging.
type: number
default: 5
required: false
env:
RAINIX_SHA: 53e96a7d0a97d7c7c75c3b2412521324776fdac6
jobs:
Expand All @@ -21,40 +31,60 @@ jobs:
- name: Install soldeer dependencies
if: hashFiles('soldeer.lock') != ''
run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge soldeer install
# Currency-check every committed generated artifact by re-running each
# consumer-provided codegen step. The final git diff fails if any
# committed file has drifted from its source. The build-meta.sh hook is
# consumer-supplied because rain meta build's invocation (input/output
# filenames, meta type) varies per repo.
- name: Regenerate meta artifacts
if: hashFiles('script/build-meta.sh') != ''
run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c ./script/build-meta.sh
# Committed generated sources must be regenerable here, or the currency
# check below passes without checking anything. The codegen script is
# `script/Build.sol`, matched exactly: a repo that renames or drops it
# goes red rather than skipping regeneration and reporting green.
- name: Regenerate generated sources
- name: Require a codegen script for committed generated sources
run: |
if [ -d src/generated ] && [ ! -f script/Build.sol ]; then
echo "::error::src/generated/ is committed but script/Build.sol was not found, so the committed sources cannot be currency checked here. The codegen script must be script/Build.sol."
exit 1
fi
if [ -f script/Build.sol ]; then
nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge script ./script/Build.sol
fi
- name: Build Solidity
run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge build
- name: Copy forge artifacts into committed location
if: hashFiles('script/CopyArtifacts.sol') != ''
run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge script ./script/CopyArtifacts.sol --ffi
# Catch-all post-forge regen hook: consumer-supplied. Runs outside any
# nix devshell so the script picks shells per command (subgraph-shell,
# sol-shell, etc.) for whatever derived artifacts it emits.
- name: Regenerate derived artifacts
if: hashFiles('script/build.sh') != ''
run: ./script/build.sh
- name: Format (so generated artifacts match committed style)
run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge fmt
# Currency-check every committed generated artifact by re-running each
# consumer-provided codegen step. The final git diff fails if any
# committed file has drifted from its source. The build-meta.sh hook is
# consumer-supplied because rain meta build's invocation (input/output
# filenames, meta type) varies per repo; build.sh is the catch-all
# post-forge hook, and is the one command here that is NOT wrapped in a
# devshell, so it can pick shells per command (subgraph-shell, sol-shell,
# etc.) for whatever derived artifacts it emits.
#
# The pipeline is looped rather than run once because its output is part
# of its own input: a pointer table is imported by the contract whose
# codehash that same table records, so one pass is one application of the
# generation function and not its fixed point. Run once, a tree that is
# several passes behind and a generation cycle that will never settle both
# reach the currency check as "stale", and its advice — regenerate and
# commit — silently only fixes the first. `codegen-fixed-point` re-runs the
# pipeline until the working tree stops changing and fails with its own
# error once `max-codegen-passes` is spent, so the two are told apart by
# the machine instead of by a developer running the loop by hand until
# they guess it is never going to settle. It observes the tree in a
# scratch index, leaving the repo's own index to the check below.
#
# A repo already at its fixed point costs exactly one pass, the same as no
# loop at all; the second pass is only ever paid by a build that is
# already going red.
- name: Regenerate committed artifacts to a fixed point
run: |
nix run github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#rainix-static -- codegen-fixed-point --max-passes ${{ inputs.max-codegen-passes }} --run '
set -eu
if [ -f script/build-meta.sh ]; then
nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c ./script/build-meta.sh
fi
if [ -f script/Build.sol ]; then
nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge script ./script/Build.sol
fi
nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge build
if [ -f script/CopyArtifacts.sol ]; then
nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge script ./script/CopyArtifacts.sol --ffi
fi
if [ -f script/build.sh ]; then
./script/build.sh
fi
nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c forge fmt
'
- name: Assert committed artifacts match freshly built
run: |
if ! git diff --exit-code; then
Expand Down
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,12 @@ Solidity artifacts from source and asserts `git diff --exit-code` — failing th
PR if a maintainer changed source without committing the regenerated files. In a
single job it runs whichever of these the repo has:

- `./script/BuildPointers.sol` → `src/generated/*.pointers.sol`
- `./script/build-meta.sh` → committed rain meta artifacts
- `./script/Build.sol` → `src/generated/*.sol`
- `forge build` + `./script/CopyArtifacts.sol --ffi` → committed ABI JSON
- `./script/build.sh` → any other derived artifact

then `forge fmt` and the `git diff` assert.
then `forge fmt`, and the `git diff` assert once that pipeline has settled.

```yaml
name: copy-artifacts
Expand All @@ -162,9 +164,34 @@ jobs:
```

This replaces the former `rainix-build-pointers` reusable — a pointer-only repo
just omits `CopyArtifacts.sol` (the copy step is skipped via `hashFiles`).
Always runs through rainix's `sol-shell` (slim), regardless of the consumer's
default devShell. `secrets: inherit` carries `CACHIX_AUTH_TOKEN`.
just omits `CopyArtifacts.sol` (the copy step is skipped when the file is
absent). Always runs through rainix's `sol-shell` (slim), regardless of the
consumer's default devShell. `secrets: inherit` carries `CACHIX_AUTH_TOKEN`.

##### The pipeline runs to a fixed point, not once

Generated sources are inputs to their own generation: a pointer table is
imported by the contract whose codehash that same table records, so one pass of
the pipeline applies the generation function rather than reaching its fixed
point. The job therefore repeats the whole pipeline until the working tree stops
changing, and a repo already at its fixed point pays exactly one pass.

`max-codegen-passes` (default `5`) bounds that. Exhausting it fails the job with
its own error — a generation cycle that does not settle, distinct from committed
artifacts that were merely not regenerated, which is what the currency check
reports. Committing whichever pass happened to diff clean is the trap the bound
exists to prevent: it records a `BYTECODE_HASH` for a contract compiled against
a different pass of the same file. Raise the bound only for a repo whose
generated sources genuinely nest deeper than five levels:

```yaml
jobs:
copy-artifacts:
uses: rainlanguage/rainix/.github/workflows/rainix-copy-artifacts.yaml@main
secrets: inherit
with:
max-codegen-passes: 8
```

#### rainix-rs-static

Expand Down
16 changes: 10 additions & 6 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,19 @@
cargoLock.lockFile = ./rainix-static/Cargo.lock;
nativeCheckInputs = [ pkgs.git ];
nativeBuildInputs = [ pkgs.makeWrapper ];
# The subcommands shell out to git (`ls-files`, `diff`) and curl
# (`rpc-preflight`'s probes, `soldeer-gate`'s fetches). The composite
# actions invoke this binary with `nix run`, i.e. OUTSIDE any devshell,
# so ambient PATH is whatever the runner image happens to ship. Wrap it
# with the pinned tools and a CA bundle so the checks are hermetic and
# cannot fail on a host with no curl, no git, or no root certs.
# The subcommands shell out to git (`ls-files`, `diff`,
# `codegen-fixed-point`'s tree observations), curl (`rpc-preflight`'s
# probes, `soldeer-gate`'s fetches) and bash (the regeneration pipeline
# `codegen-fixed-point` loops over). The composite actions invoke this
# binary with `nix run`, i.e. OUTSIDE any devshell, so ambient PATH is
# whatever the runner image happens to ship. Wrap it with the pinned
# tools and a CA bundle so the checks are hermetic and cannot fail on a
# host with no curl, no git, no bash, or no root certs.
postInstall = ''
wrapProgram $out/bin/rainix-static \
--prefix PATH : ${
pkgs.lib.makeBinPath [
pkgs.bash
pkgs.curl
pkgs.git
]
Expand Down Expand Up @@ -448,6 +451,7 @@
bats test/bats/devshell/default/prettier-bundle.test.bats
bats test/bats/action/rpc-preflight.test.bats
bats test/bats/action/prompt-cap.test.bats
bats test/bats/workflow/copy-artifacts-fixed-point.test.bats
bats test/bats/task/skip-simulation.test.bats
bats test/bats/task/subgraph-build.test.bats
bats test/bats/task/subgraph-deploy-version.test.bats
Expand Down
Loading
Loading