brasa 0.4.2 → 0.5.0

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: 3618063ca02e720d10a6a270976219d508a078ff13917d3de2fc9f81d9ab8e73
4
+ data.tar.gz: 781b40094dfc2baef08fc091d266cb79acd984dfabed6d3f54b9f68e2616c6c7
5
5
  SHA512:
6
- metadata.gz: fdad15d4954607bff0bb6a2737a4be3d8959e7213393cd32ec0ccfb995f0c42d8f416a6046810f9aa8d3ac88fd57dd34989e429be6856a9c5830d1b8d8466580
7
- data.tar.gz: 6d2293ceb1e6ab6ae539e6a8f6e28b22bf82b29a64e31d383c103125e9891e7a0287bd0acabd02d2bf02c6ce5ce773c7331b1cd33ef24f6c3a2b6f935680b0bd
6
+ metadata.gz: edbd4b233d795fd78006f054791743985f6a1513c90d37951313739d92a3e1c945cdf5254d7981941400575a23d26e891cb0c991554216419710001912214b22
7
+ data.tar.gz: c7db4110649d9378c0eb9d5351a15da213a716ca0037612c56632d00ea3cca2bb10e5188d43f1a709bb8dcec35cd2ff874d89f1b5bc0129e2371a08069a0cef0
@@ -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 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
+ 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,63 @@ 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
+ status == "active" ? true : (error("\nErro no provisionamento."); false)
104
+ rescue Websocket::CableClient::ConnectionError => e
105
+ warn_fallback(e)
106
+ wait_for_provisioning_polling(slug)
107
+ ensure
108
+ ws&.disconnect
109
+ end
110
+
111
+ def wait_for_deploy_complete(slug, deploy_id)
112
+ ws = connect_websocket
113
+ started = Time.now
114
+
115
+ ws.on_log { |msg| print msg["line"] + "\n" if msg["line"] }
116
+ ws.subscribe("DeployLogChannel", deploy_id: deploy_id)
117
+
118
+ status = ws.wait_for("DeployStatusChannel",
119
+ params: { deploy_id: deploy_id },
120
+ until_status: %w[live failed],
121
+ timeout: DEPLOY_TIMEOUT)
122
+
123
+ status == "live" ? true : (error("\nDeploy falhou."); false)
124
+ rescue Websocket::CableClient::ConnectionError => e
125
+ warn_fallback(e)
126
+ wait_for_deploy_polling(slug, deploy_id)
127
+ ensure
128
+ ws&.disconnect
129
+ end
130
+
131
+ def connect_websocket
132
+ require "brasa/websocket/cable_client"
133
+ Websocket::CableClient.new.tap(&:connect!)
134
+ end
135
+
136
+ def warn_fallback(err)
137
+ $stderr.puts "\n [WebSocket indisponível: #{err.message}. Usando polling...]"
138
+ end
139
+
140
+ # ── Fallback polling ──
141
+
142
+ def wait_for_provisioning_polling(slug)
97
143
  started = Time.now
98
144
  last_status = nil
99
145
  PROVISION_MAX_POLLS.times do |i|
100
146
  app = api.get("/api/v1/apps/#{slug}")
101
147
  return true if app["status"] == "active"
102
- if app["status"] == "error"
103
- error("\nErro no provisionamento.")
104
- return false
105
- end
148
+ return (error("\nErro no provisionamento."); false) if app["status"] == "error"
106
149
  if app["status"] != last_status
107
- print "\n #{status_label(app["status"])}" if last_status
150
+ print "\n #{status_label(app["status"])}"
108
151
  last_status = app["status"]
109
152
  end
110
153
  print "."
@@ -114,23 +157,19 @@ module Brasa
114
157
  print "!"
115
158
  sleep(POLL_INTERVAL)
116
159
  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")
160
+ error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
119
161
  false
120
162
  end
121
163
 
122
- def wait_for_deploy(slug, deploy_id)
164
+ def wait_for_deploy_polling(slug, deploy_id)
123
165
  started = Time.now
