class Releaser

Constants

FILES_TO_IGNORE
FRAMEWORKS

Order dependent. E.g. Action Mailbox depends on Active Record so it should be after.

Attributes

Public Class Methods

# File tools/releaser/lib/releaser.rb, line 26
def initialize(root, version)
  @root = Pathname.new(root)
  @version = version
  @tag = "v#{version}"
  @major, @minor, @tiny, @pre = @version.split(".", 4)
  define
end

Public Instance Methods

# File tools/releaser/lib/releaser.rb, line 34
def define
  directory "#{root}/pkg"

  (FRAMEWORKS + ["rails"]).each do |framework|
    namespace framework do
      task :clean do
        Dir.chdir(root) do
          rm_f gem_path(framework)
        end
      end

      task :update_versions do
        update_versions(framework)
      end

      task gem_path(framework) => [:update_versions, "#{root}/pkg"] do
        dir = if framework == "rails"
          root
        else
          root + framework
        end

        Dir.chdir(dir) do
          sh "gem build #{gemspec(framework)} && mv #{gem_file(framework)} #{root}/pkg/"
        end
      end

      task build: [:clean, gem_path(framework)]
      task install: :build do
        Dir.chdir(root) do
          sh "gem install --pre #{gem_path(framework)}"
        end
      end

      task push: :build do
        Dir.chdir(root) do
          sh "gem push #{gem_path(framework)}#{gem_otp}"

          if File.exist?("#{framework}/package.json")
            Dir.chdir("#{framework}") do
              sh "npm publish --tag #{npm_tag}#{npm_otp}"
            end
          end
        end
      end
    end
  end

  desc "Install gems for all projects."
  task install: FRAMEWORKS.map { |f| "#{f}:install" } + ["rails:install"]

  task :ensure_clean_state do
    if tree_dirty?
      abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed"
    end

    unless ENV["SKIP_TAG"] || inexistent_tag?
      abort "[ABORTING] `git tag` shows that #{tag} already exists. Has this version already\n"\
            "           been released? Git tagging can be skipped by setting SKIP_TAG=1"
    end
  end

  namespace :changelog do
    task :header do
      require "date"

      (FRAMEWORKS + ["guides"]).each do |fw|
        fname = File.join root, fw, "CHANGELOG.md"
        current_contents = File.read(fname)

        header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n"
        header += "*   No changes.\n\n\n" if current_contents.start_with?("##")
        contents = header + current_contents
        File.write(fname, contents)
      end
    end
  end

  desc "Build gem files for all projects"
  task build: FRAMEWORKS.map { |f| "#{f}:build" } + ["rails:build"]

  task checksums: :build do
    Dir.chdir(root) do
      puts
      [*FRAMEWORKS, "rails"].each do |fw|
        path = gem_path(fw)
        sha = ::Digest::SHA256.file(path)
        puts "#{sha}  #{path}"
      end
      puts
    end
  end

  task :bundle do
    sh "bundle check"
  end

  desc "Prepare the release"
  task prep_release: %w(ensure_clean_state changelog:header build bundle)

  task :check_gh_client do
    sh "gh auth status" do |ok, res|
      unless ok
        raise "GitHub CLI is not logged in. Please run `gh auth login` to log in."
      end
    end
    default_repo = `git config --local --get-regexp '\.gh-resolved$'`.strip
    if !$?.success? || default_repo.empty?
      raise "GitHub CLI does not have a default repo configured. Please run `gh repo set-default rails/rails`"
    end
  end

  task :commit do
    Dir.chdir(root) do
      unless `git status -s`.strip.empty?
        File.open("pkg/commit_message.txt", "w") do |f|
          f.puts "# Preparing for #{version} release\n"
          f.puts
          f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT"
        end

        sh "git add . && git commit --verbose --template=pkg/commit_message.txt"
        rm_f "pkg/commit_message.txt"
      end
    end
  end

  task :tag do
    sh "git push"
    sh "git tag -s -m '#{tag} release' #{tag}"
    sh "git push --tags"
  end

  desc "Create GitHub release"
  task create_release: :check_gh_client do
    Dir.chdir(root) do
      File.write("pkg/#{version}.md", release_notes)

      sh "gh release create --verify-tag #{tag} -t #{version} -F pkg/#{version}.md --draft#{pre_release? ? " --prerelease" : ""}"
    end
  end

  desc "Release all gems and create a tag"
  task release: %w(check_gh_client prep_release commit tag create_release)

  task pre_push: [:build, :checksums]

  desc "Push the gem to rubygems.org and the npm package to npmjs.com"
  task push: [:pre_push] + FRAMEWORKS.map { |f| "#{f}:push" } + ["rails:push"]
