afterlife 1.2.1 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a748ce7d12a9ee07b9811468a33b25852ab857dc26e35af99ace77826f150c3
4
- data.tar.gz: d969d2522b6cf783cb43426d3f8556ac5049f009fad5fba08b16065a91fd15cc
3
+ metadata.gz: 50bb80b8a5c213397eb84b52d78f39fc6ff72491a45df415e0c7700d1c2eb4bc
4
+ data.tar.gz: d6a8963da3e0d48f511e182d4bcf5c49f20e4e7449e29e9d8cc1f35c2a43fcc8
5
5
  SHA512:
6
- metadata.gz: 873c5cfa91704940ccbc7fff7a70d339c9f73271c63ba60e2f2b7011e61d58b601595982027f26e94f006ece37f2abd4061ac05179ba0a0f7ed444ce21dfc735
7
- data.tar.gz: 1e064e211cd77e8ae137e047eaa877b9174ba6098296c8723aa05d2a9f6bf7bc9268ffa8e43ea877e25b05613499588548e3ee09120e5702133ba380c1e04156
6
+ metadata.gz: '0594d2bce7f949b133c67536d8a3115b2bcdc3a12dc6826135335d6aa5d1cd5e0bd938088a5437bb0381aef2b509b06f990c5268b5a21e8d667882b1355ed274'
7
+ data.tar.gz: 8b40060493294350391a2aec32c866cadd04a7aaf087ada970fb267430e836c3f724b4dc1ef683beb3337e59cd8b3a77f420afd168830d6972b7c8ee301e8fb8
data/lib/afterlife/cli.rb CHANGED
@@ -31,12 +31,13 @@ module Afterlife
31
31
  desc 'deploy <stage>', 'Deploy current repo'
32
32
  option 'no-build', type: :boolean
33
33
  option 'no-install', type: :boolean
34
+ option 'no-auth', type: :boolean
34
35
  option 'dry-run', type: :boolean
35
36
  option 'skip-after-hooks', type: :boolean
36
37
  option :yes, type: :boolean
37
38
  def deploy(stage)
38
39
  invoke Deploy::Cli, :call, [stage], options
39
- rescue Deploy::Error
40
+ rescue Error
40
41
  fatal!(e.message)
41
42
  rescue Interrupt
42
43
  log_interrupted
@@ -3,10 +3,6 @@
3
3
  module Afterlife
4
4
  module Config
5
5
  class Provider
6
- def self.file
7
- Afterlife.local_path.join('config.yml')
8
- end
9
-
10
6
  def respond_to_missing?(sym, include_all)
11
7
  config.key?(sym) || super
12
8
  end
@@ -15,6 +11,12 @@ module Afterlife
15
11
  config.key?(sym) ? config[sym] : super
16
12
  end
17
13
 
14
+ def file_exist?
15
+ return @file_exist if defined?(@file_exist)
16
+
17
+ @file_exist ||= file.exist?
18
+ end
19
+
18
20
  def stages
19
21
  @stages ||= config.delete(:stages)&.to_h do |name, val|
20
22
  [name, Stage.new(val.merge(name: name.to_s))]
@@ -24,7 +26,28 @@ module Afterlife
24
26
  private
25
27
 
26
28
  def config
27
- @config ||= YAML.load_file(Provider.file).deep_symbolize_keys
29
+ @config ||= !file_exist? && default_config
30
+ @config ||= YAML.load_file(file).deep_symbolize_keys
31
+ end
32
+
33
+ def default_config
34
+ {
35
+ credentials: {
36
+ aws: {
37
+ profile: ENV.fetch('AWS_PROFILE', nil),
38
+ },
39
+ },
40
+ stages: {},
41
+ }
42
+ end
43
+
44
+ def file
45
+ Pathname(
46
+ ENV.fetch(
47
+ 'AFTERLIFE_CONFIG_PATH',
48
+ Afterlife.local_path.join('config.yml'),
49
+ ),
50
+ )
28
51
  end
29
52
  end
30
53
  end
@@ -12,12 +12,14 @@ module Afterlife
12
12
  end
13
13
 
14
14
  def setup