124
166
  last_status = nil
125
167
  DEPLOY_MAX_POLLS.times do |i|
126
168
  deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
127
169
  return true if deploy["status"] == "live"
128
- if deploy["status"] == "failed"
129
- error("\nDeploy falhou.")
130
- return false
131
- end
170
+ return (error("\nDeploy falhou."); false) if deploy["status"] == "failed"
132
171
  if deploy["status"] != last_status
133
- print "\n #{status_label(deploy["status"])}" if last_status
172
+ print "\n #{status_label(deploy["status"])}"
134
173
  last_status = deploy["status"]
135
174
  end
136
175
  print "."
@@ -140,24 +179,19 @@ module Brasa
140
179
  print "!"
141
180
  sleep(POLL_INTERVAL)
142
181
  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")
182
+ error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: brasa status")
145
183
  false
146
184
  end
147
185
 
148
186
  def status_label(status)
149
- { "pending" => "Aguardando...",
150
- "provisioning" => "Provisionando VM...",
151
- "queued" => "Na fila...",
152
- "building" => "Construindo imagem...",
187
+ { "pending" => "Aguardando...", "provisioning" => "Provisionando VM...",
188
+ "queued" => "Na fila...", "building" => "Construindo imagem...",
153
189
  "deploying" => "Deployando..." }[status] || status
154
190
  end
155
191
 
156
192
  def show_elapsed(started)
157
193
  elapsed = (Time.now - started).to_i
158
- min = elapsed / 60
159
- sec = elapsed % 60
160
- print " (#{min}m#{sec}s)"
194
+ print " (#{elapsed / 60}m#{elapsed % 60}s)"
161
195
  end
162
196
  end
163
197
  end
data/lib/brasa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Brasa
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -0,0 +1,123 @@
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, "No token. Run `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 (#{timeout}s)" if Time.now > deadline
51
+ raise ConnectionError, "Connection lost" unless @connected
52
+ sleep 0.2
53
+ end
54
+ ensure
55
+ send_command("unsubscribe", identifier) if identifier && @connected
56
+ end
57
+
58
+ def on_log(&block)
59
+ @log_callback = block
60
+ end
61
+
62
+ def disconnect
63
+ @ws&.close
64
+ @connected = false
65
+ end
66
+
67
+ private
68
+
69
+ def setup_handlers
70
+ client = self
71
+
72
+ @ws.on :message do |msg|
73
+ data = JSON.parse(msg.data) rescue next
74
+ client.send(:handle_message, data)
75
+ end
76
+
77
+ @ws.on :close do |_|
78
+ client.instance_variable_set(:@connected, false)
79
+ end
80
+
81
+ @ws.on :error do |_|
82
+ client.instance_variable_set(:@connected, false)
83
+ end
84
+ end
85
+
86
+ def handle_message(data)
87
+ case data["type"]
88
+ when "welcome"
89
+ @connected = true
90
+ when "ping"
91
+ # keepalive — noop
92
+ when "confirm_subscription"
93
+ # confirmed
94
+ when "reject_subscription"
95
+ raise ConnectionError, "Subscription rejected"
96
+ else
97
+ identifier = data["identifier"]
98
+ message = data["message"]
99
+ return unless message
100
+
101
+ callback = @subscriptions[identifier]
102
+ callback&.call(message)
103
+
104
+ @log_callback&.call(message) if message["line"]
105
+ end
106
+ end
107
+
108
+ def wait_for_welcome
109
+ deadline = Time.now + 10
110
+ loop do
111
+ return if @connected
112
+ raise ConnectionError, "WebSocket connection timeout" if Time.now > deadline
113
+ sleep 0.1
114
+ end
115
+ end
116
+
117
+ def send_command(command, identifier)
118
+ return unless @ws&.open?
119
+ @ws.send({ command: command, identifier: identifier }.to_json)
120
+ end
121
+ end
122
+ end
123
+ 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.0
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