capistrano-nomad 0.14.0 → 0.15.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: 077546bb337f4b20a5558aedd518010f9958ce8b59a8caf67b2dc602ea046c0c
4
- data.tar.gz: c45ffbfc217ef342abf933f4883ebb06fbb8f4adb6f5fe1b93883048a3fe1e2a
3
+ metadata.gz: 3d733a824e5d9c7403669bbf958bbd8ee0583fe9a054d45065fd1b71eec0941d
4
+ data.tar.gz: b5a12159f7bf4c6fa241732c21a732a60953dc548fcf2139b97f94348566a584
5
5
  SHA512:
6
- metadata.gz: 0bdceb473ffecfa4e8dc10564214eea210016749d470cbb6fd11a59df0fdd5dca72a623c76f99b25f4163ab934d0796cec547d546581486a155ff487856ae001
7
- data.tar.gz: 572627c25f188ef9788d2b23c2fa4bfcc66850af933c63ed6210347aa5f94ab3662f2df3d5f789a4c3c490fdc115c8d9c5d180dbb621239f2ddf448cc35142f3
6
+ metadata.gz: 7c97291b411d604e7308ed135548d4f3d092158b2ddec7a8675e5e4d1af29a1d66f8e7e9e181d4753dd5042d0ebf2c0ac609e9f03c4341a077b163e0b582835e
7
+ data.tar.gz: 6819e434bdde1b26b7c7f80d18ccdc23422f6fedb30c4b37f54657ba452b9bcc4309c34b744e9faab0e88df47a8b221e69fee9149a354a02dc956544b1a163e4
@@ -0,0 +1,39 @@
1
+ ---
2
+ description: Release a new version of the gem
3
+ agent: build
4
+ ---
5
+
6
+ Release a new version of the capistrano-nomad gem.
7
+
8
+ ## Current State
9
+
10
+ Current version: !`grep -m1 'spec.version' capistrano-nomad.gemspec | sed 's/.*"\(.*\)"/\1/'`
11
+
12
+ Changes pending release:
13
+ !`sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | head -20`
14
+
15
+ ## Steps to perform:
16
+
17
+ 1. **Ask what version to release** (unless specified via arguments: $ARGUMENTS)
18
+
19
+ 2. **Update the version** in `capistrano-nomad.gemspec` (line 5: `spec.version = "X.Y.Z"`)
20
+
21
+ 3. **Update CHANGELOG.md**:
22
+ - Rename `[Unreleased]` section to the new version number (e.g., `## [0.15.0]`)
23
+ - Add a new empty `## [Unreleased]` section at the top
24
+
25
+ 4. **Commit the version bump**:
26
+ ```shell
27
+ git add capistrano-nomad.gemspec CHANGELOG.md
28
+ git commit -m "X.Y.Z"
29
+ ```
30
+
31
+ 5. **Run the release command**:
32
+ ```shell
33
+ bundle exec rake release
34
+ ```
35
+
36
+ This will:
37
+ - Create a git tag for the version
38
+ - Push git commits and the created tag
39
+ - Push the `.gem` file to rubygems.org
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.15.0]
4
+
5
+ - Fix command escaping for console task to support complex commands with special characters using base64 encoding
6
+
7
+ ## [0.14.1]
8
+
9
+ - Support for `IS_DETACHED` environment variable to run jobs in detached mode (e.g. `IS_DETACHED=true cap production nomad:app:deploy`)
10
+
3
11
  ## [0.14.0]
4
12
 
5
13
  - `nomad:deploy` properly deploys all jobs across all namespaces now
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capistrano-nomad (0.14.0)
4
+ capistrano-nomad (0.15.0)
5
5
  activesupport (<= 7.0.8)
6
6
  byebug
7
7
  capistrano (~> 3.0)
data/README.md CHANGED
@@ -174,6 +174,14 @@ cap production nomad:app:revert VERSION=4
174
174
  cap production nomad:app:revert DOCKER_IMAGE=v1.4.4
175
175
  ```
176
176
 
177
+ Run jobs in detached mode (fire and forget)
178
+
179
+ ```shell
180
+ IS_DETACHED=true cap production nomad:app:deploy
181
+ IS_DETACHED=true cap production nomad:app:run
182
+ IS_DETACHED=true cap production nomad:analytics:grafana:upload_run
183
+ ```
184
+
177
185
  Open job in web UI
178
186
 
179
187
  ```shell
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "capistrano-nomad"
5
- spec.version = "0.14.0"
5
+ spec.version = "0.15.0"
6
6
  spec.authors = ["James Hu"]
7
7
 
8
8
  spec.summary = "Capistrano plugin for deploying and managing Nomad jobs"
@@ -9,3 +9,7 @@ end
9
9
  def capistrano_nomad_run_remotely(&block)
10
10
  on(roles(:manager), &block)
11
11
  end
12
+
13
+ def capistrano_nomad_job_detached_overridden?
14
+ ENV["IS_DETACHED"]&.downcase == "true"
15
+ end
@@ -88,12 +88,12 @@ def nomad_job(name, attributes = {})
88
88
 
89
89
  desc("Run #{description_name} job")
90
90
  task(:run) do
91
- capistrano_nomad_run_jobs([name], namespace: namespace, is_detached: false)
91
+ capistrano_nomad_run_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
92
92
  end
93
93
 
94
94
  desc("Purge and run #{description_name} job again")
95
95
  task(:rerun) do
96
- capistrano_nomad_rerun_jobs([name], namespace: namespace, is_detached: false)
96
+ capistrano_nomad_rerun_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
97
97
  end
98
98
 
99
99
  desc("Upload and plan #{description_name} job")
@@ -103,17 +103,17 @@ def nomad_job(name, attributes = {})
103
103
 
