Skip to content

Commit 1658a40

Browse files
justin808claude
andauthored
Improve release script with OTP support, reduced verbosity, and faster execution (#2047)
## Summary Major improvements to the release process addressing all issues found during production testing. ## Fixed Issues ### 1. ✅ NPM OTP Asked 3 Times **Before:** User had to enter OTP separately for each of 3 NPM packages **After:** Use `NPM_OTP=<code> rake release[version]` to reuse OTP across all NPM publishes ### 2. ✅ Ruby Gem OTP Never Prompted (Crash) **Before:** `NoMethodError: undefined method 'sh' for module ReleaseHelpers` **After:** Fixed by moving helper method to top-level instead of module function, now has proper access to Rake's `sh` method ### 3. ✅ Git Hooks Add Significant Time **Before:** Pre-commit and pre-push hooks ran on every git operation (~10-30 seconds each) **After:** All git operations now use `LEFTHOOK=0` to skip hooks during release ### 4. ✅ Logs Excessively Verbose **Before:** Massive output from bundle install operations **After:** Uses `--quiet` flag unless `VERBOSE=1` is set ## Key Improvements ### OTP Support - Add `NPM_OTP` env var to reuse OTP across all 3 NPM package publishes - Add `RUBYGEMS_OTP` env var to reuse OTP across both gem publishes - Add `--otp` flag support for `gem release` command - Helpful messages suggesting OTP env var usage when not provided ### Performance - Skip git hooks during release (`LEFTHOOK=0`) - saves ~40-60 seconds total - Add `--quiet` flag to bundle install commands (unless `VERBOSE=1`) - Reduce verbose output significantly ### Reliability - Increase retry delay from 3 to 5 seconds (matches inter-publication delay) - More specific exception handling (`Gem::CommandException`, `IOError` vs `StandardError`) - Make max_retries configurable via `GEM_RELEASE_MAX_RETRIES` env var (default: 3) ### Usability - Add `VERBOSE=1` env var to enable full logging when debugging - Improved error messages and retry information - Better documentation with environment variable examples ## Usage Examples ```bash # Basic release (will prompt for OTP 5 times total: 3 NPM + 2 RubyGems) rake release[patch] # Fast release with no OTP prompts NPM_OTP=123456 RUBYGEMS_OTP=789012 rake release[patch] # Verbose mode for debugging VERBOSE=1 rake release[patch] # Custom retry count GEM_RELEASE_MAX_RETRIES=5 rake release[patch] ``` ## Test Plan - [x] Code passes `bundle exec rubocop` with no offenses - [x] Pre-commit hooks pass - [ ] Manual testing: Next release should complete much faster with optional OTP env vars ## Breaking Changes None - all improvements are backward compatible. Existing release workflow works as before, new features are opt-in via environment variables. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent c245590 commit 1658a40

File tree

1 file changed

+77
-54
lines changed

1 file changed

+77
-54
lines changed

rakelib/release.rake

Lines changed: 77 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,40 @@ class RaisingMessageHandler
1313
end
1414
end
1515

16-
# Helper module for release-specific tasks
17-
module ReleaseHelpers
18-
include ReactOnRails::TaskHelpers
19-
20-
# Publish a gem with retry logic for OTP failures
21-
def publish_gem_with_retry(dir, gem_name, max_retries: 3)
22-
puts "\nCarefully add your OTP for Rubygems when prompted."
16+
# Helper methods for release-specific tasks
17+
# These are defined at the top level so they have access to Rake's sh method
18+
def publish_gem_with_retry(dir, gem_name, otp: nil, max_retries: ENV.fetch("GEM_RELEASE_MAX_RETRIES", "3").to_i)
19+
puts "\nPublishing #{gem_name} gem to RubyGems.org..."
20+
if otp
21+
puts "Using provided OTP code..."
22+
else
23+
puts "Carefully add your OTP for Rubygems when prompted."
2324
puts "NOTE: OTP codes expire quickly (typically 30 seconds). Generate a fresh code when prompted."
25+
end
2426

25-
retry_count = 0
26-
success = false
27-
28-
while retry_count < max_retries && !success
29-
begin
30-
sh_in_dir(dir, "gem release")
31-
success = true
32-
rescue StandardError => e
33-
retry_count += 1
34-
if retry_count < max_retries
35-
puts "\n⚠️ #{gem_name} release failed (attempt #{retry_count}/#{max_retries})"
36-
puts "Common causes:"
37-
puts " - OTP code expired or already used"
38-
puts " - Network timeout"
39-
puts "\nGenerating a FRESH OTP code and retrying in 3 seconds..."
40-
sleep 3
41-
else
42-
puts "\n❌ Failed to publish #{gem_name} after #{max_retries} attempts"
43-
raise e
44-
end
27+
retry_count = 0
28+
success = false
29+
30+
while retry_count < max_retries && !success
31+
begin
32+
otp_flag = otp ? "--otp #{otp}" : ""
33+
sh %(cd #{dir} && gem release #{otp_flag})
34+
success = true
35+
rescue Gem::CommandException, IOError => e
36+
retry_count += 1
37+
if retry_count < max_retries
38+
puts "\n⚠️ #{gem_name} release failed (attempt #{retry_count}/#{max_retries})"
39+
puts "Common causes:"
40+
puts " - OTP code expired or already used"
41+
puts " - Network timeout"
42+
puts "\nGenerating a FRESH OTP code and retrying in 5 seconds..."
43+
sleep 5
44+
else
45+
puts "\n❌ Failed to publish #{gem_name} after #{max_retries} attempts"
46+
raise e
4547
end
4648
end
4749
end
48-
module_function :publish_gem_with_retry
4950
end
5051

5152
# rubocop:disable Metrics/BlockLength
@@ -73,6 +74,12 @@ This will update and release:
7374
3rd argument: Registry (verdaccio/npm, default: npm)
7475
4th argument: Skip push (skip_push to skip, default: push)
7576
77+
Environment variables:
78+
VERBOSE=1 # Enable verbose logging (shows all output)
79+
NPM_OTP=<code> # Provide NPM one-time password (reused for all NPM publishes)
80+
RUBYGEMS_OTP=<code> # Provide RubyGems one-time password (reused for both gems)
81+
GEM_RELEASE_MAX_RETRIES=<n> # Override max retry attempts (default: 3)
82+
7683
Examples:
7784
rake release[patch] # Bump patch version (16.1.1 → 16.1.2)
7885
rake release[minor] # Bump minor version (16.1.1 → 16.2.0)
@@ -81,7 +88,9 @@ Examples:
8188
rake release[16.2.0.beta.1] # Set pre-release version (→ 16.2.0-beta.1 for NPM)
8289
rake release[patch,true] # Dry run
8390
rake release[16.2.0,false,verdaccio] # Test with Verdaccio
84-
rake release[16.2.0,false,npm,skip_push] # Release without pushing to remote")
91+
rake release[16.2.0,false,npm,skip_push] # Release without pushing to remote
92+
VERBOSE=1 rake release[patch] # Release with verbose logging
93+
NPM_OTP=123456 RUBYGEMS_OTP=789012 rake release[patch] # Skip OTP prompts")
8594
task :release, %i[version dry_run registry skip_push] do |_t, args|
8695
include ReactOnRails::TaskHelpers
8796

@@ -90,6 +99,12 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
9099
args_hash = args.to_hash
91100

92101
is_dry_run = ReactOnRails::Utils.object_to_boolean(args_hash[:dry_run])
102+
is_verbose = ENV["VERBOSE"] == "1"
103+
npm_otp = ENV.fetch("NPM_OTP", nil)
104+
rubygems_otp = ENV.fetch("RUBYGEMS_OTP", nil)
105+
106+
# Configure output verbosity
107+
verbose(is_verbose)
93108

94109
# Validate registry parameter
95110
registry_value = args_hash.fetch(:registry, "")
@@ -188,14 +203,15 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
188203
puts " Updated #{file}"
189204
end
190205

191-
bundle_install_in(gem_root)
192-
# Update dummy app's Gemfile.lock
193-
bundle_install_in(dummy_app_dir)
194-
# Update pro dummy app's Gemfile.lock
206+
puts "\nUpdating Gemfile.lock files..."
207+
bundle_quiet_flag = is_verbose ? "" : " --quiet"
208+
209+
# Update all Gemfile.lock files
210+
unbundled_sh_in_dir(gem_root, "bundle install#{bundle_quiet_flag}")
211+
unbundled_sh_in_dir(dummy_app_dir, "bundle install#{bundle_quiet_flag}")
195212
pro_dummy_app_dir = File.join(gem_root, "react_on_rails_pro", "spec", "dummy")
196-
bundle_install_in(pro_dummy_app_dir) if Dir.exist?(pro_dummy_app_dir)
197-
# Update pro root Gemfile.lock
198-
bundle_install_in(pro_gem_root)
213+
unbundled_sh_in_dir(pro_dummy_app_dir, "bundle install#{bundle_quiet_flag}") if Dir.exist?(pro_dummy_app_dir)
214+
unbundled_sh_in_dir(pro_gem_root, "bundle install#{bundle_quiet_flag}")
199215

200216
# Prepare NPM registry configuration
201217
npm_registry_url = use_verdaccio ? "http://localhost:4873/" : "https://registry.npmjs.org/"
@@ -214,31 +230,38 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
214230
end
215231

216232
unless is_dry_run
217-
# Commit all version changes
218-
sh_in_dir(gem_root, "git add -A")
219-
sh_in_dir(gem_root, "git commit -m 'Bump version to #{actual_gem_version}'")
233+
# Commit all version changes (skip git hooks to save time)
234+
sh_in_dir(gem_root, "LEFTHOOK=0 git add -A")
235+
sh_in_dir(gem_root, "LEFTHOOK=0 git commit -m 'Bump version to #{actual_gem_version}'")
220236

221237
# Create git tag
222238
sh_in_dir(gem_root, "git tag v#{actual_gem_version}")
223239

224-
# Push commits and tags
240+
# Push commits and tags (skip git hooks)
225241
unless skip_push
226-
sh_in_dir(gem_root, "git push")
227-
sh_in_dir(gem_root, "git push --tags")
242+
sh_in_dir(gem_root, "LEFTHOOK=0 git push")
243+
sh_in_dir(gem_root, "LEFTHOOK=0 git push --tags")
228244
end
229245

230246
puts "\n#{'=' * 80}"
231247
puts "Publishing PUBLIC packages to #{use_verdaccio ? 'Verdaccio (local)' : 'npmjs.org'}..."
232248
puts "=" * 80
233249

250+
# Configure NPM OTP
251+
if npm_otp && !use_verdaccio
252+
npm_publish_args += " --otp #{npm_otp}"
253+
puts "Using provided NPM OTP for all NPM package publications..."
254+
elsif !use_verdaccio
255+
puts "\nNOTE: You will be prompted for NPM OTP code for each of the 3 NPM packages."
256+
puts "TIP: Set NPM_OTP environment variable to avoid repeated prompts."
257+
end
258+
234259
# Publish react-on-rails NPM package
235260
puts "\nPublishing react-on-rails@#{actual_npm_version}..."
236-
puts "Carefully add your OTP for NPM when prompted." unless use_verdaccio
237261
sh_in_dir(gem_root, "yarn workspace react-on-rails publish --new-version #{actual_npm_version} #{npm_publish_args}")
238262

239263
# Publish react-on-rails-pro NPM package
240264
puts "\nPublishing react-on-rails-pro@#{actual_npm_version}..."
241-
puts "Carefully add your OTP for NPM when prompted." unless use_verdaccio
242265
sh_in_dir(gem_root,
243266
"yarn workspace react-on-rails-pro publish --new-version #{actual_npm_version} #{npm_publish_args}")
244267

@@ -252,32 +275,32 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
252275
# package.json is in react_on_rails_pro/ which is not defined as a workspace
253276
node_renderer_name = "react-on-rails-pro-node-renderer"
254277
puts "\nPublishing #{node_renderer_name}@#{actual_npm_version}..."
255-
puts "Carefully add your OTP for NPM when prompted." unless use_verdaccio
256278
sh_in_dir(pro_gem_root,
257279
"yarn publish --new-version #{actual_npm_version} --no-git-tag-version #{npm_publish_args}")
258280

259281
if use_verdaccio
260282
puts "\nSkipping Ruby gem publication (Verdaccio is NPM-only)"
261283
else
262284
puts "\n#{'=' * 80}"
263-
puts "Publishing PUBLIC Ruby gem..."
285+
puts "Publishing PUBLIC Ruby gems..."
264286
puts "=" * 80
265287

288+
if rubygems_otp
289+
puts "Using provided RubyGems OTP for both gem publications..."
290+
else
291+
puts "\nNOTE: You will be prompted for RubyGems OTP code for each of the 2 gems."
292+
puts "TIP: Set RUBYGEMS_OTP environment variable to avoid repeated prompts."
293+
end
294+
266295
# Publish react_on_rails Ruby gem with retry logic
267-
ReleaseHelpers.publish_gem_with_retry(gem_root, "react_on_rails")
296+
publish_gem_with_retry(gem_root, "react_on_rails", otp: rubygems_otp)
268297

269298
# Add delay before next OTP operation to ensure clean separation
270299
puts "\n⏳ Waiting 5 seconds before next publication to ensure OTP separation..."
271300
sleep 5
272301

273-
puts "\n#{'=' * 80}"
274-
puts "Publishing PUBLIC Pro Ruby gem to RubyGems.org..."
275-
puts "=" * 80
276-
277302
# Publish react_on_rails_pro Ruby gem to RubyGems.org with retry logic
278-
puts "\nPublishing react_on_rails_pro gem to RubyGems.org..."
279-
puts "NOTE: Generate a FRESH OTP code (different from the previous one)."
280-
ReleaseHelpers.publish_gem_with_retry(pro_gem_root, "react_on_rails_pro")
303+
publish_gem_with_retry(pro_gem_root, "react_on_rails_pro", otp: rubygems_otp)
281304
end
282305
end
283306

0 commit comments

Comments
 (0)