brasa 0.4.2 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8770bfdcdf312ee8493341a83de7356e76dfb6b6cebd4fa4d0155ae2ec8f3425
4
- data.tar.gz: e27282ef7d4dd88a159c2463320c7f1393a2bfa9c93a07e0e234ca73b7fb8df8
3
+ metadata.gz: f58d1ef47800a723afab0a3b260562f7704a4b72e0ac364471a0dcc2b0445078
4
+ data.tar.gz: c1c9063b7c415605ab60e4bf2deb7274e6780c4bcf7ed7ad755b609c24d2c845
5
5
  SHA512:
6
- metadata.gz: fdad15d4954607bff0bb6a2737a4be3d8959e7213393cd32ec0ccfb995f0c42d8f416a6046810f9aa8d3ac88fd57dd34989e429be6856a9c5830d1b8d8466580
7
- data.tar.gz: 6d2293ceb1e6ab6ae539e6a8f6e28b22bf82b29a64e31d383c103125e9891e7a0287bd0acabd02d2bf02c6ce5ce773c7331b1cd33ef24f6c3a2b6f935680b0bd
6
+ metadata.gz: 60d66b1edd7299319e0ae56964e9b1595a18f7c49879aca09bdf02f5d023d256a31d3ce0d75719964fef60dfcbc55517497fb715ee2c778feacf3648b118742b
7
+ data.tar.gz: 10b6eed8ee2bf1236e28c83ab98d0d4151da0d3a59bb6efcfb01af585f92a24339a1a53bd7f7e01f5dfc24d7ef9a718e84b19211afda4e3425d5cd39c3ae7b9a
@@ -5,7 +5,8 @@ module Brasa
5
5
  module Commands
6
6
  class Deploy < Base
7
7
  POLL_INTERVAL = 5
8
- DEPLOY_MAX_POLLS = 360 # 30 minutes
8
+ DEPLOY_TIMEOUT = 1800
9
+ DEPLOY_MAX_POLLS = 360
9
10
 
10
11
  def execute(options = {})
11
12
  require_auth!
@@ -13,18 +14,16 @@ module Brasa
13
14
 
14
15
  info("Empacotando código...")
15
16
  archive_path = SourcePacker.pack
16
-
17
17
  size_kb = File.size(archive_path) / 1024
18
18
  info("Enviando #{size_kb}KB para o servidor...")
19
19
 
20
- deploy = api.upload(
21
- "/api/v1/apps/#{slug}/deploys",
20
+ deploy = api.upload("/api/v1/apps/#{slug}/deploys",
22
21
  file_path: archive_path,
23
- params: { branch: options["branch"] || project_config[:branch] || "main" }
24
- )
25
- info("Deploy ##{deploy["id"]} criado. Aguardando build e deploy...")
22
+ params: { branch: options["branch"] || project_config[:branch] || "main" })
23
+ info("Deploy ##{deploy["id"]} criado.")
24
+ info(" Na fila...")
26
25
 
27
- wait_for_deploy(slug, deploy["id"])
26
+ wait_for_deploy_complete(slug, deploy["id"])
28
27
  rescue Api::Client::ApiError => e
29
28
  error("Erro: #{e.message}")
30
29
  ensure
@@ -33,24 +32,51 @@ module Brasa
33
32
 
34
33
  private
35
34
 
36
- def wait_for_deploy(slug, deploy_id)
35
+ def wait_for_deploy_complete(slug, deploy_id)
36
+ ws = connect_websocket
37
+ started = Time.now
38
+
39
+ ws.on_log { |msg| print msg["line"] + "\n" if msg["line"] }
40
+ ws.subscribe("DeployLogChannel", deploy_id: deploy_id)
41
+
42
+ status = ws.wait_for("DeployStatusChannel",
43
+ params: { deploy_id: deploy_id },
44
+ until_status: %w[live failed],
45
+ timeout: DEPLOY_TIMEOUT)
46
+
47
+ elapsed = (Time.now - started).to_i
48
+ if status == "live"
49
+ success("\nDeploy concluído com sucesso! (#{elapsed}s)")
50
+ else
51
+ error("\nDeploy falhou.")
52
+ end
53
+ rescue Brasa::Websocket::CableClient::ConnectionError => e
54
+ $stderr.puts "\n [WebSocket indisponível: #{e.message}. Usando polling...]"
55
+ wait_for_deploy_polling(slug, deploy_id)
56
+ ensure
57
+ ws&.disconnect
58
+ end
59
+
60
+ def connect_websocket
61
+ require "brasa/websocket/cable_client"
62
+ Brasa::Websocket::CableClient.new.tap(&:connect!)
63
+ end
64
+
65
+ def wait_for_deploy_polling(slug, deploy_id)
37
66
  started = Time.now
