# A rake task to update the bundled version of a gem # Assumes constant SAFETY_FILE and function repository_type are defined in another rake task. namespace :bundle do desc <<~USAGE Update to a later version of a gem interactively. Usage: bundle:update gem=rails [version=6.0.4.7] Updates the bundled gem (e.g. rails) version to e.g. 6.0.4.7 and provides instructions for committing changes. It will attempt to modify a hardcoded version in the Gemfile if necessary. USAGE task(:update) do unless %w[git git-svn].include?(repository_type) warn 'Error: Requires a git working copy. Aborting.' exit 1 end gem = ENV['gem'] if gem.blank? || gem !~ /\A[a-zA-Z0-9_.-]+\z/ warn "Error: missing or invalid required 'gem' parameter. Aborting.\n\n" system('rake -D bundle:update') exit 1 end gem_list = Bundler.with_unbundled_env { `bundle exec gem list ^#{gem}$` } # Needs to match e.g. "nokogiri (1.12.5 x86_64-darwin)" old_gem_version = gem_list.match(/ \(([0-9.]+)( [a-z0-9_-]*)?\)$/).to_a&.second unless old_gem_version warn <<~MSG.chomp Cannot determine gem version for gem=#{gem}. Aborting. Output from bundle exec gem list: #{gem_list} MSG exit 1 end puts "Old #{gem} version from bundle: #{old_gem_version}" new_gem_version = ENV['version'].presence if new_gem_version && new_gem_version !~ /\A[0-9.a-zA-Z-]+\z/ warn "Error: invalid 'version' parameter. Aborting.\n\n" system('rake -D bundle:update') exit 1 end unless Bundler.with_unbundled_env { system('bundle check 2> /dev/null') } warn('Error: bundle check fails before doing anything.') warn('Please clean up the Gemfile before running this. Aborting.') exit 1 end if gem == 'rails' # If updating Rails and using activemodel-caution, prompt to put # activemodel-caution gem in place, unless it's already installed for this rails version. activemodel_caution = Bundler. with_unbundled_env { `bundle exec gem list activemodel-caution` }. match?(/^activemodel-caution \([0-9.]+\)$/) if activemodel_caution && new_gem_version file_pattern = "activemodel-caution-#{new_gem_version}*.gem" unless Dir.glob("vendor/cache/#{file_pattern}").any? || Bundler.with_unbundled_env do `gem list ^activemodel-caution$ -i -v #{new_gem_version}` end.match?(/^true$/) warn("Error: missing #{file_pattern} file in vendor/cache") warn('Copy this file to vendor/cache, then run this command again.') exit 1 end end end related_gems = if gem == 'rails' gem_list2 = Bundler.with_unbundled_env do `bundle exec gem list` end gem_list2.split("\n"). grep(/[ (]#{old_gem_version}(.0)*[,)]/). collect { |row| row.split.first } else [gem] end puts "Gems to update: #{related_gems.join(' ')}" if new_gem_version puts 'Tweaking Gemfile for new gem version' cmd = ['sed', '-i', '.bak', '-E'] related_gems.each do |rgem| cmd += ['-e', "s/(gem '(#{rgem})', '(~> )?)#{old_gem_version}(')/\\1#{new_gem_version}\\4/"] end cmd += %w[Gemfile] system(*cmd) File.delete('Gemfile.bak') system('git diff Gemfile') end cmd = "bundle update --conservative --minor #{related_gems.join(' ')}" puts "Running: #{cmd}" Bundler.with_unbundled_env do system(cmd) end unless Bundler.with_unbundled_env { system('bundle check 2> /dev/null') } warn <<~MSG Error: bundle check fails after trying to update Rails version. Aborting. You will need to check your working copy, especially Gemfile, Gemfile.lock, vendor/cache MSG exit 1 end if File.exist?(SAFETY_FILE) # Remove references to unused files in code_safety.yml system('rake audit:tidy_code_safety_file') end gem_list = Bundler.with_unbundled_env { `bundle exec gem list ^#{gem}$` } new_gem_version2 = gem_list.match(/ \(([0-9.]+)( [a-z0-9_-]*)?\)$/).to_a&.second if new_gem_version && new_gem_version != new_gem_version2 puts <<~MSG Error: Tried to update gem #{gem} to version #{new_gem_version} but ended up at version #{new_gem_version2}. Aborting. You will need to check your working copy, especially Gemfile, Gemfile.lock, vendor/cache Try running: bundle exec rake bundle:update gem=#{gem} version=#{new_gem_version2} MSG exit 1 end # At this point, we have successfully updated all the local files. # All that remains is to set up a branch, if necessary, and inform the user what to commit. puts "Looking for changed files using git status\n\n" files_to_git_rm = `git status vendor/cache/|grep 'deleted: ' | \ grep -o ': .*' | sed -e 's/^: *//'`.split("\n") files_to_git_add = `git status Gemfile Gemfile.lock code_safety.yml|grep 'modified: ' | \ grep -o ': .*' | sed -e 's/^: *//'`.split("\n") files_to_git_add += `git status vendor/cache|expand|grep '^\s*vendor/cache' | \ sed -e 's/^ *//'`.split("\n") if files_to_git_rm.empty? && files_to_git_add.empty? puts <<~MSG No changes were made. Please manually update the Gemfile, run bundle update --conservative --minor #{related_gems.join(' ')} MSG puts ' rake audit:tidy_code_safety_file' if File.exist?(SAFETY_FILE) puts <<~MSG then run tests and git rm / git add any changes including vendor/cache Gemfile Gemfile.lock code_safety.yml then git commit MSG exit end if repository_type == 'git' # Check out a fresh branch, if a git working copy (but not git-svn) branch_name = "#{gem}_#{new_gem_version2.gsub('.', '_')}" system('git', 'checkout', '-b', branch_name) # Create a new git branch end puts <<~MSG Gemfile updated. Please use "git status" and "git diff" to check the local changes, manually add any additional platform-specific gems required (e.g. for nokogiri), re-run tests locally, then run the following to commit the changes: $ git rm #{files_to_git_rm.join(' ')} $ git add #{files_to_git_add.join(' ')} $ git commit -m '# Bump #{gem} to #{new_gem_version2}' MSG end end