104
104
  desc("Upload and run #{description_name} job")
105
105
  task(:upload_run) do
106
- capistrano_nomad_upload_run_jobs([name], namespace: namespace, is_detached: false)
106
+ capistrano_nomad_upload_run_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
107
107
  end
108
108
 
109
109
  desc("Upload and re-run #{description_name} job")
110
110
  task(:upload_rerun) do
111
- capistrano_nomad_upload_rerun_jobs([name], namespace: namespace, is_detached: false)
111
+ capistrano_nomad_upload_rerun_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
112
112
  end
113
113
 
114
114
  desc("Deploy #{description_name} job")
115
115
  task(:deploy) do
116
- capistrano_nomad_deploy_jobs([name], namespace: namespace, is_detached: false)
116
+ capistrano_nomad_deploy_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
117
117
  end
118
118
 
119
119
  desc("Start #{description_name} job")
@@ -142,7 +142,7 @@ def nomad_job(name, attributes = {})
142
142
 
143
143
  desc("Purge #{description_name} job")
144
144
  task(:purge) do
145
- capistrano_nomad_purge_jobs([name], namespace: namespace, is_detached: false)
145
+ capistrano_nomad_purge_jobs([name], namespace: namespace, is_detached: capistrano_nomad_job_detached_overridden?)
146
146
  end
147
147
 
148
148
  desc("Display status of #{description_name} job")
@@ -1,4 +1,5 @@
1
1
  require "active_support/core_ext/string"
2
+ require "base64"
2
3
  require "sshkit/interactive"
3
4
 
4
5
  class CapistranoNomadErbNamespace
@@ -29,6 +30,48 @@ def capistrano_nomad_ensure_absolute_path(path)
29
30
  path[0] == "/" ? path : "/#{path}"
30
31
  end
31
32
 
33
+ # Escapes a command string for use with sshkit-interactive.
34
+ #
35
+ # sshkit-interactive wraps commands in '$SHELL -l -c "..."' and naively
36
+ # replaces all single quotes with \". This breaks commands that have
37
+ # content inside single quotes (e.g., bin/rails runner 'puts "hello"').
38
+ #
39
+ # This function pre-processes the command to handle single-quoted sections
40
+ # properly by:
41
+ # 1. Converting single quotes to escaped double quotes (\")
42
+ # 2. Escaping content inside those sections for double-quote context
43
+ # (backslashes become \\, double quotes become \\\")
44
+ #
45
+ # After this transformation, sshkit-interactive's gsub("'", '\\"') becomes
46
+ # a no-op since there are no single quotes left.
47
+ def capistrano_nomad_escape_command(command)
48
+ # Process single-quoted sections: 'content' -> \"escaped_content\"
49
+ command.gsub(/'([^']*)'/) do |_match|
50
+ content = Regexp.last_match(1)
51
+
52
+ # Escape for double-quote shell context:
53
+ # - Backslashes need to be doubled (\ -> \\)
54
+ # - Double quotes need to become \\\" to survive shell parsing
55
+ escaped_content = content
56
+ .gsub("\\", "\\\\\\\\")
57
+ .gsub('"', '\\\\\\\\\\\"')
58
+
59
+ '\"' + escaped_content + '\"'
60
+ end
61
+ end
62
+
63
+ # Escapes a command string for use with sshkit-interactive by base64 encoding.
64
+ #
65
+ # sshkit-interactive wraps commands in '$SHELL -l -c "..."' and naively
66
+ # replaces all single quotes with \". Base64 avoids all quoting issues by
67
+ # decoding the command inside the Nomad task and executing it with /bin/sh.
68
+ def capistrano_nomad_escape_command(command)
69
+ encoded_command = Base64.strict_encode64(command)
70
+ decoded_command = "printf\\ %s\\ #{encoded_command}\\ \\|\\ base64\\ -d\\ \\|\\ /bin/sh"
71
+
72
+ "/bin/sh -lc #{decoded_command}"
73
+ end
74
+
32
75
  def capistrano_nomad_build_file_path(parent_path, basename, kind: nil, **options)
33
76
  capistrano_nomad_ensure_options!(options)
34
77
  namespace = options[:namespace]
@@ -189,6 +232,9 @@ end
189
232
  def capistrano_nomad_exec_within_job(name, command, task: nil, **options)
190
233
  capistrano_nomad_ensure_options!(options)
191
234
 
235
+ # Escape command for SSH transport via sshkit-interactive
236
+ escaped_command = capistrano_nomad_escape_command(command)
237
+
192
238
  capistrano_nomad_run_remotely do
193
239
  if (task_details = capistrano_nomad_find_job_task_details(name, task: task, **options))
194
240
  capistrano_nomad_execute_nomad_command(
@@ -196,7 +242,7 @@ def capistrano_nomad_exec_within_job(name, command, task: nil, **options)
196
242
  :exec,
197
243
  options.merge(task: task_details[:name]),
198
244
  task_details[:alloc_id],
199
- command,
245
+ escaped_command,
200
246
  )
201
247
  else
202
248
  # If alloc can't be determined then choose at random
@@ -205,7 +251,7 @@ def capistrano_nomad_exec_within_job(name, command, task: nil, **options)
205
251
  :exec,
206
252
  options.merge(job: true),
207
253
  task,
208
- command,
254
+ escaped_command,
209
255
  )
210
256
  end
211
257
  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.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Hu
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-06 00:00:00.000000000 Z
11
+ date: 2026-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -114,6 +114,7 @@ executables: []
114
114
  extensions: []
115
115
  extra_rdoc_files: []
116
116
  files:
117
+ - ".opencode/commands/release.md"
117
118
  - CHANGELOG.md
118
119
  - Gemfile
119
120
  - Gemfile.lock