38
67
  last_status = nil
39
68
  DEPLOY_MAX_POLLS.times do |i|
40
69
  deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
41
-
42
70
  case deploy["status"]
43
71
  when "live"
44
- elapsed = (Time.now - started).to_i
45
- success("\nDeploy concluído com sucesso! (#{elapsed}s)")
72
+ success("\nDeploy concluído com sucesso! (#{(Time.now - started).to_i}s)")
46
73
  return
47
74
  when "failed"
48
75
  error("\nDeploy falhou.")
49
76
  return
50
77
  end
51
-
52
78
  if deploy["status"] != last_status
53
- print "\n #{status_label(deploy["status"])}" if last_status
79
+ print "\n #{status_label(deploy["status"])}"
54
80
  last_status = deploy["status"]
55
81
  end
56
82
  print "."
@@ -60,21 +86,17 @@ module Brasa
60
86
  print "!"
61
87
  sleep(POLL_INTERVAL)
62
88
  end
63
- elapsed = (Time.now - started).to_i
64
- error("\nTimeout aguardando deploy (#{elapsed}s). O processo pode ainda estar rodando — verifique com: brasa status")
89
+ error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
65
90
  end
66
91
 
67
92
  def status_label(status)
68
- { "queued" => "Na fila...",
69
- "building" => "Construindo imagem...",
93
+ { "queued" => "Na fila...", "building" => "Construindo imagem...",
70
94
  "deploying" => "Deployando..." }[status] || status
71
95
  end
72
96
 
73
97
  def show_elapsed(started)
74
98
  elapsed = (Time.now - started).to_i
75
- min = elapsed / 60
76
- sec = elapsed % 60
77
- print " (#{min}m#{sec}s)"
99
+ print " (#{elapsed / 60}m#{elapsed % 60}s)"
78
100
  end
79
101
  end
80
102
  end
@@ -5,8 +5,10 @@ module Brasa
5
5
  module Commands
6
6
  class Up < Base
7
7
  POLL_INTERVAL = 5
8
- PROVISION_MAX_POLLS = 240 # 20 minutes (cloud-init + readiness checks + managed DB)
9
- DEPLOY_MAX_POLLS = 360 # 30 minutes (docker build + push + deploy)
8
+ PROVISION_TIMEOUT = 1200
9
+ DEPLOY_TIMEOUT = 1800
10
+ PROVISION_MAX_POLLS = 240
11
+ DEPLOY_MAX_POLLS = 360
10
12
 
11
13
  def execute(options = {})
12
14
  require_auth!
@@ -19,7 +21,8 @@ module Brasa
19
21
  info("Iniciando deploy...")
20
22
  else
21
23
  info("Provisionando infraestrutura...")
22
- unless wait_for_provisioning(slug)
24
+ trigger_provision(slug) if %w[pending error].include?(@app["status"])
25
+ unless wait_for_provisioning(@app["id"], slug)
23
26
  return
24
27
  end
25
28
  success("Infraestrutura provisionada!")
@@ -27,8 +30,9 @@ module Brasa
27
30
  end
28
31
 
29
32
  deploy = trigger_deploy(slug)
33
+ info(" Na fila...")
30
34
 
31
- if wait_for_deploy(slug, deploy["id"])
35
+ if wait_for_deploy_complete(slug, deploy["id"])
32
36
  subdomain = @app["subdomain"] || slug
33
37
  success("Deploy concluído! App disponível em https://#{subdomain}.usebrasa.com.br")
34
38
  end
@@ -50,23 +54,22 @@ module Brasa
50
54
  create_app(config)
51
55
  end
52
56
 
57
+ def trigger_provision(slug)
58
+ api.post("/api/v1/apps/#{slug}/provision")
59
+ rescue Api::Client::ApiError
60
+ end
61
+
53
62
  def ensure_repo_url(slug)
54
63
  repo = detect_git_remote
55
64
  return unless repo
56
-
57
65
  api.patch("/api/v1/apps/#{slug}", body: { app: { repo_url: repo } })
58
66
  rescue Api::Client::ApiError
59
- # Não bloquear o fluxo se falhar ao atualizar repo
60
67
  end
61
68
 
62
69
  def create_app(config)
63
70
  app_params = {
64
- name: config[:app],
65
- stack: config[:stack],
66
- preset: config[:preset],
67
- region: config[:region],
68
- repo_url: detect_git_remote,
69
- repo_branch: config[:branch]
71
+ name: config[:app], stack: config[:stack], preset: config[:preset],
72
+ region: config[:region], repo_url: detect_git_remote, repo_branch: config[:branch]
70
73
  }
71
74
  app_params[:database_engine] = config[:database_engine] if config[:database_engine]
72
75
  api.post("/api/v1/apps", body: { app: app_params })
@@ -77,13 +80,8 @@ module Brasa
77
80
  archive_path = SourcePacker.pack
78
81
  size_kb = File.size(archive_path) / 1024
79
82
  info("Enviando #{size_kb}KB para o servidor...")
80
-
81
- deploy = api.upload(
82
- "/api/v1/apps/#{slug}/deploys",
83
- file_path: archive_path,
84
- params: { branch: project_config[:branch] || "main" }
85
- )
86
- deploy
83
+ api.upload("/api/v1/apps/#{slug}/deploys", file_path: archive_path,
84
+ params: { branch: project_config[:branch] || "main" })
87
85
  ensure
88
86
  FileUtils.rm_f(archive_path) if archive_path
89
87
  end
@@ -93,18 +91,73 @@ module Brasa
93
91
  remote.empty? ? nil : remote
94
92
  end
95
93
 
96
- def wait_for_provisioning(slug)
94
+ # ── WebSocket methods ──
95
+
96
+ def wait_for_provisioning(app_id, slug)
97
+ ws = connect_websocket
98
+ status = ws.wait_for("ProvisionStatusChannel",
99
+ params: { app_id: app_id },
100
+ until_status: %w[active error],
101
+ timeout: PROVISION_TIMEOUT)
102
+
103
+ if status == "active"
104
+ true
105
+ else
106
+ error("\nErro no provisionamento.")
107
+ false
108
+ end
109
+ rescue Brasa::Websocket::CableClient::ConnectionError => e
110
+ warn_fallback(e)
111
+ wait_for_provisioning_polling(slug)
112
+ ensure
113
+ ws&.disconnect
114
+ end
115
+
116
+ def wait_for_deploy_complete(slug, deploy_id)
117
+ ws = connect_websocket
118
+ started = Time.now
119
+
120
+ ws.on_log { |msg| print msg["line"] + "\n" if msg["line"] }
121
+ ws.subscribe("DeployLogChannel", deploy_id: deploy_id)
122
+
123
+ status = ws.wait_for("DeployStatusChannel",
124
+ params: { deploy_id: deploy_id },
125
+ until_status: %w[live failed],
126
+ timeout: DEPLOY_TIMEOUT)
127
+
128
+ if status == "live"
129
+ true
130
+ else
131
+ error("\nDeploy falhou.")
132
+ false
133
+ end
134
+ rescue Brasa::Websocket::CableClient::ConnectionError => e
135
+ warn_fallback(e)
136
+ wait_for_deploy_polling(slug, deploy_id)
137
+ ensure
138
+ ws&.disconnect
139
+ end
140
+
141
+ def connect_websocket
142
+ require "brasa/websocket/cable_client"
143
+ Brasa::Websocket::CableClient.new.tap(&:connect!)
144
+ end
145
+
146
+ def warn_fallback(err)
147
+ $stderr.puts "\n [WebSocket indisponível: #{err.message}. Usando polling...]"
148
+ end
149
+
150
+ # ── Fallback polling ──
151
+
152
+ def wait_for_provisioning_polling(slug)
97
153
  started = Time.now
98
154
  last_status = nil
99
155
  PROVISION_MAX_POLLS.times do |i|
100
156
  app = api.get("/api/v1/apps/#{slug}")
101
157
  return true if app["status"] == "active"
102
- if app["status"] == "error"
103
- error("\nErro no provisionamento.")
104
- return false
105
- end
158
+ return (error("\nErro no provisionamento."); false) if app["status"] == "error"
106
159
  if app["status"] != last_status
107
- print "\n #{status_label(app["status"])}" if last_status
160
+ print "\n #{status_label(app["status"])}"
108
161
  last_status = app["status"]
109
162
  end
110
163
  print "."
@@ -114,23 +167,19 @@ module Brasa
114
167
  print "!"
115
168
  sleep(POLL_INTERVAL)
116
169
  end
117
- elapsed = (Time.now - started).to_i
118
- error("\nTimeout aguardando provisionamento (#{elapsed}s). O processo pode ainda estar rodando no servidor — verifique com: brasa status")
170
+ error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
119
171
  false
120
172
  end
121
173
 
122
- def wait_for_deploy(slug, deploy_id)
174
+ def wait_for_deploy_polling(slug, deploy_id)
123
175
  started = Time.now
124
176
  last_status = nil
125
177
  DEPLOY_MAX_POLLS.times do |i|
126
178
  deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
127
179
  return true if deploy["status"] == "live"
128
- if deploy["status"] == "failed"
129
- error("\nDeploy falhou.")
130
- return false
131
- end
180
+ return (error("\nDeploy falhou."); false) if deploy["status"] == "failed"
132
181
  if deploy["status"] != last_status
133
- print "\n #{status_label(deploy["status"])}" if last_status
182
+ print "\n #{status_label(deploy["status"])}"
134
183
  last_status = deploy["status"]
135
184
  end
136
185
  print "."
@@ -140,24 +189,19 @@ module Brasa
140
189
  print "!"
141
190
  sleep(POLL_INTERVAL)
142
191
  end
143
- elapsed = (Time.now - started).to_i
144
- error("\nTimeout aguardando deploy (#{elapsed}s). O processo pode ainda estar rodando no servidor — verifique com: brasa status")
192
+ error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
145
193
  false
146
194
  end
147
195
 
148
196
  def status_label(status)
149
- { "pending" => "Aguardando...",
150
- "provisioning" => "Provisionando VM...",
151
- "queued" => "Na fila...",
152
- "building" => "Construindo imagem...",
197
+ { "pending" => "Aguardando...", "provisioning" => "Provisionando VM...",
198
+ "queued" => "Na fila...", "building" => "Construindo imagem...",
153
199
  "deploying" => "Deployando..." }[status] || status
154
200
  end
155
201
 
156
202
  def show_elapsed(started)
157
203
  elapsed = (Time.now - started).to_i
158
- min = elapsed / 60
159
- sec = elapsed % 60
160
- print " (#{min}m#{sec}s)"
204
+ print " (#{elapsed / 60}m#{elapsed % 60}s)"
161
205
  end
162
206
  end
163
207
  end
data/lib/brasa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Brasa
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.1"
3
3
  end
@@ -0,0 +1,146 @@
1
+ require "websocket-client-simple"
2
+ require "json"
3
+
4
+ module Brasa
5
+ module Websocket
6
+ class CableClient
7
+ class ConnectionError < StandardError; end
8
+
9
+ RECONNECT_DELAYS = [1, 3, 10].freeze
10
+
11
+ def initialize(api_url: nil, token: nil)
12
+ @api_url = api_url || Brasa::Config.api_url
13
+ @token = token || Brasa::Config.token
14
+ @subscriptions = {}
15
+ @connected = false
16
+ @log_callback = nil
17
+ end
18
+
19
+ def connect!
20
+ raise ConnectionError, "Token nao encontrado. Execute `brasa login`." unless @token
21
+
22
+ ws_url = @api_url.sub(%r{^https?://}, "wss://").chomp("/") + "/cable"
23
+ @ws = WebSocket::Client::Simple.connect(ws_url, headers: {
24
+ "Authorization" => "Bearer #{@token}",
25
+ "Origin" => @api_url
26
+ })
27
+
28
+ setup_handlers
29
+ wait_for_welcome
30
+ self
31
+ end
32
+
33
+ def subscribe(channel, params = {}, &callback)
34
+ identifier = { channel: channel }.merge(params).to_json
35
+ @subscriptions[identifier] = callback
36
+ send_command("subscribe", identifier)
37
+ identifier
38
+ end
39
+
40
+ def wait_for(channel, params: {}, until_status: [], timeout: 1800)
41
+ result = nil
42
+ identifier = subscribe(channel, params) do |msg|
43
+ status = msg["status"]
44
+ result = status if until_status.include?(status)
45
+ end
46
+
47
+ deadline = Time.now + timeout
48
+ loop do
49
+ return result if result
50
+ raise ConnectionError, "Timeout aguardando status (#{timeout}s)" if Time.now > deadline
51
+
52
+ unless @connected
53
+ reconnect!
54
+ # Re-subscribe after reconnect
55
+ @subscriptions.each_key { |id| send_command("subscribe", id) }
56
+ end
57
+
58
+ sleep 0.2
59
+ end
60
+ ensure
61
+ send_command("unsubscribe", identifier) if identifier && @connected
62
+ end
63
+
64
+ def on_log(&block)
65
+ @log_callback = block
66
+ end
67
+
68
+ def disconnect
69
+ @ws&.close
70
+ @connected = false
71
+ end
72
+
73
+ private
74
+
75
+ def setup_handlers
76
+ client = self
77
+
78
+ @ws.on :message do |msg|
79
+ begin
80
+ data = JSON.parse(msg.data)
81
+ client.send(:handle_message, data)
82
+ rescue JSON::ParserError
83
+ $stderr.puts "[WS] Mensagem malformada ignorada: #{msg.data.to_s.truncate(100)}" if ENV["BRASA_DEBUG"]
84
+ end
85
+ end
86
+
87
+ @ws.on :close do |_|
88
+ client.instance_variable_set(:@connected, false)
89
+ end
90
+
91
+ @ws.on :error do |_|
92
+ client.instance_variable_set(:@connected, false)
93
+ end
94
+ end
95
+
96
+ def handle_message(data)
97
+ case data["type"]
98
+ when "welcome"
99
+ @connected = true
100
+ when "ping"
101
+ # keepalive — noop
102
+ when "confirm_subscription"
103
+ # confirmed
104
+ when "reject_subscription"
105
+ @connected = false
106
+ else
107
+ identifier = data["identifier"]
108
+ message = data["message"]
109
+ return unless message
110
+
111
+ callback = @subscriptions[identifier]
112
+ callback&.call(message)
113
+
114
+ @log_callback&.call(message) if message["line"]
115
+ end
116
+ end
117
+
118
+ def wait_for_welcome
119
+ deadline = Time.now + 10
120
+ loop do
121
+ return if @connected
122
+ raise ConnectionError, "Timeout na conexao WebSocket" if Time.now > deadline
123
+ sleep 0.1
124
+ end
125
+ end
126
+
127
+ def reconnect!
128
+ RECONNECT_DELAYS.each do |delay|
129
+ sleep delay
130
+ begin
131
+ connect!
132
+ return
133
+ rescue StandardError
134
+ next
135
+ end
136
+ end
137
+ raise ConnectionError, "Falha na reconexao apos #{RECONNECT_DELAYS.size} tentativas"
138
+ end
139
+
140
+ def send_command(command, identifier)
141
+ return unless @ws&.open?
142
+ @ws.send({ command: command, identifier: identifier }.to_json)
143
+ end
144
+ end
145
+ end
146
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brasa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brasa
@@ -107,6 +107,20 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: websocket-client-simple
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.8'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.8'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: rspec
112
126
  requirement: !ruby/object:Gem::Requirement
@@ -168,6 +182,7 @@ files:
168
182
  - lib/brasa/config.rb
169
183
  - lib/brasa/source_packer.rb
170
184
  - lib/brasa/version.rb
185
+ - lib/brasa/websocket/cable_client.rb
171
186
  homepage: https://usebrasa.com.br
172
187
  licenses:
173
188
  - MIT