feat(autopublish): soldeer next-version release lifecycle (one process)#264
Conversation
Replace the soldeer autopublish behaviour with the single next-version lifecycle — no mode flag, no branch. `[package].version` is the NEXT, unpublished version; on a content change vs the published revision the job publishes THAT version, bumps to the next, runs an optional generate hook, and commits — one merge-driven process every consumer adopts. - **Source-only change gate.** Hash excludes `src/generated/` (per-release `<tag>/` snapshots + generated aliasing libs): they're derived from source and a fresh `<tag>/` appears every release, so hashing them would flag "changed" on every merge and republish identical bytecode. This also removes the old per-constant `*_DEPLOYED_..._x_y_z` pin-stripping — the versioned pins live in the excluded snapshots, so pinning can't self-trigger the bump→pin→bump loop the hack existed to prevent. - **Publish the in-dev version.** Publishes `[package].version` (LOCAL) from the PRE-bump tree so the package contents match the version it's published under, then bumps to the next. - **`soldeer-generate-cmd` hook** (new, optional). Runs right after the bump, before the release commit, to re-key per-release artifacts; staged into the "Package Release" commit so the bump lands born-green. Empty = no-op. - **Stale-toml guard.** If `[package].version` isn't ahead of the registry (e.g. a prior run published but its bump-commit push failed), fail loud with a clear action (bump the version) rather than silently re-publish / mis-bump. - Remove `soldeer-next-version` framing — there is one process, not two. REVIEW-CRITICAL (can't be end-to-end tested from a branch — autopublish only runs on push-to-main): the publish-before-push ordering + its stale-toml recovery, and the `git add -A` capture of the generate output. Suggest a sandbox-repo test-merge before this lands. Paired st0x change: drop `soldeer-next-version`, set `soldeer-generate-cmd` to the forge BuildPointers regen (st0x.deploy#243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe ChangesSoldeer release tooling
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubActions
participant RainixStatic
participant SoldeerRegistry
participant Forge
participant Repository
GitHubActions->>RainixStatic: run soldeer-gate
RainixStatic->>SoldeerRegistry: fetch published revision metadata
RainixStatic->>Forge: create dry-run package zip
Forge-->>RainixStatic: local package contents
RainixStatic-->>GitHubActions: changed, version, next
GitHubActions->>Repository: publish current version
GitHubActions->>Repository: bump, regenerate, stage, and commit next version
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The soldeer content gate used a base64'd Python hash helper + python3 for the registry-JSON parse and version math — the same ambient-python3 reliance the next-version work is eliminating elsewhere (st0x #243's forge codegen; rainix #261). rainix is the Rust/nix toolchain; the gate shouldn't lean on Python. - Content hash is now a small inline bash `nh()` — unzip + find (excluding src/generated/) + blank foundry.toml version + sort + sha256sum — mirroring the pure-bash `norm_hash` the cargo gate directly above already uses. - Registry JSON parsed with `jq` instead of `python3 -c`. - Version bump + the ahead-of-registry compare done in bash (`IFS=. read`, `sort -V`). No behaviour change; zero Python in the soldeer path. (The cargo gate's own python3 JSON parse is pre-existing and out of scope here.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oling The Soldeer content gate was inline bash (jq/awk/sha256sum/sort -V). Move all its logic into a Rust `rainix-static soldeer-gate` subcommand: read foundry.toml, parse the registry JSON, enforce the next-version ahead-invariant, normalize-hash both package zips (exclude src/generated/**, blank the version line, byte-sorted name\0content -> SHA-256), semver-bump, emit changed/version/ next. Only `forge soldeer push --dry-run` and the two HTTP GETs are external-tool calls, so curl joins sol-build-inputs. rainix-static broadens from "static checks" to general rainix tooling: on PATH in every shell (common-shell-inputs). The workflow gate is now one line that runs it inside sol-shell. CLAUDE.md documents the rule this enforces: tooling is Rust, never Python or bash logic. - 14 unit tests (norm-hash bump-invariance / generated-exclusion / order- independence / source-detection, semver parse+compare+bump, registry parse, foundry read) run in the flake build's doCheck. - `nix flake check` + CI-way sol-shell closure test green: the added binary does not leak the rust toolchain into the slim sol-shell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rainix-static/src/main.rs`:
- Around line 243-246: Handle the `None` result from `curl_stdout` separately
instead of converting it to `"{}"`: propagate or retain the fetch failure, and
fail the workflow when the local project has a version but the registry cannot
be reached. Only treat a successfully fetched, valid empty registry response as
a first-ever publish, preserving the existing version checks around
`parse_registry` and the next-version gate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 773fba21-3fc2-45d0-9c1d-e48f8485fe96
⛔ Files ignored due to path filters (1)
rainix-static/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
.github/workflows/rainix-autopublish.yamlCLAUDE.mdflake.nixrainix-static/Cargo.tomlrainix-static/src/main.rs
| let json = curl_stdout(&format!( | ||
| "https://api.soldeer.xyz/api/v1/revision?project_name={pkg}&offset=0&limit=1" | ||
| )) | ||
| .unwrap_or_else(|| "{}".to_string()); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Registry fetch failure is indistinguishable from "nothing published" — can cause silent re-publish.
curl_stdout returns None on any failure (network error, timeout, non-2xx), and unwrap_or("{}".to_string()) converts that to empty-registry JSON. parse_registry("{}") then yields (None, None), so the next-version invariant check at lines 253-260 is skipped entirely. If the registry is temporarily unreachable but a prior run already published the current in-dev version (e.g., its bump-commit push failed), the gate would see changed=true (old_hash="none") and the workflow would re-publish the same version to Soldeer.
Consider distinguishing fetch failure from empty-registry by propagating the Option from curl_stdout and failing when the registry is unreachable but a local version exists, while still allowing first-ever publishes when the registry returns a valid empty response.
🛡️ Proposed fix
// Latest published revision (version + zip url); {} on any fetch failure.
- let json = curl_stdout(&format!(
+ let json = match curl_stdout(&format!(
"https://api.soldeer.xyz/api/v1/revision?project_name={pkg}&offset=0&limit=1"
- ))
- .unwrap_or_else(|| "{}".to_string());
+ )) {
+ Some(body) => body,
+ None => fail(&format!(
+ "soldeer-gate: cannot reach Soldeer registry to check published revisions; \
+ refusing to proceed without knowing the published state (possible re-publish)."
+ )),
+ };
let (remote, url) = parse_registry(&json);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let json = curl_stdout(&format!( | |
| "https://api.soldeer.xyz/api/v1/revision?project_name={pkg}&offset=0&limit=1" | |
| )) | |
| .unwrap_or_else(|| "{}".to_string()); | |
| let json = match curl_stdout(&format!( | |
| "https://api.soldeer.xyz/api/v1/revision?project_name={pkg}&offset=0&limit=1" | |
| )) { | |
| Some(body) => body, | |
| None => fail(&format!( | |
| "soldeer-gate: cannot reach Soldeer registry to check published revisions; \ | |
| refusing to proceed without knowing the published state (possible re-publish)." | |
| )), | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@rainix-static/src/main.rs` around lines 243 - 246, Handle the `None` result
from `curl_stdout` separately instead of converting it to `"{}"`: propagate or
retain the fetch failure, and fail the workflow when the local project has a
version but the registry cannot be reached. Only treat a successfully fetched,
valid empty registry response as a first-ever publish, preserving the existing
version checks around `parse_registry` and the next-version gate.
|
🤖 ai:vetter |
main.rs becomes a thin dispatcher (arg parse + crate::fail); the two subcommands move to their own modules — no_submodules.rs (static check) and soldeer_gate.rs (the Soldeer content gate). Pure refactor, no behavior change: the 14 unit tests move with their code and still pass; clippy/fmt/reuse clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rainix-static/src/soldeer_gate.rs`:
- Around line 198-210: Add a finite curl timeout to both network call sites:
include a suitable --max-time value in the curl arguments used for the published
zip download in the old_hash calculation and the registry API fetch near the
corresponding registry request, preserving the existing flags and behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 20410bcd-6457-42da-9a68-021617df44c6
📒 Files selected for processing (3)
rainix-static/src/main.rsrainix-static/src/no_submodules.rsrainix-static/src/soldeer_gate.rs
| let old_hash = match (&remote, url.as_deref()) { | ||
| (Some(_), Some(u)) if !u.is_empty() => { | ||
| let tmp = std::env::temp_dir().join("soldeer_pub.zip"); | ||
| run_cmd( | ||
| Command::new("curl").args(["-fsSL", u, "-o"]).arg(&tmp), | ||
| "curl published zip", | ||
| ); | ||
| let mut pub_entries = read_zip(&tmp); | ||
| let _ = std::fs::remove_file(&tmp); | ||
| norm_hash(&mut pub_entries) | ||
| } | ||
| _ => "none".to_string(), | ||
| }; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Add --max-time to curl commands to prevent indefinite hangs.
Neither the published-zip download (line 202) nor the registry API fetch (line 253) sets a curl timeout. If the Soldeer API or download server accepts the connection but never responds, the gate blocks until the GitHub Actions job timeout, wasting CI minutes. Adding --max-time is a one-line fix per call site.
⏱️ Proposed fix
fn curl_stdout(url: &str) -> Option<String> {
- let out = Command::new("curl").args(["-fsSL", url]).output().ok()?;
+ let out = Command::new("curl").args(["-fsSL", "--max-time", "30", url]).output().ok()?;And for the published zip download:
run_cmd(
- Command::new("curl").args(["-fsSL", u, "-o"]).arg(&tmp),
+ Command::new("curl").args(["-fsSL", "--max-time", "60", u, "-o"]).arg(&tmp),
"curl published zip",
);Also applies to: 251-257
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@rainix-static/src/soldeer_gate.rs` around lines 198 - 210, Add a finite curl
timeout to both network call sites: include a suitable --max-time value in the
curl arguments used for the published zip download in the old_hash calculation
and the registry API fetch near the corresponding registry request, preserving
the existing flags and behavior.
|
Reviewed deb7a4f: Soldeer content gate ported from inline bash (jq/awk/sha256sum/sort -V) to a Rust |
|
@coderabbitai assess this PR size classification for the totality of the PR with the following criterias and report it in your comment: S/M/L PR Classification Guidelines:This guide helps classify merged pull requests by effort and complexity rather than just line count. The goal is to assess the difficulty and scope of changes after they have been completed. Small (S)Characteristics:
Review Effort: Would have taken 5-10 minutes Examples:
Medium (M)Characteristics:
Review Effort: Would have taken 15-30 minutes Examples:
Large (L)Characteristics:
Review Effort: Would have taken 45+ minutes Examples:
Additional Factors to ConsiderWhen deciding between sizes, also consider:
Notes:
|
|
Tip For best results, initiate chat on the files or code changes. SIZE=L |
The paired rainix PR (rainlanguage/rainix#264) merged, so reconcile this repo's package-release call and version with the final contract: - Drop `soldeer-next-version: true` — that input was removed from the reusable; the next-version lifecycle is implicit whenever `soldeer-package` is set. Passing a now-undefined input would fail the reusable at startup. - `soldeer-generate-cmd` is now bare `forge script ./script/BuildPointers.sol` — the reusable already runs it inside its own PINNED sol-shell, so the nested `nix develop github:rainlanguage/rainix#sol-shell` was redundant and reintroduced an unpinned (429-prone) flake ref. - Bump foundry.toml [package].version 0.1.5 -> 0.1.6: 0.1.5 is now the published revision, so the in-dev version must be the next unpublished one (the new ahead-invariant fails loud otherwise). - Regenerate: new src/generated/0_1_6/ snapshot + LibProdDeploy{V4,Current}. Deterministic (idempotent re-run = no diff); bytecode identical to 0_1_5 since the contract source is unchanged. LibProdDeployV4.t: 37/37 pass (tagged keccak(RUNTIME)==CODEHASH incl 0_1_6, frozen-redeploy). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the soldeer autopublish behaviour with the single next-version lifecycle — no mode flag, no branch.
[package].versionis the NEXT, unpublished version; on a content change vs the published revision the job publishes THAT version, bumps to the next, runs an optional generate hook, and commits. One merge-driven process every consumer adopts (adoption = a one-time toml bump to the next unpublished version; the generate hook is empty for repos with nothing to generate).Changes (soldeer path only)
src/generated/(per-release<tag>/snapshots + generated aliasing libs). They're derived from source and a fresh<tag>/dir appears every release, so hashing them would flag "changed" on every merge and republish identical bytecode forever. This also removes the old per-constant*_DEPLOYED_..._x_y_zpin-stripping: the versioned pins live in the excluded snapshots, so pinning can no longer self-trigger the bump→pin→bump loop that hack existed to prevent.[package].versionfrom the pre-bump tree so the package contents match the version it's published under, then bumps.soldeer-generate-cmdinput (optional). Runs in the sol-shell right after the bump, before the release commit, to re-key per-release artifacts;git add -Astages its output into the born-green "Package Release" commit. Empty default = no-op.[package].versionisn't ahead of the registry (e.g. a prior run published but its bump-commit push failed), the gate fails loud with a clear action (bump the version) rather than silently re-publishing / mis-bumping — matching the 'CI never red without a clear action to green it' philosophy.soldeer-next-versiontoggle: there is one process, not two.Autopublish only exercises on a real push-to-main, so this is a draft for review. The parts that most warrant your eyes:
git add -Acapture of the generate output into the release commit.Tag and pushrebase step.Suggest a sandbox-repo test-merge before this lands.
Paired change
st0x.deploy #243 (the first next-version consumer): drop
soldeer-next-version, setsoldeer-generate-cmdto the forgeBuildPointersregen. It's the reason this input existed as a not-yet-supported call, and it merges after this.🤖 Generated with Claude Code
Summary by CodeRabbit