15
- repo.env.merge!(
16
- 'AWS_PROFILE' => Afterlife.config.credentials.dig(:aws, :profile),
15
+ # only use the defined AWS_PROFILE if its not already been defined
16
+ repo.env.set(
17
+ 'AWS_PROFILE' => Afterlife.config&.credentials&.dig(:aws, :profile),
17
18
  'AWS_BUCKET' => Afterlife.current_stage.bucket,
18
19
  'AWS_REGION' => Afterlife.current_stage.region,
19
- 'S3_FULL_PATH' => s3_full_path,
20
20
  )
21
+ # in nested deployments we need the current path, not the parent's
22
+ repo.env.set!('S3_FULL_PATH' => s3_full_path)
21
23
  end
22
24
 
23
25
  def initial_message
@@ -33,14 +35,12 @@ module Afterlife
33
35
 
34
36
  def commands
35
37
  [
36
- repo_command || javascripts,
37
- revision,
38
- ].flatten
38
+ repo_command || upload_javascripts,
39
+ upload_revision,
40
+ ]
39
41
  end
40
42
 
41
- def javascripts
42
- # by default we use no-cache, if something else is needed
43
- # do it in the deploy.command option
43
+ def upload_javascripts
44
44
  <<-BASH
45
45
  aws s3 sync
46
46
  %<DIST_PATH>s
@@ -52,7 +52,7 @@ module Afterlife
52
52
  BASH
53
53
  end
54
54
 
55
- def revision
55
+ def upload_revision
56
56
  <<-BASH
57
57
  echo "#{repo.current_revision}" |
58
58
  aws s3 cp --content-type text/plain --cache-control 'no-cache' - #{s3_revision_path}
@@ -7,19 +7,31 @@ module Afterlife
7
7
 
8
8
  def commands
9
9
  [
10
- add_revision_command,
11
- repo_command || javascripts,
12
- revision,
10
+ repo_command || [upload_parts, upload_esm],
11
+ upload_revision,
13
12
  ]
14
13
  end
15
14
 
16
- def add_revision_command
17
- Dir["#{repo.dist_path}/*.esm.js"].map do |file|
18
- next if file.include?(repo.current_revision)
15
+ def upload_parts
16
+ <<-BASH
17
+ aws s3 sync
18
+ %<DIST_PATH>s
19
+ %<S3_FULL_PATH>s
20
+ --exclude '*.ts'
21
+ --exclude '*.tsx'
22
+ --exclude '*.esm.js'
23
+ --size-only
24
+ --cache-control 'public, max-age=31540000'
25
+ BASH
26
+ end
19
27
 
20
- new_name = file.gsub('.esm.js', "-#{repo.current_revision}.esm.js")
21
- "mv #{file} #{new_name}"
22
- end
28
+ def upload_esm
29
+ <<-BASH
30
+ aws s3 cp
31
+ --cache-control 'public, max-age=31540000'
32
+ #{repo.dist_path}/#{repo.name}.esm.js
33
+ #{s3_full_path}/#{repo.name}-#{repo.current_revision}.esm.js
34
+ BASH
23
35
  end
24
36
 
25
37
  def s3_full_path
@@ -11,14 +11,14 @@ module Afterlife
11
11
  desc 'deploy <stage>', 'Deploy current repo'
12
12
  def call(stage)
13
13
  Afterlife.cli = self
14
- Deploy.call(stage) do |deployment|
14
+ Deploy.call(stage, options) do |deployment|
15
15
  setup_deploy(deployment)
16
16
  say_status 'Deploying', deployment.initial_message
17
17
  deployment.run
18
18
  say_status 'Deployed', deployment.output
19
19
  run_after_hooks
20
20
  end
21
- rescue Deploy::Error => e
21
+ rescue Afterlife::Error => e
22
22
  fatal!(e.message)
23
23
  end
24
24
 
@@ -3,6 +3,13 @@
3
3
  module Afterlife
4
4
  module Deploy
5
5
  class Deployment
6
+
7
+ attr_reader :options
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
6
13
  def run
7
14
  fail NotImplementedError
