# frozen_string_literal: true require "rake" require "mina" require "pp" require "open3" module Groundskeeper # Formulas for managing releases and deployments. # rubocop:disable Metrics/ClassLength class Commands RAKEFILE = File.join( File.dirname(__FILE__), "..", "..", "config", "deploy.rb" ) STAGE = "stage" STAGING = "staging" PRODUCTION = "production" DEFAULT_STAGE = STAGING TAG = "tag" INITIAL_VERSION = "0.0.1" # rubocop:disable Metrics/AbcSize,Metrics/MethodLength def self.build(console) repository = Repository.new project = Project.build(repository.name) deploy_url = "https://#{project.full_dns(stage)}" website = Website.new(deploy_url) git_hub = GitHub.build( username: project.source_control_username, repository_name: repository.name ) sentry = Sentry.build( project_name: project.sentry_project, version_prefix: repository.name ) new( changelog: Changelog.build, console: console, git: Git.build, git_hub: git_hub, jira: Jira.build(project.jira_prefix), project: project, repository: repository, rubygems: Rubygems, sentry: sentry, version_file: RailsVersion.build, website: website ) end # rubocop:enable Metrics/AbcSize,Metrics/MethodLength def self.stage ENV[STAGE] == PRODUCTION ? PRODUCTION : DEFAULT_STAGE end # rubocop:disable Metrics/MethodLength,Metrics/ParameterLists def initialize( changelog: nil, console:, git: nil, git_hub: nil, jira: nil, project: nil, repository: nil, rubygems: nil, sentry: nil, website: nil, version_file: ) @changelog = changelog @console = console @git = git @git_hub = git_hub @jira = jira @project = project @repository = repository @rubygems = rubygems @sentry = sentry @website = website @version_file = version_file @did_checkout_branch = false @did_push_to_remote = false @current_step = 1 end # rubocop:enable Metrics/MethodLength,Metrics/ParameterLists # rubocop:disable Metrics/MethodLength,Metrics/AbcSize def info(options = {}) return unrecognized_version unless version_file.exists? return unless check_groundskeeper_version Executable.verbose = options[:verbose] pull_project_details announce_latest_tag console.say( "version in current branch: " + version_file.current_version, :yellow ) console.say( "tag contained in: #{git.branches_containing_latest_tag}\n\n", :yellow ) end # rubocop:enable Metrics/AbcSize # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity def release(options = {}) is_initial_release = !version_file.exists? return missing_jira_credentials unless jira.credentials? return unless check_groundskeeper_version return unrecognized_version unless version_file.rails? version_file.create_initial_version! unless version_file.exists? Executable.verbose = options[:verbose] pull_project_details summarize_recent_commits unless is_initial_release ask_next_version(is_initial_release) ask_new_branch update_version_file update_changelog commit_changes_and_tag(is_initial_release) ask_create_jira_version ask_push_with_tags ask_add_version_to_jira_issues unless is_initial_release open_pull_request_page end # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity # rubocop:enable Metrics/MethodLength # :nocov: def predeploy(options = {}) return unrecognized_version unless version_file.exists? return unless check_groundskeeper_version Executable.verbose = options[:verbose] pull_project_details mina "predeploy", options end # :nocov: # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity # rubocop:disable Metrics/MethodLength def deploy(options = {}) return unrecognized_version unless version_file.exists? return unrecognized_tag unless tag_present_in_git?(ENV[TAG]) return missing_jira_credentials unless jira.credentials? return unless check_groundskeeper_version Executable.verbose = options[:verbose] pull_project_details ENV["whenever"] = "1" if project.uses_whenever? mina "deploy", options announce_step "Wait for deployed application to restart" update_deployed_issues release_version if self.class.stage == PRODUCTION add_version_to_sentry end # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity # rubocop:enable Metrics/MethodLength private # collaborators attr_reader :changelog, :console, :git, :git_hub, :jira, :project, :repository, :rubygems, :sentry, :version_file, :website # state attr_reader :current_step, :did_checkout_branch, :did_push_to_remote, :next_version, :recent_commits def mina(command, options) announce_step "Run mina to deploy" cmd = String.new command cmd << " -s" if options[:simulate] cmd << " -v" if options[:verbose] cmd << " force_asset_precompile=true" if options[:force_asset_precompile] run_mina cmd end def pull_project_details @pull_project_details ||= begin announce_step("Updating ~/.project_details from "\ "#{git.remote_url('.project_details')}") git.pull(".project_details") end end # rubocop:disable Metrics/MethodLength def check_groundskeeper_version @check_groundskeeper_version ||= begin console.say("Groundskeeper version #{Groundskeeper::VERSION}\n\n", :bold) latest_version = rubygems.latest_groundskeeper_version if SemanticVersion.new(latest_version) > SemanticVersion.new(VERSION) console.say( "Groundskeeper is outdated, please install #{latest_version}", :red ) false else true end end end # rubocop:enable Metrics/MethodLength def announce_latest_tag console.say( "latest tag on this repository: " + git.latest_tag_name_across_branches, :yellow ) end def summarize_recent_commits console.say("commits since last tag", :bold) @recent_commits = repository.changes_since_latest_tag console.say(recent_commits.join("\n"), :green) console.say("") end def ask_next_version(is_initial_release) if is_initial_release @next_version = INITIAL_VERSION else type = console.ask("Major, minor, or patch release?", :cyan, limited_to: %w[M m p]) @next_version = repository.bumped_semantic_version(type) end console.say("next tag will be: #{next_version}", :green) end def ask_new_branch @did_checkout_branch = console.yes?( "Checkout new branch #{new_branch_name}? [y, n]", :cyan ) git.create_and_checkout_branch(new_branch_name) if did_checkout_branch end def update_version_file version_file.update_version! next_version console.say("# updated version file", :green) end def update_changelog changelog.update_file(next_version, recent_commits) console.say("# updated changelog", :green) end # rubocop:disable Metrics/AbcSize def commit_changes_and_tag(is_initial_release) is_initial_release ? git.add_all : git.add_update git.commit(format(Repository::RELEASE_MESSAGE, next_version)) console.say("# committed changes", :green) git.add_tag(next_version) console.say("# tagged", :green) end # rubocop:enable Metrics/AbcSize def ask_create_jira_version create_jira_version = console.yes?("Create version #{next_version} in Jira? [y, n]", :cyan) jira.create_remote_version(next_jira_version_name) if create_jira_version end def ask_push_with_tags @did_push_to_remote = console.yes?( "Push to remote with tags? [y, n]", :cyan ) git.push_with_tags && console.say("# pushed tag") if @did_push_to_remote end def ask_add_version_to_jira_issues issue_ids = jira.included_issues(recent_commits) add_version_to_jira_issues = console.yes?( "Add #{next_jira_version_name} to #{issue_ids}? [y, n]", :cyan ) return unless add_version_to_jira_issues # :nocov: jira.add_version_to_remote_issues(next_jira_version_name, issue_ids) console.say("# added version to Jira issues", :green) # :nocov: end def next_jira_version_name "#{repository.name} #{next_version}" end def add_version_to_sentry announce_step "Add release to Sentry and mark deployed" sentry.create_release(ENV[TAG]) sentry.associate_commits(ENV[TAG]) sentry.deploy(ENV[TAG], self.class.stage) console.say("# added version to Sentry", :green) end def tag_present_in_git?(tag) tag.nil? ? false : git.list_tags.include?(tag) end def unrecognized_tag console.say( "Tag not found, try again with an existing tag.", :red ) end def unrecognized_version console.say( "Unable to find version file, is this a Rails application?", :red ) end def missing_jira_credentials console.say( "Please configure your Jira environment variables.", :red ) end def open_pull_request_page return unless did_checkout_branch && did_push_to_remote git_hub.open_pull_request_page(new_branch_name) end def new_branch_name "v#{next_version}" end def run_mina(arguments) command = "mina #{arguments} -f #{RAKEFILE}" Open3.popen3(command) do |_stdout, stderr, _status, _thread| # :nocov: # rubocop:disable Lint/AssignmentInCondition while line = stderr.gets puts line end # rubocop:enable Lint/AssignmentInCondition # :nocov: end end def update_deployed_issues announce_step( "Check deployed version at #{website.uri}/#{Website::VERSION_PATH}" ) deployed_version = website.version if deployed_version == ENV[TAG] console.say("# deployment successful", :green) transition_remote_issues deployed_version else # :nocov: console.say("something went wrong", :red) # :nocov: end end def release_version jira.release_version(project, website.version) console.say("#{project.repo_name} #{website.version} released", :green) end def transition_remote_issues(version) version_name = "#{repository.name} #{version}" deployed_issues = jira.fetch_issues_by_fix_version(version_name) action = self.class.stage == PRODUCTION ? Jira::DELIVER : Jira::DEPLOY_TO_STAGING announce_step( "Transitioning deployed issues in Jira: " \ "#{action} #{deployed_issues.join(', ')}" ) jira.transition_remote_issues(action, deployed_issues) end def announce_step(message) console.say(">>>", :magenta) console.say(">>> #{@current_step}. #{message}", :magenta) console.say(">>>", :magenta) console.say("") @current_step += 1 end end # rubocop:enable Metrics/ClassLength end