end
# File tools/releaser/lib/releaser.rb, line 285
def framework_name(framework)
  framework.split(/(?<=active|action)/).map(&:capitalize).join(" ")
end
# File tools/releaser/lib/releaser.rb, line 220
def gem_file(framework)
  "#{framework}-#{version}.gem"
end
# File tools/releaser/lib/releaser.rb, line 216
def gem_path(framework)
  "pkg/#{gem_file(framework)}"
end
# File tools/releaser/lib/releaser.rb, line 224
def gemspec(framework)
  "#{framework}.gemspec"
end
# File tools/releaser/lib/releaser.rb, line 189
def npm_tag
  pre_release? ? "pre" : "latest"
end

This “npm-ifies” the current version number With npm, versions such as “5.0.0.rc1” or “5.0.0.beta1.1” are not compliant with its versioning system, so they must be transformed to “5.0.0-rc1” and “5.0.0-beta1-1” respectively. “5.0.0” –> “5.0.0” “5.0.1” –> “5.0.100” “5.0.0.1” –> “5.0.1” “5.0.1.1” –> “5.0.101” “5.0.0.rc1” –> “5.0.0-rc1” “5.0.0.beta1.1” –> “5.0.0-beta1-1”

# File tools/releaser/lib/releaser.rb, line 202
def npm_version
  @npm_version ||= begin
    if pre_release?
      pre_release = pre.tr(".", "-")
      npm_pre = 0
    else
      npm_pre = pre.to_i
      pre_release = nil
    end

    "#{major}.#{minor}.#{(tiny.to_i * 100) + npm_pre}#{pre_release ? "-#{pre_release}" : ""}"
  end
end
# File tools/releaser/lib/releaser.rb, line 185
def pre_release?
  @pre_release ||= pre && pre.match?(/rc|beta|alpha/)
end
# File tools/releaser/lib/releaser.rb, line 265
def release_notes
  release_notes = +""

  (FRAMEWORKS + ["guides"]).each do |framework|
    release_notes << "## #{framework_name(framework)}\n"
    file_name = File.join root, framework, "CHANGELOG.md"
    contents = File.readlines file_name
    contents.shift # Remove the header
    changes = []

    until end_of_notes?(contents) || contents.empty?
      changes << contents.shift
    end

    release_notes << changes.join
  end

  release_notes
end
# File tools/releaser/lib/releaser.rb, line 228
def update_versions(framework)
  return if framework == "rails"

  Dir.chdir(root) do
    glob = "#{framework}/lib/*/gem_version.rb"

    file = Dir[glob].first
    ruby = File.read(file)

    ruby.gsub!(/^(\s*)MAJOR(\s*)= .*?$/, "\\1MAJOR = #{major}")
    raise "Could not insert MAJOR in #{file}" unless $1

    ruby.gsub!(/^(\s*)MINOR(\s*)= .*?$/, "\\1MINOR = #{minor}")
    raise "Could not insert MINOR in #{file}" unless $1

    ruby.gsub!(/^(\s*)TINY(\s*)= .*?$/, "\\1TINY  = #{tiny}")
    raise "Could not insert TINY in #{file}" unless $1

    ruby.gsub!(/^(\s*)PRE(\s*)= .*?$/, "\\1PRE   = #{pre.inspect}")
    raise "Could not insert PRE in #{file}" unless $1

    File.open(file, "w") { |f| f.write ruby }

    package_json = "#{framework}/package.json"

    if File.exist?(package_json) && JSON.parse(File.read(package_json))["version"] != npm_version
      Dir.chdir("#{framework}") do
        if sh("which npm > /dev/null 2>&1", verbose: false)
          sh "npm version #{npm_version} --no-git-tag-version > /dev/null 2>&1", verbose: false
        else
          raise "You must have npm installed to release Rails."
        end
      end
    end
  end
end