require 'pathname' module Shipit class DeploySpec Error = Class.new(StandardError) class << self def load(json) config = json.blank? ? {} : JSON.parse(json) new(config) end def dump(spec) JSON.dump(spec.cacheable.config) if spec end def bundle_path Rails.root.join('data', 'bundler') end end def initialize(config) @config = config end delegate :bundle_path, to: :class def cacheable self end def config(*keys, &default) default ||= -> { nil } keys.flatten.reduce(@config) do |hash, key| if hash.is_a?(Hash) hash.fetch(key) { return default.call } else return default.call end end end def supports_fetch_deployed_revision? fetch_deployed_revision_steps.present? end def supports_rollback? rollback_steps.present? end def machine_env config('machine', 'environment') || {} end def directory config('machine', 'directory') end def dependencies_steps around_steps('dependencies') do config('dependencies', 'override') { discover_dependencies_steps || [] } end end alias_method :dependencies_steps!, :dependencies_steps def maximum_commits_per_deploy config('deploy', 'max_commits') { 8 } end def release_status? !!release_status_context end def release_status_context config('status', 'context') end def release_status_delay if delay = config('status', 'delay') { config('deploy', 'interval') } Duration.parse(delay) end end def pause_between_deploys Duration.parse(config('deploy', 'interval') { 0 }) end def deploy_steps around_steps('deploy') do config('deploy', 'override') { discover_deploy_steps } end end def deploy_steps! deploy_steps || cant_detect!(:deploy) end def deploy_variables Array.wrap(config('deploy', 'variables')).map(&VariableDefinition.method(:new)) end def default_deploy_env deploy_variables.map { |v| [v.name, v.default] }.to_h end def rollback_steps around_steps('rollback') do config('rollback', 'override') { discover_rollback_steps } end end def rollback_steps! rollback_steps || cant_detect!(:rollback) end def fetch_deployed_revision_steps config('fetch') || discover_fetch_deployed_revision_steps end def fetch_deployed_revision_steps! fetch_deployed_revision_steps || cant_detect!(:fetch) end def task_definitions discover_task_definitions.merge(config('tasks') || {}).map do |name, definition| TaskDefinition.new(name, coerce_task_definition(definition)) end end def find_task_definition(id) definition = config('tasks', id) || discover_task_definitions[id] TaskDefinition.new(id, coerce_task_definition(definition) || task_not_found!(id)) end def filter_deploy_envs(env) EnvironmentVariables.with(env).permit(deploy_variables) end def review_checklist (config('review', 'checklist') || discover_review_checklist || []).map(&:strip).select(&:present?) end def review_monitoring (config('review', 'monitoring') || []).select(&:present?) end def hidden_statuses Array.wrap(config('ci', 'hide')) + [release_status_context].compact end def required_statuses (Array.wrap(config('ci', 'require')) + blocking_statuses).uniq end def soft_failing_statuses Array.wrap(config('ci', 'allow_failures')) end def blocking_statuses Array.wrap(config('ci', 'blocking')) end def pull_request_merge_method method = config('merge', 'method') method if %w(merge rebase squash).include?(method) end def pull_request_required_statuses if config('merge', 'require') || config('merge', 'ignore') Array.wrap(config('merge', 'require')) else required_statuses end end def pull_request_ignored_statuses if config('merge', 'require') || config('merge', 'ignore') Array.wrap(config('merge', 'ignore')) + [release_status_context].compact else soft_failing_statuses | hidden_statuses end end def revalidate_pull_requests_after if timeout = config('merge', 'revalidate_after') begin Duration.parse(timeout) rescue Duration::ParseError end end end def max_divergence_commits config('merge', 'max_divergence', 'commits') end def max_divergence_age if timeout = config('merge', 'max_divergence', 'age') begin Duration.parse(timeout) rescue Duration::ParseError end end end def review_checks config('review', 'checks') || [] end def plugins config('plugins') || {} end def clear_working_directory? config('machine', 'cleanup') { true } end def links config('links') { {} } end private def around_steps(section) steps = yield return unless steps config(section, 'pre') { [] } + steps + config(section, 'post') { [] } end def coerce_task_definition(config) config end def discover_review_checklist end def discover_task_definitions {} end def discover_dependencies_steps end def discover_deploy_steps end def discover_rollback_steps end def discover_fetch_deployed_revision_steps end def discover_machine_env {} end def task_not_found!(id) raise TaskDefinition::NotFound.new("No definition for task #{id.inspect}") end def cant_detect!(type) raise DeploySpec::Error.new(I18n.t("deploy_spec.hint.#{type}")) end end end