afterlife 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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