afterlife 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ class Deploy
5
+ Error = Class.new StandardError
6
+
7
+ def self.call
8
+ new.deploy
9
+ end
10
+
11
+ def deploy
12
+ deployment_output.tap do
13
+ Exec.run(after_deploy_command) if after_deploy_command
14
+ end
15
+ rescue Exec::Error => e
16
+ raise Error, e
17
+ end
18
+
19
+ private
20
+
21
+ def deployment_output
22
+ case deployment_type
23
+ when 'cdn'
24
+ CdnDeployment.run
25
+ end
26
+ end
27
+
28
+ def deployment_type
29
+ repo.conf.dig(:deploy, :type).tap do |result|
30
+ fail Error, 'deploy.type must be defined if you want to deploy with Afterlife' unless result
31
+ end
32
+ end
33
+
34
+ def after_deploy_command
35
+ return if Afterlife.cli.options['skip-after-hooks']
36
+
37
+ repo.conf.dig(:deploy, :after)
38
+ end
39
+
40
+ def repo
41
+ Afterlife.current_repo
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ class Environment
5
+ attr_reader :repo, :env
6
+
7
+ def self.from(repo)
8
+ env = new(repo).tap(&:call).env
9
+ yield env if block_given?
10
+ env
11
+ end
12
+
13
+ def initialize(repo)
14
+ @repo = repo
15
+ @env = {}
16
+ end
17
+
18
+ def call
19
+ from_flat_envs
20
+ from_branch_envs
21
+ from_stage_envs
22
+ end
23
+
24
+ def env_hash_by_branch
25
+ @env_hash_by_branch ||= env_hash&.dig(:by_branch, repo.current_branch.to_sym)
26
+ end
27
+
28
+ def env_hash_by_stage
29
+ @env_hash_by_stage ||= env_hash&.dig(:by_stage, Afterlife.current_stage.name.to_sym)
30
+ end
31
+
32
+ def env_hash
33
+ @env_hash ||= repo.conf.dig(:deploy, :env)
34
+ end
35
+
36
+ private
37
+
38
+ def from_flat_envs
39
+ return unless env_hash.is_a?(Hash)
40
+
41
+ env_hash.each do |key, value|
42
+ next if key == :by_branch
43
+ next if key == :by_stage
44
+
45
+ @env[key.to_s] = value.to_s
46
+ end
47
+ end
48
+
49
+ def from_branch_envs
50
+ return unless env_hash_by_branch.is_a?(Hash)
51
+
52
+ env_hash_by_branch.each do |key, value|
53
+ @env[key.to_s] = value.to_s
54
+ end
55
+ end
56
+
57
+ def from_stage_envs
58
+ return unless env_hash_by_stage.is_a?(Hash)
59
+
60
+ env_hash_by_stage.each do |key, value|
61
+ @env[key.to_s] = value.to_s
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # adds String#squish and others
4
+ require 'active_support/core_ext/string'
5
+
6
+ module Afterlife
7
+ class Exec
8
+ Error = Class.new StandardError
9
+
10
+ def self.run(arg)
11
+ new(arg).run
12
+ end
13
+
14
+ def self.parse(arg, env)
15
+ Array(arg).map do |command|
16
+ format(command, env.symbolize_keys)
17
+ end
18
+ end
19
+
20
+ attr_reader :commands
21
+
22
+ def initialize(arg)
23
+ @commands = Array(arg)
24
+ end
25
+
26
+ def run
27
+ parsed_commands.each do |command|
28
+ Afterlife.cli.log_info(command) if Afterlife.cli.options['verbose']
29
+ system(repo.env, command.squish, exception: true) unless Afterlife.cli.options['dry-run']
30
+ end
31
+ rescue RuntimeError => e
32
+ raise Error, e
33
+ end
34
+
35
+ def parsed_commands
36
+ Exec.parse(commands, repo.env)
37
+ end
38
+
39
+ def repo
40
+ Afterlife.current_repo
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git'
4
+
5
+ module Afterlife
6
+ module Release
7
+ class ChangeVersion
8
+ RUBY_REGEXP = /VERSION\s*=\s*['"]#{Bump::Semver::VERSION_REGEX}['"]/.freeze
9
+ REGEXPS = {
10
+ ruby: RUBY_REGEXP,
11
+ gem: RUBY_REGEXP,
12
+ node: /"version":\s*"#{Bump::Semver::VERSION_REGEX}"/,
13
+ }.freeze
14
+
15
+ def self.call(new_version)
16
+ Afterlife.current_repo.version_files.map do |file_def|
17
+ instance = new(new_version, file_def)
18
+ instance.call
19
+ instance.file_path
20
+ end
21
+ end
22
+
23
+ attr_reader :file_path, :regexp, :new_version
24
+
25
+ def initialize(new_version, options)
26
+ @file_path = options[:real_path]
27
+ @regexp = REGEXPS[options[:type].to_sym]
28
+ @new_version = new_version
29
+ end
30
+
31
+ def call
32
+ fail "could not find a version for #{file_path}" unless current_version
33
+
34
+ File.write(
35
+ file_path,
36
+ content.sub(current_version, new_version),
37
+ )
38
+ end
39
+
40
+ def current_version
41
+ content[regexp]
42
+ Regexp.last_match(1)
43
+ end
44
+
45
+ def content
46
+ @content ||= File.read(file_path)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ module Release
5
+ class Cli < Thor
6
+ include BaseCli
7
+
8
+ class_option :name,
9
+ type: :string,
10
+ aliases: '-n',
11
+ desc: 'The name of the branch, the default depends on the action'
12
+ class_option :yes,
13
+ type: :boolean,
14
+ desc: 'Skip confirmation'
15
+ class_option :force,
16
+ desc: 'Skip validations'
17
+ class_option :quiet, type: :boolean, group: :runtime
18
+
19
+ desc 'create <part>', 'creates a release branch based on the part (major|minor|patch)'
20
+ option :from,
21
+ type: :string,
22
+ aliases: '-f',
23
+ default: CreateHelper::DEFAULT_BASE_BRANCH,
24
+ desc: 'Base branch'
25
+ def create(part)
26
+ fatal! "Part '#{part}' is not allowed" unless CreateHelper::PARTS.include?(part.to_sym)
27
+
28
+ CreateHelper.call(self, options.merge(part: part))
29
+ end
30
+
31
+ desc 'hotfix', 'create a hotfix branch'
32
+ def hotfix
33
+ args = options.merge(part: :patch, from: :master, prefix: :hotfix)
34
+ CreateHelper.call(self, args)
35
+ end
36
+
37
+ desc 'pre', 'create a prerelease tag'
38
+ option :push, type: :boolean, default: true
39
+ def pre
40
+ PreHelper.call(self, options)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git'
4
+
5
+ module Afterlife
6
+ module Release
7
+ class CreateHelper < Helper
8
+ DEFAULT_BASE_BRANCH = 'develop'
9
+ PARTS = %i[major minor patch].freeze
10
+
11
+ attr_reader :part, :branch_name, :from
12
+
13
+ def initialize(cli, args)
14
+ super
15
+ @part = args[:part]
16
+ @branch_name = [
17
+ args.fetch(:prefix, 'release'),
18
+ args.fetch(:name, "v#{new_version}"),
19
+ ].join('/')
20
+ @from = args.fetch(:from, DEFAULT_BASE_BRANCH)
21
+ end
22
+
23
+ def call
24
+ confirm_creation unless cli.options[:yes]
25
+ make_assertions unless cli.options[:force]
26
+ create_branch
27
+ ChangeVersion.call(new_version)
28
+ commit
29
+ end
30
+
31
+ private
32
+
33
+ def make_assertions
34
+ check_git_status
35
+ check_local_and_remote_on_same_commit
36
+ end
37
+
38
+ def check_git_status
39
+ cli.say_status 'Checking that there are no uncommitted changes...', nil
40
+ return cli.say 'OK' if `git status --porcelain --untracked-files no | wc -l`.to_i.zero?
41
+
42
+ cli.fatal! 'Working directory has changes, aborting...'
43
+ end
44
+
45
+ def check_local_and_remote_on_same_commit
46
+ cli.say_status 'Checking that local and remote are on the same commit...', nil
47
+ local = `git rev-parse #{from}`.chomp
48
+ remote = `git ls-remote '#{git.remotes.first.name}' '#{from}' | cut -f 1`.chomp
49
+ return cli.say 'OK' if local == remote
50
+
51
+ cli.fatal! 'Local and remote are not on the same commit'
52
+ end
53
+
54
+ def confirm_creation
55
+ cli.sure? 'You are about to create the branch ' \
56
+ "#{cli.set_color(branch_name, :bold)} " \
57
+ "from #{cli.set_color(from, :bold)}"
58
+ end
59
+
60
+ def create_branch
61
+ git.checkout(branch_name, new_branch: true, start_point: from)
62
+ end
63
+
64
+ def already_exists?
65
+ git.branches.map(&:name).include?(branch_name)
66
+ end
67
+
68
+ def commit
69
+ version_files = ChangeVersion.call(new_version)
70
+ version_files.each { |path| git.add(path) }
71
+ git.commit("bump v#{new_version}", no_verify: true)
72
+ end
73
+
74
+ def new_version
75
+ @new_version ||= Bump::Semver.calculate_next(part)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ module Release
5
+ class Helper
6
+ def self.call(cli, args)
7
+ new(cli, args).call
8
+ rescue Git::GitExecuteError => e
9
+ cli.fatal! e.message.chomp.split(':').last.strip
10
+ rescue Interrupt
11
+ cli.log_interrupted
12
+ end
13
+
14
+ attr_reader :cli
15
+
16
+ def initialize(cli, args)
17
+ args.compact!
18
+ @cli = cli
19
+ end
20
+
21
+ def git
22
+ @git ||= Git.open('.')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ module Release
5
+ class PreHelper < Helper
6
+ def call
7
+ confirm_creation
8
+ check_tag_does_not_exist
9
+ commit
10
+ create_tag
11
+ push_tag if cli.options[:push]
12
+ end
13
+
14
+ def confirm_creation
15
+ return if cli.options[:yes]
16
+
17
+ cli.sure? 'You are about to create ' \
18
+ "#{cli.options[:push] ? '(and push) ' : ''}" \
19
+ "the tag #{cli.set_color(tag, :bold)} " \
20
+ 'in the current commit'
21
+ end
22
+
23
+ def check_tag_does_not_exist
24
+ tags = `git tag --list "#{tag}"`.chomp
25
+ cli.fatal! "tag #{tag} already exist" unless tags.split.empty?
26
+
27
+ `git ls-remote --tags --exit-code '#{git.remotes.first.name}' #{tag} >/dev/null 2>&1`
28
+ cli.fatal! "tag #{tag} already exist on remote" if $CHILD_STATUS.success?
29
+ end
30
+
31
+ def create_tag
32
+ git.add_tag(tag)
33
+ end
34
+
35
+ def push_tag
36
+ cli.say_status 'Publishing', tag
37
+ git.push(git.remote('origin'), tag)
38
+ end
39
+
40
+ def commit
41
+ version_files = ChangeVersion.call(new_version)
42
+ version_files.each { |path| git.add(path) }
43
+ git.commit("bump #{tag}", no_verify: true)
44
+ end
45
+
46
+ def tag
47
+ "v#{new_version}"
48
+ end
49
+
50
+ def new_version
51
+ @new_version ||= Bump::Semver.calculate_next(:pre, cli.options[:name])
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ module Release
5
+ end
6
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash'
4
+
5
+ module Afterlife
6
+ class Repo
7
+ DEFAULT_DIST = 'dist'
8
+ DEFAULT_BUILD_COMMAND = 'yarn build'
9
+ DEFAULT_INSTALL = 'yarn install'
10
+ VERSION_FILES = [
11
+ {
12
+ type: :ruby,
13
+ blob: './config/application.rb',
14
+ }, {
15
+ type: :ruby,
16
+ blob: './lib/*/version.rb',
17
+ }, {
18
+ type: :node,
19
+ blob: './package.json',
20
+ },
21
+ ].freeze
22
+
23
+ def self.all
24
+ @all ||= Afterlife.config.repos[:cdnize]&.flat_map do |path|
25
+ repo = Repo.new(path)
26
+ packages = repo.conf.dig(:cdn, :packages)
27
+ next repo unless packages
28
+
29
+ packages.map do |package|
30
+ Repo.new("#{path}/packages/#{package}")
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.base_path
36
+ @base_path ||= Pathname(Afterlife.config.repos[:path])
37
+ end
38
+
39
+ attr_reader :full_path
40
+
41
+ def initialize(path)
42
+ @full_path = path.start_with?('/') ? Pathname(path) : Repo.base_path.join(path)
43
+ end
44
+
45
+ def dist_path
46
+ full_path.join(conf.dig(:cdn, :dist) || DEFAULT_DIST)
47
+ end
48
+
49
+ def env
50
+ @env ||= Environment.from(self)
51
+ end
52
+
53
+ def install_dependencies_command
54
+ return DEFAULT_INSTALL unless conf.key?(:install)
55
+
56
+ conf[:install]
57
+ end
58
+
59
+ def build_command
60
+ return DEFAULT_BUILD_COMMAND unless conf.key?(:build)
61
+
62
+ conf[:build]
63
+ end
64
+
65
+ def confirmation_message
66
+ conf.dig(:deploy, :confirmation_message)
67
+ end
68
+
69
+ def current_branch
70
+ @current_branch ||= `git rev-parse --abbrev-ref HEAD`.chomp
71
+ end
72
+
73
+ def current_revision
74
+ @current_revision ||= `git rev-parse HEAD`.chomp
75
+ end
76
+
77
+ def name
78
+ full_name.split('/').last.gsub('-', '_')
79
+ end
80
+
81
+ def full_name
82
+ package_json[:name]
83
+ end
84
+
85
+ # Files that define versions
86
+ def version_files
87
+ @version_files ||= conf.dig(:release, :version_files)
88
+ @version_files ||= VERSION_FILES.map do |arg|
89
+ val = arg.dup
90
+ val[:real_path] = Dir[val[:blob]].first
91
+ next unless val[:real_path]
92
+
93
+ val
94
+ end.compact
95
+ end
96
+
97
+ def version
98
+ # this migth not be the same as the package.json
99
+ # Bump::Semver.version
100
+ package_json[:version]
101
+ end
102
+
103
+ def package_json
104
+ @pkg_path ||= conf.dig(:cdn, :package_json) || full_path.join('package.json')
105
+ @package_json ||= JSON.parse(File.read(@pkg_path), symbolize_names: true)
106
+ end
107
+
108
+ def branch_conf
109
+ conf.dig(:deploy, :env, :by_branch, current_branch.to_sym)
110
+ end
111
+
112
+ def conf
113
+ @conf ||= Config.new(full_path).config
114
+ end
115
+
116
+ class Config
117
+ CONFIG_NAME = '.afterlife.yml'
118
+
119
+ attr_reader :full_path
120
+
121
+ def initialize(full_path)
122
+ @full_path = full_path
123
+ end
124
+
125
+ def config
126
+ return @config if defined?(@config)
127
+
128
+ if config_file.exist?
129
+ @config = base_config
130
+ @config = parent.deep_merge(base_config) if parent
131
+ end
132
+
133
+ @config ||= {}
134
+ end
135
+
136
+ private
137
+
138
+ def parent
139
+ return @parent if defined?(@parent)
140
+
141
+ inherit_from = base_config.delete(:inherit_from)
142
+ return @parent = nil unless inherit_from
143
+
144
+ @parent = Config.new(full_path.join(inherit_from)).config
145
+ @parent[:cdn]&.delete(:packages)
146
+ @parent
147
+ end
148
+
149
+ def base_config
150
+ @base_config ||= YAML.load_file(config_file).deep_symbolize_keys
151
+ end
152
+
153
+ def config_file
154
+ @config_file ||= full_path.join(CONFIG_NAME)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ Stage = Struct.new(
5
+ :name,
6
+ :bucket,
7
+ :region,
8
+ :cdn_url,
9
+ keyword_init: true,
10
+ )
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ VERSION = '1.0.0'
5
+ end
data/lib/afterlife.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require 'git'
5
+ require 'pry'
6
+ require 'yaml'
7
+ require 'pathname'
8
+ require 'active_support'
9
+ require_relative 'afterlife/version'
10
+
11
+ module Afterlife
12
+ LOCAL_PATH = '~/.afterlife'
13
+
14
+ class Error < StandardError; end
15
+
16
+ def self.root
17
+ @root
18
+ end
19
+
20
+ def self.root=(other)
21
+ @root = other
22
+ end
23
+
24
+ def self.local_path
25
+ @local_path ||= Pathname(File.expand_path(LOCAL_PATH))
26
+ end
27
+
28
+ def self.config
29
+ @config ||= Config.new
30
+ end
31
+
32
+ def self.current_repo
33
+ @current_repo ||= Repo.new(Dir.pwd)
34
+ end
35
+
36
+ def self.current_stage=(other)
37
+ @current_stage = other
38
+ end
39
+
40
+ def self.current_stage
41
+ @current_stage
42
+ end
43
+
44
+ def self.cli=(other)
45
+ @cli = other
46
+ end
47
+
48
+ def self.cli
49
+ @cli
50
+ end
51
+ end
52
+
53
+ loader = Zeitwerk::Loader.for_gem
54
+ loader.tag = 'afterlife'
55
+ loader.setup
56
+ loader.eager_load
data/logs/.keep ADDED
File without changes