Skip to content
31 changes: 31 additions & 0 deletions .github/actions/cut-release/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: cut-release
description: >-
Cuts a deploy repo's release snapshot: regenerates the rolling src/generated/candidate/ pins from the current source, formats, and only then freezes a byte-identical copy as src/generated/<tag>/ for the foundry.toml [package].version. The ordering is the reason this is a tool and not a consumer script — freezing before regenerating writes a stale candidate into a dir the frozen-snapshots-append-only gate protects forever while the regeneration moves candidate on, so the release publishes one address and permanently records another, and nothing downstream compares the two. Refuses a version that is not strict X.Y.Z (its tag dir would be invisible to that gate — an orphan snapshot nothing protects), a missing or empty candidate, and any tag dir that already exists.
inputs:
generate-cmd:
description: >-
Command that regenerates src/generated/candidate/ from the current source. Empty (the default) uses the org convention, `forge script ./script/BuildPointers.sol`. This is the ONLY repo-specific part of the freeze; everything else — the version read, the guards, the format, the copy and the byte-identity assertion — is the same everywhere and lives in rainix-static. It runs strictly BEFORE the freeze, so the inverted order cannot be expressed through it.
required: false
default: ''
runs:
using: composite
steps:
- name: Cut the release snapshot
shell: bash
env:
# Via env, not interpolated into the script body: a `with:` value spliced
# into `run:` text is a template-injection surface, and the binary reads
# it as a single argv element either way.
RAINIX_GENERATE_CMD: ${{ inputs.generate-cmd }}
run: |
set -euo pipefail
# Single source of truth: the Rust rainix-static binary (its unit tests
# run inside the nix build). The path: flake ref runs it — and the forge
# it drives, from the same sol-shell — out of this composite's own
# checkout, so the freeze logic always matches the action version
# regardless of any RAINIX_SHA the caller pins, and a new subcommand is
# usable the moment the action lands rather than after a sha bump. A
# path: ref also makes no api.github.com call, so it cannot hit the 429
# that pinning exists to avoid.
nix develop "path:$(cd "$GITHUB_ACTION_PATH/../../.." && pwd)#sol-shell" -c \
rainix-static cut-release --generate-cmd "$RAINIX_GENERATE_CMD"
31 changes: 23 additions & 8 deletions .github/workflows/rainix-tag-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ on:
required: false
type: string
default: sol-v
snapshot-generate-cmd:
pointers-generate-cmd:
description: >-
Command that regenerates the deploy-pin snapshot from the (deterministic) bytecode into src/generated/<tag>/, DEPLOY_TAG and any pointer libs, then formats. Run after [package].version is set to the release version, so the generated tag matches it. e.g. `forge script ./script/BuildPointers.sol && forge fmt`.
required: true
Command that regenerates the rolling `src/generated/candidate/` pins from the current source, e.g. `forge script ./script/BuildPointers.sol` — which is also the default when this is empty, so a repo following the convention passes nothing. This is the ONLY repo-specific part of a release freeze: the version read, the guards, the format, the copy into `src/generated/<tag>/` and the byte-identity assertion are identical everywhere and live in `rainix-static cut-release`. It is deliberately a GENERATE command, not a freeze script — it runs strictly before the freeze, so the freeze-then-regenerate order (which permanently records an address the release does not publish) cannot be expressed.
required: false
type: string
default: ''
test-cmd:
description: >-
Pre-publish verification gate. Run against the regenerated snapshot; for a deploy repo this is the fork suite that reads the live chain and asserts it matches the fresh pins, so a release that snapshots addresses the chain does not actually carry fails loud BEFORE publishing. Default `forge test`.
Expand Down Expand Up @@ -196,11 +197,25 @@ jobs:
echo "::error::foundry.toml has no [package] version line to set to ${VERSION}" >&2
exit 1
}
- name: Regenerate the deploy-pin snapshot
# Deterministic: the pins are computed from bytecode (address = f(bytecode)
# under CREATE2), so this needs no chain access and produces the exact
# src/generated/<tag>/ the release publishes and commits.
run: nix develop github:rainlanguage/rainix/${{ env.RAINIX_SHA }}#sol-shell -c bash -c '${{ inputs.snapshot-generate-cmd }}'
- name: Cut the release snapshot
# Regenerate the rolling candidate pins, format, then freeze a
# byte-identical copy as src/generated/<tag>/. Deterministic: the pins are
# computed from bytecode (address = f(bytecode) under CREATE2), so this
# needs no chain access and produces the exact snapshot the release
# publishes and commits.
#
# The ORDER is the reason this is a rainix tool rather than a consumer
# script (rainlanguage/rainix#301). Freezing before regenerating writes a
# drifted candidate into an append-only dir while the regeneration moves
# candidate on, so the release publishes one address and permanently
# records another — invisible downstream, because the repo's
# self-consistency test checks the REGENERATED candidate against source and
# nothing compares a numbered dir to candidate. The consumer supplies only
# the generate command, which runs strictly first, so the inversion has
# nowhere to live.
uses: rainlanguage/rainix/.github/actions/cut-release@main
with:
generate-cmd: ${{ inputs.pointers-generate-cmd }}
- name: Commit the release snapshot
# Commit BEFORE the append-only gate and Soldeer push so both operate on a
# clean, inspectable tree. The commit is what lands on main below.
Expand Down
Loading
Loading