bard-api 0.5.1 → 0.6.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: 247fb68f718adad762c323e0d6fc04515553de744e4a3ebe35a327707fc1e728
4
- data.tar.gz: c59cf65732a9cc8d17486e2ff20ae35535677b30952b104f29d69442d8606c61
3
+ metadata.gz: ee48d71ea32a1a05c4a540cdab7b8127f94e9366cfcc259a829f2843d0ce8fe2
4
+ data.tar.gz: f3baa8c04d7acf644692c9cc5dbd6dc143504c22d1e4b8380134d4cb29217728
5
5
  SHA512:
6
- metadata.gz: 75e2d1c6d652fa389be4ccdf465d6678c8c84287f8fccc5fee548233c1b4fabdcce9f0759d7f530a1e446dd386ec818833f7929988d8498a37c329a2abfde3e8
7
- data.tar.gz: 65c8245cafc0e13d5f5c47c3ece4fcae6b9c0ef0e8028958c2261d83f8f68ad7ecb62841b07273acd8d2a40c3a47ebcf27a0edfb5eb0d20221054ff83cd566d7
6
+ metadata.gz: 0ca7e1d49a6dfac34bf2c55e01f6e2dbb4d287ca003301a371a092ae57e3ab4e62110d694ee8d9863053fe32fbff97bd2834338128c8b8afd47fafe88d4e36c7
7
+ data.tar.gz: 5b1167e219a79526de91876267bf769ad895c6aaf8cea1df2e5c6fe316307a05e33b29da3f5c39748037e5027b978d82b4da7e2647f10a6c301b535414fbae55
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
@@ -8,6 +8,35 @@ require "bard/backup"
8
8
  module Bard
9
9
  module Api
10
10
  class App
11
+ class << self
12
+ attr_writer :backup_runner, :deploy_runner
13
+
14
+ # Runs the backup task out-of-band so the request returns immediately.
15
+ # Override to wire backups into a project's own job queue.
16
+ def backup_runner
17
+ @backup_runner ||= ->(task) { Thread.new { task.call } }
18
+ end
19
+
20
+ # bin/setup restarts Puma via `procsd restart` → `systemctl --user restart`, which
21
+ # SIGKILLs the app unit's whole cgroup. So the deploy can't run in this worker (or any
22
+ # plain child of it) — it'd be killed mid-deploy. We run it in its own systemd --user
23
+ # scope: a separate cgroup that survives the restart.
24
+ def deploy_runner
25
+ @deploy_runner ||= method(:spawn_detached_deploy)
26
+ end
27
+
28
+ def spawn_detached_deploy(command)
29
+ pid = Process.spawn(
30
+ "systemd-run", "--user", "--scope", "--collect", "--quiet", "bash", "-lc", command,
31
+ :in => "/dev/null", %i[out err] => ["log/bard-deploy.log", "a"],
32
+ )
33
+ Process.detach(pid)
34
+ end
35
+ end
36
+
37
+ DEPLOY_LOCK = "tmp/bard-deploy.lock"
38
+ DEPLOY_COMMAND = "flock -n #{DEPLOY_LOCK} -c 'git pull --ff-only origin master && bin/setup'"
39
+
11
40
  def call(env)
12
41
  request = Rack::Request.new(env)
13
42
  method = request.request_method
@@ -22,6 +51,8 @@ module Bard
22
51
  latest_backup(request)
23
52
  when ["GET", "/config"]
24
53
  config(request)
54
+ when ["POST", "/deploy"]
55
+ create_deploy(request)
25
56
  else
26
57
  not_found
27
58
  end
@@ -40,13 +71,30 @@ module Bard
40
71
  s3 = payload["s3"].transform_keys(&:to_sym)
41
72
  project_name = Bard::Config.current.project_name
42
73
 
43
- backup = Bard::Backup.create!(
44
- type: :s3,
45
- path: "bard-backup/#{project_name}",
46
- **s3,
47
- )
74
+ self.class.backup_runner.call(-> { run_backup(project_name, s3) })
48
75
 
49
- json_response(200, backup.as_json)
76
+ json_response(202, { status: "started" })
77
+ end
78
+ end
79
+
80
+ def run_backup(project_name, s3)
81
+ Bard::Backup.create!(
82
+ type: :s3,
83
+ path: "bard-backup/#{project_name}",
84
+ **s3,
85
+ )
86
+ rescue => e
87
+ log_backup_error(e)
88
+ end
89
+
90
+ def log_backup_error(error)
91
+ message = "[bard-api] backup failed: #{error.class}: #{error.message}"
92
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
93
+ Rails.logger.error(message)
94
+ Rails.logger.error(error.backtrace.join("\n")) if error.backtrace
95
+ else
96
+ warn message
97
+ warn error.backtrace.join("\n") if error.backtrace
50
98
  end
51
99
  end
52
100
 
@@ -65,6 +113,35 @@ module Bard
65
113
  end
66
114
  end
67
115
 
116
+ # Unauthenticated on purpose: it can only fast-forward prod to origin/master (master IS prod)
117
+ # and is a no-op when already there.
118
+ #
119
+ # bin/setup restarts Puma, so we can't run the deploy in this worker thread — the restart
120
+ # would kill it mid-flight. We hand off to an out-of-band process (its own systemd scope,
121
+ # see .deploy_runner), serialize concurrent deploys with an flock, and return 202
122
+ # immediately. The caller polls until HEAD == sha.
123
+ def create_deploy(request)
124
+ target = JSON.parse(request.body.read)["sha"]
125
+ sha = current_sha
126
+ return json_response(200, { status: "noop", sha: sha }) if target == sha
127
+ return json_response(409, { status: "deploying", sha: sha }) if deploy_in_progress?
128
+
129
+ self.class.deploy_runner.call(DEPLOY_COMMAND)
130
+ json_response(202, { status: "deploying", sha: sha })
131
+ end
132
+
133
+ def current_sha
134
+ `git rev-parse HEAD`.chomp
135
+ end
136
+
137
+ def deploy_in_progress?
138
+ File.open(DEPLOY_LOCK, File::RDWR | File::CREAT, 0o644) do |lock|
139
+ acquired = lock.flock(File::LOCK_EX | File::LOCK_NB)
140
+ lock.flock(File::LOCK_UN) if acquired
141
+ !acquired
142
+ end
143
+ end
144
+
68
145
  def serialize_config(bard_config)
69
146
  backup = bard_config.backup
70
147
  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.0"
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.0
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: 2026-07-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: jwt