brut 0.18.2 → 0.19.1

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.
@@ -1,13 +1,29 @@
1
1
  require "brut/cli"
2
+ require "fileutils"
3
+ require "pathname"
2
4
 
3
5
  class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
6
+ def name = "deploy"
7
+
4
8
  def description = "Deploy your Brut-powered app to production"
5
9
 
6
10
  def default_rack_env = nil
7
11
 
8
12
  class Heroku < Brut::CLI::Commands::BaseCommand
9
13
  def description = "Deploy to Heroku using container-based deployment"
14
+ def opts = [
15
+ [ "--[no-]deploy", "If true, actually deploy the pushed images (default true)" ],
16
+ [ "--skip-checks", "If true, skip pre-build checks" ],
17
+ ]
18
+
19
+ def default_rack_env = "development"
20
+
10
21
  def run
22
+ if delegate_to_command(Brut::CLI::Apps::Deploy::Build.new) != 0
23
+ error "<== Build failed."
24
+ return 1
25
+ end
26
+ options.set_default(:deploy, true)
11
27
  version = ""
12
28
  git_guess = %{git rev-parse HEAD}
13
29
  system!(git_guess) do |output|
@@ -15,57 +31,53 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
15
31
  end
16
32
  version.strip!.chomp!
17
33
  if version == ""
18
- stderr.puts "Attempt to use git via command '#{git_guess}' to figure out the version failed"
34
+ error "Attempt to use git via command '#{git_guess}' to figure out the version failed"
19
35
  return 1
20
36
  end
21
37
  short_version = version[0..7]
22
38
  app_docker_files = AppDockerImages.new(
23
39
  project_root: Brut.container.project_root,
24
- organization: Brut.container.organization,
40
+ organization: Brut.container.app_organization,
25
41
  app_id: Brut.container.app_id,
26
42
  short_version:
27
43
  )
