Skip to content

Commit 1593bf9

Browse files
justin808claude
andauthored
Add npm authentication pre-flight check to release script (#2207)
## Summary Fixes #2206 The release script now verifies npm authentication at the start, before making any version bumps or git operations. This prevents partially completed releases when npm tokens have expired. - Add `verify_npm_auth` function that runs `npm whoami` to check authentication - Call it early in the release task, before any changes are made - Skip the check for Verdaccio (local registry) and dry runs - Update docs with pre-release checklist and troubleshooting section ## Test plan - [ ] Test with valid npm credentials: `rake release[patch,true]` (dry run) should show "✓ Logged in to NPM as: username" - [ ] Test with expired/missing credentials: `npm logout && rake release[patch,true]` should abort with clear error message - [ ] Test with Verdaccio: `rake release[patch,true,verdaccio]` should skip npm auth check 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added a Pre-Release Checklist, reordered release steps to verify NPM auth earlier, and expanded NPM authentication guidance (token/2FA notes) plus troubleshooting for expired tokens and re-login. * **Chores** * Release now performs pre-flight NPM auth checks with an automatic login attempt; the auth check is skipped when a local registry is used to avoid false failures. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0eb54fa commit 1593bf9

File tree

2 files changed

+113
-130
lines changed

2 files changed

+113
-130
lines changed

docs/contributor-info/releasing.md

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,35 @@ For pre-release versions, the gem version format is automatically converted to N
9191
- Gem: `3.0.0.beta.1`
9292
- NPM: `3.0.0-beta.1`
9393

94+
### Pre-Release Checklist
95+
96+
Before running the release command, verify:
97+
98+
1. **NPM authentication**: Run `npm whoami` to confirm you're logged in
99+
- If not logged in, the release script will automatically run `npm login` for you
100+
101+
2. **RubyGems authentication**: Ensure you have valid credentials for `gem push`
102+
103+
3. **No uncommitted changes**: Run `git status` to verify clean working tree
104+
94105
### Release Process
95106

96107
When you run `rake release[X.Y.Z]`, the task will:
97108

98109
1. Check for uncommitted changes (will abort if found)
99-
2. Pull latest changes from the remote repository
100-
3. Clean up example directories
101-
4. Bump the gem version in `lib/react_on_rails/version.rb`
102-
5. Update all package.json files with the new version
103-
6. Update the Pro package's dependency on react-on-rails
104-
7. Update the dummy app's Gemfile.lock
105-
8. Commit all version changes with message "Bump version to X.Y.Z"
106-
9. Create a git tag `vX.Y.Z`
107-
10. Push commits and tags to the remote repository
108-
11. Publish `react-on-rails` to NPM (requires 2FA token)
109-
12. Publish `react-on-rails-pro` to NPM (requires 2FA token)
110-
13. Publish `react_on_rails` to RubyGems (requires 2FA token)
110+
2. Verify NPM authentication (will run `npm login` if needed)
111+
3. Pull latest changes from the remote repository
112+
4. Clean up example directories
113+
5. Bump the gem version in `lib/react_on_rails/version.rb`
114+
6. Update all package.json files with the new version
115+
7. Update the Pro package's dependency on react-on-rails
116+
8. Update the dummy app's Gemfile.lock
117+
9. Commit all version changes with message "Bump version to X.Y.Z"
118+
10. Create a git tag `vX.Y.Z`
119+
11. Push commits and tags to the remote repository
120+
12. Publish `react-on-rails` to NPM (requires 2FA token)
121+
13. Publish `react-on-rails-pro` to NPM (requires 2FA token)
122+
14. Publish `react_on_rails` to RubyGems (requires 2FA token)
111123

112124
### Two-Factor Authentication
113125

@@ -232,6 +244,16 @@ rake release[16.2.0,true]
232244

233245
This shows you exactly what would be updated without making any changes.
234246

247+
### NPM Authentication Issues
248+
249+
If you see errors like "Access token expired" or "E404 Not Found" during NPM publish:
250+
251+
1. Your NPM token has expired (tokens now expire after 90 days)
252+
2. Run `npm login` to refresh your credentials
253+
3. Retry the release
254+
255+
The release script now checks NPM authentication at the start and will automatically run `npm login` if needed, so this issue will be caught and handled before any changes are made.
256+
235257
### If Release Fails
236258

237259
If the release fails partway through (e.g., during NPM publish):

rakelib/release.rake

Lines changed: 79 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,37 @@ end
1515

1616
# Helper methods for release-specific tasks
1717
# These are defined at the top level so they have access to Rake's sh method
18+
19+
def verify_npm_auth(registry_url = "https://registry.npmjs.org/")
20+
result = `npm whoami --registry #{registry_url} 2>&1`
21+
unless $CHILD_STATUS.success?
22+
puts <<~MESSAGE
23+
⚠️ NPM authentication required!
24+
25+
You are not logged in to NPM. Running 'npm login' now...
26+
27+
MESSAGE
28+
29+
# Run npm login interactively
30+
system("npm login --registry #{registry_url}")
31+
32+
# Verify login succeeded
33+
result = `npm whoami --registry #{registry_url} 2>&1`
34+
unless $CHILD_STATUS.success?
35+
abort <<~ERROR
36+
❌ NPM login failed!
37+
38+
Please manually run 'npm login' and retry the release.
39+
40+
Technical details:
41+
Registry: #{registry_url}
42+
Error: #{result.strip}
43+
ERROR
44+
end
45+
end
46+
puts "✓ Logged in to NPM as: #{result.strip}"
47+
end
48+
1849
def publish_gem_with_retry(dir, gem_name, otp: nil, max_retries: ENV.fetch("GEM_RELEASE_MAX_RETRIES", "3").to_i)
1950
puts "\nPublishing #{gem_name} gem to RubyGems.org..."
2051
if otp
@@ -74,8 +105,6 @@ This will update and release:
74105
75106
1st argument: Version (patch/minor/major OR explicit version like 16.2.0)
76107
2nd argument: Dry run (true/false, default: false)
77-
3rd argument: Registry (verdaccio/npm, default: npm)
78-
4th argument: Skip push (skip_push to skip, default: push)
79108
80109
Environment variables:
81110
VERBOSE=1 # Enable verbose logging (shows all output)
@@ -90,11 +119,9 @@ Examples:
90119
rake release[16.2.0] # Set explicit version
91120
rake release[16.2.0.beta.1] # Set pre-release version (→ 16.2.0-beta.1 for NPM)
92121
rake release[patch,true] # Dry run
93-
rake release[16.2.0,false,verdaccio] # Test with Verdaccio
94-
rake release[16.2.0,false,npm,skip_push] # Release without pushing to remote
95122
VERBOSE=1 rake release[patch] # Release with verbose logging
96123
NPM_OTP=123456 RUBYGEMS_OTP=789012 rake release[patch] # Skip OTP prompts")
97-
task :release, %i[version dry_run registry skip_push] do |_t, args|
124+
task :release, %i[version dry_run] do |_t, args|
98125
include ReactOnRails::TaskHelpers
99126

100127
# Check if there are uncommitted changes
@@ -109,23 +136,6 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
109136
# Configure output verbosity
110137
verbose(is_verbose)
111138

112-
# Validate registry parameter
113-
registry_value = args_hash.fetch(:registry, "")
114-
unless registry_value.empty? || registry_value == "verdaccio" || registry_value == "npm"
115-
raise ArgumentError,
116-
"Invalid registry value '#{registry_value}'. Valid values are: 'verdaccio', 'npm', or empty string"
117-
end
118-
119-
use_verdaccio = registry_value == "verdaccio"
120-
121-
# Validate skip_push parameter
122-
skip_push_value = args_hash.fetch(:skip_push, "")
123-
unless skip_push_value.empty? || skip_push_value == "skip_push"
124-
raise ArgumentError, "Invalid skip_push value '#{skip_push_value}'. Valid values are: 'skip_push' or empty string"
125-
end
126-
127-
skip_push = skip_push_value == "skip_push"
128-
129139
# Detect if this is a test/pre-release version (contains test, beta, alpha, rc, etc.)
130140
version_input = args_hash.fetch(:version, "")
131141
is_prerelease = version_input.match?(/\.(test|beta|alpha|rc|pre)\./i)
@@ -135,15 +145,23 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
135145
"Version argument is required. Use 'patch', 'minor', 'major', or explicit version (e.g., '16.2.0')"
136146
end
137147

148+
# Pre-flight authentication checks (skip for dry runs)
149+
unless is_dry_run
150+
puts "\n#{'=' * 80}"
151+
puts "PRE-FLIGHT CHECKS"
152+
puts "=" * 80
153+
verify_npm_auth
154+
end
155+
138156
# Having the examples prevents publishing
139157
Rake::Task["shakapacker_examples:clobber"].invoke
140158
# Delete any react_on_rails.gemspec except the root one
141159
sh_in_dir(gem_root, "find . -mindepth 2 -name 'react_on_rails.gemspec' -delete")
142160
# Delete any react_on_rails_pro.gemspec except the one in react_on_rails_pro directory
143161
sh_in_dir(gem_root, "find . -mindepth 3 -name 'react_on_rails_pro.gemspec' -delete")
144162

145-
# Pull latest changes (skip in dry-run mode or when skip_push is set)
146-
sh_in_dir(monorepo_root, "git pull --rebase") unless is_dry_run || skip_push
163+
# Pull latest changes (skip in dry-run mode)
164+
sh_in_dir(monorepo_root, "git pull --rebase") unless is_dry_run
147165

148166
# Determine if version_input is semver keyword or explicit version
149167
semver_keywords = %w[patch minor major]
@@ -196,7 +214,7 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
196214
package_json_files.each do |file|
197215
content = JSON.parse(File.read(file))
198216
content["version"] = actual_npm_version
199-
# Note: workspace:* dependencies (e.g., in react-on-rails-pro) are automatically
217+
# NOTE: workspace:* dependencies (e.g., in react-on-rails-pro) are automatically
200218
# converted to exact versions by pnpm during publish. No manual conversion needed.
201219

202220
File.write(file, "#{JSON.pretty_generate(content)}\n")
@@ -212,21 +230,8 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
212230
unbundled_sh_in_dir(pro_dummy_app_dir, "bundle install#{bundle_quiet_flag}") if Dir.exist?(pro_dummy_app_dir)
213231
unbundled_sh_in_dir(pro_gem_root, "bundle install#{bundle_quiet_flag}")
214232

215-
# Prepare NPM registry configuration
216-
npm_registry_url = use_verdaccio ? "http://localhost:4873/" : "https://registry.npmjs.org/"
217-
npm_publish_args = use_verdaccio ? "--registry #{npm_registry_url}" : ""
218-
219-
if use_verdaccio
220-
puts "\n#{'=' * 80}"
221-
puts "VERDACCIO LOCAL REGISTRY MODE"
222-
puts "=" * 80
223-
puts "\nBefore proceeding, ensure:"
224-
puts " 1. Verdaccio server is running on http://localhost:4873/"
225-
puts " 2. You are authenticated with Verdaccio:"
226-
puts " npm adduser --registry http://localhost:4873/"
227-
puts "\nPress ENTER to continue or Ctrl+C to cancel..."
228-
$stdin.gets unless is_dry_run
229-
end
233+
# Prepare NPM publish args
234+
npm_publish_args = ""
230235

231236
unless is_dry_run
232237
# Commit all version changes (skip git hooks to save time)
@@ -250,20 +255,18 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
250255
end
251256

252257
# Push commits and tags (skip git hooks)
253-
unless skip_push
254-
sh_in_dir(monorepo_root, "LEFTHOOK=0 git push")
255-
sh_in_dir(monorepo_root, "LEFTHOOK=0 git push --tags")
256-
end
258+
sh_in_dir(monorepo_root, "LEFTHOOK=0 git push")
259+
sh_in_dir(monorepo_root, "LEFTHOOK=0 git push --tags")
257260

258261
puts "\n#{'=' * 80}"
259-
puts "Publishing PUBLIC packages to #{use_verdaccio ? 'Verdaccio (local)' : 'npmjs.org'}..."
262+
puts "Publishing PUBLIC packages to npmjs.org..."
260263
puts "=" * 80
261264

262265
# Configure NPM OTP
263-
if npm_otp && !use_verdaccio
266+
if npm_otp
264267
npm_publish_args += " --otp #{npm_otp}"
265268
puts "Using provided NPM OTP for all NPM package publications..."
266-
elsif !use_verdaccio
269+
else
267270
puts "\nNOTE: You will be prompted for NPM OTP code for each of the 3 NPM packages."
268271
puts "TIP: Set NPM_OTP environment variable to avoid repeated prompts."
269272
end
@@ -284,7 +287,7 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
284287

285288
# Publish node-renderer NPM package (PUBLIC on npmjs.org)
286289
puts "\n#{'=' * 80}"
287-
puts "Publishing PUBLIC node-renderer to #{use_verdaccio ? 'Verdaccio (local)' : 'npmjs.org'}..."
290+
puts "Publishing PUBLIC node-renderer to npmjs.org..."
288291
puts "=" * 80
289292

290293
# Publish react-on-rails-pro-node-renderer NPM package
@@ -293,44 +296,33 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
293296
puts "\nPublishing #{node_renderer_name}@#{actual_npm_version}..."
294297
sh_in_dir(node_renderer_dir, "pnpm publish #{npm_publish_args}")
295298

296-
if use_verdaccio
297-
puts "\nSkipping Ruby gem publication (Verdaccio is NPM-only)"
298-
else
299-
puts "\n#{'=' * 80}"
300-
puts "Publishing PUBLIC Ruby gems..."
301-
puts "=" * 80
299+
puts "\n#{'=' * 80}"
300+
puts "Publishing PUBLIC Ruby gems..."
301+
puts "=" * 80
302302

303-
if rubygems_otp
304-
puts "Using provided RubyGems OTP for both gem publications..."
305-
else
306-
puts "\nNOTE: You will be prompted for RubyGems OTP code for each of the 2 gems."
307-
puts "TIP: Set RUBYGEMS_OTP environment variable to avoid repeated prompts."
308-
end
303+
if rubygems_otp
304+
puts "Using provided RubyGems OTP for both gem publications..."
305+
else
306+
puts "\nNOTE: You will be prompted for RubyGems OTP code for each of the 2 gems."
307+
puts "TIP: Set RUBYGEMS_OTP environment variable to avoid repeated prompts."
308+
end
309309

310-
# Publish react_on_rails Ruby gem with retry logic
311-
publish_gem_with_retry(gem_root, "react_on_rails", otp: rubygems_otp)
310+
# Publish react_on_rails Ruby gem with retry logic
311+
publish_gem_with_retry(gem_root, "react_on_rails", otp: rubygems_otp)
312312

313-
# Add delay before next OTP operation to ensure clean separation
314-
puts "\n⏳ Waiting 5 seconds before next publication to ensure OTP separation..."
315-
sleep 5
313+
# Add delay before next OTP operation to ensure clean separation
314+
puts "\n⏳ Waiting 5 seconds before next publication to ensure OTP separation..."
315+
sleep 5
316316

317-
# Publish react_on_rails_pro Ruby gem to RubyGems.org with retry logic
318-
publish_gem_with_retry(pro_gem_root, "react_on_rails_pro", otp: rubygems_otp)
319-
end
317+
# Publish react_on_rails_pro Ruby gem to RubyGems.org with retry logic
318+
publish_gem_with_retry(pro_gem_root, "react_on_rails_pro", otp: rubygems_otp)
320319
end
321320

322-
npm_registry_note = if use_verdaccio
323-
"Verdaccio (http://localhost:4873/)"
324-
else
325-
"npmjs.org"
326-
end
327-
328321
if is_dry_run
329322
puts "\n#{'=' * 80}"
330323
puts "DRY RUN COMPLETE"
331324
puts "=" * 80
332325
puts "Version would be bumped to: #{actual_gem_version} (gem) / #{actual_npm_version} (npm)"
333-
puts "NPM Registry: #{npm_registry_note}"
334326
puts "\nFiles that would be updated:"
335327
puts " - react_on_rails/lib/react_on_rails/version.rb"
336328
puts " - react_on_rails_pro/lib/react_on_rails_pro/version.rb"
@@ -341,61 +333,30 @@ task :release, %i[version dry_run registry skip_push] do |_t, args|
341333
puts " - Gemfile.lock files (root, dummy apps, pro)"
342334
puts "\nAuto-synced (no write needed):"
343335
puts " - react_on_rails_pro/react_on_rails_pro.gemspec (uses ReactOnRails::VERSION)"
344-
registry_arg = use_verdaccio ? ",false,verdaccio" : ""
345-
puts "\nTo actually release, run: rake release[#{actual_gem_version}#{registry_arg}]"
336+
puts "\nTo actually release, run: rake release[#{actual_gem_version}]"
346337
else
347338
msg = <<~MSG
348339
349340
#{'=' * 80}
350-
RELEASE COMPLETE! 🎉
341+
RELEASE COMPLETE!
351342
#{'=' * 80}
352343
353-
Published to #{npm_registry_note}:
344+
Published to npmjs.org:
354345
- react-on-rails@#{actual_npm_version}
355346
- react-on-rails-pro@#{actual_npm_version}
356347
- react-on-rails-pro-node-renderer@#{actual_npm_version}
357-
MSG
358-
359-
unless use_verdaccio
360-
msg += "\n Ruby Gems (RubyGems.org):\n"
361-
msg += " - react_on_rails #{actual_gem_version}\n"
362-
msg += " - react_on_rails_pro #{actual_gem_version}\n"
363-
end
364-
365-
if skip_push
366-
msg += <<~SKIP_PUSH
367-
368-
⚠️ Git push was skipped. Don't forget to push manually:
369-
git push
370-
git push --tags
371-
372-
SKIP_PUSH
373-
end
374-
375-
msg += if use_verdaccio
376-
<<~VERDACCIO
377-
378-
Verdaccio test packages published successfully!
379348
380-
To test installation:
381-
npm install --registry http://localhost:4873/ react-on-rails@#{actual_npm_version}
382-
npm install --registry http://localhost:4873/ react-on-rails-pro@#{actual_npm_version}
383-
npm install --registry http://localhost:4873/ react-on-rails-pro-node-renderer@#{actual_npm_version}
349+
Ruby Gems (RubyGems.org):
350+
- react_on_rails #{actual_gem_version}
351+
- react_on_rails_pro #{actual_gem_version}
384352
385-
Note: Ruby gems were not published (Verdaccio is NPM-only)
353+
Next steps:
354+
1. Update CHANGELOG.md: bundle exec rake update_changelog
355+
2. Update pro CHANGELOG.md: cd react_on_rails_pro && bundle exec rake update_changelog
356+
3. Commit CHANGELOGs: git commit -a -m 'Update CHANGELOG.md files'
357+
4. Push changes: git push
386358
387-
VERDACCIO
388-
else
389-
<<~PRODUCTION
390-
391-
Next steps:
392-
1. Update CHANGELOG.md: bundle exec rake update_changelog
393-
2. Update pro CHANGELOG.md: cd react_on_rails_pro && bundle exec rake update_changelog
394-
3. Commit CHANGELOGs: git commit -a -m 'Update CHANGELOG.md files'
395-
4. Push changes: git push
396-
397-
PRODUCTION
398-
end
359+
MSG
399360

400361
puts msg
401362
end

0 commit comments

Comments
 (0)