class Buildr::ReleaseTool

Public Class Methods

define_release_task(options = {}) { |release_tool| ... } click to toggle source
# File addon/buildr/release_tool.rb, line 18
def define_release_task(options = {})
  task_name = options[:task_name] || 'perform_release'
  description = options[:description] || 'Perform a release'
  workspace_dir = options[:workspace_dir] || File.dirname(Buildr.application.buildfile.to_s)

  ENV['PREVIOUS_PRODUCT_VERSION'] = nil if ENV['PREVIOUS_PRODUCT_VERSION'].to_s == ''
  ENV['PRODUCT_VERSION'] = nil if ENV['PRODUCT_VERSION'].to_s == ''

  desc description
  task task_name do
    in_dir(workspace_dir) do
      yield ReleaseTool.new
    end
    if ENV['STAGE']
      if ENV['LAST_STAGE'] == ENV['STAGE']
        puts "LAST_STAGE specified '#{ENV['LAST_STAGE']}', later stages were skipped"
      else
        raise "Invalid STAGE specified '#{ENV['STAGE']}' that did not match any stage"
      end
    end
  end
end
derive_next_version(current_version, options = {}) click to toggle source
# File addon/buildr/release_tool.rb, line 46
def derive_next_version(current_version, options = {})
  return options[:next_version_action].call(current_version) if options[:next_version_action]
  version_parts = current_version.split('.')
  "#{version_parts[0]}.#{sprintf('%02d', version_parts[1].to_i + 1)}#{version_parts.length > 2 ? ".#{version_parts[2]}" : ''}"
