bard-api 0.5.1 → 0.6.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: 247fb68f718adad762c323e0d6fc04515553de744e4a3ebe35a327707fc1e728
4
- data.tar.gz: c59cf65732a9cc8d17486e2ff20ae35535677b30952b104f29d69442d8606c61
3
+ metadata.gz: 563830ec8c572c92d9ccfe0baacef1e2e24746600e7a8d8508a3fe2819c6650e
4
+ data.tar.gz: f015d645e8d3bee75b1975b496ae53a93dc7e3bceeb2f21489a954e142862209
5
5
  SHA512:
6
- metadata.gz: 75e2d1c6d652fa389be4ccdf465d6678c8c84287f8fccc5fee548233c1b4fabdcce9f0759d7f530a1e446dd386ec818833f7929988d8498a37c329a2abfde3e8
7
- data.tar.gz: 65c8245cafc0e13d5f5c47c3ece4fcae6b9c0ef0e8028958c2261d83f8f68ad7ecb62841b07273acd8d2a40c3a47ebcf27a0edfb5eb0d20221054ff83cd566d7
6
+ metadata.gz: 9f49e14b5cb6ab5b9a56c7b5f138d17c725ccdea8c995f9e9e811580ff0cb7801a65c77ce3ee7a6bc8e93cbc4dfb5748702c858873488e9551da8bc63356a518
7
+ data.tar.gz: ccfa6aec43edc4d1f30b23cb92b837e9535ef46f1e3b997791bcd0d0a25bb38b736682ef5a468d28652297b670cd0b80d6ebb8f30a8c9444bd446589d25b4290
data/README.md CHANGED
@@ -11,19 +11,20 @@ The bard-api gem enables BARD Tracker to manage Rails applications through a RES
11
11
 
12
12
  ## Usage
13
13
 
14
- ### Mounting in Rails
14
+ ### Mounting
15
15
 
16
- Add to your `config/routes.rb`:
16
+ With `bard-rails` this mounts automatically at `/bard` — nothing to do. Otherwise add to your
17
+ `config/routes.rb`:
17
18
 
18
19
  ```ruby
19
- mount Bard::Api::App.new => "/bard-api"
20
+ mount Bard::Api::App.new => "/bard"
20
21
  ```
21
22
 
22
- This makes the API available at `/bard-api/*` endpoints.
23
+ Either way the API is served at `/bard/*`.
23
24
 
24
25
  ### Endpoints
25
26
 
26
- #### GET /bard-api/health
27
+ #### GET /bard/health
27
28
 
28
29
  Health check endpoint (no authentication required).
29
30
 
@@ -34,7 +35,7 @@ Health check endpoint (no authentication required).
34
35
  }
35
36
  ```
36
37
 
37
- #### POST /bard-api/backups
38
+ #### POST /bard/backups
38
39
 
39
40
  Trigger a backup (requires JWT authentication).
40
41
 
@@ -67,7 +68,7 @@ Authorization: Bearer <jwt-token>
67
68
  }
68
69
  ```
69
70
 
70
- #### GET /bard-api/backups/latest
71
+ #### GET /bard/backups/latest
71
72
 
72
73
  Get status of most recent backup (requires JWT authentication).
73
74
 
@@ -98,7 +99,7 @@ Authorization: Bearer <jwt-token>
98
99
  }
