diff --git a/docs/contributor-info/releasing.md b/docs/contributor-info/releasing.md index 3887d6cb9d..c10a847f54 100644 --- a/docs/contributor-info/releasing.md +++ b/docs/contributor-info/releasing.md @@ -1,77 +1,185 @@ # Install and Release -We're now releasing this as a combined ruby gem plus npm package. We will keep the version numbers in sync. +We're releasing this as a combined Ruby gem plus two NPM packages. We keep the version numbers in sync across all packages. ## Testing the Gem before Release from a Rails App See [Contributing](https://github.com/shakacode/react_on_rails/tree/master/CONTRIBUTING.md) -## Releasing a new gem version +## Releasing a New Version Run `rake -D release` to see instructions on how to release via the rake task. -As of 01-26-2016, this would give you an output like this: +### Release Command +```bash +rake release[gem_version,dry_run] ``` -rake release[gem_version,dry_run,tools_install] - Releases both the gem and node package using the given version. - IMPORTANT: the gem version must be in valid rubygem format (no dashes). - It will be automatically converted to a valid npm semver by the rake task - for the node package version. This only makes a difference for pre-release - versions such as `3.0.0.beta.1` (npm version would be `3.0.0-beta.1`). +**Arguments:** - This task will also globally install gem-release (ruby gem) and - release-it (node package) unless you specify skip installing tools. +- `gem_version`: The new version in rubygem format (no dashes). Pass no argument to automatically perform a patch version bump. +- `dry_run`: Optional. Pass `true` to see what would happen without actually releasing. - 2nd argument: Perform a dry run by passing 'true' as a second argument. - 3rd argument: Skip installing tools by passing 'false' as a third argument (default is true). +**Example:** - Example: `rake release[2.1.0,false,false]` +```bash +rake release[16.2.0] # Release version 16.2.0 +rake release[16.2.0,true] # Dry run to preview changes +rake release # Auto-bump patch version ``` -Running `rake release[2.1.0]` will create a commit that looks like this: +### What Gets Released +The release task publishes three packages with the same version number: + +1. **react-on-rails** NPM package +2. **react-on-rails-pro** NPM package +3. **react_on_rails** Ruby gem + +### Version Synchronization + +The task updates versions in all the following files: + +- `lib/react_on_rails/version.rb` (source of truth) +- `package.json` (root workspace) +- `packages/react-on-rails/package.json` +- `packages/react-on-rails-pro/package.json` (both version field and react-on-rails dependency) +- `spec/dummy/Gemfile.lock` + +**Note:** The `react-on-rails-pro` package declares an exact version dependency on `react-on-rails` (e.g., `"react-on-rails": "16.2.0"`). This ensures users install compatible versions of both packages. + +### Pre-release Versions + +For pre-release versions, the gem version format is automatically converted to NPM semver format: + +- Gem: `3.0.0.beta.1` +- NPM: `3.0.0-beta.1` + +### Release Process + +When you run `rake release[X.Y.Z]`, the task will: + +1. Check for uncommitted changes (will abort if found) +2. Pull latest changes from the remote repository +3. Clean up example directories +4. Bump the gem version in `lib/react_on_rails/version.rb` +5. Update all package.json files with the new version +6. Update the Pro package's dependency on react-on-rails +7. Update the dummy app's Gemfile.lock +8. Commit all version changes with message "Bump version to X.Y.Z" +9. Create a git tag `vX.Y.Z` +10. Push commits and tags to the remote repository +11. Publish `react-on-rails` to NPM (requires 2FA token) +12. Publish `react-on-rails-pro` to NPM (requires 2FA token) +13. Publish `react_on_rails` to RubyGems (requires 2FA token) + +### Two-Factor Authentication + +You'll need to enter OTP tokens when prompted: + +- Once for publishing `react-on-rails` to NPM +- Once for publishing `react-on-rails-pro` to NPM +- Once for publishing `react_on_rails` to RubyGems + +### Post-Release Steps + +After a successful release, you'll see instructions to: + +1. Update the CHANGELOG.md: + + ```bash + bundle exec rake update_changelog + ``` + +2. Update the dummy app's Gemfile.lock: + + ```bash + cd spec/dummy && bundle update react_on_rails + ``` + +3. Commit the CHANGELOG and Gemfile.lock: + ```bash + cd /path/to/react_on_rails + git commit -a -m 'Update CHANGELOG.md and spec/dummy Gemfile.lock' + git push + ``` + +## Requirements + +This task depends on the `gem-release` Ruby gem, which is installed via `bundle install`. + +For NPM publishing, you must be logged in to npm and have publish permissions for both packages: + +```bash +npm login ``` -commit d07005cde9784c69e41d73fb9a0ebe8922e556b3 -Author: Rob Wise -Date: Tue Jan 26 19:49:14 2016 -0500 - Release 2.1.0 +## Troubleshooting + +### Dry Run First + +Always test with a dry run before actually releasing: + +```bash +rake release[16.2.0,true] +``` + +This shows you exactly what would be updated without making any changes. + +### If Release Fails + +If the release fails partway through (e.g., during NPM publish): + +1. Check what was published: + + - NPM: `npm view react-on-rails@X.Y.Z` + - RubyGems: `gem list react_on_rails -r -a` + +2. If the git tag was created but packages weren't published: + + - Delete the tag: `git tag -d vX.Y.Z && git push origin :vX.Y.Z` + - Revert the version commit: `git reset --hard HEAD~1 && git push -f` + - Start over with `rake release[X.Y.Z]` + +3. If some packages were published but not others: + - You can manually publish the missing packages: + ```bash + cd packages/react-on-rails && yarn publish --new-version X.Y.Z + cd ../react-on-rails-pro && yarn publish --new-version X.Y.Z + gem release + ``` + +## Version History + +Running `rake release[X.Y.Z]` will create a commit that looks like this: + +``` +commit abc123... +Author: Your Name +Date: Mon Jan 1 12:00:00 2024 -0500 + + Bump version to 16.2.0 diff --git a/lib/react_on_rails/version.rb b/lib/react_on_rails/version.rb -index 3de9606..b71aa7a 100644 +index 1234567..abcdefg 100644 --- a/lib/react_on_rails/version.rb +++ b/lib/react_on_rails/version.rb @@ -1,3 +1,3 @@ module ReactOnRails -- VERSION = "2.0.2".freeze -+ VERSION = "2.1.0".freeze +- VERSION = "16.1.1" ++ VERSION = "16.2.0" end + diff --git a/package.json b/package.json -index aa7b000..af8761e 100644 +index 2345678..bcdefgh 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "react-on-rails", -- "version": "2.0.2", -+ "version": "2.1.0", - "description": "react-on-rails JavaScript for react_on_rails Ruby gem", - "main": "packages/react-on-rails/lib/ReactOnRails.js", - "directories": { -diff --git a/spec/dummy/Gemfile.lock b/spec/dummy/Gemfile.lock -index 8ef51df..4489bfe 100644 ---- a/spec/dummy/Gemfile.lock -+++ b/spec/dummy/Gemfile.lock -@@ -1,7 +1,7 @@ - PATH - remote: ../.. - specs: -- react_on_rails (2.0.2) -+ react_on_rails (2.1.0) - connection_pool - execjs (~> 2.5) - rails (>= 3.2) -(END) + "name": "react-on-rails-workspace", +- "version": "16.1.1", ++ "version": "16.2.0", + ... +} ``` diff --git a/package.json b/package.json index bab36c5cd3..f9392fbf50 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,6 @@ "yalc:publish": "yarn workspaces run yalc:publish", "yalc": "yarn workspaces run yalc", "publish": "yarn workspaces run publish", - "release:patch": "yarn workspaces run release:patch", - "release:minor": "yarn workspaces run release:minor", - "release:major": "yarn workspaces run release:major", "postinstall": "test -f .lefthook.yml && test -d .git && command -v bundle >/dev/null 2>&1 && bundle exec lefthook install || true" }, "repository": { diff --git a/packages/react-on-rails-pro/package.json b/packages/react-on-rails-pro/package.json index cbdafc24f2..4378d0c103 100644 --- a/packages/react-on-rails-pro/package.json +++ b/packages/react-on-rails-pro/package.json @@ -54,7 +54,7 @@ "./ServerComponentFetchError": "./lib/ServerComponentFetchError.js" }, "dependencies": { - "react-on-rails": "*" + "react-on-rails": "16.1.1" }, "peerDependencies": { "react": ">= 16", diff --git a/packages/react-on-rails/package.json b/packages/react-on-rails/package.json index a5d31d3543..3f2e2d4824 100644 --- a/packages/react-on-rails/package.json +++ b/packages/react-on-rails/package.json @@ -14,10 +14,7 @@ "prepare": "nps build.prepack", "prepublishOnly": "yarn run build", "yalc:publish": "yalc publish", - "yalc": "yalc", - "release:patch": "./scripts/release patch", - "release:minor": "./scripts/release minor", - "release:major": "./scripts/release major" + "yalc": "yalc" }, "repository": { "type": "git", diff --git a/packages/react-on-rails/scripts/release b/packages/react-on-rails/scripts/release deleted file mode 100644 index 5cf37bc1f0..0000000000 --- a/packages/react-on-rails/scripts/release +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -yarn version $1 -m "Bump version to %s" -git push -u origin master -git push --tags -yarn publish diff --git a/rakelib/release.rake b/rakelib/release.rake index f5c03aeebb..85c7e63a88 100644 --- a/rakelib/release.rake +++ b/rakelib/release.rake @@ -1,6 +1,7 @@ # frozen_string_literal: true require "bundler" +require "json" require_relative "task_helpers" require_relative File.join(gem_root, "lib", "react_on_rails", "version_syntax_converter") require_relative File.join(gem_root, "lib", "react_on_rails", "git_utils") @@ -14,24 +15,29 @@ end # rubocop:disable Metrics/BlockLength -desc("Releases both the gem and node package using the given version. +desc("Releases the gem and both NPM packages (react-on-rails and react-on-rails-pro). IMPORTANT: the gem version must be in valid rubygem format (no dashes). -It will be automatically converted to a valid yarn semver by the rake task -for the node package version. This only makes a difference for pre-release -versions such as `3.0.0.beta.1` (yarn version would be `3.0.0-beta.1`). +It will be automatically converted to valid npm semver by the rake task +for the node package versions. This only makes a difference for pre-release +versions such as `3.0.0.beta.1` (npm version would be `3.0.0-beta.1`). -This task depends on the gem-release (ruby gem) and release-it (node package) -which are installed via `bundle install` and `yarn global add release-it` +This task depends on the gem-release ruby gem which is installed via `bundle install`. 1st argument: The new version in rubygem format (no dashes). Pass no argument to automatically perform a patch version bump. 2nd argument: Perform a dry run by passing 'true' as a second argument. - -Note, accept defaults for npmjs options. Script will pause to get 2FA tokens. - -Example: `rake release[2.1.0,false]`") -task :release, %i[gem_version dry_run tools_install] do |_t, args| +3rd argument: Use Verdaccio local registry by passing 'verdaccio' as a third argument. + Requires Verdaccio server running on http://localhost:4873/ +4th argument: Skip pushing to remote by passing 'skip_push' as a fourth argument. + Useful for testing the release process locally. + +Examples: + rake release[16.2.0] # Release to production + rake release[16.2.0,true] # Dry run + rake release[16.2.0,false,verdaccio] # Test release to Verdaccio + rake release[16.2.0,false,npm,skip_push] # Release without pushing to remote") +task :release, %i[gem_version dry_run registry skip_push] do |_t, args| include ReactOnRails::TaskHelpers # Check if there are uncommitted changes @@ -40,66 +46,197 @@ task :release, %i[gem_version dry_run tools_install] do |_t, args| is_dry_run = ReactOnRails::Utils.object_to_boolean(args_hash[:dry_run]) - gem_version = args_hash.fetch(:gem_version, "") + # Validate registry parameter + registry_value = args_hash.fetch(:registry, "") + unless registry_value.empty? || registry_value == "verdaccio" || registry_value == "npm" + raise ArgumentError, + "Invalid registry value '#{registry_value}'. Valid values are: 'verdaccio', 'npm', or empty string" + end + + use_verdaccio = registry_value == "verdaccio" - npm_version = if gem_version.strip.empty? - "" - else - ReactOnRails::VersionSyntaxConverter.new.rubygem_to_npm(gem_version) - end + # Validate skip_push parameter + skip_push_value = args_hash.fetch(:skip_push, "") + unless skip_push_value.empty? || skip_push_value == "skip_push" + raise ArgumentError, "Invalid skip_push value '#{skip_push_value}'. Valid values are: 'skip_push' or empty string" + end + + skip_push = skip_push_value == "skip_push" + + gem_version = args_hash.fetch(:gem_version, "") # Having the examples prevents publishing Rake::Task["shakapacker_examples:clobber"].invoke # Delete any react_on_rails.gemspec except the root one sh_in_dir(gem_root, "find . -mindepth 2 -name 'react_on_rails.gemspec' -delete") - # See https://github.com/svenfuchs/gem-release + # Pull latest changes sh_in_dir(gem_root, "git pull --rebase") + + # Bump gem version using gem-release sh_in_dir(gem_root, "gem bump --no-commit #{%(--version #{gem_version}) unless gem_version.strip.empty?}") + # Read the actual version that was set + actual_gem_version = begin + version_file = File.join(gem_root, "lib", "react_on_rails", "version.rb") + version_content = File.read(version_file) + version_content.match(/VERSION = "(.+)"/)[1] + end + + actual_npm_version = ReactOnRails::VersionSyntaxConverter.new.rubygem_to_npm(actual_gem_version) + + puts "Updating package.json files to version #{actual_npm_version}..." + + # Update all package.json files + package_json_files = [ + File.join(gem_root, "package.json"), + File.join(gem_root, "packages", "react-on-rails", "package.json"), + File.join(gem_root, "packages", "react-on-rails-pro", "package.json") + ] + + package_json_files.each do |file| + content = JSON.parse(File.read(file)) + content["version"] = actual_npm_version + + # For react-on-rails-pro, also update the react-on-rails dependency + if file.include?("react-on-rails-pro") + content["dependencies"] ||= {} + content["dependencies"]["react-on-rails"] = actual_npm_version + end + + File.write(file, "#{JSON.pretty_generate(content)}\n") + puts " Updated #{file}" + end + + bundle_install_in(gem_root) # Update dummy app's Gemfile.lock bundle_install_in(dummy_app_dir) - puts "Carefully add your OTP for NPM. If you get an error, 'git reset --hard' and start over." - # Will bump the yarn version, commit, tag the commit, push to repo, and release on yarn - release_it_command = +"release-it" - release_it_command << " #{npm_version}" unless npm_version.strip.empty? - release_it_command << " --npm.publish --no-git.requireCleanWorkingDir" - release_it_command << " --dry-run --verbose" if is_dry_run - # Disable lefthook pre-commit hooks during release to prevent file modifications - sh_in_dir(gem_root, "LEFTHOOK=0 #{release_it_command}") + # Prepare NPM registry configuration + npm_registry_url = use_verdaccio ? "http://localhost:4873/" : "https://registry.npmjs.org/" + npm_publish_args = use_verdaccio ? "--registry #{npm_registry_url}" : "" + + if use_verdaccio + puts "\n#{'=' * 80}" + puts "VERDACCIO LOCAL REGISTRY MODE" + puts "=" * 80 + puts "\nBefore proceeding, ensure:" + puts " 1. Verdaccio server is running on http://localhost:4873/" + puts " 2. You are authenticated with Verdaccio:" + puts " npm adduser --registry http://localhost:4873/" + puts "\nPress ENTER to continue or Ctrl+C to cancel..." + $stdin.gets unless is_dry_run + end - # Commit the Gemfile.lock changes made by release-it before gem release unless is_dry_run - sh_in_dir(gem_root, "git add Gemfile.lock") - # Only commit if there are staged changes - if `cd #{gem_root} && git diff --cached --quiet; echo $?`.strip == "0" - puts "No Gemfile.lock changes to commit" + # Commit all version changes + sh_in_dir(gem_root, "git add -A") + sh_in_dir(gem_root, "git commit -m 'Bump version to #{actual_gem_version}'") + + # Create git tag + sh_in_dir(gem_root, "git tag v#{actual_gem_version}") + + # Push commits and tags + unless skip_push + sh_in_dir(gem_root, "git push") + sh_in_dir(gem_root, "git push --tags") + end + + puts "\n#{'=' * 80}" + puts "Publishing NPM packages to #{use_verdaccio ? 'Verdaccio (local)' : 'npmjs.org'}..." + puts "=" * 80 + + # Publish react-on-rails NPM package + puts "\nPublishing react-on-rails@#{actual_npm_version}..." + puts "Carefully add your OTP for NPM when prompted." unless use_verdaccio + sh_in_dir(gem_root, "yarn workspace react-on-rails publish --new-version #{actual_npm_version} #{npm_publish_args}") + + # Publish react-on-rails-pro NPM package + puts "\nPublishing react-on-rails-pro@#{actual_npm_version}..." + puts "Carefully add your OTP for NPM when prompted." unless use_verdaccio + sh_in_dir(gem_root, + "yarn workspace react-on-rails-pro publish --new-version #{actual_npm_version} #{npm_publish_args}") + + if use_verdaccio + puts "Skipping Ruby gem publication (Verdaccio mode)" + puts "=" * 80 else - sh_in_dir(gem_root, "git commit -m 'Update Gemfile.lock for version #{gem_version}'") + puts "\n#{'=' * 80}" + puts "Publishing Ruby gem..." + puts "=" * 80 + + # Publish Ruby gem + puts "\nCarefully add your OTP for Rubygems when prompted." + sh_in_dir(gem_root, "gem release") end end - # Release the new gem version + npm_registry_note = if use_verdaccio + "Verdaccio (http://localhost:4873/)" + else + "npmjs.org" + end + + if is_dry_run + puts "\n#{'=' * 80}" + puts "DRY RUN COMPLETE" + puts "=" * 80 + puts "Version would be bumped to: #{actual_gem_version} (gem) / #{actual_npm_version} (npm)" + puts "NPM Registry: #{npm_registry_note}" + puts "Files that would be updated:" + puts " - lib/react_on_rails/version.rb" + puts " - package.json (root)" + puts " - packages/react-on-rails/package.json" + puts " - packages/react-on-rails-pro/package.json (version + dependency)" + puts " - spec/dummy/Gemfile.lock" + registry_arg = use_verdaccio ? ",false,verdaccio" : "" + puts "\nTo actually release, run: rake release[#{actual_gem_version}#{registry_arg}]" + else + msg = <<~MSG + + #{'=' * 80} + RELEASE COMPLETE! 🎉 + #{'=' * 80} + + Published to #{npm_registry_note}: + - react-on-rails@#{actual_npm_version} + - react-on-rails-pro@#{actual_npm_version} + MSG + + msg += " - react_on_rails #{actual_gem_version} (RubyGems)\n" unless use_verdaccio + + if skip_push + msg += <<~SKIP_PUSH + + ⚠️ Git push was skipped. Don't forget to push manually: + git push + git push --tags + + SKIP_PUSH + end - puts "Carefully add your OTP for Rubygems. If you get an error, run 'gem release' again." - sh_in_dir(gem_root, "gem release") unless is_dry_run + if use_verdaccio + msg += <<~VERDACCIO - msg = <<~MSG - Once you have successfully published, run these commands to update CHANGELOG.md: + Verdaccio test packages published successfully! - bundle exec rake update_changelog - cd #{dummy_app_dir}; bundle update react_on_rails - cd #{gem_root} - git commit -a -m 'Update CHANGELOG.md and spec/dummy Gemfile.lock' - git push - MSG - puts msg -end -# rubocop:enable Metrics/BlockLength + To test installation: + npm install --registry http://localhost:4873/ react-on-rails@#{actual_npm_version} + npm install --registry http://localhost:4873/ react-on-rails-pro@#{actual_npm_version} + + VERDACCIO + else + msg += <<~PRODUCTION + + Next steps: + 1. Update CHANGELOG.md: bundle exec rake update_changelog + 3. Commit CHANGELOG: cd #{gem_root} && git commit -a -m 'Update CHANGELOG.md and spec/dummy Gemfile.lock' + 4. Push changes: git push -task :test do - unbundled_sh_in_dir(gem_root, "cd #{dummy_app_dir}; bundle update react_on_rails") - sh_in_dir(gem_root, "git commit -a -m 'Update Gemfile.lock for spec app'") - sh_in_dir(gem_root, "git push") + PRODUCTION + end + + puts msg + end end +# rubocop:enable Metrics/BlockLength