Skip to content

Instantly share code, notes, and snippets.

@grosser
Last active August 29, 2015 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grosser/69c137b8d0e21e58cae8 to your computer and use it in GitHub Desktop.
Save grosser/69c137b8d0e21e58cae8 to your computer and use it in GitHub Desktop.
Updating rails versions while keeping the business running

Updating rails versions on big apps can be dangerous and time consuming. This is the workflow we use to deploy and test our Rails upgrades in isolation, before releasing them to everybody and without blocking other changes.

Gemfile

ln -s Gemfile Gemfile.rails4 and use BUNDLE_GEMFILE=Gemfile.rails4 bundle exec rails c to run rails 4.

if ENV['BUNDLE_GEMFILE'].to_s.include?('rails4')
  gem "rails", "4.0.13"
  ... other gems that are different ...

  # make bundler write to a different cache directory
  class << Bundler
    def app_cache(custom_path = nil)
      path = custom_path || root
      path.join("vendor/cache.rails4")
    end
  end

  Bundler::Runtime.class_eval do
    def cache_path
      Bundler.app_cache
    end
  end
else
  gem "rails", "3.2.21"
  ... other gems that are different ...
end

... unaffected gems ...

Ensuring locked gem versions are the same on both versions

Keeping 2 gemfiles means other developers might forget to add a new gem or lock an unintendedly different version.

We use this code for our pre-commit hook in config/pre-commit.rb and a "cleanliness" test to make sure only versions we allow are different.

# config/pre-commit.rb
module PreCommitChecks
  # compare versions and git refs between both lockfiles
  def self.check_lockfile_diff(files)
    return [] if !files.include?("Gemfile.lock") && !files.include?("Gemfile.rails4.lock")
    a, b = ["Gemfile.lock", "Gemfile.rails4.lock"].map do |file|
      file = File.read(file)
      file.scan(/^  remote: \S+\n  revision: \S+$/)+ file.scan(/^    \S+ \(\S+\)/)
    end

    diff = ((a | b) - (a & b))
    diff.map! { |match| match[/\S+github\.com\S+\/(\S+?)(\.git)?\s/, 1] || match[/\S+/] }.uniq

    allowed = [
      "actionmailer",
      "actionpack",
      "activemodel",
      "activerecord",
      "activesupport",
      "arel",
      "builder",
      "coffee-rails",
      "journey",
      "rack",
      "rack-cache",
      "rack-ssl",
      "rails",
      "railties",
      "rdoc",
      "sass",
      "sass-rails",
      "sprockets",
      "sprockets-rails",
      "strong_parameters",
    ]

    (diff - allowed).sort.uniq
  end
end

if $0 == "config/pre-commit.rb" # executed via pre-commit hook
  errors = []
  errors += PreCommitChecks.check_lockfile_diff(changed)
  ...
  
  if errors.any?
    puts errors
    exit 1
  end
end

# test/unit/cleanliness_test.rb
require './config/pre-commit'

it 'does not have accidental diff to rails 4' do
  PreCommitChecks.check_lockfile_diff(["Gemfile.lock", "Gemfile.rails4.lock"]).must_equal []
end

CI

If you have enough worker capacity this is good enough:

gemfile:
 - Gemfile
 - Gemfile.rails4

If worker capacity is an issue, then make a branch with the changed gemfile and rebase that once in a while until everything is green.

Deployment

We run the new Gemfile on staging and once that works, deploy 1 production server with BUNDLE_GEMFILE=Gemfile.rails4 to fix issues until we are confident to switch everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment