diff --git a/.github/workflows/package-release.yaml b/.github/workflows/package-release.yaml index 9a54daf..88447b7 100644 --- a/.github/workflows/package-release.yaml +++ b/.github/workflows/package-release.yaml @@ -3,8 +3,8 @@ name: Package Release # carries deployed concretes (`AddressRegistry`, `MigrationRegistry`) whose # address + codehash consumers pin, which is exactly the shape # rainix-tag-release exists for and exactly the shape rainix-autopublish's -# merge-driven, next-version lifecycle is wrong for: autopublish bumps -# [package].version on every merge while the frozen deploy tag only advances at +# merge-driven, next-version lifecycle is wrong for: autopublish bumps the +# release version on every merge while the frozen deploy tag only advances at # deploy time. # # The tag names the version; rainix-tag-release runs `cutRelease()`, which diff --git a/CLAUDE.md b/CLAUDE.md index ae07fcd..bad8085 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,9 +13,9 @@ relocated. - **Broadcasting is key custody and real money.** Nothing automatic ever broadcasts: `Manual sol artifacts` is `workflow_dispatch` only, and no merge, tag or schedule may be given a path to it. -- **A `sol-v*` tag is the sole release trigger.** `[package].version` in - `foundry.toml` is the version of the LAST Soldeer publish, not a next-version - slot, so an ordinary PR does not bump it. +- **A `sol-v*` tag is the sole release trigger.** + `[external.package].version` in `foundry.toml` is the version of the LAST + Soldeer publish, not a next-version slot, so an ordinary PR does not bump it. - **`src/generated//` is an append-only record.** `cutRelease()` writes a tag directory once. A cut tag can never be un-cut and consumers pin what it holds, so a frozen record is never edited, renamed or deleted. diff --git a/README.md b/README.md index 63d08f8..b9aec77 100644 --- a/README.md +++ b/README.md @@ -388,11 +388,11 @@ Three separate steps, in this order. Nothing automatic ever broadcasts. This is a deploy repo: it carries deployed concretes whose addresses and codehashes consumers pin, so releases are **manual `sol-v*` tags**, not merges. -`[package].version` is the version of the LAST Soldeer publish, and only a -release moves it. A release cut under this lifecycle also names the frozen -`src/generated//` record `cutRelease()` wrote for it. Every version -published under the previous merge-driven lifecycle predates that record and has -none, so `src/generated/` holds no directory for it; those versions stay +`[external.package].version` is the version of the LAST Soldeer publish, and +only a release moves it. A release cut under this lifecycle also names the +frozen `src/generated//` record `cutRelease()` wrote for it. Every version +published under the previous merge-driven lifecycle predates that record and +has none, so `src/generated/` holds no directory for it; those versions stay published, and consumers pin exact versions and are unaffected. ## Install diff --git a/foundry.toml b/foundry.toml index 7a9c3ef..c9fc44b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,4 +1,8 @@ -[package] +# Release metadata, not foundry config: `LibRainDeploySnapshot.deployTag` reads +# `version` as the release being built, and `rainix-tag-release` rewrites it +# from the tag. `[external.*]` is the section foundry reserves for another +# tool's config: it never merges it into a profile and never warns about it. +[external.package] name = "rain-deploy" # Deploy repo: this is the version of the LAST Soldeer publish, not a # next-version slot. A normal PR does not bump it; only a `sol-v*` tag release diff --git a/src/lib/LibRainDeploySnapshot.sol b/src/lib/LibRainDeploySnapshot.sol index f3b4749..aef7df7 100644 --- a/src/lib/LibRainDeploySnapshot.sol +++ b/src/lib/LibRainDeploySnapshot.sol @@ -12,10 +12,10 @@ import {GENERATED_DIR, LibFs} from "rain-sol-codegen-0.1.36/src/lib/LibFs.sol"; import {DeploySuite} from "../abstract/RainDeploySuitesBase.sol"; import {LibRainDeploy} from "./LibRainDeploy.sol"; -/// Thrown when `[package].version` is not strict `X.Y.Z`. A version like -/// `0.1.7-rc1` maps to the directory `0_1_7-rc1`, which the append-only gate's -/// tag predicate ignores forever — an orphan snapshot nothing protects. Refused -/// rather than frozen. +/// Thrown when `[external.package].version` is not strict `X.Y.Z`. A version +/// like `0.1.7-rc1` maps to the directory `0_1_7-rc1`, which the append-only +/// gate's tag predicate ignores forever — an orphan snapshot nothing protects. +/// Refused rather than frozen. /// @param version The version read from `foundry.toml`. error UnreleasableVersion(string version); @@ -54,8 +54,8 @@ error NonMonotonicRelease(string tag, string newestFrozenTag); /// frozen. Release machinery, not code generation. /// /// It lives here rather than in `rain-sol-codegen` deliberately. Emitting a -/// Solidity constant is codegen; reading `[package].version`, deciding a -/// release directory and making that directory immutable is the deploy +/// Solidity constant is codegen; reading `[external.package].version`, deciding +/// a release directory and making that directory immutable is the deploy /// lifecycle, which is this repo's subject. `LibCodeGen` still emits every /// constant — that split is the point, not an oversight. /// @@ -79,8 +79,9 @@ error NonMonotonicRelease(string tag, string newestFrozenTag); /// that freezes without regenerating, so "freeze, then regenerate" has nowhere /// to be written. /// -/// @dev The consuming repo's `foundry.toml` must grant read access to itself so -/// `deployTag` can read the release version from it: +/// @dev The consuming repo's `foundry.toml` must declare the release version +/// at `[external.package].version` and grant read access to itself so +/// `deployTag` can read it: /// `fs_permissions = [{ access = "read", path = "./foundry.toml" }, ...]` /// alongside read-write access to `./src`. library LibRainDeploySnapshot { @@ -90,15 +91,15 @@ library LibRainDeploySnapshot { /// looking. string constant CANDIDATE = "candidate"; - /// The canonical release tag: `foundry.toml` `[package].version` with dots - /// converted to underscores (`0.1.7` -> `0_1_7`) for the Solidity directory - /// form. The single definition of the tag form — the version in + /// The canonical release tag: `foundry.toml` `[external.package].version` + /// with dots converted to underscores (`0.1.7` -> `0_1_7`) for the Solidity + /// directory form. The single definition of the tag form — the version in /// `foundry.toml` is the one source of truth for which release is being /// built, so every path derives from it rather than restating it. /// @param vm The Vm instance for file operations. /// @return The tag. function deployTag(Vm vm) internal view returns (string memory) { - return tagForVersion(vm.parseTomlString(vm.readFile("foundry.toml"), ".package.version")); + return tagForVersion(vm.parseTomlString(vm.readFile("foundry.toml"), ".external.package.version")); } /// Whether `subject` is three numbers joined by exactly two `separator`s, diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index 99ec771..5602059 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -271,10 +271,27 @@ contract LibRainDeploySnapshotTest is Test { function testDeployTagUsesTheGuardedConversion() external view { assertEq( LibRainDeploySnapshot.deployTag(vm), - LibRainDeploySnapshot.tagForVersion(vm.parseTomlString(vm.readFile("foundry.toml"), ".package.version")) + LibRainDeploySnapshot.tagForVersion( + vm.parseTomlString(vm.readFile("foundry.toml"), ".external.package.version") + ) ); } + /// PROPERTY: the release version lives at `[external.package].version`, and + /// `foundry.toml` carries no top-level `[package]` section. + /// + /// `external` is the only unreserved root section foundry ignores; every + /// other one it reads as a profile and warns about on every invocation, + /// and `forge config --fix` rewrites into `[profile.*]`. Both halves are + /// asserted because the release read and the silence are separate: a repo + /// that gained a second copy of the version under `[package]` would read + /// correctly and warn anyway. + function testReleaseVersionLivesUnderTheExternalSection() external view { + string memory config = vm.readFile("foundry.toml"); + assertTrue(vm.keyExistsToml(config, ".external.package.version")); + assertFalse(vm.keyExistsToml(config, ".package")); + } + /// Snapshot paths MUST agree with `LibFs`, which is what writes them. function testSnapshotPathsAgreeWithTheWriter() external pure { assertEq(LibRainDeploySnapshot.dirForSnapshot("0_1_7"), "src/generated/0_1_7");