capistrano-nomad 0.14.1 → 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: f23cd98ffcf93c05fd2dc4b20a9acff9865d2904fd105e015d2e3565b7a18957
4
- data.tar.gz: 0f4f57b9f66a0477fcbd9f92cbf3f43e5abdfb331e7a629c8f683e4ff5772e4c
3
+ metadata.gz: 3d733a824e5d9c7403669bbf958bbd8ee0583fe9a054d45065fd1b71eec0941d
4
+ data.tar.gz: b5a12159f7bf4c6fa241732c21a732a60953dc548fcf2139b97f94348566a584
5
5
  SHA512:
6
- metadata.gz: 98cd1097efdf727564e167a52da40754fe344661d5d6707841d1efa109047a333f31c99e2adbf6a71639b59b5d461701ca314fb8e72dd6ad778c756481f06a5e
7
- data.tar.gz: 8de1096141ae90c3c83a5a2d3bb267a8b8785f2f51fff979671ccbff8a05ba4981f618e42c8f3b6aec6f54c996398de361efe9a0bd41fd65f44396c3339550c5
6
+ metadata.gz: 7c97291b411d604e7308ed135548d4f3d092158b2ddec7a8675e5e4d1af29a1d66f8e7e9e181d4753dd5042d0ebf2c0ac609e9f03c4341a077b163e0b582835e
7
+ data.tar.gz: 6819e434bdde1b26b7c7f80d18ccdc23422f6fedb30c4b37f54657ba452b9bcc4309c34b744e9faab0e88df47a8b221e69fee9149a354a02dc956544b1a163e4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
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
+
3
7
  ## [0.14.1]
4
8
 
5
9
  - Support for `IS_DETACHED` environment variable to run jobs in detached mode (e.g. `IS_DETACHED=true cap production nomad:app:deploy`)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- capistrano-nomad (0.14.1)
4
+ capistrano-nomad (0.15.0)
5
5
  activesupport (<= 7.0.8)
6
6
  byebug
7
7
  capistrano (~> 3.0)
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "capistrano-nomad"
5
- spec.version = "0.14.1"
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"
@@ -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.1
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: 2026-01-29 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