99
100
  ```
100
101
 
101
- #### GET /bard-api/config
102
+ #### GET /bard/config
102
103
 
103
104
  Read the project's live backup and uptime configuration (requires JWT authentication). Used by axis to discover what each project actually has deployed.
104
105
 
data/lib/bard/api/app.rb CHANGED
@@ -2,12 +2,51 @@
2
2
 
3
3
  require "rack"
4
4
  require "json"
5
+ require "bundler"
5
6
  require_relative "auth"
6
7
  require "bard/backup"
7
8
 
8
9
  module Bard
9
10
  module Api
10
11
  class App
12
+ class << self
13
+ attr_writer :backup_runner, :deploy_runner
14
+
15
+ # Runs the backup task out-of-band so the request returns immediately.
16
+ # Override to wire backups into a project's own job queue.
17
+ def backup_runner
18
+ @backup_runner ||= ->(task) { Thread.new { task.call } }
19
+ end
20
+
21
+ # bin/setup restarts Puma via `procsd restart` → `systemctl --user restart`, which
22
+ # SIGKILLs the app unit's whole cgroup. So the deploy can't run in this worker (or any
23
+ # plain child of it) — it'd be killed mid-deploy. We run it in its own systemd --user
24
+ # scope: a separate cgroup that survives the restart.
25
+ def deploy_runner
26
+ @deploy_runner ||= method(:spawn_detached_deploy)
27
+ end
28
+
29
+ # with_unbundled_env is essential: this worker runs under the app's bundle
30
+ # (RUBYOPT=-rbundler/setup, BUNDLE_GEMFILE). Inherited into the deploy, that makes every
31
+ # ruby command — bundle install included — crash at startup the moment git pull lands a
32
+ # lockfile with not-yet-installed gems. The deploy must start from a clean env.
33
+ def spawn_detached_deploy(command)
34
+ Bundler.with_unbundled_env do
35
+ pid = Process.spawn(
36
+ "systemd-run", "--user", "--scope", "--collect", "--quiet", "bash", "-lc", command,
37
+ :in => "/dev/null", %i[out err] => ["log/bard-deploy.log", "a"],
38
+ )
39
+ Process.detach(pid)
40
+ end
41
+ end
42
+ end
43
+
44
+ DEPLOY_LOCK = "tmp/bard-deploy.lock"
45
+ DEPLOYED_SHA = "tmp/bard-deployed.sha"
46
+ # Record the deployed sha only after bin/setup fully succeeds — a bare `git pull` advances
47
+ # HEAD before the bundle/migrate/restart run, so HEAD alone would read as "done" too early.
48
+ DEPLOY_COMMAND = "flock -n #{DEPLOY_LOCK} -c 'git pull --ff-only origin master && bin/setup && git rev-parse HEAD > #{DEPLOYED_SHA}'"
49
+
11
50
  def call(env)
12
51
  request = Rack::Request.new(env)
13
52
  method = request.request_method
@@ -22,6 +61,8 @@ module Bard
22
61
  latest_backup(request)
23
62
  when ["GET", "/config"]
24
63
  config(request)
64
+ when ["POST", "/deploy"]
65
+ create_deploy(request)
25
66
  else
26
67
  not_found
27
68
  end
@@ -40,13 +81,30 @@ module Bard
40
81
  s3 = payload["s3"].transform_keys(&:to_sym)
41
82
  project_name = Bard::Config.current.project_name
42
83
 
43
- backup = Bard::Backup.create!(
44
- type: :s3,
45
- path: "bard-backup/#{project_name}",
46
- **s3,
47
- )
84
+ self.class.backup_runner.call(-> { run_backup(project_name, s3) })
48
85
 
49
- json_response(200, backup.as_json)
86
+ json_response(202, { status: "started" })
87
+ end
88
+ end
89
+
90
+ def run_backup(project_name, s3)
91
+ Bard::Backup.create!(
92
+ type: :s3,
93
+ path: "bard-backup/#{project_name}",
94
+ **s3,
95
+ )
96
+ rescue => e
97
+ log_backup_error(e)
98
+ end
99
+
100
+ def log_backup_error(error)
101
+ message = "[bard-api] backup failed: #{error.class}: #{error.message}"
102
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
103
+ Rails.logger.error(message)
104
+ Rails.logger.error(error.backtrace.join("\n")) if error.backtrace
105
+ else
106
+ warn message
107
+ warn error.backtrace.join("\n") if error.backtrace
50
108
  end
51
109
  end
52
110
 
@@ -65,6 +123,39 @@ module Bard
65
123
  end
66
124
  end
67
125
 
126
+ # Unauthenticated on purpose: it can only fast-forward prod to origin/master (master IS prod)
127
+ # and is a no-op when already there.
128
+ #
129
+ # bin/setup restarts Puma, so we can't run the deploy in this worker thread — the restart
130
+ # would kill it mid-flight. We hand off to an out-of-band process (its own systemd scope,
131
+ # see .deploy_runner), serialize concurrent deploys with an flock, and return 202
132
+ # immediately. The caller polls until HEAD == sha.
133
+ def create_deploy(request)
134
+ target = JSON.parse(request.body.read)["sha"]
135
+ sha = current_sha
136
+ return json_response(200, { status: "noop", sha: sha }) if target == sha
137
+ return json_response(409, { status: "deploying", sha: sha }) if deploy_in_progress?
138
+
139
+ self.class.deploy_runner.call(DEPLOY_COMMAND)
140
+ json_response(202, { status: "deploying", sha: sha })
141
+ end
142
+
143
+ # The last FULLY deployed sha (bin/setup succeeded), not merely where git HEAD points.
144
+ # Absent marker => nothing has completed yet, so it can never match the requested sha and
145
+ # the caller keeps polling until bin/setup writes it — or times out on a real failure.
146
+ def current_sha
147
+ return "" unless File.exist?(DEPLOYED_SHA)
148
+ File.read(DEPLOYED_SHA).chomp
149
+ end
150
+
151
+ def deploy_in_progress?
152
+ File.open(DEPLOY_LOCK, File::RDWR | File::CREAT, 0o644) do |lock|
153
+ acquired = lock.flock(File::LOCK_EX | File::LOCK_NB)
154
+ lock.flock(File::LOCK_UN) if acquired
155
+ !acquired
156
+ end
157
+ end
158
+
68
159
  def serialize_config(bard_config)
69
160
  backup = bard_config.backup
70
161
  production = bard_config.targets[:production]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bard
4
4
  module Api
5
- VERSION = "0.5.1"
5
+ VERSION = "0.6.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bard-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-05-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: jwt
@@ -131,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
131
  - !ruby/object:Gem::Version
132
132
  version: '0'
133
133
  requirements: []
134
- rubygems_version: 3.6.2
134
+ rubygems_version: 4.0.6
135
135
  specification_version: 4
136
136
  summary: REST API for BARD-managed Rails projects
137
137
  test_files: []