brainiac 0.0.28 → 0.0.29
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/Gemfile.lock +2 -2
- data/lib/brainiac/handlers/shared/belt.rb +409 -0
- data/lib/brainiac/version.rb +1 -1
- data/receiver.rb +4 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37f135fffb0c94919fa376fee96e12053af77062f93dadc520cce7285ee86e78
|
|
4
|
+
data.tar.gz: 553b10319590bde4fee8f89cf227059fec83d750d6ad7188d56aa5243a6cf169
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e04d26de1e9010e175b69032358e400223af29ffabde834e48b206d6cff58340fe1082262b36843fe6995aff752df2ed053793a78f416cc47d1dd4b47b881a99
|
|
7
|
+
data.tar.gz: fe32e3768064d1728a2b28d40aef900c1afb454d04e192b67a9e4f631bf117a147ebef547898709ead380b286336dee64da8a40b62cf6f10b7a327ca1bb3d505
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
brainiac (0.0.
|
|
4
|
+
brainiac (0.0.29)
|
|
5
5
|
puma (~> 7.2)
|
|
6
6
|
rackup (~> 2.3)
|
|
7
7
|
sinatra (~> 4.1)
|
|
@@ -84,7 +84,7 @@ DEPENDENCIES
|
|
|
84
84
|
CHECKSUMS
|
|
85
85
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
86
86
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
87
|
-
brainiac (0.0.
|
|
87
|
+
brainiac (0.0.29)
|
|
88
88
|
json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
|
|
89
89
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
90
90
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Belt application detection and environment management utilities.
|
|
4
|
+
#
|
|
5
|
+
# Belt is a Ruby framework for building serverless applications on AWS Lambda.
|
|
6
|
+
# Projects using Belt have infrastructure defined in `config/routes.rb` or the
|
|
7
|
+
# legacy `infrastructure/routes.tf.rb` path.
|
|
8
|
+
#
|
|
9
|
+
# This module provides detection and ephemeral environment helpers that plugins
|
|
10
|
+
# (brainiac-fizzy, brainiac-github, brainiac-basecamp) can use.
|
|
11
|
+
|
|
12
|
+
# Module containing Belt-related helpers accessible from Sinatra routes.
|
|
13
|
+
module BeltHelpers
|
|
14
|
+
# Detect if a directory is a Belt application by checking for routes file.
|
|
15
|
+
#
|
|
16
|
+
# Belt apps are detected by the presence of any of these files:
|
|
17
|
+
# - config/routes.rb (current)
|
|
18
|
+
# - config/routes.tf.rb (legacy)
|
|
19
|
+
# - infrastructure/routes.tf.rb (legacy)
|
|
20
|
+
#
|
|
21
|
+
# @param path [String] Path to check (repo root or worktree)
|
|
22
|
+
# @return [Boolean] True if the path contains a Belt application
|
|
23
|
+
def belt_app?(path)
|
|
24
|
+
return false unless path && File.directory?(path)
|
|
25
|
+
|
|
26
|
+
candidates = [
|
|
27
|
+
File.join(path, "config/routes.rb"),
|
|
28
|
+
File.join(path, "config/routes.tf.rb"),
|
|
29
|
+
File.join(path, "infrastructure/routes.tf.rb")
|
|
30
|
+
]
|
|
31
|
+
candidates.any? { |f| File.exist?(f) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if a Belt app routes file exists and contains actual route definitions.
|
|
35
|
+
# This is a stricter check than belt_app? — it verifies the file has Belt routes,
|
|
36
|
+
# not just a Rails routes.rb file (which wouldn't have Belt-style route definitions).
|
|
37
|
+
#
|
|
38
|
+
# @param path [String] Path to check
|
|
39
|
+
# @return [Boolean] True if the path contains a Belt routes file with Belt syntax
|
|
40
|
+
def belt_routes_file?(path)
|
|
41
|
+
routes_file = belt_routes_path(path)
|
|
42
|
+
return false unless routes_file && File.exist?(routes_file)
|
|
43
|
+
|
|
44
|
+
# Check for Belt-specific syntax (e.g., `app.get`, `api`, `resources`, etc.)
|
|
45
|
+
content = File.read(routes_file, 4096) # Read first 4KB
|
|
46
|
+
content.match?(/\bapp\.(get|post|put|patch|delete)\b/) ||
|
|
47
|
+
content.match?(/\bapi\s+do\b/) ||
|
|
48
|
+
content.match?(/\bresources?\s+:/) ||
|
|
49
|
+
content.match?(/\bBelt\.routes\b/)
|
|
50
|
+
rescue StandardError
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get the path to the Belt routes file (checking all possible locations).
|
|
55
|
+
#
|
|
56
|
+
# @param path [String] Path to check
|
|
57
|
+
# @return [String, nil] Path to routes file or nil
|
|
58
|
+
def belt_routes_path(path)
|
|
59
|
+
return nil unless path && File.directory?(path)
|
|
60
|
+
|
|
61
|
+
candidates = [
|
|
62
|
+
File.join(path, "config/routes.rb"),
|
|
63
|
+
File.join(path, "config/routes.tf.rb"),
|
|
64
|
+
File.join(path, "infrastructure/routes.tf.rb")
|
|
65
|
+
]
|
|
66
|
+
candidates.find { |f| File.exist?(f) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Configuration for Belt ephemeral environments.
|
|
71
|
+
# Stored in ~/.brainiac/basecamp.json under the "deploy" key.
|
|
72
|
+
module BeltConfig
|
|
73
|
+
BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
|
|
74
|
+
BASECAMP_CONFIG_FILE = File.join(BRAINIAC_DIR, "basecamp.json")
|
|
75
|
+
|
|
76
|
+
class << self
|
|
77
|
+
# Load the basecamp config, caching for performance.
|
|
78
|
+
# @return [Hash]
|
|
79
|
+
def load_config
|
|
80
|
+
return {} unless File.exist?(BASECAMP_CONFIG_FILE)
|
|
81
|
+
|
|
82
|
+
JSON.parse(File.read(BASECAMP_CONFIG_FILE))
|
|
83
|
+
rescue JSON::ParserError => e
|
|
84
|
+
LOG.error "[Belt] Failed to parse basecamp.json: #{e.message}" if defined?(LOG)
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get the parent environment for a project (used as base for ephemeral envs).
|
|
89
|
+
# This is where `belt g environment <name> <parent>` copies settings from.
|
|
90
|
+
#
|
|
91
|
+
# @param project_key [String] Brainiac project key
|
|
92
|
+
# @return [String, nil] Parent environment name (e.g., "dev02", "dev") or nil
|
|
93
|
+
def parent_env_for(project_key)
|
|
94
|
+
config = load_config
|
|
95
|
+
config.dig("deploy", "project_envs", project_key)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if ephemeral deploys are enabled globally.
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
def ephemeral_deploys_enabled?
|
|
101
|
+
config = load_config
|
|
102
|
+
# Default to true if deploy section exists but enabled isn't specified
|
|
103
|
+
deploy_config = config["deploy"] || {}
|
|
104
|
+
deploy_config.fetch("ephemeral_enabled", true)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Track an ephemeral environment in deployment state.
|
|
108
|
+
# @param env_name [String] Environment name (e.g., "fizzy-123")
|
|
109
|
+
# @param metadata [Hash] Metadata about the ephemeral env
|
|
110
|
+
def track_ephemeral_env(env_name, metadata = {})
|
|
111
|
+
state_file = File.join(BRAINIAC_DIR, "ephemeral_envs.json")
|
|
112
|
+
state = File.exist?(state_file) ? JSON.parse(File.read(state_file)) : {}
|
|
113
|
+
|
|
114
|
+
state[env_name] = {
|
|
115
|
+
"created_at" => Time.now.iso8601,
|
|
116
|
+
"status" => "active"
|
|
117
|
+
}.merge(metadata)
|
|
118
|
+
|
|
119
|
+
File.write(state_file, JSON.pretty_generate(state))
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
LOG.error "[Belt] Failed to track ephemeral env: #{e.message}" if defined?(LOG)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if an environment is ephemeral.
|
|
125
|
+
# @param env_name [String] Environment name
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def ephemeral_env?(env_name)
|
|
128
|
+
state_file = File.join(BRAINIAC_DIR, "ephemeral_envs.json")
|
|
129
|
+
return false unless File.exist?(state_file)
|
|
130
|
+
|
|
131
|
+
state = JSON.parse(File.read(state_file))
|
|
132
|
+
state.key?(env_name) && state[env_name]["status"] == "active"
|
|
133
|
+
rescue StandardError
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get ephemeral environment metadata.
|
|
138
|
+
# @param env_name [String] Environment name
|
|
139
|
+
# @return [Hash, nil]
|
|
140
|
+
def ephemeral_env_info(env_name)
|
|
141
|
+
state_file = File.join(BRAINIAC_DIR, "ephemeral_envs.json")
|
|
142
|
+
return nil unless File.exist?(state_file)
|
|
143
|
+
|
|
144
|
+
state = JSON.parse(File.read(state_file))
|
|
145
|
+
state[env_name]
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Mark an ephemeral environment as destroyed.
|
|
151
|
+
# @param env_name [String] Environment name
|
|
152
|
+
def mark_ephemeral_destroyed(env_name)
|
|
153
|
+
state_file = File.join(BRAINIAC_DIR, "ephemeral_envs.json")
|
|
154
|
+
return unless File.exist?(state_file)
|
|
155
|
+
|
|
156
|
+
state = JSON.parse(File.read(state_file))
|
|
157
|
+
return unless state[env_name]
|
|
158
|
+
|
|
159
|
+
state[env_name]["status"] = "destroyed"
|
|
160
|
+
state[env_name]["destroyed_at"] = Time.now.iso8601
|
|
161
|
+
|
|
162
|
+
File.write(state_file, JSON.pretty_generate(state))
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
LOG.error "[Belt] Failed to mark ephemeral env destroyed: #{e.message}" if defined?(LOG)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Find ephemeral environment by card number.
|
|
168
|
+
# @param card_number [Integer, String] Fizzy card number
|
|
169
|
+
# @return [String, nil] Environment name or nil
|
|
170
|
+
def ephemeral_env_for_card(card_number)
|
|
171
|
+
"fizzy-#{card_number}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Find ephemeral environment by epic number.
|
|
175
|
+
# @param epic_number [Integer, String] Epic number
|
|
176
|
+
# @return [String, nil] Environment name or nil
|
|
177
|
+
def ephemeral_env_for_epic(epic_number)
|
|
178
|
+
"epic-#{epic_number}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Belt environment operations (create, deploy, destroy).
|
|
184
|
+
# These wrap the `belt` CLI commands for ephemeral environment management.
|
|
185
|
+
module BeltEnvironment
|
|
186
|
+
class << self
|
|
187
|
+
include BeltHelpers
|
|
188
|
+
|
|
189
|
+
# Check whether an environment is configured in a worktree.
|
|
190
|
+
# `belt g environment` writes infrastructure/<env_name>/; that directory
|
|
191
|
+
# is the source of truth, not the ephemeral_envs.json tracking file.
|
|
192
|
+
#
|
|
193
|
+
# @param worktree [String] Path to the worktree
|
|
194
|
+
# @param env_name [String] Environment name (e.g. "fizzy-1299")
|
|
195
|
+
# @return [Boolean]
|
|
196
|
+
def environment_configured?(worktree:, env_name:)
|
|
197
|
+
return false unless worktree && env_name && File.directory?(worktree)
|
|
198
|
+
|
|
199
|
+
File.directory?(File.join(worktree, "infrastructure", env_name.to_s))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Create an ephemeral environment from a parent environment.
|
|
203
|
+
#
|
|
204
|
+
# @param worktree [String] Path to the worktree
|
|
205
|
+
# @param env_name [String] Name for the ephemeral environment
|
|
206
|
+
# @param parent_env [String] Parent environment to copy from
|
|
207
|
+
# @return [Boolean] True on success
|
|
208
|
+
def create_environment(worktree:, env_name:, parent_env:)
|
|
209
|
+
return false unless belt_app?(worktree)
|
|
210
|
+
|
|
211
|
+
LOG.info "[Belt] Creating ephemeral environment '#{env_name}' from parent '#{parent_env}'"
|
|
212
|
+
|
|
213
|
+
_, stderr, status = Open3.capture3("belt", "g", "environment", env_name, parent_env, chdir: worktree)
|
|
214
|
+
|
|
215
|
+
if status.success?
|
|
216
|
+
LOG.info "[Belt] Successfully created environment '#{env_name}'"
|
|
217
|
+
true
|
|
218
|
+
else
|
|
219
|
+
LOG.error "[Belt] Failed to create environment '#{env_name}': #{stderr.strip}"
|
|
220
|
+
false
|
|
221
|
+
end
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
LOG.error "[Belt] Error creating environment: #{e.message}"
|
|
224
|
+
false
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Deploy to an environment.
|
|
228
|
+
#
|
|
229
|
+
# Always non-interactive: `belt deploy` prompts "Apply these changes? [y/N]"
|
|
230
|
+
# unless `--auto` is passed. Open3.capture3 provides empty stdin, so without
|
|
231
|
+
# `--auto` belt prints "Cancelled." and still exits 0 — a silent no-op.
|
|
232
|
+
# Frontend-only uses `belt deploy frontend <env>` (subcommand first).
|
|
233
|
+
#
|
|
234
|
+
# @param worktree [String] Path to the worktree
|
|
235
|
+
# @param env_name [String] Environment name
|
|
236
|
+
# @param frontend_only [Boolean] If true, only deploy frontend
|
|
237
|
+
# @return [Boolean] True on success
|
|
238
|
+
def deploy(worktree:, env_name:, frontend_only: false, capture3: nil)
|
|
239
|
+
return false unless belt_app?(worktree)
|
|
240
|
+
|
|
241
|
+
cmd = deploy_command(env_name, frontend_only: frontend_only)
|
|
242
|
+
|
|
243
|
+
LOG.info "[Belt] Deploying to '#{env_name}'#{" (frontend only)" if frontend_only}"
|
|
244
|
+
LOG.info "[Belt] Running: #{cmd.join(" ")} (in #{worktree})"
|
|
245
|
+
|
|
246
|
+
runner = capture3 || Open3.method(:capture3)
|
|
247
|
+
stdout, stderr, status = runner.call(*cmd, chdir: worktree)
|
|
248
|
+
log_cli_tail(stdout, stderr)
|
|
249
|
+
|
|
250
|
+
if deploy_cancelled?(stdout, stderr)
|
|
251
|
+
LOG.error "[Belt] Deploy to '#{env_name}' cancelled — non-interactive belt deploy needs --auto"
|
|
252
|
+
return false
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if status.success?
|
|
256
|
+
LOG.info "[Belt] Successfully deployed to '#{env_name}'"
|
|
257
|
+
true
|
|
258
|
+
else
|
|
259
|
+
LOG.error "[Belt] Failed to deploy to '#{env_name}': #{stderr.strip}"
|
|
260
|
+
false
|
|
261
|
+
end
|
|
262
|
+
rescue StandardError => e
|
|
263
|
+
LOG.error "[Belt] Error deploying: #{e.message}"
|
|
264
|
+
false
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# argv for `belt deploy`. Extracted so tests can assert the command without
|
|
268
|
+
# stubbing Open3 for every call site.
|
|
269
|
+
def deploy_command(env_name, frontend_only: false)
|
|
270
|
+
if frontend_only
|
|
271
|
+
["belt", "deploy", "frontend", env_name]
|
|
272
|
+
else
|
|
273
|
+
["belt", "deploy", env_name, "--auto"]
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Destroy an ephemeral environment.
|
|
278
|
+
#
|
|
279
|
+
# @param worktree [String] Path to the worktree (infrastructure lives here)
|
|
280
|
+
# @param env_name [String] Environment name
|
|
281
|
+
# @return [Boolean] True on success
|
|
282
|
+
def destroy_environment(worktree:, env_name:)
|
|
283
|
+
return false unless belt_app?(worktree)
|
|
284
|
+
|
|
285
|
+
LOG.info "[Belt] Destroying ephemeral environment '#{env_name}'"
|
|
286
|
+
|
|
287
|
+
_, stderr, status = Open3.capture3("belt", "destroy", "environment", env_name, "--full", chdir: worktree)
|
|
288
|
+
|
|
289
|
+
if status.success?
|
|
290
|
+
LOG.info "[Belt] Successfully destroyed environment '#{env_name}'"
|
|
291
|
+
BeltConfig.mark_ephemeral_destroyed(env_name)
|
|
292
|
+
true
|
|
293
|
+
else
|
|
294
|
+
LOG.error "[Belt] Failed to destroy environment '#{env_name}': #{stderr.strip}"
|
|
295
|
+
false
|
|
296
|
+
end
|
|
297
|
+
rescue StandardError => e
|
|
298
|
+
LOG.error "[Belt] Error destroying environment: #{e.message}"
|
|
299
|
+
false
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Check if changes are frontend-only by examining the diff.
|
|
303
|
+
# Frontend-only changes can be deployed faster with `belt deploy frontend <env>`.
|
|
304
|
+
#
|
|
305
|
+
# @param worktree [String] Path to the worktree
|
|
306
|
+
# @param base_branch [String, nil] Optional base (e.g. "master", "origin/main").
|
|
307
|
+
# When omitted, uses origin/HEAD, then origin/main, then origin/master.
|
|
308
|
+
# @return [Boolean] True if changes are frontend-only
|
|
309
|
+
def frontend_only_changes?(worktree:, base_branch: nil)
|
|
310
|
+
base_ref = resolve_frontend_diff_base(worktree, base_branch)
|
|
311
|
+
unless base_ref
|
|
312
|
+
LOG.warn "[Belt] No base ref for frontend-only check in #{worktree}"
|
|
313
|
+
return false
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
stdout, stderr, status = Open3.capture3("git", "diff", "--name-only", base_ref, "--", chdir: worktree)
|
|
317
|
+
unless status.success?
|
|
318
|
+
LOG.warn "[Belt] Could not diff against #{base_ref}: #{stderr.strip}"
|
|
319
|
+
return false
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
changed_files = stdout.strip.split("\n")
|
|
323
|
+
return false if changed_files.empty?
|
|
324
|
+
|
|
325
|
+
# Frontend directories that don't affect backend
|
|
326
|
+
frontend_patterns = %w[
|
|
327
|
+
frontend/
|
|
328
|
+
app/javascript/
|
|
329
|
+
app/assets/
|
|
330
|
+
public/
|
|
331
|
+
static/
|
|
332
|
+
src/
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
# Backend patterns that require full deploy
|
|
336
|
+
backend_patterns = %w[
|
|
337
|
+
lambda/
|
|
338
|
+
infrastructure/
|
|
339
|
+
config/routes
|
|
340
|
+
config/contracts
|
|
341
|
+
Gemfile
|
|
342
|
+
*.gemspec
|
|
343
|
+
Rakefile
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
# Check if all changes are frontend-only
|
|
347
|
+
changed_files.all? do |file|
|
|
348
|
+
frontend_patterns.any? { |pattern| file.start_with?(pattern) } &&
|
|
349
|
+
backend_patterns.none? { |pattern| file.start_with?(pattern.delete("*")) || File.fnmatch?(pattern, file) }
|
|
350
|
+
end
|
|
351
|
+
rescue StandardError => e
|
|
352
|
+
LOG.warn "[Belt] Error checking frontend-only changes: #{e.message}"
|
|
353
|
+
false
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Resolve a git ref to diff against. Never assumes origin/main.
|
|
357
|
+
# Prefer an explicit PR/base branch, then origin/HEAD, then main/master.
|
|
358
|
+
def resolve_frontend_diff_base(worktree, explicit)
|
|
359
|
+
candidates = []
|
|
360
|
+
if explicit && !explicit.to_s.strip.empty?
|
|
361
|
+
ref = explicit.to_s.strip
|
|
362
|
+
ref = "origin/#{ref}" unless ref.include?("/")
|
|
363
|
+
candidates << ref
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
head = origin_head_branch(worktree)
|
|
367
|
+
candidates << "origin/#{head}" if head
|
|
368
|
+
candidates.push("origin/main", "origin/master")
|
|
369
|
+
candidates.uniq.find { |ref| git_commit?(worktree, ref) }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def origin_head_branch(worktree)
|
|
373
|
+
stdout, _stderr, status = Open3.capture3(
|
|
374
|
+
"git", "symbolic-ref", "--short", "refs/remotes/origin/HEAD",
|
|
375
|
+
chdir: worktree
|
|
376
|
+
)
|
|
377
|
+
return nil unless status.success?
|
|
378
|
+
|
|
379
|
+
name = stdout.strip.delete_prefix("origin/")
|
|
380
|
+
name.empty? ? nil : name
|
|
381
|
+
rescue StandardError
|
|
382
|
+
nil
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def git_commit?(worktree, ref)
|
|
386
|
+
_stdout, _stderr, status = Open3.capture3(
|
|
387
|
+
"git", "rev-parse", "--verify", "#{ref}^{commit}",
|
|
388
|
+
chdir: worktree
|
|
389
|
+
)
|
|
390
|
+
status.success?
|
|
391
|
+
rescue StandardError
|
|
392
|
+
false
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def deploy_cancelled?(stdout, stderr)
|
|
396
|
+
[stdout, stderr].any? { |s| s.to_s.match?(/\bCancelled\.?\s*$/) }
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def log_cli_tail(stdout, stderr, limit: 25)
|
|
400
|
+
lines = []
|
|
401
|
+
lines.concat(stdout.to_s.lines) unless stdout.to_s.strip.empty?
|
|
402
|
+
lines.concat(stderr.to_s.lines.map { |l| "stderr: #{l}" }) unless stderr.to_s.strip.empty?
|
|
403
|
+
return if lines.empty?
|
|
404
|
+
|
|
405
|
+
tail = lines.last(limit).join
|
|
406
|
+
LOG.info "[Belt] Output (last #{[lines.size, limit].min} lines):\n#{tail}"
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
data/lib/brainiac/version.rb
CHANGED
data/receiver.rb
CHANGED
|
@@ -28,6 +28,7 @@ require_relative "lib/brainiac/intent"
|
|
|
28
28
|
require_relative "lib/brainiac/plugins"
|
|
29
29
|
require_relative "lib/brainiac/handlers/shared/git"
|
|
30
30
|
require_relative "lib/brainiac/handlers/shared/inline_tags"
|
|
31
|
+
require_relative "lib/brainiac/handlers/shared/belt"
|
|
31
32
|
|
|
32
33
|
# Namespace for gem-based plugins (brainiac-whatsapp, brainiac-slack, etc.)
|
|
33
34
|
module Brainiac
|
|
@@ -120,6 +121,9 @@ end
|
|
|
120
121
|
# from within Sinatra route handlers
|
|
121
122
|
helpers GitHelpers
|
|
122
123
|
|
|
124
|
+
# Register BeltHelpers for Belt app detection
|
|
125
|
+
helpers BeltHelpers
|
|
126
|
+
|
|
123
127
|
# Dashboard authentication helpers
|
|
124
128
|
helpers do
|
|
125
129
|
def authenticate_dashboard!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brainiac
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.29
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -130,6 +130,7 @@ files:
|
|
|
130
130
|
- lib/brainiac/brain.rb
|
|
131
131
|
- lib/brainiac/config.rb
|
|
132
132
|
- lib/brainiac/cron.rb
|
|
133
|
+
- lib/brainiac/handlers/shared/belt.rb
|
|
133
134
|
- lib/brainiac/handlers/shared/git.rb
|
|
134
135
|
- lib/brainiac/handlers/shared/inline_tags.rb
|
|
135
136
|
- lib/brainiac/helpers.rb
|