8
15
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ module Deploy
5
+ class KubernetesDeployment < Deployment
6
+ def setup
7
+ Afterlife.current_repo.env.set('PLATFORM' => 'linux/amd64') if deploy_stage?
8
+
9
+ Afterlife.current_repo.env.set!(
10
+ 'REGISTRY' => registry,
11
+ 'TAG' => repo.current_revision,
12
+ )
13
+ end
14
+
15
+ def run
16
+ Exec.run(commands)
17
+ end
18
+
19
+ def confirmation_message
20
+ 'You are about to deploy the current directory'
21
+ end
22
+
23
+ private
24
+
25
+ def commands
26
+ [
27
+ authenticate_command,
28
+ build_command,
29
+ set_image_command,
30
+ local_stage? ? delete_kubernetes_resource : nil,
31
+ apply_kubernetes_settings,
32
+ ].flatten.compact
33
+ end
34
+
35
+ # commands
36
+
37
+ def authenticate_command
38
+ return if options['no-auth']
39
+
40
+ <<-BASH
41
+ echo "#{aws_ecr_token}" | docker login --username AWS --password-stdin #{registry} &&
42
+ kubectl delete secret regcred &&
43
+ kubectl create secret docker-registry regcred
44
+ --docker-server=#{registry}
45
+ --docker-username=AWS
46
+ --docker-password=#{aws_ecr_token}
47
+ --docker-email=devs@mifiel.com
48
+ BASH
49
+ end
50
+
51
+ def build_command
52
+ return if options['no-build']
53
+
54
+ <<-BASH
55
+ docker buildx bake -f docker-bake.hcl #{local_stage? ? '--load' : '--push'}
56
+ BASH
57
+ end
58
+
59
+ def set_image_command
60
+ <<-BASH
61
+ $(cd #{kubelocation} && kustomize edit set image #{image_name}:latest=#{full_image_name})
62
+ BASH
63
+ end
64
+
65
+ def delete_kubernetes_resource
66
+ <<-BASH
67
+ kubectl delete -k #{kubelocation}
68
+ BASH
69
+ end
70
+
71
+ def apply_kubernetes_settings
72
+ <<-BASH
73
+ kubectl apply -k #{kubelocation}
74
+ BASH
75
+ end
76
+
77
+ # utils
78
+
79
+ def local_stage?
80
+ Afterlife.current_stage.name.to_sym == :local
81
+ end
82
+
83
+ def deploy_stage?
84
+ %i[qa production staging].include?(Afterlife.current_stage.name.to_sym)
85
+ end
86
+
87
+ def kubelocation
88
+ ".afterlife/#{Afterlife.current_stage.name}"
89
+ end
90
+
91
+ def full_image_name
92
+ "#{registry}/#{image_name}:#{repo.current_revision}"
93
+ end
94
+
95
+ def image_name
96
+ @image_name ||= repo.conf.dig(:deploy, :image_name)
97
+ end
98
+
99
+ # Priority:
100
+ # 1. Uses AFTERLIFE_DEPLOY_REGISTRY from the ENV if it's defined. It can be
101
+ # either in real system ENV, or defined in the env section of the repo
102
+ # .afterlife.yml config
103
+ # 2. Uses repo .afterlife.yml config at deploy.registry
104
+ # 3. Uses stages.$current_stage.registry if exists in ~/.afterlife/config.yml
105
+ def registry
106
+ @registry ||= Afterlife.current_repo.variable('deploy.registry') || Afterlife.current_stage.registry
107
+ end
108
+
109
+ # command outputs
110
+
111
+ def aws_ecr_token
112
+ @aws_ecr_token ||= `aws ecr get-login-password --region us-west-2`.strip
113
+ end
114
+ end
115
+ end
116
+ end
@@ -4,11 +4,9 @@ module Afterlife
4
4
  module Deploy
5
5
  module_function
6
6
 
7
- Error = Class.new StandardError
8
-
9
- def call(stage)
10
- deployment = klass.new
11
- setup_stage(stage)
7
+ def call(stage, options)
8
+ deployment = klass.new(options)
9
+ Afterlife.current_stage = stage
12
10
  fill_env
13
11
  deployment.setup
14
12
  yield deployment
@@ -28,16 +26,9 @@ module Afterlife
28
26
  end
29
27
  end
30
28
 
31
- def setup_stage(stage)
32
- unless Afterlife.config.stages.key?(stage.to_sym)
33
- fail Error, "invalid stage '#{stage}'. Possible values: #{Afterlife.config.stages.keys.map(&:to_s)}"
34
- end
35
-
36
- Afterlife.current_stage = Afterlife.config.stages[stage.to_sym]
37
- end
38
-
39
29
  def fill_env
40
- Afterlife.current_repo.env.merge!(
30
+ # in nested deployments we need the current definition, not the parent's
31
+ Afterlife.current_repo.env.set!(
41
32
  'DIST_PATH' => Afterlife.current_repo.dist_path.to_s,
42
33
  'CURRENT_BRANCH' => Afterlife.current_repo.current_branch,
43
34
  'AFTERLIFE_STAGE' => Afterlife.current_stage.name,
@@ -5,7 +5,7 @@ module Afterlife
5
5
  attr_reader :repo, :env
6
6
 
7
7
  def self.from(repo)
8
- env = new(repo).tap(&:call).env
8
+ env = new(repo).tap(&:call)
9
9
  yield env if block_given?
10
10
  env
11
11
  end
@@ -21,24 +21,38 @@ module Afterlife
21
21
  from_stage_envs
22
22
  end
23
23
 
24
- def env_hash_by_branch
25
- @env_hash_by_branch ||= env_hash&.dig(:by_branch, repo.current_branch.to_sym)
24
+ def [](key)
25
+ key = key.gsub('.', '_').upcase
26
+ env_name = "AFTERLIFE_#{key}"
27
+ return ENV[env_name] if ENV.key?(env_name)
28
+
29
+ @env[env_name]
26
30
  end
27
31
 
28
- def env_hash_by_stage
29
- @env_hash_by_stage ||= env_hash&.dig(:by_stage, Afterlife.current_stage.name.to_sym)
32
+ # soft set, do not override existent ENVs
33
+ def set(arg)
34
+ arg.each do |k, v|
35
+ fail ArgumentError, "key '#{k}' must be a string" unless k.is_a?(String)
36
+
37
+ @env[k] ||= ENV.fetch(k, v)
38
+ end
30
39
  end
31
40
 
32
- def env_hash
33
- @env_hash ||= repo.conf.dig(:deploy, :env)
41
+ # forces to be exactly dthe value
42
+ def set!(arg)
43
+ arg.each do |k, v|
44
+ fail ArgumentError, "key '#{k}' must be a string" unless k.is_a?(String)
45
+
46
+ @env[k] = v
47
+ end
34
48
  end
35
49
 
36
50
  private
37
51
 
38
52
  def from_flat_envs
39
- return unless env_hash.is_a?(Hash)
53
+ return unless repo_env_config.is_a?(Hash)
40
54
 
41
- env_hash.each do |key, value|
55
+ repo_env_config.each do |key, value|
42
56
  next if key == :by_branch
43
57
  next if key == :by_stage
44
58
 
@@ -54,6 +68,10 @@ module Afterlife
54
68
  end
55
69
  end
56
70
 
71
+ def env_hash_by_branch
72
+ @env_hash_by_branch ||= repo_env_config&.dig(:by_branch, repo.current_branch.to_sym)
73
+ end
74
+
57
75
  def from_stage_envs
58
76
  return unless env_hash_by_stage.is_a?(Hash)
59
77
 
@@ -61,5 +79,15 @@ module Afterlife
61
79
  @env[key.to_s] = value.to_s
62
80
  end
63
81
  end
82
+
83
+ def env_hash_by_stage
84
+ return unless Afterlife.current_stage
85
+
86
+ @env_hash_by_stage ||= repo_env_config&.dig(:by_stage, Afterlife.current_stage.name.to_sym)
87
+ end
88
+
89
+ def repo_env_config
90
+ @repo_env_config ||= repo.conf.dig(:deploy, :env) || {}
91
+ end
64
92
  end
65
93
  end
@@ -26,18 +26,22 @@ module Afterlife
26
26
  def run
27
27
  parsed_commands.each do |command|
28
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']
29
+ system(env_hash, command.squish, exception: true) unless Afterlife.cli.options['dry-run']
30
30
  end
31
31
  rescue RuntimeError => e
32
32
  raise Error, e
33
33
  end
34
34
 
35
35
  def parsed_commands
36
- Exec.parse(commands, repo.env)
36
+ Exec.parse(commands, env_hash)
37
37
  end
38
38
 
39
39
  def repo
40
40
  Afterlife.current_repo
41
41
  end
42
+
43
+ def env_hash
44
+ repo.env.env
45
+ end
42
46
  end
43
47
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Afterlife
4
4
  class Repo
5
+ # Reads configuration from the repo,
6
+ # either from YML or from the repo instance methods
5
7
  class Config
6
8
  Error = Class.new StandardError
7
9
 
@@ -12,7 +14,7 @@ module Afterlife
12
14
  end
13
15
 
14
16
  def initialize(path)
15
- @path = path.split('.').map(&:to_sym)
17
+ @path = path
16
18
  end
17
19
 
18
20
  def read(&block) # rubocop:disable Metrics/MethodLength
@@ -24,7 +26,7 @@ module Afterlife
24
26
  when String, Symbol
25
27
  yield(from_yml)
26
28
  else
27
- fail Error, "No config with path '#{path.join('.')}'" unless repo_method?
29
+ fail Error, "No config with path '#{path}'" unless repo_method?
28
30
 
29
31
  yield(repo.public_send(repo_method))
30
32
  end
@@ -33,7 +35,7 @@ module Afterlife
33
35
  private
34
36
 
35
37
  def repo_method?
36
- path.count == 1 && repo.public_methods(false).include?(repo_method)
38
+ path.count('.').zero? && repo.public_methods(false).include?(repo_method)
37
39
  end
38
40
 
39
41
  def repo_method
@@ -47,7 +49,7 @@ module Afterlife
47
49
  def from_yml
48
50
  return @from_yml if defined?(@from_yml)
49
51
 
50
- @from_yml ||= repo.conf.dig(*path)
52
+ @from_yml ||= repo.variable(path)
51
53
  end
52
54
  end
53
55
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Afterlife
4
4
  class Repo
5
+ # Reads (and builds) the .afterlife.yml file in the repo
5
6
  class Provider
6
7
  CONFIG_NAME = '.afterlife.yml'
7
8
 
@@ -46,6 +46,14 @@ module Afterlife
46
46
  full_path.join(conf.dig(:cdn, :dist) || DEFAULT_DIST)
47
47
  end
48
48
 
49
+ # 1. Searchs for the AFTERLIFE_#{name} env var
50
+ # 2. Searchs the path in the repo .afterlife.yml config
51
+ def variable(name)
52
+ return env[name] if env[name]
53
+
54
+ conf.dig(*name.split('.').map(&:to_sym))
55
+ end
56
+
49
57
  def env
50
58
  @env ||= Environment.from(self)
51
59
  end
@@ -79,7 +87,7 @@ module Afterlife
79
87
  end
80
88
 
81
89
  def name
82
- full_name.split('/').last.gsub('-', '_')
90
+ full_name.split('/').last
83
91
  end
84
92
 
85
93
  def full_name
@@ -109,10 +117,6 @@ module Afterlife
109
117
  @package_json ||= JSON.parse(File.read(@pkg_path), symbolize_names: true)
110
118
  end
111
119
 
112
- def branch_conf
113
- conf.dig(:deploy, :env, :by_branch, current_branch.to_sym)
114
- end
115
-
116
120
  def conf
117
121
  @conf ||= Provider.new(full_path).config
118
122
  end
@@ -6,6 +6,27 @@ module Afterlife
6
6
  :bucket,
7
7
  :region,
8
8
  :cdn_url,
9
+ :registry,
9
10
  keyword_init: true,
10
- )
11
+ ) do
12
+ def self.build(name = nil)
13
+ Stage.new(
14
+ name: ENV.fetch('AFTERLIFE_STAGE', name),
15
+ bucket: ENV.fetch('AWS_BUCKET', nil),
16
+ region: ENV.fetch('AWS_REGION'),
17
+ cdn_url: ENV.fetch('AWS_BUCKET', nil),
18
+ registry: ENV.fetch('AFTERLIFE_DEPLOY_REGISTRY', nil),
19
+ )
20
+ end
21
+
22
+ def self.find(stage)
23
+ return build(stage) unless Afterlife.config.file_exist?
24
+
25
+ unless Afterlife.config.stages.key?(stage.to_sym)
26
+ fail Error, "invalid stage '#{stage}'. Possible values: #{Afterlife.config.stages.keys.map(&:to_s)}"
27
+ end
28
+
29
+ Afterlife.config.stages[stage.to_sym]
30
+ end
31
+ end
11
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Afterlife
4
- VERSION = '1.2.1'
4
+ VERSION = '1.4.0'
5
5
  end
data/lib/afterlife.rb CHANGED
@@ -33,7 +33,7 @@ module Afterlife
33
33
  end
34
34
 
35
35
  def self.current_stage=(other)
36
- @current_stage = other
36
+ @current_stage = Stage.find(other)
37
37
  end
38
38
 
39
39
  def self.current_stage
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: afterlife
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Genaro Madrid
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-16 00:00:00.000000000 Z
11
+ date: 2024-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -130,6 +130,7 @@ files:
130
130
  - lib/afterlife/deploy/cli.rb
131
131
  - lib/afterlife/deploy/custom_deployment.rb
132
132
  - lib/afterlife/deploy/deployment.rb
133
+ - lib/afterlife/deploy/kubernetes_deployment.rb
133
134
  - lib/afterlife/environment.rb
134
135
  - lib/afterlife/exec.rb
135
136
  - lib/afterlife/release.rb