feat(ci): rainix-tag-release — tag-triggered deploy-repo release - #280
Conversation
Adds the deploy-repo counterpart to rainix-autopublish. Deploy repos run a frozen-snapshot lifecycle that is mutually exclusive with autopublish's next-version lifecycle; running the latter on a deploy repo desyncs [package].version from DEPLOY_TAG on every merge, leaving a permanently-red identity test (testDeployTag) that trains reviewers to ignore CI. Here nothing moves on merge. A human tag is the sole release trigger: the workflow sets the version from the tag, regenerates the deterministic deploy-pin snapshot, enforces append-only, verifies the live chain against the fresh pins, publishes to Soldeer, and commits the snapshot back to main so the drift sweep tracks the current release. Reuses the checkout / nix-cachix-setup / frozen-snapshots-append-only / gh-release composites. A sibling rather than a mode on rainix-autopublish, to keep zero blast radius on the library repos that depend on it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughChangesTag release workflow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant Guard
participant DeployJob
participant ReleaseJob
participant GitRepository
participant LiveChain
participant Soldeer
participant GitHubRelease
Caller->>Guard: invoke workflow with tag inputs and secrets
Guard->>GitRepository: verify tag commit is merged into main-branch
Guard->>DeployJob: allow deployment
DeployJob->>ReleaseJob: complete sequential deploy suites
ReleaseJob->>GitRepository: derive VERSION and update package version
ReleaseJob->>GitRepository: regenerate and commit deploy-pin snapshot
ReleaseJob->>LiveChain: run configured verification tests
LiveChain-->>ReleaseJob: return verification result
ReleaseJob->>Soldeer: publish package as soldeer-package~VERSION
ReleaseJob->>GitRepository: rebase onto main-branch and push
ReleaseJob->>GitHubRelease: create release for github.ref_name
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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 @.github/workflows/rainix-tag-release.yaml:
- Around line 113-116: Strengthen the tag guard in the release workflow by
fetching the selected release branch and verifying that the tag commit is an
ancestor of that branch before generation or publishing. Update the guard around
“Guard - the trigger ref is a tag” to reject tags from unmerged branches while
preserving the existing refs/tags validation.
- Around line 157-165: Remove the broad git checkout from the “Commit the
release snapshot” step so it does not discard the foundry.toml version update or
tracked snapshot artifacts. Replace it with cleanup targeting only explicitly
known temporary devShell leftovers, then retain git add -A so the regenerated
release files and version changes are staged together.
- Around line 187-209: The release workflow’s Soldeer publication and main
commit-back are not resumable across partial failures. Update the release
finalization steps around “Publish to Soldeer” and “Commit the snapshot back to
main” to persist durable release state, record whether publication completed,
and on retry skip or safely reconcile an already-published package before
retrying the rebase/push; ensure failures leave enough state for idempotent
recovery without republishing non-idempotently.
- Around line 131-151: Update the release-version step to validate VERSION
against the repository’s supported package version format, rejecting arbitrary
suffixes such as sol-vfoo before exporting it. In the foundry.toml update step,
safely pass the validated VERSION into sed so replacement characters cannot
alter the result, and verify that the first package version line was actually
updated, failing the workflow otherwise. Anchor changes to the VERSION parsing
block and the “Set foundry.toml version to the release version” step.
- Around line 77-78: Update the SOLDEER_API_TOKEN workflow input configuration
to set required to true, ensuring callers provide the secret before the release
workflow performs preparation and invokes forge soldeer push.
- Around line 98-100: Remove the unused id-token permission from the workflow
permissions block, retaining contents: write for the pinned rainix/gh-release
composite and leaving the rest of the release workflow unchanged.
- Line 118: Update the tag-trigger validation step around the error echo to pass
github.ref through an environment variable, then reference that variable with a
quoted "%s" format in the shell command. Remove direct github.ref interpolation
from the generated script while preserving the existing error message 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 Plus
Run ID: 6f0faa7f-9958-41ec-8a6c-ec0fb20fa4dd
📒 Files selected for processing (1)
.github/workflows/rainix-tag-release.yaml
| # sense for the release tags the caller's `on: push: tags` filter allows. | ||
| if: ${{ !startsWith(github.ref, 'refs/tags/') }} | ||
| run: | | ||
| echo "::error::rainix-tag-release must be triggered by a tag push, got ${{ github.ref }}" >&2 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## workflow excerpt"
if [ -f .github/workflows/rainix-tag-release.yaml ]; then
nl -ba .github/workflows/rainix-tag-release.yaml | sed -n '90,130p'
else
echo "missing .github/workflows/rainix-tag-release.yaml"
fi
echo
echo "## all github.ref interpolations in this workflow"
rg -n '\$\{\{\s*github\.ref\s*\}\}' .github/workflows/rainix-tag-release.yaml || true
echo
echo "## repository workflow files under .github/workflows"
git ls-files '.github/workflows/*' | sed -n '1,120p'Repository: rainlanguage/rainix
Length of output: 217
🌐 Web query:
GitHub Actions avoid injection of untrusted values ref branch-name run shell command
💡 Result:
To avoid shell command injection when using untrusted values like branch names (e.g., github.head_ref) in GitHub Actions run commands, you must never directly interpolate these values into your script using the ${{ }} syntax [1][2][3]. The ${{ }} syntax is evaluated by the GitHub Actions runner before the script is executed, effectively splicing the untrusted input directly into your code [4][2]. If a branch name contains shell metacharacters (e.g., ";", "&", "|"), an attacker can use them to break out of the intended command and execute arbitrary code on the runner [1][5][2]. The industry-standard solution is to use intermediate environment variables [6][4][7]. By assigning the untrusted input to an environment variable, you ensure the value is passed to the shell as a literal string rather than being interpreted as part of the command structure [2][8]. Recommended implementation: 1. Define an environment variable in the step's env block [6][8]. 2. Use the shell's native variable expansion syntax (e.g., "$VAR") within the run block to reference it [6][2][8]. Example of secure usage: - name: Process branch name env: BRANCH_NAME: ${{ github.head_ref }} run: | # Use the variable in quotes to prevent word splitting and command injection echo "Processing branch: $BRANCH_NAME" git checkout "$BRANCH_NAME" Always treat values from the github context—particularly those ending in ref, name, message, title, or body—as untrusted input [1][3]. In addition to using environment variables, ensure that your environment variables are always double-quoted within your scripts to prevent word splitting vulnerabilities [3][7]. Avoiding the use of risky triggers like pull_request_target on untrusted forks further helps mitigate the impact if an injection vulnerability exists [7].
Citations:
- 1: https://docs.github.com/en/actions/concepts/security/script-injections
- 2: https://phpunit.expert/articles/hardening-github-actions-workflows.html
- 3: https://securitylab.github.com/resources/github-actions-untrusted-input/
- 4: https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/
- 5: GHSA-8v8w-v8xg-79rf
- 6: https://github.blog/security/supply-chain-security/four-tips-to-keep-your-github-actions-workflows-secure/
- 7: https://docs.github.com/en/actions/reference/security/secure-use
- 8: https://codeql.github.com/codeql-query-help/actions/actions-code-injection-critical/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## workflow excerpt"
if [ -f .github/workflows/rainix-tag-release.yaml ]; then
awk '{printf "%5d\t%s\n", NR, $0}' .github/workflows/rainix-tag-release.yaml | sed -n '100,125p'
else
echo "missing .github/workflows/rainix-tag-release.yaml"
fi
echo
echo "## all github.ref interpolations in this workflow"
grep -nE '\$\{\{\s*github\.ref\s*\}\}' .github/workflows/rainix-tag-release.yaml || true
echo
echo "## repository workflow files under .github/workflows"
git ls-files '.github/workflows/*'Repository: rainlanguage/rainix
Length of output: 2682
Pass github.ref through an environment variable.
Direct $\$\{\{ github.ref \}\} interpolation in run exposes refs/tags/.../branch refs to the generated shell script. Use an intermediate env var and "%s" so shell-significant ref names cannot alter the command.
Suggested fix
- name: Guard - the trigger ref is a tag
+ env:
+ TRIGGER_REF: ${{ github.ref }}
...
run: |
- echo "::error::rainix-tag-release must be triggered by a tag push, got ${{ github.ref }}" >&2
+ printf '::error::rainix-tag-release must be triggered by a tag push, got %s\n' "$TRIGGER_REF" >&2
exit 1📝 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.
| echo "::error::rainix-tag-release must be triggered by a tag push, got ${{ github.ref }}" >&2 | |
| - name: Guard - the trigger ref is a tag | |
| env: | |
| TRIGGER_REF: ${{ github.ref }} | |
| ... | |
| run: | | |
| printf '::error::rainix-tag-release must be triggered by a tag push, got %s\n' "$TRIGGER_REF" >&2 | |
| exit 1 |
🧰 Tools
🪛 zizmor (1.26.1)
[error] 118-118: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 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 @.github/workflows/rainix-tag-release.yaml at line 118, Update the
tag-trigger validation step around the error echo to pass github.ref through an
environment variable, then reference that variable with a quoted "%s" format in
the shell command. Remove direct github.ref interpolation from the generated
script while preserving the existing error message and behavior.
Source: Linters/SAST tools
Add a `deploy` job that fans rainix-manual-sol-artifacts over `deploy-suites` in dependency order (max-parallel 1, one forge run per suite for Zoltu nonce isolation); the `release` job now `needs:` it, so publish/snapshot run only once the chain carries the code the pins name. Composes the existing deploy reusable rather than re-implementing its broadcast/env. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…validation - guard job: require the release tag to be an ancestor of the release branch before deploy/publish, so a tag from an unmerged branch can't rebase unreviewed commits onto main or broadcast unreviewed bytecode (CRITICAL). Refs via env, not github.ref interpolation. - commit step: drop `git checkout -- .`, which reverted the foundry.toml version bump and regenerated tracked files, breaking version/snapshot lockstep (CRITICAL). - validate VERSION as MAJOR.MINOR.PATCH: rejects junk tags and removes any sed-special char before the foundry.toml edit; fail if the version line is absent. - SOLDEER_API_TOKEN required:true (always publishes); drop unused id-token perm. Resumability of publish-then-commit-back is a known, documented limitation left as follow-up (heavy lift; needs durable release state). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the CodeRabbit findings in f1fa5a9:
Not fixed — documented follow-up: publish-then-commit-back resumability (was L281). If Soldeer publish succeeds but the push to main fails, the package is published without its snapshot on main. This is the atomicity limitation already noted in the PR description; a proper fix needs durable release state and is a heavy lift for a follow-up, not this PR. @coderabbitai review |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
workflow=".github/workflows/rainix-tag-release.yaml"
printf '%s\n' '--- workflow outline ---'
ast-grep outline "$workflow" --items all || true
printf '%s\n' '--- current workflow with line numbers ---'
cat -n "$workflow"
printf '%s\n' '--- commit diff (f1fa5a9) ---'
git show --format=fuller --find-renames --find-copies -- "$workflow" f1fa5a9
printf '%s\n' '--- PR branch diff against merge base ---'
base="$(git merge-base HEAD origin/main 2>/dev/null || true)"
if [ -n "$base" ]; then
git diff --no-ext-diff --unified=40 "$base"...HEAD -- "$workflow"
else
git diff --no-ext-diff --unified=40 HEAD^..HEAD -- "$workflow"
fiLength of output: 47604 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- locate and map checkout composite ---'
fd -a '^checkout\.ya?ml$' .github/actions | while IFS= read -r file; do
echo "### $file"
ast-grep outline "$file" --items all || true
cat -n "$file"
done
printf '%s\n' '--- locate tag-release callers and comparable tag guards ---'
rg -n -C 8 --glob '*.yml' --glob '*.yaml' \
'(rainix-tag-release|workflow_call:|refs/tags/|GITHUB_REF_NAME|merge-base --is-ancestor)' \
.github
printf '%s\n' '--- workflow files and reusable-action declarations ---'
fd -a -e yaml -e yml .github/workflows .github/actions | sortLength of output: 19586 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- action metadata available on this PR branch ---'
git ls-tree -r --name-only HEAD -- .github/actions | \
grep -E '(^|/)(action\.ya?ml|checkout\.ya?ml)$' || true
printf '%s\n' '--- checkout action metadata from repository default branch ---'
for path in .github/actions/checkout/action.yml .github/actions/checkout/action.yaml; do
echo "### $path"
gh api "repos/rainlanguage/rainix/contents/$path?ref=main" \
--jq '.content' 2>/dev/null | base64 --decode 2>/dev/null || true
done
printf '%s\n' '--- YAML parse check ---'
ruby -ryaml -e '
YAML.load_file(".github/workflows/rainix-tag-release.yaml")
puts "YAML parsed successfully"
'Length of output: 2392
The publish-then-commit-back atomicity limitation remains explicitly documented as a follow-up. No additional issue surfaced in this focused verification. ✅ Action performedReview finished.
|
|
Reviewed f1fa5a9: approve All 12 rainix checks green on this sha. The two CodeRabbit Criticals (tag provenance, worktree restore) and three Majors (VERSION validation, SOLDEER_API_TOKEN required, unused id-token) are fixed in f1fa5a9 — those threads are now outdated. The one remaining finding, publish/commit-back resumability, is a documented heavy-lift follow-up (a rare partial-failure recovery path), not a defect in what lands here; thread resolved with that note. Reviewed my own authored change adversarially: the guard job's ancestry check closes the unmerged-tag hole, and removing |
|
@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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- PR metadata and changed files ---'
gh pr view 280 --repo rainlanguage/rainix --json baseRefName,headRefName,commits,files \
--jq '{base: .baseRefName, head: .headRefName, commitCount: (.commits | length), files: [.files[] | {path, additions, deletions}]}'
printf '%s\n' '--- merge-base diff statistics ---'
base="$(git merge-base HEAD "origin/$(gh pr view 280 --repo rainlanguage/rainix --json baseRefName --jq .baseRefName)")"
git diff --stat "$base"...HEAD
git diff --numstat "$base"...HEAD
printf '%s\n' '--- changed workflow ---'
workflow=".github/workflows/rainix-tag-release.yaml"
if [ -f "$workflow" ]; then
cat -n "$workflow"
fiLength of output: 21227 SIZE=L |
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 @.github/workflows/rainix-tag-release.yaml:
- Around line 144-166: Add explicit least-privilege permissions to both the
guard and deploy jobs: declare contents read access for guard, and ensure deploy
passes only contents read access to the called rainix-manual-sol-artifacts.yaml
reusable workflow. Keep release’s existing contents write permission unchanged.
🪄 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 Plus
Run ID: 0a8f5c21-886c-4cb3-86e7-c681bdf086cc
📒 Files selected for processing (1)
.github/workflows/rainix-tag-release.yaml
| guard: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: rainlanguage/rainix/.github/actions/checkout@main | ||
| - name: Tag must be a tag on the release branch | ||
| env: | ||
| MAIN: ${{ inputs.main-branch }} | ||
| run: | | ||
| set -euo pipefail | ||
| case "$GITHUB_REF" in | ||
| refs/tags/*) : ;; | ||
| *) echo "::error::rainix-tag-release must be triggered by a tag push, got $GITHUB_REF" >&2; exit 1 ;; | ||
| esac | ||
| # The shared checkout is shallow; unshallow so the ancestry test can see | ||
| # whether the tag commit is on the release branch. | ||
| if [ -f "$(git rev-parse --git-dir)/shallow" ]; then | ||
| git fetch --no-tags --unshallow origin | ||
| fi | ||
| git fetch --no-tags origin "$MAIN" | ||
| if ! git merge-base --is-ancestor "$GITHUB_SHA" "origin/$MAIN"; then | ||
| echo "::error::tag $GITHUB_REF_NAME ($GITHUB_SHA) is not on origin/$MAIN — refusing to release an unmerged commit" >&2 | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Declare explicit least-privilege permissions on guard and deploy.
Neither job declares a permissions: block, so both fall back to the runner/org default token permissions (flagged by zizmor for the deploy job, which also grants those defaults to the called rainix-manual-sol-artifacts.yaml reusable workflow). Only read access to repo contents is needed here; release already does this correctly with contents: write.
🔒 Proposed fix
guard:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
steps: deploy:
needs: guard
+ permissions:
+ contents: read
strategy:
max-parallel: 1Also applies to: 172-198
🤖 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 @.github/workflows/rainix-tag-release.yaml around lines 144 - 166, Add
explicit least-privilege permissions to both the guard and deploy jobs: declare
contents read access for guard, and ensure deploy passes only contents read
access to the called rainix-manual-sol-artifacts.yaml reusable workflow. Keep
release’s existing contents write permission unchanged.
Source: Linters/SAST tools
Why
Deploy repos and library repos need mutually exclusive release lifecycles, and today
st0x.deploy(a deploy repo) is wrongly on the library one. The symptom istestDeployTag, red onst0x.deploymain for weeks — I watched it climb0_1_8 != 0_1_9 → 0_1_26 → 0_1_27 → 0_1_28 → 0_1_29across a single day of merges.That test compares generated
LibProdDeployCurrent.DEPLOY_TAG(the frozen snapshot's version) againstfoundry.toml's[package].version. Underrainix-autopublish's next-version lifecycle,[package].versionis the next, unpublished version — deliberately one ahead of the registry, bumped on every content-changing merge.DEPLOY_TAGonly advances when a deploy regenerates the pins. So the two are designed to differ; the test is green only in the instant after a regeneration and red on main between every merge and the next deploy.A permanently-red check is worse than a missing one — it trains everyone to wave CI through. This PR gives deploy repos their own lifecycle where version and snapshot move only together, at release time.
The two lifecycles
rainix-autopublish)rainix-tag-release, this PR)main[package].versionsrc/generated/<tag>/pins, frozen + append-onlyA repo is strictly one or the other.
What a release does
Nothing on merge — a PR lands source only, main stays at the last release (its live contracts still match its pins, so the fork suite stays green). A human tags
sol-v<X>, and this workflow runs deploy + publish + snapshot as one act:deployjob — broadcasts every suite in dependency order by fanning the existingrainix-manual-sol-artifactsreusable overdeploy-suitesatmax-parallel: 1(one forge run per suite — the Zoltu nonce isolation the manual dispatch already enforces). Composes the deploy reusable rather than re-implementing its broadcast/env.releasejob (needs: deploy) — so publish/snapshot only run once the chain carries the code:foundry.tomlversion to<X>(from the tag).address = f(bytecode)under CREATE2, no chain access — viasnapshot-generate-cmd(e.g.forge script ./script/BuildPointers.sol && forge fmt).frozen-snapshots-append-only): a release only addssrc/generated/<X>/, never edits a frozen tag.<X>.main, so the daily drift sweep has the current release's pins.testDeployTagthen holds by construction: version andDEPLOY_TAGadvance only in the same commit.Design decisions
Sibling, not a mode on
rainix-autopublish— decided. The lifecycles share ~40% of steps (deploy-key checkout, nix setup, git config, Soldeer push, GH release) — all already in composite actions this reuses — and differ on the other ~60% (content-gate + next-version bump vs. deploy + snapshot-regen + append-only + commit-back). Guarding every autopublish step with apublish-triggerconditional would put two lifecycles in one file that float, raindex, and other critical packages depend on for their releases, where a subtle conditional error breaks a real publish. A sibling has zero blast radius; autopublish is untouched.Manual git tag is the trigger — decided. The human pushes
sol-v<X>; that fires the release. The tag ref points at the tagged source commit, while the release snapshot lands onmain(a commit above it) and in the published Soldeer package — which is the canonical distribution consumers pin. Noworkflow_dispatch/tag-moving; the tag is exactly where the human put it.Deploy composes the reusable, order enforced two ways. The
deployjob is a matrix overrainix-manual-sol-artifacts(single source of truth for how a suite broadcasts). Sequencing rests onmax-parallel: 1running the suites in listed order and the on-chain dep-codehash check, which fails loud if a dependent suite somehow runs before its dependency — so an ordering slip is a loud re-runnable failure (Zoltu deploys are idempotent), never silent corruption.Consumer change (follow-up PR on
st0x.deploy, not here)package-release.yamlflips fromon: push: branches+rainix-autopublishto:(The suite list is lifted verbatim from
st0x.deploy's currentmanual-sol-artifacts.yamldispatch choices, already in dependency order.)Validation
deploy(matrix, max-parallel 1,uses: rainix-manual-sol-artifacts) →release(needs: deploy); no duplicate keys.action.yml/ reusableon:(checkoutssh-key,nix-cachix-setup,gh-release,frozen-snapshots-append-only,rainix-manual-sol-artifactssuite/script/verify).pre-commitclean (yamlfmt,no-consumer-prettier).st0x.deployconsumer flip and a real release tag, and the fork verify gate is currently blocked org-wide by a disabled Ankr RPC key (separate infra issue), so the consumer PR waits on that regardless.🤖 Generated with Claude Code
Summary by CodeRabbit