belt 0.1.5 → 0.1.7
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 +4 -4
- data/lib/belt/assets/belt-default.jpg +0 -0
- data/lib/belt/assets/welcome.css +1 -1
- data/lib/belt/cli/app_detection.rb +23 -1
- data/lib/belt/cli/deploy_command.rb +334 -6
- data/lib/belt/cli/destroy_command.rb +287 -0
- data/lib/belt/cli/environment_command.rb +11 -0
- data/lib/belt/cli/frontend_command.rb +35 -3
- data/lib/belt/cli/frontend_setup_command.rb +5 -2
- data/lib/belt/cli/generate_command.rb +49 -19
- data/lib/belt/cli/new_command.rb +5 -5
- data/lib/belt/cli/routes_command/route_inference.rb +4 -0
- data/lib/belt/cli/server_command.rb +3 -3
- data/lib/belt/cli.rb +24 -3
- data/lib/belt/controllers/welcome_controller.rb +9 -65
- data/lib/belt/rendering.rb +136 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt/views/welcome/show.html.erb +51 -0
- data/lib/belt.rb +1 -0
- data/lib/belt_controller/base.rb +2 -0
- data/lib/templates/environment/main.tf.erb +16 -3
- data/lib/templates/environment/outputs.tf.erb +5 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +0 -4
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +1 -1
- data/lib/templates/new_app/lambda/api.rb.erb +2 -2
- data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +1 -1
- data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +1 -1
- metadata +5 -2
- data/lib/belt/assets/belt-default.png +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 479b619266e398d1d3052c96eb18f05c8452306a01aa46576b82e072372820b5
|
|
4
|
+
data.tar.gz: d5697750193b1a1cbba8d39e81190035d8fd40db490efbdb24903e7647374767
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73ac84ffd0e2238b929180f89972f70c55a7d43a92d0f9de23b85cc6c318982c40c9466f49fe1d2d0960a47685acc90e1352b43f7e390ffee2951c83568ba432
|
|
7
|
+
data.tar.gz: 74db2374a336ba5ac774a5a8a4f7c053136de9dc652907e4abeebd69ec6bfa6485f35d3339787e7e5b17feadb3cf23014ae356d8050e817c90a2ba1fbf817ba6
|
|
Binary file
|
data/lib/belt/assets/welcome.css
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module Belt
|
|
4
4
|
module CLI
|
|
5
5
|
module AppDetection
|
|
6
|
-
|
|
6
|
+
# Detects the primary namespace from the route definitions.
|
|
7
|
+
# Used by generators to determine controller directory and route file naming.
|
|
8
|
+
def detect_namespace
|
|
7
9
|
routes_file = 'infrastructure/routes.tf.rb'
|
|
8
10
|
if File.exist?(routes_file)
|
|
9
11
|
match = File.read(routes_file).match(/namespace :(\w+)/)
|
|
@@ -12,6 +14,26 @@ module Belt
|
|
|
12
14
|
File.basename(Dir.pwd)
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
# Detects the application name.
|
|
18
|
+
# Prefers terraform.tfvars app_name, then falls back to directory name.
|
|
19
|
+
def detect_app_name
|
|
20
|
+
# Check any environment tfvars for the app_name
|
|
21
|
+
Dir.glob('infrastructure/*/terraform.tfvars').each do |f|
|
|
22
|
+
content = File.read(f)
|
|
23
|
+
match = content.match(/app_name\s*=\s*"([^"]+)"/)
|
|
24
|
+
return match[1] if match
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Fall back to directory name
|
|
28
|
+
File.basename(Dir.pwd)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Detects existing environments by scanning infrastructure/ directories.
|
|
32
|
+
# Each subdirectory with a main.tf is considered an environment.
|
|
33
|
+
def detect_environments
|
|
34
|
+
Dir.glob('infrastructure/*/main.tf').map { |f| File.basename(File.dirname(f)) }.sort
|
|
35
|
+
end
|
|
36
|
+
|
|
15
37
|
# S3 bucket names follow DNS rules — underscores are not allowed.
|
|
16
38
|
# App names often use underscores (Ruby namespaces); convert for S3.
|
|
17
39
|
def s3_safe_name(name)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'tmpdir'
|
|
3
7
|
require_relative 'env_resolver'
|
|
4
8
|
require_relative 'terraform_command'
|
|
5
9
|
|
|
@@ -15,12 +19,15 @@ module Belt
|
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
auto_approve = false
|
|
22
|
+
rebuild = false
|
|
18
23
|
filtered_args = []
|
|
19
24
|
|
|
20
25
|
args.each do |arg|
|
|
21
26
|
case arg
|
|
22
27
|
when '--auto', '--yes', '-y'
|
|
23
28
|
auto_approve = true
|
|
29
|
+
when '--rebuild'
|
|
30
|
+
rebuild = true
|
|
24
31
|
when '-h', '--help'
|
|
25
32
|
puts help_text
|
|
26
33
|
exit 0
|
|
@@ -41,18 +48,24 @@ module Belt
|
|
|
41
48
|
puts "\nDefaults to BELT_ENV or the first available environment."
|
|
42
49
|
puts "\nOptions:"
|
|
43
50
|
puts ' --auto, --yes, -y Skip confirmation prompt (auto-approve)'
|
|
51
|
+
puts ' --rebuild Rebuild and push Lambda code directly (bypasses Terraform)'
|
|
44
52
|
puts ' -h, --help Show this help'
|
|
45
53
|
puts "\nExamples:"
|
|
46
54
|
puts ' belt deploy # Deploy dev (or BELT_ENV)'
|
|
47
55
|
puts ' belt deploy prod # Deploy to prod'
|
|
48
56
|
puts ' belt deploy dev --auto # Deploy without confirmation'
|
|
57
|
+
puts ' belt deploy --rebuild # Fast code push (no infra changes)'
|
|
49
58
|
puts ' belt deploy frontend dev # Deploy frontend only'
|
|
50
59
|
puts "\nNo environments found. Run `belt generate environment dev` first."
|
|
51
60
|
exit 1
|
|
52
61
|
end
|
|
53
62
|
end
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
if rebuild
|
|
65
|
+
new(env, auto_approve: auto_approve, extra_args: filtered_args).run_rebuild
|
|
66
|
+
else
|
|
67
|
+
new(env, auto_approve: auto_approve, extra_args: filtered_args).run
|
|
68
|
+
end
|
|
56
69
|
end
|
|
57
70
|
|
|
58
71
|
def self.help_text
|
|
@@ -63,22 +76,28 @@ module Belt
|
|
|
63
76
|
belt deploy frontend <environment>
|
|
64
77
|
|
|
65
78
|
This runs the full deployment lifecycle:
|
|
66
|
-
1.
|
|
67
|
-
2. terraform
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
79
|
+
1. Ensure Gemfile.lock is consistent (fix stale PATH refs)
|
|
80
|
+
2. terraform init (initialize providers/modules)
|
|
81
|
+
3. terraform plan (preview changes)
|
|
82
|
+
4. Prompt for confirmation (unless --auto)
|
|
83
|
+
5. terraform apply (deploy changes)
|
|
70
84
|
|
|
71
85
|
Options:
|
|
72
86
|
--auto, --yes, -y Skip confirmation prompt (auto-approve)
|
|
87
|
+
--rebuild Rebuild and push Lambda code directly (bypasses Terraform).
|
|
88
|
+
Much faster for code-only changes — packages gems via Docker,
|
|
89
|
+
zips, and pushes with `aws lambda update-function-code`.
|
|
73
90
|
-h, --help Show this help
|
|
74
91
|
|
|
75
92
|
Environment:
|
|
76
|
-
Defaults to BELT_ENV if set, otherwise
|
|
93
|
+
Defaults to BELT_ENV if set, otherwise the first available environment.
|
|
77
94
|
|
|
78
95
|
Examples:
|
|
79
96
|
belt deploy # Deploy dev (or BELT_ENV)
|
|
80
97
|
belt deploy prod # Deploy to prod
|
|
81
98
|
belt deploy dev --auto # Deploy without confirmation (CI mode)
|
|
99
|
+
belt deploy --rebuild # Fast code push to dev (no infra changes)
|
|
100
|
+
belt deploy prod --rebuild # Fast code push to prod
|
|
82
101
|
belt deploy frontend dev # Deploy frontend assets only
|
|
83
102
|
HELP
|
|
84
103
|
end
|
|
@@ -88,6 +107,7 @@ module Belt
|
|
|
88
107
|
@auto_approve = auto_approve
|
|
89
108
|
@extra_args = extra_args
|
|
90
109
|
@infra_dir = TerraformCommand.find_infrastructure_dir
|
|
110
|
+
@project_root = find_project_root
|
|
91
111
|
end
|
|
92
112
|
|
|
93
113
|
def run
|
|
@@ -96,6 +116,8 @@ module Belt
|
|
|
96
116
|
|
|
97
117
|
puts "belt → deploying #{@env} (in #{env_dir}/)\n\n"
|
|
98
118
|
|
|
119
|
+
ensure_lockfile_consistent!
|
|
120
|
+
|
|
99
121
|
Dir.chdir(env_dir) do
|
|
100
122
|
run_init
|
|
101
123
|
run_plan
|
|
@@ -105,11 +127,49 @@ module Belt
|
|
|
105
127
|
|
|
106
128
|
puts "\n✅ Deployed #{@env} successfully!"
|
|
107
129
|
print_outputs(env_dir)
|
|
130
|
+
|
|
131
|
+
deploy_frontend_if_exists
|
|
132
|
+
|
|
108
133
|
puts "\n Run `belt server` to view your app locally (auto-connects to the deployed API)."
|
|
109
134
|
end
|
|
110
135
|
|
|
136
|
+
def run_rebuild
|
|
137
|
+
validate!
|
|
138
|
+
validate_aws!
|
|
139
|
+
|
|
140
|
+
puts "belt → rebuilding Lambda for #{@env}\n\n"
|
|
141
|
+
|
|
142
|
+
lambda_dir = find_lambda_dir
|
|
143
|
+
abort 'Error: Cannot find lambda/ directory.' unless lambda_dir
|
|
144
|
+
|
|
145
|
+
function_name = find_lambda_function_name
|
|
146
|
+
abort 'Error: Cannot determine Lambda function name from Terraform state.' unless function_name
|
|
147
|
+
|
|
148
|
+
ensure_lockfile_consistent!
|
|
149
|
+
generate_routes(lambda_dir)
|
|
150
|
+
build_and_deploy(lambda_dir, function_name)
|
|
151
|
+
end
|
|
152
|
+
|
|
111
153
|
private
|
|
112
154
|
|
|
155
|
+
def find_project_root
|
|
156
|
+
# Walk up from infra dir to find project root (where Gemfile lives)
|
|
157
|
+
dir = @infra_dir ? File.dirname(@infra_dir) : Dir.pwd
|
|
158
|
+
while dir != '/'
|
|
159
|
+
return dir if File.exist?(File.join(dir, 'Gemfile'))
|
|
160
|
+
dir = File.dirname(dir)
|
|
161
|
+
end
|
|
162
|
+
Dir.pwd
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def find_lambda_dir
|
|
166
|
+
candidates = [
|
|
167
|
+
File.join(@project_root, 'lambda'),
|
|
168
|
+
File.join(Dir.pwd, 'lambda')
|
|
169
|
+
]
|
|
170
|
+
candidates.find { |d| Dir.exist?(d) }
|
|
171
|
+
end
|
|
172
|
+
|
|
113
173
|
def validate!
|
|
114
174
|
unless @infra_dir
|
|
115
175
|
abort "Error: No infrastructure/ directory found.\n" \
|
|
@@ -124,6 +184,262 @@ module Belt
|
|
|
124
184
|
"Create it with: belt generate environment #{@env}"
|
|
125
185
|
end
|
|
126
186
|
|
|
187
|
+
def validate_aws!
|
|
188
|
+
stdout, status = Open3.capture2('aws', 'sts', 'get-caller-identity', '--output', 'json')
|
|
189
|
+
unless status.success?
|
|
190
|
+
abort "Error: AWS credentials not available.\n" \
|
|
191
|
+
"Run `aws sso login` or configure AWS_PROFILE."
|
|
192
|
+
end
|
|
193
|
+
@aws_account = JSON.parse(stdout)['Account'] rescue nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Ensure Gemfile.lock doesn't have stale PATH references that will break
|
|
197
|
+
# the Docker-based gem build. If the Gemfile no longer uses `path:` but the
|
|
198
|
+
# lockfile still references a PATH source, refresh the lock.
|
|
199
|
+
def ensure_lockfile_consistent!
|
|
200
|
+
gemfile = File.join(@project_root, 'Gemfile')
|
|
201
|
+
lockfile = File.join(@project_root, 'Gemfile.lock')
|
|
202
|
+
return unless File.exist?(gemfile) && File.exist?(lockfile)
|
|
203
|
+
|
|
204
|
+
gemfile_content = File.read(gemfile)
|
|
205
|
+
lockfile_content = File.read(lockfile)
|
|
206
|
+
|
|
207
|
+
# Find gems that are PATH-sourced in the lockfile but NOT path-sourced in the Gemfile
|
|
208
|
+
stale_path_gems = detect_stale_path_gems(gemfile_content, lockfile_content)
|
|
209
|
+
return if stale_path_gems.empty?
|
|
210
|
+
|
|
211
|
+
puts " 🔧 Fixing stale Gemfile.lock (#{stale_path_gems.join(', ')} referenced as PATH but Gemfile uses RubyGems)"
|
|
212
|
+
Dir.chdir(@project_root) do
|
|
213
|
+
success = system('bundle', 'lock', '--update', *stale_path_gems)
|
|
214
|
+
unless success
|
|
215
|
+
puts " ⚠ `bundle lock --update #{stale_path_gems.join(' ')}` failed — trying full bundle update"
|
|
216
|
+
system('bundle', 'update', *stale_path_gems)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
puts ''
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def detect_stale_path_gems(gemfile_content, lockfile_content)
|
|
223
|
+
stale = []
|
|
224
|
+
|
|
225
|
+
# Parse PATH sections from lockfile
|
|
226
|
+
# Format:
|
|
227
|
+
# PATH
|
|
228
|
+
# remote: ../something
|
|
229
|
+
# specs:
|
|
230
|
+
# gem_name (version)
|
|
231
|
+
lockfile_content.scan(/^PATH\n\s+remote:.*?\n\s+specs:\n((?:\s{4}\S.*\n?)+)/m).each do |match|
|
|
232
|
+
match[0].scan(/^\s{4}(\S+)\s/).each do |gem_match|
|
|
233
|
+
gem_name = gem_match[0]
|
|
234
|
+
# Check if the Gemfile still uses path: for this gem
|
|
235
|
+
# Match patterns like: gem 'name', path: ... or gem "name", path: ...
|
|
236
|
+
has_path_in_gemfile = gemfile_content.match?(/gem\s+['"]#{Regexp.escape(gem_name)}['"].*path:/)
|
|
237
|
+
stale << gem_name unless has_path_in_gemfile
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
stale
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def find_lambda_function_name
|
|
245
|
+
env_dir = File.join(@infra_dir, @env)
|
|
246
|
+
return nil unless Dir.exist?(File.join(env_dir, '.terraform'))
|
|
247
|
+
|
|
248
|
+
# Get function name from terraform state
|
|
249
|
+
Dir.chdir(env_dir) do
|
|
250
|
+
output, status = Open3.capture2('terraform', 'output', '-json')
|
|
251
|
+
return nil unless status.success?
|
|
252
|
+
|
|
253
|
+
data = JSON.parse(output) rescue {}
|
|
254
|
+
|
|
255
|
+
# Try lambda_functions output first
|
|
256
|
+
if data['lambda_functions']
|
|
257
|
+
funcs = data['lambda_functions']['value']
|
|
258
|
+
if funcs.is_a?(Hash) && funcs.any?
|
|
259
|
+
return funcs.values.first # ARN — we need just the function name
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Fallback: parse from terraform state directly
|
|
264
|
+
state_output, state_status = Open3.capture2('terraform', 'state', 'show', 'conveyor_belt.main')
|
|
265
|
+
return nil unless state_status.success?
|
|
266
|
+
|
|
267
|
+
# Look for lambda function names in state
|
|
268
|
+
state_output.match(/function_name\s*=\s*"([^"]+)"/)&.captures&.first
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def generate_routes(lambda_dir)
|
|
273
|
+
routes_dir = File.join(lambda_dir, 'lib', 'routes')
|
|
274
|
+
return unless Dir.exist?(routes_dir)
|
|
275
|
+
|
|
276
|
+
puts ' 🗺️ Regenerating route manifests...'
|
|
277
|
+
success = system('belt', 'routes', '--namespace', 'all', '--output-dir', routes_dir)
|
|
278
|
+
if success
|
|
279
|
+
puts ' ✅ Routes updated'
|
|
280
|
+
else
|
|
281
|
+
puts ' ⚠ Route generation failed — using existing manifests'
|
|
282
|
+
end
|
|
283
|
+
puts ''
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def build_and_deploy(lambda_dir, function_identifier)
|
|
287
|
+
# Extract function name from ARN if needed
|
|
288
|
+
function_name = if function_identifier.include?(':')
|
|
289
|
+
function_identifier.split(':').last
|
|
290
|
+
else
|
|
291
|
+
function_identifier
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
build_dir = Dir.mktmpdir('belt-rebuild-')
|
|
295
|
+
|
|
296
|
+
begin
|
|
297
|
+
puts ' 📦 Building Lambda package...'
|
|
298
|
+
assemble_package(lambda_dir, build_dir)
|
|
299
|
+
build_gems(build_dir)
|
|
300
|
+
zip_file = create_zip(build_dir)
|
|
301
|
+
|
|
302
|
+
puts ' 🚀 Deploying to AWS...'
|
|
303
|
+
deploy_to_lambda(function_name, zip_file)
|
|
304
|
+
|
|
305
|
+
puts "\n✅ Lambda #{function_name} rebuilt and deployed!"
|
|
306
|
+
puts ''
|
|
307
|
+
puts ' 📋 View logs:'
|
|
308
|
+
puts " aws logs tail /aws/lambda/#{function_name} --follow"
|
|
309
|
+
ensure
|
|
310
|
+
FileUtils.rm_rf(build_dir)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def assemble_package(lambda_dir, build_dir)
|
|
315
|
+
# Copy handler file(s) — .rb files at root of lambda/
|
|
316
|
+
Dir.glob(File.join(lambda_dir, '*.rb')).each do |f|
|
|
317
|
+
FileUtils.cp(f, build_dir)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Copy shared directories
|
|
321
|
+
%w[controllers models lib helpers templates serializers jobs config].each do |dir|
|
|
322
|
+
src = File.join(lambda_dir, dir)
|
|
323
|
+
FileUtils.cp_r(src, File.join(build_dir, dir)) if Dir.exist?(src)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Copy Gemfile from project root
|
|
327
|
+
gemfile = File.join(@project_root, 'Gemfile')
|
|
328
|
+
lockfile = File.join(@project_root, 'Gemfile.lock')
|
|
329
|
+
FileUtils.cp(gemfile, build_dir) if File.exist?(gemfile)
|
|
330
|
+
FileUtils.cp(lockfile, build_dir) if File.exist?(lockfile)
|
|
331
|
+
|
|
332
|
+
puts ' 📁 Copied handlers, controllers, models, lib, config'
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def build_gems(build_dir)
|
|
336
|
+
puts ' 🐳 Building gems in Docker (Lambda-compatible)...'
|
|
337
|
+
|
|
338
|
+
docker_cmd = [
|
|
339
|
+
'docker', 'run', '--rm',
|
|
340
|
+
'--platform', 'linux/amd64',
|
|
341
|
+
'-v', "#{build_dir}:/var/task",
|
|
342
|
+
'-w', '/var/task',
|
|
343
|
+
'public.ecr.aws/sam/build-ruby3.4:latest-x86_64',
|
|
344
|
+
'/bin/bash', '-c',
|
|
345
|
+
"bundle config set --local path 'vendor/bundle' && " \
|
|
346
|
+
"bundle config set --local without 'development test' && " \
|
|
347
|
+
'bundle config set silence_root_warning 1 && ' \
|
|
348
|
+
'bundle install --jobs 4 && ' \
|
|
349
|
+
'bundle clean --force'
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
output, status = Open3.capture2e(*docker_cmd)
|
|
353
|
+
unless status.success?
|
|
354
|
+
puts output
|
|
355
|
+
abort "\n✗ Gem build failed. Check Docker is running and the Gemfile is valid."
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Strip fat from vendor bundle
|
|
359
|
+
strip_vendor_fat(build_dir)
|
|
360
|
+
puts ' ✅ Gems installed'
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def strip_vendor_fat(build_dir)
|
|
364
|
+
vendor_dir = File.join(build_dir, 'vendor')
|
|
365
|
+
return unless Dir.exist?(vendor_dir)
|
|
366
|
+
|
|
367
|
+
# Remove docs, tests, and non-runtime files from gems
|
|
368
|
+
Dir.glob(File.join(vendor_dir, 'bundle/ruby/*/gems/*/')).each do |gem_dir|
|
|
369
|
+
%w[spec test tests doc docs examples benchmarks].each do |fat_dir|
|
|
370
|
+
FileUtils.rm_rf(File.join(gem_dir, fat_dir))
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Remove non-essential files
|
|
375
|
+
exts = %w[*.md *.rdoc *.txt *.c *.h *.o Makefile *.log CHANGELOG* HISTORY* LICENSE* README* .gitignore .travis.yml .rubocop.yml Rakefile]
|
|
376
|
+
exts.each do |pattern|
|
|
377
|
+
Dir.glob(File.join(vendor_dir, "**", pattern)).each do |f|
|
|
378
|
+
File.delete(f) if File.file?(f)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Strip debug symbols from native extensions
|
|
383
|
+
Dir.glob(File.join(vendor_dir, '**/*.so')).each do |so_file|
|
|
384
|
+
system('strip', '--strip-debug', so_file, err: File::NULL, out: File::NULL)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def create_zip(build_dir)
|
|
389
|
+
zip_file = File.join(Dir.tmpdir, "belt-lambda-#{@env}-#{Time.now.to_i}.zip")
|
|
390
|
+
|
|
391
|
+
Dir.chdir(build_dir) do
|
|
392
|
+
output, status = Open3.capture2e('zip', '-qr', zip_file, '.')
|
|
393
|
+
unless status.success?
|
|
394
|
+
puts output
|
|
395
|
+
abort "\n✗ Failed to create ZIP"
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
size_mb = (File.size(zip_file) / 1024.0 / 1024.0).round(1)
|
|
400
|
+
puts " 🗜️ Package: #{size_mb}MB"
|
|
401
|
+
zip_file
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def deploy_to_lambda(function_name, zip_file)
|
|
405
|
+
_output, status = Open3.capture2(
|
|
406
|
+
'aws', 'lambda', 'update-function-code',
|
|
407
|
+
'--function-name', function_name,
|
|
408
|
+
'--zip-file', "fileb://#{zip_file}",
|
|
409
|
+
'--output', 'json'
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
File.delete(zip_file) if File.exist?(zip_file)
|
|
413
|
+
|
|
414
|
+
unless status.success?
|
|
415
|
+
abort "\n✗ Failed to update Lambda function code.\n" \
|
|
416
|
+
" Function: #{function_name}\n" \
|
|
417
|
+
" Check that the function exists and you have permissions."
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Wait for update to complete
|
|
421
|
+
print ' Waiting for Lambda to be ready...'
|
|
422
|
+
30.times do
|
|
423
|
+
sleep 1
|
|
424
|
+
state_output, = Open3.capture2(
|
|
425
|
+
'aws', 'lambda', 'get-function',
|
|
426
|
+
'--function-name', function_name,
|
|
427
|
+
'--query', 'Configuration.LastUpdateStatus',
|
|
428
|
+
'--output', 'text'
|
|
429
|
+
)
|
|
430
|
+
case state_output.strip
|
|
431
|
+
when 'Successful'
|
|
432
|
+
puts ' ✅'
|
|
433
|
+
return
|
|
434
|
+
when 'Failed'
|
|
435
|
+
puts ' ✗'
|
|
436
|
+
abort ' Lambda update failed. Check CloudWatch logs.'
|
|
437
|
+
end
|
|
438
|
+
print '.'
|
|
439
|
+
end
|
|
440
|
+
puts ' (timed out waiting, but code was pushed)'
|
|
441
|
+
end
|
|
442
|
+
|
|
127
443
|
def run_init
|
|
128
444
|
puts '━━━ terraform init ━━━'
|
|
129
445
|
success = system('terraform', 'init')
|
|
@@ -166,6 +482,18 @@ module Belt
|
|
|
166
482
|
File.delete('tfplan') if File.exist?('tfplan')
|
|
167
483
|
end
|
|
168
484
|
|
|
485
|
+
def deploy_frontend_if_exists
|
|
486
|
+
frontend_dir = File.join(@project_root, 'frontend')
|
|
487
|
+
return unless Dir.exist?(frontend_dir) && File.exist?(File.join(frontend_dir, 'package.json'))
|
|
488
|
+
|
|
489
|
+
puts "\n━━━ frontend deploy ━━━"
|
|
490
|
+
require_relative 'frontend_deploy_command'
|
|
491
|
+
Belt::CLI::FrontendDeployCommand.new(@env).run
|
|
492
|
+
rescue StandardError => e
|
|
493
|
+
puts "\n ⚠ Frontend deploy failed: #{e.message}"
|
|
494
|
+
puts " Run `belt deploy frontend #{@env}` manually to retry."
|
|
495
|
+
end
|
|
496
|
+
|
|
169
497
|
def print_outputs(env_dir)
|
|
170
498
|
Dir.chdir(env_dir) do
|
|
171
499
|
output = `terraform output 2>/dev/null`.strip
|