afterlife 1.2.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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