Class: Bard::Api::App

Inherits:
Object
  • Object
show all
Defined in:
lib/bard/api/app.rb

Constant Summary collapse

DEPLOY_LOCK =
"tmp/bard-deploy.lock"
DEPLOYED_SHA =
"tmp/bard-deployed.sha"
FAILED_SHA =
"tmp/bard-deploy-failed.sha"
DEPLOY_LOG =
"log/bard-deploy.log"
FAILED_COOLDOWN =

How long a failed sha short-circuits re-deploy: long enough to end the caller's poll loop without a re-spawn, short enough that a fresh bard deploy run can retry the same sha.

60
DEPLOY_COMMAND =

Record the deployed sha only after bin/setup fully succeeds — a bare git pull advances HEAD before the bundle/migrate/restart run, so HEAD alone would read as "done" too early. On any failure, stamp the attempted sha so we don't re-spawn (and restart Puma) on every poll; the marker is written inside the flock, so it's on disk before the lock releases. Then roll the tree back to the pre-pull sha: code sitting ahead of its installed gems is a time bomb — the next app-process respawn (e.g. an nginx reload) can't boot and the site 500s until someone intervenes. Better to keep running the old code.

"flock -n #{DEPLOY_LOCK} -c '" \
"rm -f #{FAILED_SHA}; " \
"before=$(git rev-parse HEAD); " \
"git pull --ff-only origin master && bin/setup && git rev-parse HEAD > #{DEPLOYED_SHA} " \
"|| { git rev-parse origin/master > #{FAILED_SHA}; git reset --hard \"$before\"; }'"

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.backup_runnerObject

Runs the backup task out-of-band so the request returns immediately. Override to wire backups into a project's own job queue.



17
18
19
# File 'lib/bard/api/app.rb', line 17

def backup_runner
  @backup_runner ||= ->(task) { Thread.new { task.call } }
end

.deploy_runnerObject

bin/setup restarts Puma via procsd restartsystemctl --user restart, which SIGKILLs the app unit's whole cgroup. So the deploy can't run in this worker (or any plain child of it) — it'd be killed mid-deploy. We run it in its own systemd --user scope: a separate cgroup that survives the restart.



25
26
27
# File 'lib/bard/api/app.rb', line 25

def deploy_runner
  @deploy_runner ||= method(:spawn_detached_deploy)
end

Class Method Details

.deploy_envObject

with_unbundled_env alone isn't enough: it restores Bundler::ORIGINAL_ENV, and under Passenger RUBYOPT=-rbundler/setup is injected before the app boots, so it's part of ORIGINAL_ENV and survives the scrub (took brianporter.com down on 2026-07-15). Strip the bundler vars explicitly — nil values remove them from the spawned env. XDG_RUNTIME_DIR: a Passenger web worker is spawned without a login session, so it's unset; systemd-run --user needs it to find the (linger-backed) user bus.



50
51
52
53
54
# File 'lib/bard/api/app.rb', line 50

def deploy_env
  bundler_vars = ENV.keys.grep(/\A(BUNDLE_|BUNDLER_)/) + %w[RUBYOPT RUBYLIB]
  bundler_vars.to_h { |key| [key, nil] }
    .merge("XDG_RUNTIME_DIR" => "/run/user/#{Process.uid}")
end

.spawn_detached_deploy(command) ⇒ Object

The deploy must start from a clean env: this worker runs under the app's bundle (RUBYOPT=-rbundler/setup, BUNDLE_GEMFILE). Inherited into the deploy, that makes every ruby command — bin/setup and bundle install included — crash at startup the moment git pull lands a lockfile with not-yet-installed gems.



33
34
35
36
37
38
39
40
41
42
# File 'lib/bard/api/app.rb', line 33

def spawn_detached_deploy(command)
  Bundler.with_unbundled_env do
    pid = Process.spawn(
      deploy_env,
      "systemd-run", "--user", "--scope", "--collect", "--quiet", "bash", "-lc", command,
      :in => "/dev/null", %i[out err] => ["log/bard-deploy.log", "a"],
    )
    Process.detach(pid)
  end
end

Instance Method Details

#call(env) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/bard/api/app.rb', line 79

def call(env)
  request = Rack::Request.new(env)
  method = request.request_method
  path = request.path_info

  case [method, path]
  when ["GET", "/health"]
    health(request)
  when ["POST", "/backups"]
    create_backup(request)
  when ["GET", "/backups/latest"]
    latest_backup(request)
  when ["GET", "/config"]
    config(request)
  when ["POST", "/deploy"]
    create_deploy(request)
  else
    not_found
  end
rescue => e
  json_response(500, { error: e.message })
end