28
- #add heroku_image_name: "registry.heroku.com/#{heroku_app_name}/web",
29
- #stdout.puts "Taggging images for Heroku"
30
- #images.each do |name,metadata|
31
- # image_name = metadata.fetch(:image_name)
32
- # heroku_image_name = metadata.fetch(:heroku_image_name)
33
- #
34
- # stdout.puts "Tagging '#{image_name}' with '#{heroku_image_name}' for Heroku"
35
- # command = %{docker tag #{image_name} #{heroku_image_name}}
36
- # system!(command)
37
- # end
38
- #
39
- # if options.push?
40
- # stdout.puts "Pushing to Heroku Registry"
41
- # images.each do |name,metadata|
42
- # heroku_image_name = metadata.fetch(:heroku_image_name)
43
- #
44
- # stdout.puts "Pushing '#{heroku_image_name}'"
45
- #
46
- # command = %{docker push #{docker_quiet_option} #{heroku_image_name}}
47
- # system!(command)
48
- # end
49
- # else
50
- # stdout.puts "Not pushing images"
51
- # end
52
- #
53
- # names = images.map(&:first).join(" ")
54
- # deploy_command = "heroku container:release #{names} -a #{heroku_app_name}"
55
- # if options.deploy?
56
- # stdout.puts "Deploying images to Heroku"
57
- # system!(deploy_command)
58
- # else
59
- # stdout.puts "Not deploying. To deploy the images just pushed:"
60
- # stdout.puts ""
61
- # stdout.puts " #{deploy_command}"
62
- # end
63
- # end
44
+ names = []
45
+ puts "Logging in to Heroku Container Registry"
46
+ command = %{heroku container:login}
47
+ system!(command)
48
+ app_docker_files.each do |name:, image_name:|
49
+ heroku_image_name = "registry.heroku.com/#{Brut.container.app_id}/#{name}"
50
+ puts "Tagging '#{image_name}' with '#{heroku_image_name}' for Heroku"
51
+ command = %{docker tag #{image_name} #{heroku_image_name}}
52
+ system!(command)
53
+ begin
54
+ puts "Pushing '#{heroku_image_name}'"
55
+ command = %{docker push #{heroku_image_name}}
56
+ system!(command)
57
+ rescue Brut::CLI::SystemExecError => ex
58
+ error "Failed to push image '#{heroku_image_name}' to Heroku"
59
+ if options.log_level != "debug"
60
+ error "Could be you must re-authenticate to Heroku."
61
+ error "Try re-running with --log-level=debug to see more details"
62
+ end
63
+ return 1
64
+ end
65
+ names << name
66
+ end
67
+ deploy_command = "heroku container:release #{names.join(' ')} -a #{Brut.container.app_id}"
68
+ if options.deploy?
69
+ puts "Deploying images to Heroku"
70
+ system!(deploy_command)
71
+ else
72
+ puts "Not deploying. To deploy the images just pushed:"
73
+ puts ""
74
+ puts " #{deploy_command}"
75
+ end
64
76
  end
65
77
  end
66
78
 
67
79
  class AppDockerImages
68
- attr_reader :docker_config_filename
80
+ attr_reader :docker_config_filename, :platform
69
81
  def initialize(organization:, app_id:, project_root:, short_version:)
70
82
  @docker_config_filename = project_root / "deploy" / "docker_config"
71
83
  require_relative @docker_config_filename
@@ -74,10 +86,11 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
74
86
  end
75
87
 
76
88
  docker_config = DockerConfig.new
89
+ @platform = docker_config.platform
77
90
 
78
- additional_images = (docker_confir.additional_images || {}).map { |name,config|
91
+ additional_images = (docker_config.additional_images || {}).map { |name,config|
79
92
  cmd = config.fetch(:cmd)
80
- image_name = %{#{app_organization}/#{app_id}:#{short_version}-#{name}}
93
+ image_name = %{#{Brut.container.app_organization}/#{Brut.container.app_id}:#{short_version}-#{name}}
81
94
  [
82
95
  name,
83
96
  {
@@ -91,12 +104,12 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
91
104
  @images = {
92
105
  "web" => {
93
106
  cmd: "bin/run",
94
- image_name: %{#{app_organization}/#{app_id}:#{short_version}-web},
107
+ image_name: %{#{Brut.container.app_organization}/#{app_id}:#{short_version}-web},
95
108
  dockerfile: "deploy/Dockerfile.web",
96
109
  },
97
110
  "release" => {
98
- cmd: "bin/run",
99
- image_name: %{#{app_organization}/#{app_id}:#{short_version}-web},
111
+ cmd: "bin/release",
112
+ image_name: %{#{Brut.container.app_organization}/#{app_id}:#{short_version}-release},
100
113
  dockerfile: "deploy/Dockerfile.release",
101
114
  },
102
115
  }.merge(additional_images)
@@ -108,7 +121,7 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
108
121
  end
109
122
  @images.each do |name,metadata|
110
123
  args = {}
111
- block.params.each do |(_,param)|
124
+ block.parameters.each do |(_,param)|
112
125
  if param == :name
113
126
  args[:name] = name
114
127
  else
@@ -121,16 +134,29 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
121
134
  end
122
135
 
123
136
  class Build < Brut::CLI::Commands::BaseCommand
124
- def description = "Build artifacts for deployment"
137
+ def description = Docker.new.description
138
+ def opts = Docker.new.opts
139
+ def default_rack_env = Docker.new.default_rack_env
140
+ def run = delegate_to_command(Docker.new)
141
+
142
+ def commands = []
125
143
 
126
144
  class Docker < Brut::CLI::Commands::BaseCommand
127
145
  def description = "Build a series of Docker images from a template Dockerfile"
128
146
  def opts = [
129
147
  [ "--platform=PLATFORM","Override default platform. Can be any Docker platform." ],
130
148
  [ "--dry-run", "Only show what would happen, don't actually do anything" ],
149
+ [ "--skip-checks", "If true, skip pre-build checks (default )" ],
131
150
  ]
151
+ def default_rack_env = "development"
132
152
 
133
153
  def run
154
+ if !options.skip_checks?
155
+ if delegate_to_command(Brut::CLI::Apps::Deploy::Check.new) != 0
156
+ puts theme.error.render("Pre-build checks failed.")
157
+ return 1
158
+ end
159
+ end
134
160
  version = ""
135
161
  git_guess = %{git rev-parse HEAD}
136
162
  system!(git_guess) do |output|
@@ -138,24 +164,29 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
138
164
  end
139
165
  version.strip!.chomp!
140
166
  if version == ""
141
- stderr.puts "Attempt to use git via command '#{git_guess}' to figure out the version failed"
167
+ error "Attempt to use git via command '#{git_guess}' to figure out the version failed"
142
168
  return 1
143
169
  end
144
170
  short_version = version[0..7]
145
171
 
146
- app_docker_images = AppDockerImages.new(
172
+ short_version = version[0..7]
173
+ app_docker_files = AppDockerImages.new(
147
174
  project_root: Brut.container.project_root,
148
- organization: Brut.container.organization,
175
+ organization: Brut.container.app_organization,
149
176
  app_id: Brut.container.app_id,
150
177
  short_version:
151
178
  )
179
+ options.set_default(:platform, app_docker_files.platform || "linux/amd64")
152
180
 
153
181
  FileUtils.chdir Brut.container.project_root do
154
182
 
155
- stdout.puts "Generating Dockerfiles"
156
- images.each do |name:, cmd:, dockerfile:|
183
+ puts
184
+ puts theme.header.render("Generating Dockerfiles")
185
+ puts
186
+ rows = []
187
+ app_docker_files.each do |name:, cmd:, dockerfile:|
157
188
 
158
- stdout.puts "Creating '#{dockerfile}' for '#{name}' that will use command '#{cmd}'"
189
+ rows << [theme.subheader.render(name), theme.code.render(dockerfile), theme.code.render(cmd) ]
159
190
 
160
191
  if !options.dry_run?
161
192
  File.open(dockerfile,"w") do |file|
@@ -168,119 +199,165 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
168
199
  end
169
200
  end
170
201
  end
202
+ table = Lipgloss::Table.new.headers(["Name", "Dockerfile", "CMD"]).
203
+ rows(rows).
204
+ style_func(rows: rows.length, columns: 3) { Lipgloss::Style.new.padding_right(1).padding_left(1) }
205
+ puts table.render
171
206
 
172
- stdout.puts "Building images"
173
- docker_quiet_option = if global_options.log_level != "debug"
174
- "--quiet"
175
- else
176
- ""
177
- end
178
- images.each do |image_name:, dockerfile:|
179
- stdout.puts "Creating docker image with name '#{image_name}' and platform '#{platform}'"
180
- command = %{docker build #{docker_quiet_option} --build-arg app_git_sha1=#{version} --file #{Brut.container.project_root}/#{dockerfile} --platform #{platform} --tag #{image_name} .}
181
- if options.dry_run?
182
- stdout.puts "Would run '#{command}'"
183
- else
207
+ puts
208
+ puts theme.header.render("Images")
209
+ puts
210
+ rows = []
211
+ items = []
212
+ app_docker_files.each do |name:, image_name:, dockerfile:|
213
+ rows << [ name, theme.code.render(image_name) ]
214
+ command = %{docker build --build-arg app_git_sha1=#{version} --file #{Brut.container.project_root}/#{dockerfile} --platform #{options.platform} --tag #{image_name} . 2>&1}
215
+ items << theme.code.render(theme.wrap(command, first_indent: false, indent: 7, newline: " \\\n"))
216
+ if !options.dry_run?
217
+ puts theme.subheader.render("Building '#{name}' image")
184
218
  system!(command)
185
219
  end
186
220
  end
221
+ if options.dry_run?
222
+ table = Lipgloss::Table.new.headers(["Name", "Image Name" ]).
223
+ rows(rows).
224
+ style_func(rows: rows.length, columns: 3) { Lipgloss::Style.new.padding_right(1).padding_left(1) }
225
+ puts table.render
226
+ puts
227
+ puts theme.subheader.render("Commands:")
228
+ puts
229
+ puts Lipgloss::List.new.items(items).item_style(theme.code).render
230
+ end
187
231
  end
188
232
  end
189
233
  end
190
234
  end
191
235
 
192
- class Check < Brut::CLI::Commands::BaseCommand
236
+ class Check < Brut::CLI::Commands::BaseCommand
193
237
 
194
- def description = "Check that a deploy can be reasonably expected to succeed"
195
- def default_command_class = Git
196
- def opts = [
197
- [ "--[no-]check-branch", "If true, requires that you are on 'main' (default true)" ],
198
- [ "--[no-]check-changes", "If true, requires that you have committed all local changes (default true)" ],
199
- [ "--[no-]check-push", "If true, requires that you are in sync with origin/main (default true)" ],
200
- ]
238
+ def description = "Check that a deploy can be reasonably expected to succeed"
201
239
 
202
- class Git < Brut::CLI::Commands::BaseCommand
203
- def description = "Perform the check assuming Git is the version-control system"
204
- def opts = self.parent_command.opts
240
+ def opts = Git.new.opts
241
+ def run = delegate_to_command(Git.new)
242
+ def commands = []
205
243
 
206
- def run
244
+ class Git < Brut::CLI::Commands::BaseCommand
245
+ def description = "Perform the check assuming Git is the version-control system"
246
+ def opts = [
247
+ [ "--[no-]check-branch", "If true, requires that you are on 'main' (default true)" ],
248
+ [ "--[no-]check-changes", "If true, requires that you have committed all local changes (default true)" ],
249
+ [ "--[no-]check-push", "If true, requires that you are in sync with origin/main (default true)" ],
250
+ ]
207
251
 
208
- checks_ignored = 0
252
+ def run
253
+ puts theme.header.render("Checking Git repo to see if changes have all been pushed to main")
254
+ puts
209
255
 
210
- options.set_default(:check_branch, true)
211
- options.set_default(:check_changes, true)
212
- options.set_default(:check_push, true)
256
+ options.set_default(:check_branch, true)
257
+ options.set_default(:check_changes, true)
258
+ options.set_default(:check_push, true)
213
259
 
214
- branch = ""
215
- system!("git branch --show-current") do |output|
216
- branch << output
217
- end
218
- branch = branch.strip.chomp
219
- if branch != "main"
220
- stderr.puts "You are not on the 'main' branch, but on '#{branch}'"
221
- if options.check_branch?
222
- stderr.puts "You may only deploy from main"
223
- return 1
224
- else
225
- checks_ignored += 1
226
- stderr.puts "Ignoring..."
260
+ checks = []
261
+
262
+ branch = ""
263
+ system!("git branch --show-current") do |output|
264
+ branch << output
265
+ end
266
+ branch = branch.strip.chomp
267
+ checks << [
268
+ "Deploy from main",
269
+ ]
270
+ if branch != "main"
271
+ checks.last << "Currently on #{theme.code.render(branch)}"
272
+ checks.last << options.check_branch?
227
273
  end
228
- end
229
274
 
230
- system!("git status") do |*| # reset local caches to account for Docker/host wierdness
231
- # ignore
232
- end
233
- local_changes = ""
234
- system!("git diff-index --name-only HEAD --") do |output|
235
- local_changes << output
236
- end
237
- if local_changes.strip != ""
238
- stderr.puts "You have un-committed changes:"
239
- stderr.puts
240
- local_changes.split(/\n/).each do |change|
241
- checks_ignored += 1
242
- stderr.puts " #{change}"
275
+ system!("git status") do |*| # reset local caches to account for Docker/host wierdness
276
+ # ignore
243
277
  end
244
- stderr.puts
245
- if options.check_changes?
246
- stderr.puts "Commit or revert these, then push to origin"
247
- return 1
248
- else
249
- checks_ignored += 1
250
- stderr.puts "Ignoring..."
278
+ local_changes = ""
279
+ system!("git diff-index --name-only HEAD --") do |output|
280
+ local_changes << output
281
+ end
282
+ checks << [
283
+ "No un-committed changes",
284
+ ]
285
+ if local_changes.strip != ""
286
+ items = local_changes.split(/\n/)
287
+ list = Lipgloss::List.new.items(items).item_style(theme.error)
288
+ checks.last << "Files not committed:\n#{list.render.strip}\n"
289
+ checks.last << options.check_changes?
251
290
  end
252
- end
253
291
 
254
- rev_list = ""
255
- system!("git rev-list --left-right --count origin/main...main") do |output|
256
- rev_list << output
257
- end
258
- remote_ahead, local_ahead = rev_list.strip.chomp.split(/\t/,2).map(&:to_i)
259
- if remote_ahead != 0
260
- stderr.puts "There are commits in origin you don't have."
261
- if options.check_push?
262
- stderr.puts "Pull those in, re-run bin/ci, THEN deploy"
263
- return 1
264
- else
265
- checks_ignored += 1
266
- stderr.puts "Ignoring..."
292
+ rev_list = ""
293
+ system!("git rev-list --left-right --count origin/main...main") do |output|
294
+ rev_list << output
267
295
  end
268
- end
296
+ remote_ahead, local_ahead = rev_list.strip.chomp.split(/\t/,2).map(&:to_i)
297
+ checks << [
298
+ "Pulled from origin",
299
+ ]
300
+ if remote_ahead != 0
301
+ if remote_ahead == 1
302
+ checks.last << "There is 1 commit in origin you don't have"
303
+ else
304
+ checks.last << "There are #{remote_ahead} commits in origin you don't have"
305
+ end
306
+ checks.last << options.check_push?
307
+ end
308
+ checks << [
309
+ "Pushed to origin",
310
+ ]
269
311
 
270
- if local_ahead != 0
271
- stderr.puts "You have not pushed to origin."
272
- if options.check_push?
273
- stderr.puts "Push to origin before deploying"
312
+ if local_ahead != 0
313
+ if local_ahead == 1
314
+ checks.last << "There is 1 commit not pushed to origin"
315
+ else
316
+ checks.last << "There are #{local_ahead} commits not pushed to origin"
317
+ end
318
+ checks.last << options.check_push?
319
+ end
320
+
321
+ rows = []
322
+ checks.each do |(check,status,error)|
323
+ row = [ check ]
324
+ if status
325
+ if error
326
+ row << theme.error.render("FAILED")
327
+ else
328
+ row << theme.warning.render("Ignored")
329
+ end
330
+ row << theme.error.render(status)
331
+ else
332
+ row << theme.success.render("OK")
333
+ row << ""
334
+ end
335
+ rows << row
336
+ end
337
+ table = Lipgloss::Table.new.
338
+ headers(["Check", "Result", "Details"]).
339
+ rows(rows).
340
+ style_func(rows: rows.length, columns: 3) { |row,column|
341
+ if row == Lipgloss::Table::HEADER_ROW
342
+ Lipgloss::Style.new.inherit(theme.header).padding_left(1).padding_right(1)
343
+ elsif column == 0
344
+ Lipgloss::Style.new.inherit(theme.subheader).padding_left(1).padding_right(1).padding_bottom(1)
345
+ else
346
+ Lipgloss::Style.new.inherit(theme.none).padding_left(1).padding_right(1).padding_bottom(1)
347
+ end
348
+ }
349
+ puts table.render
350
+ checks_failed = checks.count { |(_,status,_)| status }
351
+ checks_failed_not_ignored = checks.count { |(_,status,error)| status && error }
352
+ if checks_failed > 0
353
+ if checks_failed_not_ignored > 0
354
+ puts theme.error.render("#{checks_failed} checks failed - aborting")
274
355
  return 1
275
356
  else
276
- checks_ignored += 1
277
- stderr.puts "Ignoring..."
357
+ puts theme.warning.render("#{checks_failed} checks failed but ignored")
278
358
  end
279
- end
280
- if checks_ignored == 0
281
- stdout.puts "All checks passed"
282
359
  else
283
- stdout.puts "#{checks_ignored} checks failed, but ignored"
360
+ puts theme.success.render("All checks passed")
284
361
  end
285
362
 
286
363
  0
@@ -288,4 +365,3 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
288
365
  end
289
366
  end
290
367
  end
291
-