end
derive_versions_from_changelog(options = {}) click to toggle source
# File addon/buildr/release_tool.rb, line 41
def derive_versions_from_changelog(options = {})
  ENV['PREVIOUS_PRODUCT_VERSION'] ||= IO.read('CHANGELOG.md')[/^### \[v(\d+\.\d+(\.\d+)?)\]/, 1] || '0.00'
  ENV['PRODUCT_VERSION'] ||= derive_next_version(ENV['PREVIOUS_PRODUCT_VERSION'], options)
end

Private Class Methods

in_dir(dir) { || ... } click to toggle source
# File addon/buildr/release_tool.rb, line 54
def in_dir(dir)
  current = Dir.pwd
  begin
    Dir.chdir(dir)
    yield
  ensure
    Dir.chdir(current)
  end
end

Public Instance Methods

build(options = {}) click to toggle source
# File addon/buildr/release_tool.rb, line 98
def build(options = {})
  additional_tasks = options[:additional_tasks] || ''
  stage('Build', 'Build the project to ensure that the tests pass') do
    sh "bundle exec buildr clean package #{additional_tasks} install PRODUCT_VERSION=#{ENV['PRODUCT_VERSION']}#{ENV['TEST'].nil? ? '' : " TEST=#{ENV['TEST']}"}#{Buildr.application.options.trace ? ' --trace' : ''}"
  end
end
cleanup_staging() click to toggle source
# File addon/buildr/release_tool.rb, line 92
def cleanup_staging
  stage('StagingCleanup', 'Remove artifacts from staging repository') do
    task('staging:cleanup').invoke
  end
end
ensure_git_clean() click to toggle source
# File addon/buildr/release_tool.rb, line 79
def ensure_git_clean
  stage('GitClean', 'Ensure there is nothing to commit and the working tree is clean') do
    status_output = `git status -s 2>&1`.strip
    raise 'Uncommitted changes in git repository. Please commit them prior to release.' if 0 != status_output.size
  end
end
extract_version_from_changelog(options = {}) click to toggle source
# File addon/buildr/release_tool.rb, line 65
def extract_version_from_changelog(options = {})
  stage('ExtractVersion', 'Extract the last version from CHANGELOG.md and derive next version unless specified', :always_run => true) do
    Buildr::ReleaseTool.derive_versions_from_changelog(options)
    # Also initialize release date if required
    ENV['RELEASE_DATE'] ||= Time.now.strftime('%Y-%m-%d')
  end
end
github_release(repository_name) click to toggle source
# File addon/buildr/release_tool.rb, line 224
def github_release(repository_name)
  stage('GithubRelease', 'Create a Release on GitHub') do
    changelog = IO.read('CHANGELOG.md')
    start = changelog.index("### [v#{ENV['PRODUCT_VERSION']}]")
    raise "Unable to locate version #{ENV['PRODUCT_VERSION']} in change log" if -1 == start
    start = changelog.index("\n", start)
    start = changelog.index("\n", start + 1)

    end_index = changelog.index('### [v', start)
    end_index = changelog.length if end_index.nil?

    changes = changelog[start, end_index - start]

    changes = changes.strip

    tag = "v#{ENV['PRODUCT_VERSION']}"

    version_parts = ENV['PRODUCT_VERSION'].split('.')
    prerelease = '0' == version_parts[0]

    require 'octokit'

    client = Octokit::Client.new(:netrc => true, :auto_paginate => true)
    client.login
    client.create_release(repository_name, tag, :name => tag, :body => changes, :draft => false, :prerelease => prerelease)

    candidates = client.list_milestones(repository_name).select { |m| m[:title].to_s == tag }
    unless candidates.empty?
      milestone = candidates[0]
      unless milestone[:state] == 'closed'
        client.update_milestone(repository_name, milestone[:number], :state => 'closed')
      end
    end
  end
end
maven_central_publish(options = {}) click to toggle source
# File addon/buildr/release_tool.rb, line 194
def maven_central_publish(options = {})
  additional_tasks = options[:additional_tasks] || ''
  stage('MavenCentralPublish', 'Publish artifacts to Maven Central') do
    sh "bundle exec buildr clean mcrt:publish_if_tagged #{additional_tasks} TEST=no GWT=no"
  end
end
patch_changelog(repository_name, options = {}) click to toggle source
# File addon/buildr/release_tool.rb, line 105
    def patch_changelog(repository_name, options = {})
      stage('PatchChangelog', 'Patch the changelog to update from previous release') do
        changelog = IO.read('CHANGELOG.md')
        from = '0.00' == ENV['PREVIOUS_PRODUCT_VERSION'] ? `git rev-list --max-parents=0 HEAD`.strip : "v#{ENV['PREVIOUS_PRODUCT_VERSION']}"

        header = "### [v#{ENV['PRODUCT_VERSION']}](https://github.com/#{repository_name}/tree/v#{ENV['PRODUCT_VERSION']}) (#{ENV['RELEASE_DATE']}) · [Full Changelog](https://github.com/spritz/spritz/compare/#{from}...v#{ENV['PRODUCT_VERSION']})"

        sub_header_text = ''

        api_diff_directory = options[:api_diff_directory]
        api_diff_filename = api_diff_directory ? "#{api_diff_directory}/#{ENV['PREVIOUS_PRODUCT_VERSION']}-#{ENV['PRODUCT_VERSION']}.json" : nil
        if api_diff_filename && File.exist?(api_diff_filename)

          api_diff_site = options[:api_diff_website]
          if api_diff_site
            header += " · [API Differences](#{api_diff_site}old=#{ENV['PREVIOUS_PRODUCT_VERSION']}&new=#{ENV['PRODUCT_VERSION']})"
          end

          changes = JSON.parse(IO.read(api_diff_filename))
          non_breaking_changes = changes.select { |j| j['classification']['SOURCE'] == 'NON_BREAKING' }.size
          potentially_breaking_changes = changes.select { |j| j['classification']['SOURCE'] == 'POTENTIALLY_BREAKING' }.size
          breaking_changes = changes.select { |j| j['classification']['SOURCE'] == 'BREAKING' }.size
          change_descriptions = []
          change_descriptions << "#{non_breaking_changes} non breaking API change#{1 == non_breaking_changes ? '' : 's'}" unless 0 == non_breaking_changes
          change_descriptions << "#{potentially_breaking_changes} potentially breaking API change#{1 == potentially_breaking_changes ? '' : 's'}" unless 0 == potentially_breaking_changes
          change_descriptions << "#{breaking_changes} breaking API change#{1 == breaking_changes ? '' : 's'}" unless 0 == breaking_changes

          if change_descriptions.size > 0
            description = "The release includes "
            if 1 == change_descriptions.size
              description += "#{change_descriptions[0]}."
            elsif 2 == change_descriptions.size
              description += "#{change_descriptions[0]} and #{change_descriptions[1]}."
            else
              description += "#{change_descriptions[0]}, #{change_descriptions[1]} and #{change_descriptions[2]}."
            end

            sub_header_text = description
          end
        end

        header_suffix = options[:header_suffix]
        header += header_suffix if header_suffix
        header += "\n\n#{sub_header_text}" unless sub_header_text.empty?
        header += "\n"

        header += <<CONTENT

Changes in this release:
CONTENT

        IO.write('CHANGELOG.md', changelog.gsub("### Unreleased\n", header))

        sh 'git reset 2>&1 1> /dev/null'
        sh 'git add CHANGELOG.md'
        sh 'git commit -m "Update CHANGELOG.md in preparation for release"'
      end
    end
patch_changelog_post_release() click to toggle source
# File addon/buildr/release_tool.rb, line 201
    def patch_changelog_post_release
      stage('PatchChangelogPostRelease', 'Patch the changelog post release to prepare for next development iteration') do
        changelog = IO.read('CHANGELOG.md')
        changelog = changelog.gsub("# Change Log\n", <<HEADER)
# Change Log

### Unreleased
HEADER
        IO.write('CHANGELOG.md', changelog)

        `bundle exec zapwhite`
        sh 'git add CHANGELOG.md'
        sh 'git commit -m "Update CHANGELOG.md in preparation for next development iteration"'
      end
    end
patch_maven_version_in_readme() click to toggle source
# File addon/buildr/release_tool.rb, line 164
def patch_maven_version_in_readme
  stage('PatchReadme', 'Patch the README to update from previous release') do
    contents = IO.read('README.md')
    contents = contents.
      gsub("<version>#{ENV['PREVIOUS_PRODUCT_VERSION']}</version>", "<version>#{ENV['PRODUCT_VERSION']}</version>").
      gsub("/#{ENV['PREVIOUS_PRODUCT_VERSION']}/", "/#{ENV['PRODUCT_VERSION']}/").
      gsub("-#{ENV['PREVIOUS_PRODUCT_VERSION']}-", "-#{ENV['PRODUCT_VERSION']}-")
    IO.write('README.md', contents)

    sh 'git reset 2>&1 1> /dev/null'
    sh 'git add README.md'
    sh 'git commit -m "Update README.md in preparation for release"'
  end
end
push_changes() click to toggle source
# File addon/buildr/release_tool.rb, line 217
def push_changes
  stage('PushChanges', 'Push changes to git repository') do
    sh 'git push'
    sh 'git push --tags'
  end
end
stage(stage_name, description, options = {}) { || ... } click to toggle source
# File addon/buildr/release_tool.rb, line 260
def stage(stage_name, description, options = {})
  if ENV['STAGE'].nil? || ENV['STAGE'] == stage_name || options[:always_run]
    puts "🚀 Release Stage: #{stage_name} - #{description}"
    begin
      yield
    rescue Exception => e
      puts '💣 Error completing stage.'
      puts "Fix the error and re-run release process passing: STAGE=#{stage_name}#{ ENV['PREVIOUS_PRODUCT_VERSION'] ? " PREVIOUS_PRODUCT_VERSION=#{ENV['PREVIOUS_PRODUCT_VERSION']}" : ''}#{ ENV['PREVIOUS_PRODUCT_VERSION'] ? " PRODUCT_VERSION=#{ENV['PRODUCT_VERSION']}" : ''}"
      raise e
    end
    ENV['STAGE'] = nil unless options[:always_run]
  elsif !ENV['STAGE'].nil?
    puts "Skipping Stage: #{stage_name} - #{description}"
  end
  if ENV['LAST_STAGE'] == stage_name
    ENV['STAGE'] = ENV['LAST_STAGE']
  end
end
stage_release(options = {}) click to toggle source
# File addon/buildr/release_tool.rb, line 185
def stage_release(options = {})
  release_to = options[:release_to] || (raise "StageRelease stage must specify :release_to configuration")
  stage('StageRelease', 'Stage the release') do
    IO.write('_buildr.rb', "repositories.release_to = #{release_to.inspect}")
    sh 'bundle exec buildr clean upload TEST=no GWT=no'
    sh 'rm -f _buildr.rb'
  end
end
tag_project() click to toggle source
# File addon/buildr/release_tool.rb, line 179
def tag_project
  stage('TagProject', 'Tag the project') do
    sh "git tag v#{ENV['PRODUCT_VERSION']}"
  end
end
verify_no_todo() click to toggle source
# File addon/buildr/release_tool.rb, line 86
def verify_no_todo
  stage('TodoScan', 'Verify that there are no TODO notes in codebase') do
    task('todos:scan').invoke
  end
end
zapwhite() click to toggle source
# File addon/buildr/release_tool.rb, line 73
def zapwhite
  stage('ZapWhite', 'Ensure that zapwhite produces no changes') do
    sh 'bundle exec zapwhite'
  end
end