autopublish: stop self-trigger loop + tolerate unpublished intra-workspace deps in gate#193
Conversation
…space deps in gate Fix #6: amend the cargo-release bump commit to start with "Package Release" so the job guard skips the push it triggers. cargo-release's default "chore: Release" message otherwise re-fires the workflow and concurrent re-runs race on the same bump (tag collisions). Fix #7: if 'cargo package' fails for a crate during the content gate (e.g. a first interdependent workspace publish where a dependent pins its sibling at a version not yet on crates.io), treat it as changed and continue; the dependency-ordered publish step publishes the sibling first. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe pull request updates the GitHub Actions autopublish workflow to improve robustness. It adds per-crate cargo package error handling to the hash verification loop and amends release commit messages to control job-guard behavior and prevent re-trigger races. ChangesAutopublish Workflow Robustness
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 2
🤖 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-autopublish.yaml:
- Around line 113-123: The current block treats any failure of the cargo package
run as a benign "changed" case; instead capture the cargo package stderr (for
the invocation using CRATE) and only set changed=true and continue when the
stderr matches known registry-resolution/unpublished-intra-workspace patterns
(e.g., messages about "failed to select a version", "no matching package named",
or "cannot find registry for dependency" that indicate an unpublished
intra-workspace dep). For any other cargo package failure, print the captured
stderr and exit non‑zero so the workflow fails fast. Update the block around the
nix develop ... cargo package -p "$CRATE" invocation to save stderr to a
variable/file, test it for those error patterns, and only set changed=true and
continue on matches; otherwise call echo with the stderr and exit 1.
- Around line 182-185: Validate and safely pass workflow inputs rather than
inlining them: ensure inputs.level is validated against an allowlist (e.g.,
"patch","minor","major","pre") and assigned to a safe variable (LEVEL) and
ensure CRATES is split and each crate name is validated with a strict regex
(e.g., allow only [A-Za-z0-9_-]) before use; build an argv-safe array (e.g.,
PKGS_ARGS) by appending -p "$CRATE" for each validated CRATE instead of
concatenating strings into PKGS, then invoke nix develop ... -c cargo release
... "${PKGS_ARGS[@]}" "$LEVEL" so arguments cannot break out of boundaries, and
sanitize the CRATES string used in git commit --amend (produce a safe, joined
display string from validated crate names) before embedding it in the commit
message.
🪄 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: 4611f516-e216-49fd-9181-4f019a7a0871
📒 Files selected for processing (1)
.github/workflows/rainix-autopublish.yaml
| # cargo package resolves deps against the registry. For a first | ||
| # interdependent workspace publish, a dependent (e.g. a lib that | ||
| # pins its sibling proc-macro at the new version) can't be packaged | ||
| # until that sibling is published — chicken-and-egg. If packaging | ||
| # fails, treat the crate as changed and let the dependency-ordered | ||
| # publish step sort it out (the sibling publishes first). | ||
| if ! nix develop github:rainlanguage/rainix#rust-shell -c cargo package -p "$CRATE" --allow-dirty --no-verify --quiet; then | ||
| echo "gate $CRATE: cannot package yet (likely an unpublished intra-workspace dep) -> changed" | ||
| changed=true | ||
| continue | ||
| fi |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/rainix-autopublish.yaml"
echo "== File exists? =="
ls -la "$FILE"
echo "== Snippet around requested lines (90-170) =="
nl -ba "$FILE" | sed -n '90,170p'
echo "== Look for Cargo hashes / changed usage (search) =="
rg -n "Cargo hashes|cargo package|changed=true|continue|bump|tag|publish|version" "$FILE" -n || true
echo "== Show sections that use `changed` (if any) =="
# Print lines where `changed` appears and some surrounding context
rg -n "changed" "$FILE" -n | cut -d: -f1 | sort -n | uniq | while read -r ln; do
start=$((ln-10)); end=$((ln+20))
echo "--- Context around line $ln (approx) ---"
nl -ba "$FILE" | sed -n "${start},${end}p"
done
echo "== Show any 'cargo release' / tag push / publish step context =="
rg -n "cargo release|git tag|push|publish" "$FILE" -n || trueRepository: rainlanguage/rainix
Length of output: 281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/rainix-autopublish.yaml"
echo "== Snippet around lines 100-150 =="
awk 'NR>=100 && NR<=150 {printf "%6d | %s\n", NR, $0}' "$FILE"
echo
echo "== Find all occurrences of: cargo package, changed=true, continue, publish/tag =="
rg -n "cargo package|changed=true|continue|bump|tag|publish|cargo release|git tag|push" "$FILE" || true
echo
echo "== Show contexts around each match of 'changed=true' =="
mapfile -t lines < <(rg -n "changed=true" "$FILE" | cut -d: -f1)
for ln in "${lines[@]}"; do
start=$((ln-25)); end=$((ln+40))
echo "--- Context around changed=true at line $ln ---"
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d | %s\n", NR, $0}' "$FILE"
echo
done
echo "== Show contexts around the 'cargo package' failure handler =="
# Find exact line(s) containing "cargo package" and show a window around each
mapfile -t pkglines < <(rg -n "cargo package" "$FILE" | cut -d: -f1)
for ln in "${pkglines[@]}"; do
start=$((ln-20)); end=$((ln+40))
echo "--- Context around cargo package at line $ln ---"
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d | %s\n", NR, $0}' "$FILE"
echo
doneRepository: rainlanguage/rainix
Length of output: 8333
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/rainix-autopublish.yaml"
echo "== Show around Nix 'Cargo hashes' step (70-210) =="
awk 'NR>=60 && NR<=220 {printf "%6d | %s\n", NR, $0}' "$FILE"
echo
echo "== Find all uses of outputs.changed / steps.*.outputs.*changed =="
rg -n "outputs\.changed|outputs\.[a-zA-Z0-9_]*changed|steps\.[a-zA-Z0-9_]+\.outputs\.[a-zA-Z0-9_]*changed|changed ==|changed=true|changed=false" "$FILE" || true
echo
echo "== Find conditionals around bump/tag/publish steps =="
rg -n "name: Bump versions|name: Tag and push|name: Publish|if:.*changed|if:.*hash" "$FILE" || true
echo
echo "== Show around 'Bump versions' and 'Tag and push' conditions (160-250) =="
awk 'NR>=155 && NR<=255 {printf "%6d | %s\n", NR, $0}' "$FILE"Repository: rainlanguage/rainix
Length of output: 19779
🌐 Web query:
Typical cargo error message when a dependency version can't be selected (e.g., “failed to select a version” / “no matching package named”) during cargo package``
💡 Result:
When Cargo encounters a dependency resolution failure, the primary error message format for version selection conflicts is "failed to select a version for [PACKAGE_NAME]" [1][2][3]. This error typically occurs when the dependency resolver cannot find a version of a package that satisfies all constraints imposed by the project's dependency graph [4]. The output often includes a diagnostic trace showing how different packages require different, incompatible versions of the same dependency, frequently culminating in the message, "all possible versions conflict with previously selected packages" [1][3]. Key characteristics of this error include: Diagnostic Trace: Cargo will list the chain of dependencies that led to the conflict, helping you identify which packages are requesting incompatible version requirements [5][3]. Conflict Details: It often identifies a "previously selected package" that is already locked to a version that violates the requirements of a newer or different dependency [1][3]. "No matching package": If the issue is not a version conflict but rather that the package cannot be found (e.g., due to a typo or incorrect registry configuration), Cargo will instead report, "no matching package named [PACKAGE_NAME] found" [5]. Common causes include: Conflicting Version Requirements: Two or more dependencies require SemVer-incompatible versions of a shared sub-dependency that Cargo cannot unify [4]. Lockfile Stale/Corrupt: Sometimes, the Cargo.lock file contains outdated information or manual edits that conflict with the actual requirements in Cargo.toml [1][2]. Newer Requirements: Switching a dependency source (e.g., from crates.io to a git URL) may introduce new constraints that conflict with existing locked versions [1]. Recommended troubleshooting steps: Run cargo tree to visualize your dependency graph and identify where the conflicting versions originate [4]. Attempt to update the lockfile using cargo update [1]. If the lockfile is suspected to be the issue, you can try deleting it (rm Cargo.lock) and re-running the build, or using cargo generate-lockfile to regenerate it from scratch [1][2]. Check your.cargo/config.toml or package configuration to ensure your resolver settings (such as those handling MSRV-aware resolution) are not causing unexpected constraint failures [6].
Citations:
- 1: Cargo failed to select a version for dependencies, but problem is fixed when state is reset rust-lang/cargo#9994
- 2: Duplicate major version in lock file causes "failed to select a version" rust-lang/cargo#12079
- 3: "all possible versions conflict with previously selected packages" error is not giving the appropriate diagnostic rust-lang/cargo#10189
- 4: https://doc.rust-lang.org/cargo/reference/resolver.html
- 5: https://github.com/rust-lang/cargo/blob/6d1bd93c/src/cargo/core/resolver/errors.rs
- 6: https://stackoverflow.com/questions/78816757/how-to-prevent-cargo-from-resolving-dependencies-not-supporting-current-rust-ver
Only tolerate the known unpublished-dependency case; fail fast on other cargo package errors
The workflow treats any cargo package failure as changed=true and continues, and later steps are gated on steps.cargo.outputs.changed == 'true' (Bump Cargo version → Tag and push → Publish to crates.io / GitHub Release). That means unrelated packaging defects can still trigger a version bump + tags and only fail later during publish, creating partial-release risk. Classify and tolerate only the expected registry-resolution/unpublished intra-workspace dependency error patterns; hard-fail otherwise.
Suggested hardening
- if ! nix develop github:rainlanguage/rainix#rust-shell -c cargo package -p "$CRATE" --allow-dirty --no-verify --quiet; then
- echo "gate $CRATE: cannot package yet (likely an unpublished intra-workspace dep) -> changed"
- changed=true
- continue
- fi
+ if ! pkg_err=$(nix develop github:rainlanguage/rainix#rust-shell -c cargo package -p "$CRATE" --allow-dirty --no-verify --quiet 2>&1); then
+ if printf '%s' "$pkg_err" | grep -Eq 'failed to select a version|no matching package named'; then
+ echo "gate $CRATE: cannot package yet (likely an unpublished intra-workspace dep) -> changed"
+ changed=true
+ continue
+ fi
+ printf '%s\n' "$pkg_err" >&2
+ exit 1
+ fi🤖 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-autopublish.yaml around lines 113 - 123, The
current block treats any failure of the cargo package run as a benign "changed"
case; instead capture the cargo package stderr (for the invocation using CRATE)
and only set changed=true and continue when the stderr matches known
registry-resolution/unpublished-intra-workspace patterns (e.g., messages about
"failed to select a version", "no matching package named", or "cannot find
registry for dependency" that indicate an unpublished intra-workspace dep). For
any other cargo package failure, print the captured stderr and exit non‑zero so
the workflow fails fast. Update the block around the nix develop ... cargo
package -p "$CRATE" invocation to save stderr to a variable/file, test it for
those error patterns, and only set changed=true and continue on matches;
otherwise call echo with the stderr and exit 1.
| PKGS="" | ||
| for CRATE in $CRATES; do PKGS="$PKGS -p $CRATE"; done | ||
| nix develop github:rainlanguage/rainix#rust-shell -c cargo release --no-confirm --execute --no-tag --no-push --no-publish $PKGS ${{ inputs.level }} | ||
| git commit --amend -m "Package Release: cargo version bump ($CRATES)" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify how this reusable workflow is invoked and whether `level` is constrained by callers.
fd -HI -e yml -e yaml .github/workflows | while read -r wf; do
rg -n -C2 'rainix-autopublish\.ya?ml|^\s*level:\s*' "$wf" || true
doneRepository: rainlanguage/rainix
Length of output: 437
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List workflow YAMLs under .github/workflows
workflows=$(fd -HI -e yml -e yaml .github/workflows . --type f)
# Show any definition/usage of `level:` near workflow_call
echo "$workflows" | while read -r wf; do
echo "=== $wf ==="
rg -n -C3 'workflow_call:|^\s*level:\s*|inputs:\s*$' "$wf" || true
done
# Locate other workflows that call this reusable one (look for `rainix-autopublish` reference)
echo "$workflows" | while read -r wf; do
rg -n -C3 'rainix-autopublish\.ya?ml|\.github/workflows/rainix-autopublish\.ya?ml|uses:\s*.*rainlanguage/rainix/.github/workflows/rainix-autopublish\.ya?ml' "$wf" || true
doneRepository: rainlanguage/rainix
Length of output: 437
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) List workflow YAMLs under .github/workflows
find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) | sort
# 2) Inspect the target workflow around the referenced lines
echo "---- TARGET: .github/workflows/rainix-autopublish.yaml (around 150-210) ----"
sed -n '150,210p' .github/workflows/rainix-autopublish.yaml || true
# 3) Inspect workflow_call inputs (level) definition
echo "---- TARGET: workflow_call inputs (show workflow_call block) ----"
rg -n --context=6 'workflow_call:|inputs:|level:' .github/workflows/rainix-autopublish.yaml || true
# 4) Find other workflows that call this reusable one
echo "---- CALLERS of rainix-autopublish.yaml ----"
find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) | xargs -I{} sh -c '
rg -n --context=3 "uses:.*rainlanguage/rainix/.github/workflows/rainix-autopublish\.ya?ml|uses:.*\.github/workflows/rainix-autopublish\.ya?ml" "{}" || true
'Repository: rainlanguage/rainix
Length of output: 5688
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show where CRATES and level are used/constructed in the target workflow
echo "---- rainix-autopublish.yaml: occurrences of CRATES/inputs.level ----"
rg -n 'CRATES=|inputs\.level|level\b' .github/workflows/rainix-autopublish.yaml
echo "---- rainix-autopublish.yaml: show sections around CRATES construction ----"
rg -n -C3 'CRATES' .github/workflows/rainix-autopublish.yaml
echo "---- rainix-autopublish.yaml: show workflow_call inputs for crate/crates/level ----"
sed -n '1,120p' .github/workflows/rainix-autopublish.yaml
# Find callers that use this reusable workflow
echo "---- CALLERS (workflow uses rainix-autopublish) ----"
find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 \
| xargs -0 rg -n --context=4 'uses:.*rainix-autopublish\.ya?ml' || true
echo "---- CALLERS (maybe direct path form) ----"
find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 \
| xargs -0 rg -n --context=4 'rainix-autopublish\.ya?ml' || trueRepository: rainlanguage/rainix
Length of output: 9262
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any reuse-call of this workflow via `uses: ...rainix-autopublish...`
rg -n --context=3 'uses:.*rainix-autopublish\.ya?ml' .github/workflows || true
# Search for inline level passing (just in case)
rg -n --context=3 'with:.*level:|level:' .github/workflows/rainix-*.yml .github/workflows/* .github/workflows/*.yaml 2>/dev/null || true
# Also search outside workflows in case callers reference it elsewhere
rg -n --context=3 'rainix-autopublish\.ya?ml' .github || trueRepository: rainlanguage/rainix
Length of output: 1084
Harden cargo release invocation against untrusted workflow_call inputs (inputs.level, inputs.crate(s)).
inputs.level is inlined unquoted into nix develop ... -c cargo release ... ${{ inputs.level }}, and inputs.crates/inputs.crate are inlined into CRATES="..." then expanded via for CRATE in $CRATES and a string-built PKGS. Caller-controlled values can break argument boundaries (and potentially be interpreted as shell syntax) unless validated and passed as argv-safe values.
Suggested hardening
- PKGS=""
- for CRATE in $CRATES; do PKGS="$PKGS -p $CRATE"; done
- nix develop github:rainlanguage/rainix#rust-shell -c cargo release --no-confirm --execute --no-tag --no-push --no-publish $PKGS ${{ inputs.level }}
+ LEVEL="${{ inputs.level }}"
+ case "$LEVEL" in
+ patch|minor|major) ;;
+ *) echo "invalid release level: $LEVEL" >&2; exit 1 ;;
+ esac
+ PKGS=()
+ for CRATE in $CRATES; do PKGS+=(-p "$CRATE"); done
+ nix develop github:rainlanguage/rainix#rust-shell -c cargo release --no-confirm --execute --no-tag --no-push --no-publish "${PKGS[@]}" "$LEVEL"
git commit --amend -m "Package Release: cargo version bump ($CRATES)"📝 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.
| PKGS="" | |
| for CRATE in $CRATES; do PKGS="$PKGS -p $CRATE"; done | |
| nix develop github:rainlanguage/rainix#rust-shell -c cargo release --no-confirm --execute --no-tag --no-push --no-publish $PKGS ${{ inputs.level }} | |
| git commit --amend -m "Package Release: cargo version bump ($CRATES)" | |
| LEVEL="${{ inputs.level }}" | |
| case "$LEVEL" in | |
| patch|minor|major) ;; | |
| *) echo "invalid release level: $LEVEL" >&2; exit 1 ;; | |
| esac | |
| PKGS=() | |
| for CRATE in $CRATES; do PKGS+=(-p "$CRATE"); done | |
| nix develop github:rainlanguage/rainix#rust-shell -c cargo release --no-confirm --execute --no-tag --no-push --no-publish "${PKGS[@]}" "$LEVEL" | |
| git commit --amend -m "Package Release: cargo version bump ($CRATES)" |
🧰 Tools
🪛 zizmor (1.25.2)
[error] 184-184: 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-autopublish.yaml around lines 182 - 185, Validate
and safely pass workflow inputs rather than inlining them: ensure inputs.level
is validated against an allowlist (e.g., "patch","minor","major","pre") and
assigned to a safe variable (LEVEL) and ensure CRATES is split and each crate
name is validated with a strict regex (e.g., allow only [A-Za-z0-9_-]) before
use; build an argv-safe array (e.g., PKGS_ARGS) by appending -p "$CRATE" for
each validated CRATE instead of concatenating strings into PKGS, then invoke nix
develop ... -c cargo release ... "${PKGS_ARGS[@]}" "$LEVEL" so arguments cannot
break out of boundaries, and sanitize the CRATES string used in git commit
--amend (produce a safe, joined display string from validated crate names)
before embedding it in the commit message.
|
@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=S |
Closes #6, closes #7.
#6 — self-trigger loop
cargo releasecommits with the defaultchore: Releasemessage, which does not match the job guardif: !startsWith(head_commit.message, 'Package Release'). So the bump push re-fires Package Release; concurrent re-runs then race on the same version bump and collide on the namespaced tag (fatal: tag <crate>-v<ver> already exists). NPM repos don't loop because their final commit isPackage Release npm-….Fix: amend the cargo-release bump commit to start with
Package Releaseso the triggered run is skipped by the guard.#7 — workspace-gate interdependency
The content gate runs
cargo package -p <crate>per crate. On a first interdependent workspace publish, a dependent that pins its sibling at the new version can't be packaged until the sibling is on crates.io — chicken-and-egg, and the gate step errors out before anything publishes.Fix: if
cargo packagefails, treat the crate as changed andcontinue. The dependency-ordered publish step publishes the sibling first, which resolves the dependent.🤖 Generated with Claude Code
Summary by CodeRabbit