capistrano-nomad 0.16.1 → 0.16.3

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: 137c81e55b24eb37ce7b3e39722c378d2addea5c5b331ceadeb6d5614f24402e
4
- data.tar.gz: 24f1a3cd917263e2c9a34e2baad7968696f0fff7845b9a943a55c4b4742e09a7
3
+ metadata.gz: 56224de9c341668aba37b58d2b0b404decfce7ee5f488364c325319fea262572
4
+ data.tar.gz: 49275ee4444defa39b4de8e6d193342d63b8a1edd08deb6f4946451c8c4b9cf2
5
5
  SHA512:
6
- metadata.gz: 342ae22edc3a3a423ae62a92710ed7dec2445023cd7164f299406fc423bc8e5acbee77a4bd475c2fd82f3cfe26b18eb0c864e58023fb8af1928f00d47a32d89e
7
- data.tar.gz: eff30623ea4be4debcdee3c2c31645ab928e4ef9eeef83dbd4c6a295131bffdd6c1d7c38a68c9dc97f29bfb2667791a5be3e55eb0090b4c901000e898cb55dbf
6
+ metadata.gz: 10e0218ac88de63102cb0af504e7eec43985535516d6fcd91c5404224003b4a652e500bef48895707f4532ca83ebac128aef143d6b38571e490eca632bac91c2
7
+ data.tar.gz: 7b53eee7196177a352c757527911ccfb601c8075918f49c8a3557cfc61da571fb707341802affb138fbcb903b13c92e09a619431a6f657ee2bc651ddced4df5f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.16.3]
4
+
5
+ - Add a generated `:force` task to launch a periodic child through the configured manager host and Nomad token
6
+ - Reject non-periodic and disabled periodic jobs before submitting a force request
7
+ - Fail the Capistrano command when job inspection or periodic force fails
8
+
9
+ ## [0.16.2]
10
+
11
+ - Fail `upload_plan` and other plan operations when Nomad rejects a job or cannot determine the plan
12
+ - Keep successful plans with allocation changes successful despite Nomad's exit code 1
13
+ - Include CLI regression tests in the gem
14
+
3
15
  ## [0.16.1]
4
16
 
5
17
  - Pass every configured variable file to `nomad plan` and `nomad run`
data/Gemfile CHANGED
@@ -4,3 +4,5 @@ source "https://rubygems.org"
4
4
 
5
5
  # Specify your gem's dependencies in capistrano-nomad.gemspec
6
6
  gemspec
7
+
8
+ gem "minitest", "~> 5.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capistrano-nomad (0.16.0)
4
+ capistrano-nomad (0.16.3)
5
5
  activesupport (<= 7.0.8)
6
6
  byebug
7
7
  capistrano (~> 3.0)
@@ -52,9 +52,11 @@ GEM
52
52
  PLATFORMS
53
53
  arm64-darwin-21
54
54
  arm64-darwin-23
55
+ x86_64-linux-gnu
55
56
 
56
57
  DEPENDENCIES
57
58
  capistrano-nomad!
59
+ minitest (~> 5.0)
58
60
 
59
61
  BUNDLED WITH
60
62
  2.4.20
data/README.md CHANGED
@@ -145,6 +145,15 @@ cap production nomad:analytics:grafana:restart
145
145
  cap production nomad:postgres:status
146
146
  ```
147
147
 
148
+ Force a periodic job without uploading, deploying, or restarting its parent:
149
+
150
+ ```shell
151
+ cap production nomad:maintenance:force
152
+ cap production nomad:analytics:archive:force
153
+ ```
154
+
155
+ The per-job `:force` task checks the deployed job before running `nomad job periodic force`. Non-periodic and disabled periodic jobs fail with a clear error. Inspection and force failures produce a nonzero Capistrano exit. The task uses the configured manager host and `:nomad_token`, like other job actions.
156
+
148
157
  Tasks can apply across all namespaces or be filtered by namespaces or tags
149
158
 
150
159
  ```shell
@@ -213,7 +222,9 @@ cap production nomad:version
213
222
 
214
223
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
215
224
 
