diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..cec6aed --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,123 @@ +# Release Process + +This document describes how to release a new version of the `cypress-on-rails` gem. + +## Prerequisites + +1. Install the `gem-release` gem globally: + ```bash + gem install gem-release + ``` + +2. Ensure you have write access to the rubygems.org package + +3. Set up two-factor authentication (2FA) for RubyGems and have your OTP generator ready + +## Release Steps + +### 1. Prepare for Release + +Ensure your working directory is clean: +```bash +git status +``` + +If you have uncommitted changes, commit or stash them first. + +### 2. Pull Latest Changes + +```bash +git pull --rebase +``` + +### 3. Run the Release Task + +To release a specific version: +```bash +rake release[1.19.0] +``` + +To automatically bump the patch version: +```bash +rake release +``` + +To perform a dry run (without actually publishing): +```bash +rake release[1.19.0,true] +``` + +### 4. Enter Your OTP + +When prompted, enter your one-time password (OTP) from your authenticator app for RubyGems. + +If you get an error during gem publishing, you can run `gem release` manually to retry. + +### 5. Update the CHANGELOG + +After successfully publishing the gem, update the CHANGELOG: + +```bash +bundle exec rake update_changelog +# This will: +# - Add a new version header with the release date +# - Add version comparison links +# - Prompt you to move content from [Unreleased] to the new version + +# Edit CHANGELOG.md to move unreleased changes to the new version section +git commit -a -m 'Update CHANGELOG.md' +git push +``` + +## Version Numbering + +Follow [Semantic Versioning](https://semver.org/): + +- **Major version** (X.0.0): Breaking changes +- **Minor version** (0.X.0): New features, backwards compatible +- **Patch version** (0.0.X): Bug fixes, backwards compatible +- **Pre-release versions**: Use dot notation, not dashes (e.g., `2.0.0.beta.1`, not `2.0.0-beta.1`) + +## What the Release Task Does + +The release task automates the following steps: + +1. Checks for uncommitted changes (will abort if found) +2. Pulls the latest changes from the repository +3. Bumps the version number in `lib/cypress_on_rails/version.rb` +4. Creates a git commit with the version bump +5. Creates a git tag for the new version +6. Pushes the commit and tag to GitHub +7. Builds the gem +8. Publishes the gem to RubyGems + +## Troubleshooting + +### Authentication Error + +If you get an authentication error with RubyGems: +1. Verify your OTP is correct and current +2. Ensure your RubyGems API key is valid +3. Run `gem release` manually to retry + +### Version Already Exists + +If the version already exists on RubyGems: +1. Bump to a higher version number +2. Or fix the version in `lib/cypress_on_rails/version.rb` and try again + +### Uncommitted Changes Error + +If you have uncommitted changes: +1. Review your changes with `git status` +2. Commit them with `git commit -am "Your message"` +3. Or stash them with `git stash` +4. Then retry the release + +## Post-Release + +After releasing: + +1. Announce the release on relevant channels (Slack, forum, etc.) +2. Update any documentation that references version numbers +3. Consider creating a GitHub release with release notes diff --git a/lib/tasks/release.rake b/lib/tasks/release.rake new file mode 100644 index 0000000..486a615 --- /dev/null +++ b/lib/tasks/release.rake @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +desc("Releases the gem using the given version. + +IMPORTANT: the gem version must be in valid rubygem format (no dashes). + +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. + +Example: `rake release[1.19.0,false]`") +task :release, %i[gem_version dry_run] do |_t, args| + def sh_in_dir(dir, command) + puts "Running in #{dir}: #{command}" + system("cd #{dir} && #{command}") || raise("Command failed: #{command}") + end + + def gem_root + File.expand_path('..', __dir__) + end + + # Check if there are uncommitted changes + unless `git status --porcelain`.strip.empty? + raise "You have uncommitted changes. Please commit or stash them before releasing." + end + + args_hash = args.to_hash + is_dry_run = args_hash[:dry_run] == 'true' + gem_version = args_hash.fetch(:gem_version, "") + + # See https://github.com/svenfuchs/gem-release + sh_in_dir(gem_root, "git pull --rebase") + sh_in_dir(gem_root, "gem bump --no-commit #{%(--version #{gem_version}) unless gem_version.strip.empty?}") + + # Release the new gem version + 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 + + msg = <<~MSG + Once you have successfully published, update CHANGELOG.md: + + bundle exec rake update_changelog + # Edit CHANGELOG.md to move unreleased changes to the new version section + git commit -a -m 'Update CHANGELOG.md' + git push + MSG + puts msg +end diff --git a/lib/tasks/update_changelog.rake b/lib/tasks/update_changelog.rake new file mode 100644 index 0000000..9a44ad0 --- /dev/null +++ b/lib/tasks/update_changelog.rake @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "English" + +desc "Updates CHANGELOG.md inserting headers for the new version. + +Argument: Git tag. Defaults to the latest tag." + +task :update_changelog, %i[tag] do |_, args| + tag = args[:tag] || `git describe --tags --abbrev=0`.strip + + # Remove 'v' prefix if present (e.g., v1.18.0 -> 1.18.0) + version = tag.start_with?('v') ? tag[1..-1] : tag + anchor = "[#{version}]" + + changelog = File.read("CHANGELOG.md") + + if changelog.include?(anchor) + puts "Tag #{version} is already documented in CHANGELOG.md, update manually if needed" + next + end + + tag_date_output = `git show -s --format=%cs #{tag} 2>&1` + if $CHILD_STATUS.success? + tag_date = tag_date_output.split("\n").last.strip + else + abort("Failed to find tag #{tag}") + end + + # After "## [Unreleased]", insert new version header + unreleased_section = "## [Unreleased]" + new_version_header = "\n\n## #{anchor} - #{tag_date}" + + if changelog.include?(unreleased_section) + changelog.sub!(unreleased_section, "#{unreleased_section}#{new_version_header}") + else + abort("Could not find '## [Unreleased]' section in CHANGELOG.md") + end + + # Find and update version comparison links at the bottom + # Pattern: [1.18.0]: https://github.com/shakacode/cypress-playwright-on-rails/compare/v1.17.0...v1.18.0 + compare_link_prefix = "https://github.com/shakacode/cypress-playwright-on-rails/compare" + + # Find the last version link to determine the previous version + last_version_match = changelog.match(/\[(\d+\.\d+\.\d+(?:\.\w+)?)\]:.*?compare\/v(\d+\.\d+\.\d+(?:\.\w+)?)\.\.\.v(\d+\.\d+\.\d+(?:\.\w+)?)/) + + if last_version_match + last_version = last_version_match[1] + # Add new version link at the top of the version list + new_link = "#{anchor}: #{compare_link_prefix}/v#{last_version}...v#{version}" + # Insert after the "" comment + changelog.sub!("", "\n#{new_link}") + else + puts "Warning: Could not find version comparison links. You may need to add the link manually." + end + + File.write("CHANGELOG.md", changelog) + puts "Updated CHANGELOG.md with an entry for #{version}" + puts "\nNext steps:" + puts "1. Edit CHANGELOG.md to add release notes under the [#{version}] section" + puts "2. Move content from [Unreleased] to [#{version}] if applicable" + puts "3. Review and commit the changes" +end