lux-hammer 0.3.10 → 0.3.13

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.
@@ -0,0 +1,149 @@
1
+ module LuxDeploy
2
+ # Bag of resolved state for a single command invocation.
3
+ class Context
4
+ # A deployable unit. systemd.service is the web service (caddy-fronted);
5
+ # any other config/deploy/<name>.service becomes <prefix>-<app>-<name>.
6
+ Service ||= Struct.new(:name, :template, :artifact, :unit, :web)
7
+
8
+ attr_reader :host, :ssh, :branch, :app, :app_dir, :config_dir,
9
+ :env_template_name, :config, :templates_dir
10
+ attr_accessor :ports, :domain, :rendered
11
+
12
+ # Primary web port. Convenience for the manifest / done line; the full
13
+ # set (PORT, PORT_*) lives in #ports.
14
+ def port
15
+ ports && ports[:PORT]
16
+ end
17
+
18
+ def ruby_path
19
+ @ruby_path ||= detect_ruby_path
20
+ end
21
+
22
+ # All template-substitution vars come from two places: git-derived and
23
+ # yaml. Engine-dynamic vars (PORT, DIR, RUBY, RUBY_DIR) layer on top in
24
+ # render_artifacts since they are only known after server probe. The
25
+ # rendered .env never feeds back into this namespace - .env is a
26
+ # runtime-only file the app reads at boot.
27
+ def base_vars
28
+ Template.git_vars.merge(config.template_vars).merge(
29
+ APP: @app,
30
+ APP_UNDERSCORE: @app.gsub(/[^A-Za-z0-9]+/, '_'),
31
+ HASH: domain_hash,
32
+ TAG: domain_tag
33
+ )
34
+ end
35
+
36
+ def domain_hash
37
+ require 'digest'
38
+ "h#{Digest::SHA256.hexdigest(@domain.to_s)[0, 6]}"
39
+ end
40
+
41
+ def domain_tag
42
+ require 'digest'
43
+ "s#{Digest::SHA256.hexdigest(@domain.to_s)[0, 5]}"
44
+ end
45
+
46
+ def self.read_host(opts)
47
+ override = opts[:server]
48
+ return override.to_s.strip if override && !override.to_s.strip.empty?
49
+ Config.load.server.tap do |host|
50
+ raise Error.new("config/deploy/.yaml: 'server:' is empty") if host.empty?
51
+ end
52
+ end
53
+
54
+ def self.build(opts, render: true)
55
+ ctx = new
56
+ ctx.send(:resolve!, opts)
57
+ ctx
58
+ end
59
+
60
+ # Raw template body, or nil when the file exists in neither the app's
61
+ # config/deploy nor the plugin templates_dir. Non-raising twin of
62
+ # read_template - used for cheap scans (port keys, {{RUBY}} presence).
63
+ def template_source(name)
64
+ local = File.join(@config_dir, name)
65
+ return File.read(local) if File.exist?(local)
66
+ if templates_dir
67
+ shipped = File.join(templates_dir, name)
68
+ return File.read(shipped) if File.exist?(shipped)
69
+ end
70
+ nil
71
+ end
72
+
73
+ # Look up a template by name. Apps override individual files by dropping
74
+ # them in ./config/deploy/<name>; if missing, fall back to the host-
75
+ # supplied templates_dir (set via Hammer.register), if any.
76
+ def read_template(name)
77
+ template_source(name) or
78
+ raise Error.new("template not found: #{name} (looked in #{@config_dir}" \
79
+ "#{templates_dir ? " and #{templates_dir}" : ''})")
80
+ end
81
+
82
+ # Services discovered from config/deploy/*.service. systemd.service is
83
+ # always present (web, caddy front); every other file is an extra unit.
84
+ # A leading "!" disables a file (e.g. !job.service) - lux-deploy ignores
85
+ # it everywhere (not a service, not rsync'd). Pure filesystem lookup -
86
+ # no ssh - so it works in render:false (destroy).
87
+ def services
88
+ @services ||= begin
89
+ prefix = config.service_prefix
90
+ list = [Service.new('web', 'systemd.service', 'systemd.service',
91
+ "#{prefix}-#{app}", true)]
92
+ Dir.children(@config_dir).select { |f| f.end_with?('.service') && !f.start_with?('!') }.sort.each do |f|
93
+ next if f == 'systemd.service'
94
+ name = f.sub(/\.service\z/, '')
95
+ list << Service.new(name, f, "systemd.#{name}.service",
96
+ "#{prefix}-#{app}-#{name}", false)
97
+ end
98
+ list
99
+ end
100
+ end
101
+
102
+ # True when any rendered template references {{RUBY}}/{{RUBY_DIR}}. Gates
103
+ # the (ssh) ruby probe so a Go/Python app whose unit runs a built binary
104
+ # deploys without a ruby on the box.
105
+ def ruby_used?
106
+ return @ruby_used unless @ruby_used.nil?
107
+ names = (['caddy.conf', env_template_name] + services.map(&:template)).uniq
108
+ @ruby_used = names.any? { |n| (s = template_source(n)) && s.include?('{{RUBY') }
109
+ end
110
+
111
+ private
112
+
113
+ def resolve!(opts)
114
+ @config_dir = './config/deploy'
115
+ raise Error.new("missing #{@config_dir}/ directory") unless Dir.exist?(@config_dir)
116
+
117
+ @config = Config.load
118
+ @templates_dir = opts[:templates_dir]
119
+
120
+ @host = (opts[:server].to_s.strip.empty? ? @config.server : opts[:server]).to_s.strip
121
+ raise Error.new("no server set (.yaml 'server:' or --server)") if @host.empty?
122
+
123
+ @ssh = SSH.new(@host, service_user: @config.service_user, dry_run: opts[:dry_run] || false)
124
+ @branch = Template.git_vars[:GIT_BRANCH]
125
+ @env_template_name = LuxDeploy::MAIN_BRANCHES.include?(@branch) ? '.env' : '.env.staging'
126
+
127
+ # App slug comes from yaml `domain:` only. Multi-host strings like
128
+ # "foo.com, *.foo" are allowed; the slug is the first comma-split
129
+ # segment with any leading "*." stripped.
130
+ raw = @config.domain.to_s
131
+ raise Error.new("config/deploy/.yaml: 'domain:' is empty") if raw.strip.empty?
132
+ domain = raw.split(',').first.to_s.strip.sub(/^\*\./, '')
133
+ raise Error.new('domain resolved to empty') if domain.empty?
134
+ @app = domain
135
+ @domain = domain
136
+ @app_dir = File.join(@config.remote_base, domain)
137
+ end
138
+
139
+ def detect_ruby_path
140
+ return "/home/#{config.service_user}/.local/share/mise/installs/ruby/CURRENT/bin/ruby" if @ssh.dry_run
141
+ out = @ssh.run(<<~SH, as: :service, allow_fail: true)
142
+ ls -td ~/.local/share/mise/installs/ruby/*/bin/ruby 2>/dev/null | head -n1 || which ruby
143
+ SH
144
+ path = out.lines.find { |l| l.strip.start_with?('/') }&.strip
145
+ raise Error.new("no ruby found on remote (mise not installed for #{config.service_user}?)") if path.to_s.empty?
146
+ path
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,238 @@
1
+ require 'yaml'
2
+
3
+ module LuxDeploy
4
+ # Named host checks: each entry is [label, check_cmd, fix_cmd_or_nil].
5
+ # `check_cmd` must exit 0 on PASS, non-zero on FAIL.
6
+ # `fix_cmd` is optional; if present and check fails, we run it (when --fix)
7
+ # and re-check. Anything that needs interactive judgement leaves fix_cmd nil.
8
+ module Doctor
9
+ GREEN ||= "\e[32m"
10
+ RED ||= "\e[31m"
11
+ DIM ||= "\e[2m"
12
+ RESET ||= "\e[0m"
13
+
14
+ # Placeholders the engine always provides at deploy time; templates
15
+ # may reference these without declaring them in .env or .yaml.
16
+ PROVIDED_VARS ||= %w[
17
+ GIT_BRANCH GIT_BRANCH_UNDERSCORE APP APP_UNDERSCORE HASH TAG
18
+ PORT DIR RUBY RUBY_DIR
19
+ ].freeze
20
+
21
+ module_function
22
+
23
+ def run(ssh, config, fix: true)
24
+ puts 'Local config'
25
+ local_failed = local_checks(config)
26
+ puts
27
+
28
+ puts 'Remote host'
29
+ checks = build_checks(config)
30
+ failed = local_failed
31
+
32
+ checks.each do |label, check_cmd, fix_cmd|
33
+ if passes?(ssh, check_cmd)
34
+ puts " #{GREEN}PASS#{RESET} #{label}"
35
+ next
36
+ end
37
+
38
+ if fix && fix_cmd
39
+ puts " #{DIM}FIX#{RESET} #{label}"
40
+ ssh.run(fix_cmd, allow_fail: true)
41
+ if passes?(ssh, check_cmd)
42
+ puts " #{GREEN}PASS#{RESET} #{label} (after fix)"
43
+ next
44
+ end
45
+ end
46
+
47
+ failed += 1
48
+ puts " #{RED}FAIL#{RESET} #{label}"
49
+ puts " #{DIM} check: #{check_cmd.lines.first.chomp}#{RESET}"
50
+ end
51
+
52
+ puts
53
+ if failed.zero?
54
+ puts "#{GREEN}all checks passed#{RESET}"
55
+ else
56
+ puts "#{RED}#{failed} check#{failed == 1 ? '' : 's'} failed#{RESET}"
57
+ raise Error.new("doctor reported #{failed} failure(s)")
58
+ end
59
+ end
60
+
61
+ # Run check command, return true on exit 0.
62
+ def passes?(ssh, cmd)
63
+ out = ssh.run(cmd + ' && echo __OK__ || echo __FAIL__', allow_fail: true)
64
+ out.include?('__OK__')
65
+ end
66
+
67
+ # Lifecycle hooks (run during `up` if present in config/deploy/).
68
+ # Optional - doctor reports their presence/absence, never creates.
69
+ # Scaffolds with explanatory header comments ship via `app:init`.
70
+ HOOK_FILES ||= %w[local_before.sh remote_before.sh remote_after.sh local_after.sh].freeze
71
+
72
+ # True when any local config/deploy template references {{RUBY}}. Gates
73
+ # the ruby/bundler host checks so a Go/Python app doesn't fail doctor.
74
+ def ruby_runtime?(dir = './config/deploy')
75
+ Dir.glob("#{dir}/*").any? { |f| File.file?(f) && File.read(f).include?('{{RUBY') }
76
+ end
77
+
78
+ def local_checks(_config)
79
+ dir = './config/deploy'
80
+ failed = 0
81
+
82
+ report = ->(ok, label, detail = nil) {
83
+ if ok
84
+ puts " #{GREEN}PASS#{RESET} #{label}"
85
+ else
86
+ failed += 1
87
+ puts " #{RED}FAIL#{RESET} #{label}"
88
+ puts " #{DIM} #{detail}#{RESET}" if detail
89
+ end
90
+ }
91
+
92
+ skip = ->(label) { puts " #{DIM}SKIP #{label} (does not exist)#{RESET}" }
93
+
94
+ unless Dir.exist?(dir)
95
+ report.call(false, "#{dir}/ directory present",
96
+ "missing; run: deploy app:init")
97
+ return failed
98
+ end
99
+
100
+ # Parse .yaml; bail early if absent or malformed since other
101
+ # checks depend on its keys.
102
+ yaml_path = "#{dir}/.yaml"
103
+ yaml_data = nil
104
+ if File.exist?(yaml_path)
105
+ report.call(true, "#{yaml_path} present")
106
+ begin
107
+ yaml_data = YAML.safe_load(File.read(yaml_path)) || {}
108
+ report.call(yaml_data.is_a?(Hash), "#{yaml_path} is a YAML mapping")
109
+ yaml_data = {} unless yaml_data.is_a?(Hash)
110
+ report.call(!yaml_data['server'].to_s.strip.empty?, ".yaml 'server:' set")
111
+ report.call(!yaml_data['domain'].to_s.strip.empty?, ".yaml 'domain:' set")
112
+ rescue Psych::SyntaxError => e
113
+ report.call(false, "#{yaml_path} parses", e.message)
114
+ yaml_data = {}
115
+ end
116
+ else
117
+ report.call(false, "#{yaml_path} present", "missing; run: deploy app:init")
118
+ yaml_data = {}
119
+ end
120
+
121
+ report.call(File.exist?("#{dir}/.env"), "#{dir}/.env present")
122
+ report.call(File.exist?("#{dir}/caddy.conf"), "#{dir}/caddy.conf present")
123
+ report.call(File.exist?("#{dir}/systemd.service"), "#{dir}/systemd.service present")
124
+
125
+ # Lifecycle hooks - optional. Report each as PASS (present, will run)
126
+ # or SKIP (absent, hook step will be skipped during `up`).
127
+ HOOK_FILES.each do |name|
128
+ path = "#{dir}/#{name}"
129
+ if File.exist?(path)
130
+ report.call(true, "#{path} present (will run)")
131
+ else
132
+ skip.call("#{path} (lifecycle hook)")
133
+ end
134
+ end
135
+
136
+ yaml_keys = yaml_data.keys.map { |k| k.to_s.upcase }
137
+
138
+ # Every template is rendered with the same vars: engine-provided +
139
+ # yaml. The rendered .env is uploaded verbatim; it never feeds back
140
+ # into the template namespace, so caddy.conf / *.service can only
141
+ # reference yaml keys + engine vars (not anything defined in .env).
142
+ # Every *.service file is a service; only systemd.service is required.
143
+ # PORT-prefixed placeholders are engine-provided (auto-allocated).
144
+ service_files = Dir.children(dir).select { |f| f.end_with?('.service') && !f.start_with?('!') }.sort
145
+ optional = %w[.env.staging] + (service_files - %w[systemd.service])
146
+ placeholder_targets = %w[.env .env.staging caddy.conf] + service_files
147
+ provided = PROVIDED_VARS + yaml_keys
148
+ placeholder_targets.uniq.each do |name|
149
+ path = "#{dir}/#{name}"
150
+ unless File.exist?(path)
151
+ skip.call(name) if optional.include?(name)
152
+ next
153
+ end
154
+
155
+ placeholders = File.read(path).scan(Template::PLACEHOLDER).flatten.uniq
156
+ missing = placeholders.reject { |v| provided.include?(v) || v.start_with?('PORT') }
157
+ ok = missing.empty?
158
+ report.call(ok, "#{name}: placeholders resolve",
159
+ ok ? nil : "missing: #{missing.map { |v| "{{#{v}}}" }.join(' ')}")
160
+ end
161
+
162
+ failed
163
+ end
164
+
165
+ def build_checks(config)
166
+ user = config.service_user
167
+ base = config.remote_base
168
+ [
169
+ [
170
+ "#{user} user exists",
171
+ "id #{user} >/dev/null 2>&1",
172
+ "useradd -m -s /bin/bash #{user}"
173
+ ],
174
+ [
175
+ "/home/#{user} is traversable (0755)",
176
+ # caddy + systemd need to read symlinks into ~/<base>;
177
+ # useradd defaults home to 0700 on Debian, which blocks them.
178
+ "[ \"$(stat -c %a /home/#{user})\" = 755 ]",
179
+ "chmod 0755 /home/#{user}"
180
+ ],
181
+ [
182
+ "#{user} in sudo group (passwordless)",
183
+ "grep -q '^#{user} ' /etc/sudoers.d/#{user} 2>/dev/null && grep -q NOPASSWD /etc/sudoers.d/#{user}",
184
+ "echo '#{user} ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/#{user} && chmod 0440 /etc/sudoers.d/#{user}"
185
+ ],
186
+ [
187
+ "#{base} exists and owned by #{user}",
188
+ "[ -d #{base} ] && [ \"$(stat -c %U #{base})\" = #{user} ]",
189
+ "install -d -o #{user} -g #{user} -m 0755 #{base}"
190
+ ],
191
+ [
192
+ '/etc/caddy/sites exists',
193
+ "[ -d #{CADDY_SITES} ]",
194
+ "install -d -m 0755 #{CADDY_SITES}"
195
+ ],
196
+ [
197
+ 'caddy running',
198
+ 'systemctl is-active --quiet caddy',
199
+ nil
200
+ ],
201
+ [
202
+ "caddy imports #{CADDY_SITES}/*.caddy",
203
+ "grep -Rq 'import #{CADDY_SITES}/\\*.caddy' /etc/caddy/ 2>/dev/null",
204
+ nil
205
+ ],
206
+ [
207
+ "mise installed for #{user}",
208
+ "sudo -iu #{user} bash -lc 'command -v mise >/dev/null'",
209
+ nil
210
+ ],
211
+ # Ruby/bundler only matter when a template references {{RUBY}};
212
+ # a Go/Python app builds in remote_before and needs neither.
213
+ *(ruby_runtime? ? [
214
+ [
215
+ "ruby on #{user} PATH",
216
+ "sudo -iu #{user} bash -lc 'command -v ruby >/dev/null && ruby -v'",
217
+ nil
218
+ ],
219
+ [
220
+ "bundler on #{user} PATH",
221
+ "sudo -iu #{user} bash -lc 'command -v bundle >/dev/null'",
222
+ "sudo -iu #{user} bash -lc 'gem install bundler --no-document'"
223
+ ]
224
+ ] : []),
225
+ [
226
+ 'xcaddy available (for plugin rebuilds)',
227
+ 'command -v xcaddy >/dev/null',
228
+ nil
229
+ ],
230
+ [
231
+ 'ssh password auth disabled (warn-only)',
232
+ "sshd -T 2>/dev/null | grep -qx 'passwordauthentication no'",
233
+ nil
234
+ ]
235
+ ]
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,168 @@
1
+ module LuxDeploy
2
+ # Registers the lux-deploy Hammer tasks on a host CLI. Designed to be
3
+ # called from a Hammerfile (the gem's own, the lux-fw plugin's, or any
4
+ # user's) so all task definitions live in one place.
5
+ #
6
+ # Usage from a Hammerfile (block DSL):
7
+ #
8
+ # require 'lux_deploy'
9
+ # LuxDeploy::Hammer.register(self) # top-level
10
+ # LuxDeploy::Hammer.register(self, prefix: :deploy) # under deploy:
11
+ # LuxDeploy::Hammer.register(self,
12
+ # prefix: :deploy,
13
+ # templates_dir: '/abs/path/to/plugin/templates',
14
+ # defaults: {
15
+ # service_prefix: 'lux-web',
16
+ # remote_base: '/home/deployer/lux-apps',
17
+ # }
18
+ # )
19
+ #
20
+ # `defaults` is merged under user's .yaml (yaml wins). `templates_dir`
21
+ # is what `app:init` copies from and the deploy step falls back to
22
+ # when ./config/deploy/<name> is missing.
23
+ module Hammer
24
+ module_function
25
+
26
+ def safe(opts)
27
+ yield
28
+ rescue LuxDeploy::Error => e
29
+ warn e.to_s
30
+ exit 1
31
+ rescue Interrupt
32
+ warn 'aborted'
33
+ exit 130
34
+ end
35
+
36
+ def register(receiver, prefix: nil, templates_dir: nil, defaults: nil)
37
+ LuxDeploy.set_defaults(defaults) if defaults
38
+ tdir = templates_dir
39
+ if prefix
40
+ receiver.namespace(prefix) { LuxDeploy::Hammer.define_on(self, tdir) }
41
+ else
42
+ define_on(receiver, tdir)
43
+ end
44
+ end
45
+
46
+ # `target` must respond to `task` and `namespace` (Builder or Hammer
47
+ # subclass). `tdir` is captured by task closures so `app:init` and
48
+ # deploy template fallback both see it.
49
+ def define_on(target, tdir)
50
+ target.task :up do
51
+ desc 'Deploy current branch (rsync, hooks, swap, restart)'
52
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
53
+ opt :dry_run, type: :boolean, default: false, desc: 'Print commands, do not execute'
54
+
55
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.up(opts.merge(templates_dir: tdir)) } }
56
+ end
57
+
58
+ target.task :redeploy do
59
+ desc 'Destroy then deploy (fresh PORT, blank release history)'
60
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
61
+ opt :yes, type: :boolean, default: false, desc: 'Skip destroy confirmation'
62
+ opt :dry_run, type: :boolean, default: false, desc: 'Print commands, do not execute'
63
+
64
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.redeploy(opts.merge(templates_dir: tdir)) } }
65
+ end
66
+
67
+ target.task :destroy do
68
+ desc 'Stop service, unlink caddy/systemd, remove app dir'
69
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
70
+ opt :yes, type: :boolean, default: false, desc: 'Skip type-domain-to-confirm prompt'
71
+ opt :dry_run, type: :boolean, default: false, desc: 'Print commands, do not execute'
72
+
73
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.destroy(opts.merge(templates_dir: tdir)) } }
74
+ end
75
+
76
+ target.task :doctor do
77
+ desc 'Check & prepare host: service user, dirs, caddy, ruby, bundler'
78
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
79
+ opt :fix, type: :boolean, default: true, desc: 'Auto-fix safe items (default true; --no-fix to skip)'
80
+
81
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.doctor(opts) } }
82
+ end
83
+
84
+ target.task :log do
85
+ desc 'List server logs, or dump one with --log <name> (--lines 200)'
86
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
87
+ opt :log, desc: 'Log name to dump (e.g. errors, exceptions); omit to list all logs'
88
+ opt :lines, default: 200, desc: 'Lines to show when dumping a log (default 200)'
89
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.log(opts.merge(templates_dir: tdir)) } }
90
+ end
91
+
92
+ target.namespace(:app) { LuxDeploy::Hammer.define_app_on(self, tdir) }
93
+ target.namespace(:server) { LuxDeploy::Hammer.define_server_on(self, tdir) }
94
+ target.namespace(:on) { LuxDeploy::Hammer.define_on_hooks_on(self, tdir) }
95
+ end
96
+
97
+ def define_on_hooks_on(target, tdir)
98
+ target.namespace(:remote) do
99
+ LuxDeploy::Hammer.define_hook_task_on(self, :before, :remote, :before, tdir, 'Run config/deploy/remote_before.sh on new-release/')
100
+ LuxDeploy::Hammer.define_hook_task_on(self, :after, :remote, :after, tdir, 'Run config/deploy/remote_after.sh on release/')
101
+ end
102
+
103
+ target.namespace(:local) do
104
+ LuxDeploy::Hammer.define_hook_task_on(self, :before, :local, :before, tdir, 'Run config/deploy/local_before.sh locally')
105
+ LuxDeploy::Hammer.define_hook_task_on(self, :after, :local, :after, tdir, 'Run config/deploy/local_after.sh locally')
106
+ end
107
+ end
108
+
109
+ def define_hook_task_on(target, task_name, side, timing, tdir, description)
110
+ target.task task_name do
111
+ desc description
112
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
113
+ opt :dry_run, type: :boolean, default: false, desc: 'Print commands, do not execute'
114
+
115
+ proc { |opts|
116
+ LuxDeploy::Hammer.safe(opts) {
117
+ LuxDeploy::Commands.hook(opts.merge(templates_dir: tdir), side, timing)
118
+ }
119
+ }
120
+ end
121
+ end
122
+
123
+ def define_app_on(target, tdir)
124
+ target.task :init do
125
+ desc 'Copy shipped templates into ./config/deploy/ (skips existing files)'
126
+ opt :from, desc: 'Override templates directory (default: caller-provided or bundled)'
127
+ proc { |opts|
128
+ LuxDeploy::Hammer.safe(opts) {
129
+ LuxDeploy::Commands.init(opts.merge(templates_dir: opts[:from] || tdir))
130
+ }
131
+ }
132
+ end
133
+ end
134
+
135
+ def define_server_on(target, tdir)
136
+ target.task :ssh do
137
+ desc 'SSH into the release folder as the service user'
138
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
139
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.server_ssh(opts.merge(templates_dir: tdir)) } }
140
+ end
141
+
142
+ target.task :log do
143
+ desc 'Tail systemd journal for the web service (-f, last 200)'
144
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
145
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.server_log(opts.merge(templates_dir: tdir)) } }
146
+ end
147
+
148
+ target.task :errors do
149
+ desc 'Tail -f the app error log (release/log/error.log)'
150
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
151
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.server_errors(opts.merge(templates_dir: tdir)) } }
152
+ end
153
+
154
+ target.task :restart do
155
+ desc 'systemctl restart the web service'
156
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
157
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.server_restart(opts.merge(templates_dir: tdir)) } }
158
+ end
159
+
160
+ target.task :status do
161
+ desc 'systemctl status the web service'
162
+ opt :server, desc: 'Override hostname from config/deploy/.yaml'
163
+ proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.server_status(opts.merge(templates_dir: tdir)) } }
164
+ end
165
+ end
166
+
167
+ end
168
+ end