216
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
225
+ Run `bundle exec rake test` to test the generated Capistrano `upload_plan` and `force` tasks through the CLI. The tests use local transport and controlled Nomad exit codes. Force coverage checks the job, namespace, token, rejected requests, and non-periodic jobs. To also check rejection of invalid HCL with a real Nomad binary, run `NOMAD_REAL_BINARY=/path/to/nomad bundle exec rake test`.
226
+
227
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version in `capistrano-nomad.gemspec`, run `bundle install`, and update `CHANGELOG.md`. Run `bundle exec rake test` and commit the release changes. Then run `bundle exec rake release`, which creates the version tag, pushes the release commit and tag, and publishes the gem to [RubyGems](https://rubygems.org).
217
228
 
218
229
  ## Contributing
219
230
 
data/Rakefile CHANGED
@@ -1,4 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |task|
7
+ task.libs << "test"
8
+ task.pattern = "test/**/*_test.rb"
9
+ end
10
+
11
+ task default: :test
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "capistrano-nomad"
5
- spec.version = "0.16.1"
5
+ spec.version = "0.16.3"
6
6
  spec.authors = ["James Hu"]
7
7
 
8
8
  spec.summary = "Capistrano plugin for deploying and managing Nomad jobs"
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  # Specify which files should be added to the gem when it is released. The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
19
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
20
  %x(git ls-files -z).split("\x0").reject do |f|
21
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
21
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
22
22
  end
23
23
  end
24
24
  spec.bindir = "exe"
@@ -91,6 +91,11 @@ def nomad_job(name, attributes = {})
91
91
  capistrano_nomad_run_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
92
92
  end
93
93
 
94
+ desc("Force a new periodic child of #{description_name} job")
95
+ task(:force) do
96
+ capistrano_nomad_force_job(name, namespace: namespace)
97
+ end
98
+
94
99
  desc("Purge and run #{description_name} job again")
95
100
  task(:rerun) do
96
101
  capistrano_nomad_rerun_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
@@ -138,7 +138,11 @@ def capistrano_nomad_build_release_var_file_path(name, **options)
138
138
  "#{release_path}#{capistrano_nomad_ensure_absolute_path(capistrano_nomad_build_base_var_file_path(name, **options))}"
139
139
  end
140
140
 
141
- def capistrano_nomad_run_nomad_command(kind, *args)
141
+ def capistrano_nomad_plan_command?(args)
142
+ args.first.to_s == "plan" || args.take(2).map(&:to_s) == ["job", "plan"]
143
+ end
144
+
145
+ def capistrano_nomad_run_nomad_command(kind, *args, raise_on_non_zero_exit: false)
142
146
  converted_args = args.each_with_object([]) do |arg, collection|
143
147
  # If hash then convert it as options
144
148
  if arg.is_a?(Hash)
@@ -165,24 +169,34 @@ def capistrano_nomad_run_nomad_command(kind, *args)
165
169
  end
166
170
 
167
171
  with(env_vars) do
168
- # Ignore errors
169
- public_send(kind, :nomad, *converted_args, raise_on_non_zero_exit: false)
172
+ if capistrano_nomad_plan_command?(args)
173
+ # Nomad returns 1 for a successful plan with changes; all other nonzero exits are errors
174
+ public_send(kind, :nomad, *converted_args, '; status=$?; if [ "$status" -eq 1 ]; then exit 0; else exit "$status"; fi', raise_on_non_zero_exit: true)
175
+ else
176
+ # Preserve unchecked execution unless the caller requires failures to propagate
177
+ public_send(kind, :nomad, *converted_args, raise_on_non_zero_exit: raise_on_non_zero_exit)
178
+ end
170
179
  end
171
180
  end
172
181
 
173
- def capistrano_nomad_execute_nomad_command(*args, **options)
182
+ def capistrano_nomad_execute_nomad_command(*args, raise_on_non_zero_exit: false, **options)
174
183
  capistrano_nomad_run_remotely do |host|
175
- run_interactively(host) do
176
- capistrano_nomad_run_nomad_command(:execute, *args, **options)
184
+ if capistrano_nomad_plan_command?(args) || raise_on_non_zero_exit
185
+ # Use checked execution when failures must propagate because the interactive backend does not raise
186
+ capistrano_nomad_run_nomad_command(:execute, *args, options, raise_on_non_zero_exit: raise_on_non_zero_exit)
187
+ else
188
+ run_interactively(host) do
189
+ capistrano_nomad_run_nomad_command(:execute, *args, options)
190
+ end
177
191
  end
178
192
  end
179
193
  end
180
194
 
181
- def capistrano_nomad_capture_nomad_command(*args, **options)
195
+ def capistrano_nomad_capture_nomad_command(*args, raise_on_non_zero_exit: false, **options)
182
196
  output = nil
183
197
 
184
198
  capistrano_nomad_run_remotely do
185
- output = capistrano_nomad_run_nomad_command(:capture, *args, **options)
199
+ output = capistrano_nomad_run_nomad_command(:capture, *args, options, raise_on_non_zero_exit: raise_on_non_zero_exit)
186
200
  end
187
201
 
188
202
  output
@@ -620,6 +634,17 @@ def capistrano_nomad_restart_jobs(names, **options)
620
634
  end
621
635
  end
622
636
 
637
+ def capistrano_nomad_force_job(name, **options)
638
+ capistrano_nomad_ensure_options!(options)
639
+ job = JSON.parse(capistrano_nomad_capture_nomad_command(:job, :inspect, options, name, raise_on_non_zero_exit: true))
640
+
641
+ unless job.dig("Periodic", "Enabled")
642
+ raise ArgumentError, "Job #{options[:namespace]}/#{name} is not an enabled periodic job"
643
+ end
644
+
645
+ capistrano_nomad_execute_nomad_command(:job, :periodic, :force, options, name, raise_on_non_zero_exit: true)
646
+ end
647
+
623
648
  def capistrano_nomad_start_jobs(names, **options)
624
649
  capistrano_nomad_ensure_options!(options)
625
650
 
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "fileutils"
5
+ require "open3"
6
+ require "tmpdir"
7
+
8
+ class UploadPlanTest < Minitest::Test
9
+ def test_upload_plan_succeeds_for_accepted_plans_and_fails_for_rejected_plans
10
+ Dir.mktmpdir("capistrano-nomad") do |root|
11
+ FileUtils.mkdir_p(["#{root}/config/deploy", "#{root}/nomad/jobs", "#{root}/bin"])
12
+ File.write("#{root}/Capfile", <<~RUBY)
13
+ require "capistrano/setup"
14
+ require "capistrano/deploy"
15
+ require "capistrano/nomad"
16
+ install_plugin Capistrano::Nomad
17
+ RUBY
18
+ File.write("#{root}/config/deploy.rb", <<~RUBY)
19
+ set :application, "plan-test"
20
+ set :deploy_to, #{"#{root}/remote".inspect}
21
+ set :root, #{root.inspect}
22
+ set :current_revision, "test"
23
+ set :nomad_docker_image_types, {}
24
+ SSHKit.config.command_map[:nomad] = #{"#{root}/bin/nomad".inspect}
25
+ nomad_job :example
26
+ set :nomad_token, "force-test-token"
27
+ nomad_namespace :spleencheese do
28
+ nomad_job :"myanonamouse-maintenance"
29
+ end
30
+ RUBY
31
+ File.write("#{root}/config/deploy/test.rb", <<~RUBY)
32
+ server "localhost", roles: [:manager]
33
+ set :sshkit_backend, SSHKit::Backend::Local
34
+ RUBY
35
+ File.write("#{root}/bin/ssh", <<~SH)
36
+ #!/bin/sh
37
+ for argument do command="$argument"; done
38
+ exec /bin/sh -c "$command"
39
+ SH
40
+ FileUtils.chmod(0755, "#{root}/bin/ssh")
41
+ File.write("#{root}/bin/nomad", <<~SH)
42
+ #!/bin/sh
43
+ if [ "$1" = job ]; then
44
+ [ "$NOMAD_TOKEN" = force-test-token ] || exit 98
45
+ printf '%s\n' "$*" >> #{root}/commands
46
+ if [ "$2" = inspect ]; then
47
+ printf '%s\n' "$NOMAD_TEST_JOB"
48
+ exit "${NOMAD_TEST_INSPECT_EXIT:-0}"
49
+ fi
50
+ [ "$2 $3" = 'periodic force' ] || exit 99
51
+ if [ "$NOMAD_TEST_EXIT" = 0 ]; then
52
+ echo 'Created periodic child'
53
+ else
54
+ echo 'Periodic force rejected' >&2
55
+ fi
56
+ exit "$NOMAD_TEST_EXIT"
57
+ fi
58
+ [ "$1" = plan ] || exit 99
59
+ cat "$2"
60
+ case "$NOMAD_TEST_EXIT" in
61
+ 0|1) echo 'Scheduler dry-run: accepted';;
62
+ *) echo 'Error determining plan results' >&2;;
63
+ esac
64
+ exit "$NOMAD_TEST_EXIT"
65
+ SH
66
+ FileUtils.chmod(0755, "#{root}/bin/nomad")
67
+
68
+ [0, 1, 255, 2].each do |exit_code|
69
+ job = exit_code < 2 ? 'job "example" {}' : 'job "example" {'
70
+ File.write("#{root}/nomad/jobs/example.hcl", job)
71
+ output, status = Open3.capture2e(
72
+ { "NOMAD_TEST_EXIT" => exit_code.to_s, "PATH" => "#{root}/bin:#{ENV.fetch("PATH")}" },
73
+ "bundle", "exec", "cap", "test", "nomad:example:upload_plan",
74
+ chdir: root,
75
+ )
76
+
77
+ assert_includes(output, job, output)
78
+ if exit_code < 2
79
+ assert(status.success?, output)
80
+ assert_includes(output, "Scheduler dry-run: accepted")
81
+ else
82
+ refute(status.success?, output)
83
+ assert_includes(output, "Error determining plan results")
84
+ end
85
+ end
86
+
87
+ [0, 1, 2].each do |exit_code|
88
+ File.write("#{root}/commands", "")
89
+ output, status = Open3.capture2e(
90
+ { "NOMAD_TEST_EXIT" => exit_code.to_s, "NOMAD_TEST_JOB" => '{"Periodic":{"Enabled":true}}' },
91
+ "bundle", "exec", "cap", "test", "nomad:spleencheese:myanonamouse-maintenance:force",
92
+ chdir: root,
93
+ )
94
+
95
+ assert_equal("job inspect -namespace=spleencheese myanonamouse-maintenance\njob periodic force -namespace=spleencheese myanonamouse-maintenance\n", File.read("#{root}/commands"), output)
96
+ assert_equal(exit_code.zero?, status.success?, output)
97
+ assert_includes(output, exit_code.zero? ? "Created periodic child" : "Periodic force rejected")
98
+ end
99
+
100
+ ['{"Periodic":null}', '{"Periodic":{"Enabled":false}}'].each do |job|
101
+ File.write("#{root}/commands", "")
102
+ output, status = Open3.capture2e(
103
+ { "NOMAD_TEST_JOB" => job },
104
+ "bundle", "exec", "cap", "test", "nomad:example:force",
105
+ chdir: root,
106
+ )
107
+
108
+ refute(status.success?, output)
109
+ assert_includes(output, "Job default/example is not an enabled periodic job")
110
+ assert_equal("job inspect -namespace=default example\n", File.read("#{root}/commands"), output)
111
+ end
112
+
113
+ File.write("#{root}/commands", "")
114
+ output, status = Open3.capture2e(
115
+ { "NOMAD_TEST_JOB" => '{"Periodic":{"Enabled":true}}', "NOMAD_TEST_INSPECT_EXIT" => "1" },
116
+ "bundle", "exec", "cap", "test", "nomad:example:force",
117
+ chdir: root,
118
+ )
119
+
120
+ refute(status.success?, output)
121
+ assert_equal("job inspect -namespace=default example\n", File.read("#{root}/commands"), output)
122
+
123
+ File.write("#{root}/commands", "")
124
+ output, status = Open3.capture2e(
125
+ { "NOMAD_TEST_JOB" => '{"Periodic":{"Enabled":true}}', "NOMAD_TEST_EXIT" => "0" },
126
+ "bundle", "exec", "cap", "test", "nomad:example:force",
127
+ chdir: root,
128
+ )
129
+
130
+ assert(status.success?, output)
131
+ assert_equal("job inspect -namespace=default example\njob periodic force -namespace=default example\n", File.read("#{root}/commands"), output)
132
+
133
+ if ENV["NOMAD_REAL_BINARY"]
134
+ FileUtils.cp(ENV.fetch("NOMAD_REAL_BINARY"), "#{root}/bin/nomad")
135
+ output, status = Open3.capture2e("bundle", "exec", "cap", "test", "nomad:example:upload_plan", chdir: root)
136
+
137
+ refute(status.success?, output)
138
+ assert_includes(output, "Error parsing job file")
139
+ end
140
+ end
141
+ end
142
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capistrano-nomad
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.1
4
+ version: 0.16.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Hu
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-16 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -130,6 +130,7 @@ files:
130
130
  - lib/capistrano/nomad/helpers/nomad.rb
131
131
  - lib/capistrano/nomad/scm/git_local.rb
132
132
  - lib/capistrano/nomad/tasks/nomad.rake
133
+ - test/upload_plan_test.rb
133
134
  homepage: https://github.com/axsuul/capistrano-nomad
134
135
  licenses:
135
136
  - MIT
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
153
  - !ruby/object:Gem::Version
153
154
  version: '0'
154
155
  requirements: []
155
- rubygems_version: 3.4.20
156
+ rubygems_version: 3.4.19
156
157
  signing_key:
157
158
  specification_version: 4
158
159
  summary: Capistrano plugin for deploying and managing Nomad jobs