Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/package-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<tag>/` 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.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<tag>/` 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/<tag>/` 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
Expand Down
6 changes: 5 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 13 additions & 12 deletions src/lib/LibRainDeploySnapshot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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.
///
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion test/src/lib/LibRainDeploySnapshot.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading