capistrano-nomad 0.6.4 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +35 -6
- data/capistrano-nomad.gemspec +1 -1
- data/lib/capistrano/nomad/helpers/docker.rb +19 -12
- data/lib/capistrano/nomad/helpers/dsl.rb +39 -12
- data/lib/capistrano/nomad/helpers/nomad.rb +96 -48
- data/lib/capistrano/nomad/tasks/nomad.rake +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 659b43a201195b9cefc001ff096e0edc07967303695f19466e612061028f81d7
|
4
|
+
data.tar.gz: 236271b31a3cae45ba09828afb8cc27854eee2ce2329166dfde818683610c193
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bce586af52de2b0d1d679bb7dcbb7b1d8ccf9fb23bfa8296ff9fb2cff379deb8a35071b75164e7750918f7c1d52364340beb40877fbacd41cbd70bb758af1e40
|
7
|
+
data.tar.gz: ace5161204cdc20b739a462971ba9a2d18d194205ecc4ef2beb68e7820da0e4a02e9b711317babd5e76bf368892b358b9c8f2abdcd82dcca92303faa79bcc77a
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -37,8 +37,8 @@ Within `deploy.rb`
|
|
37
37
|
set :nomad_jobs_path, "nomad/jobs"
|
38
38
|
set :nomad_var_files_path, "nomad/vars"
|
39
39
|
|
40
|
-
#
|
41
|
-
set :
|
40
|
+
# Make variables available to all template .erb files
|
41
|
+
set :nomad_template_vars, (lambda do
|
42
42
|
{
|
43
43
|
env_name: fetch(:stage).to_sym,
|
44
44
|
domain: fetch(:domain),
|
@@ -46,6 +46,15 @@ set :nomad_erb_vars, (lambda do
|
|
46
46
|
}
|
47
47
|
end)
|
48
48
|
|
49
|
+
# Make helpers available to all template .erb files
|
50
|
+
nomad_template_helpers do
|
51
|
+
def restart_stanza
|
52
|
+
<<-EOF
|
53
|
+
|
54
|
+
EOF
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
49
58
|
# Docker image types
|
50
59
|
nomad_docker_image_type :backend,
|
51
60
|
path: "local/path/backend",
|
@@ -55,10 +64,13 @@ nomad_docker_image_type :backend,
|
|
55
64
|
nomad_docker_image_type :redis,
|
56
65
|
path: "/absolute/path/redis",
|
57
66
|
alias: "gcr.io/axsuul/redis"
|
67
|
+
nomad_docker_image_type :postgres,
|
68
|
+
alias_digest: "postgres:5.0.0"
|
58
69
|
|
59
70
|
# Jobs
|
60
|
-
nomad_job :frontend
|
61
71
|
nomad_job :backend, docker_image_types: [:backend], var_files: [:rails]
|
72
|
+
nomad_job :frontend
|
73
|
+
nomad_job :postgres, docker_image_types: [:postgres]
|
62
74
|
nomad_job :redis, docker_image_types: [:redis]
|
63
75
|
nomad_job :"traefik-default", template: :traefik, erb_vars: { role: :default }
|
64
76
|
nomad_job :"traefik-secondary", template: :traefik, erb_vars: { role: :secondary }
|
@@ -74,14 +86,22 @@ Deploy all with
|
|
74
86
|
cap production nomad:all:deploy
|
75
87
|
```
|
76
88
|
|
77
|
-
Deploy individually with
|
89
|
+
Deploy jobs individually with
|
78
90
|
|
79
91
|
```shell
|
80
92
|
cap production nomad:app:deploy
|
81
|
-
cap production nomad:redis:purge
|
82
93
|
cap production nomad:analytics:grafana:deploy
|
83
94
|
```
|
84
95
|
|
96
|
+
Manage jobs with
|
97
|
+
|
98
|
+
```shell
|
99
|
+
cap production nomad:app:stop
|
100
|
+
cap production nomad:redis:purge
|
101
|
+
cap production nomad:analytics:grafana:restart
|
102
|
+
cap production nomad:postgres:status
|
103
|
+
```
|
104
|
+
|
85
105
|
Open console with
|
86
106
|
|
87
107
|
```shell
|
@@ -90,10 +110,19 @@ cap production nomad:app:console TASK=custom-task-name
|
|
90
110
|
cap production nomad:analytics:grafana:console
|
91
111
|
```
|
92
112
|
|
113
|
+
Show logs with
|
114
|
+
|
115
|
+
```shell
|
116
|
+
cap production nomad:app:logs
|
117
|
+
cap production nomad:app:stdout
|
118
|
+
cap production nomad:app:stderr
|
119
|
+
cap production nomad:analytics:grafana:follow
|
120
|
+
```
|
121
|
+
|
93
122
|
Create missing and delete unused namespaces
|
94
123
|
|
95
124
|
```shell
|
96
|
-
cap production nomad:all:
|
125
|
+
cap production nomad:all:modify_namespaces
|
97
126
|
```
|
98
127
|
|
99
128
|
## Development
|
data/capistrano-nomad.gemspec
CHANGED
@@ -70,6 +70,9 @@ def capistrano_nomad_build_docker_image_for_type(image_type)
|
|
70
70
|
|
71
71
|
return unless attributes
|
72
72
|
|
73
|
+
# No need to build if there's no path
|
74
|
+
return unless attributes[:path]
|
75
|
+
|
73
76
|
args = [
|
74
77
|
# Ensure images are built for x86_64 which is production env otherwise it will default to local development env
|
75
78
|
# which can be arm64 (Apple Silicon)
|
@@ -106,26 +109,30 @@ def capistrano_nomad_build_docker_image_for_type(image_type)
|
|
106
109
|
end
|
107
110
|
|
108
111
|
def capistrano_nomad_push_docker_image_for_type(image_type, is_manifest_updated: true)
|
109
|
-
|
110
|
-
|
112
|
+
attributes = fetch(:nomad_docker_image_types)[image_type]
|
113
|
+
alias_digest = attributes&.dig(:alias_digest)
|
111
114
|
|
112
115
|
run_locally do
|
113
|
-
|
114
|
-
|
116
|
+
# Don't push Docker image if alias digest is already passed in
|
117
|
+
unless alias_digest
|
118
|
+
interaction_handler = CapistranoNomadDockerPushImageInteractionHandler.new
|
119
|
+
image_alias = capistrano_nomad_build_docker_image_alias(image_type)
|
120
|
+
|
121
|
+
# We should not proceed if image cannot be pushed
|
122
|
+
unless execute("docker push #{image_alias}", interaction_handler: interaction_handler)
|
123
|
+
raise StandardError, "Docker image push unsuccessful!"
|
124
|
+
end
|
115
125
|
|
116
|
-
|
117
|
-
unless execute("docker push #{image_alias}", interaction_handler: interaction_handler)
|
118
|
-
raise StandardError, "Docker image push unsuccessful!"
|
119
|
-
end
|
126
|
+
return unless is_manifest_updated
|
120
127
|
|
121
|
-
|
128
|
+
# Has the @sha256:xxxx appended so we have the ability to also target by digest
|
129
|
+
alias_digest = "#{image_alias}@#{interaction_handler.last_digest}"
|
130
|
+
end
|
122
131
|
|
123
132
|
# Update image type manifest
|
124
133
|
capistrano_nomad_update_docker_image_types_manifest(image_type,
|
125
134
|
alias: image_alias,
|
126
|
-
|
127
|
-
# Has the @sha256:xxxx appended so we have the ability to also target by digest
|
128
|
-
alias_digest: "#{image_alias}@#{interaction_handler.last_digest}",
|
135
|
+
alias_digest: alias_digest,
|
129
136
|
)
|
130
137
|
end
|
131
138
|
end
|
@@ -50,12 +50,12 @@ def nomad_job(name, attributes = {})
|
|
50
50
|
|
51
51
|
desc "Run #{description_name} job"
|
52
52
|
task :run do
|
53
|
-
capistrano_nomad_run_jobs([name], namespace: namespace)
|
53
|
+
capistrano_nomad_run_jobs([name], namespace: namespace, is_detached: false)
|
54
54
|
end
|
55
55
|
|
56
56
|
desc "Purge and run #{description_name} job again"
|
57
57
|
task :rerun do
|
58
|
-
capistrano_nomad_rerun_jobs([name], namespace: namespace)
|
58
|
+
capistrano_nomad_rerun_jobs([name], namespace: namespace, is_detached: false)
|
59
59
|
end
|
60
60
|
|
61
61
|
desc "Upload and plan #{description_name} job"
|
@@ -65,22 +65,17 @@ def nomad_job(name, attributes = {})
|
|
65
65
|
|
66
66
|
desc "Upload and run #{description_name} job"
|
67
67
|
task :upload_run do
|
68
|
-
capistrano_nomad_upload_run_jobs([name], namespace: namespace)
|
68
|
+
capistrano_nomad_upload_run_jobs([name], namespace: namespace, is_detached: false)
|
69
69
|
end
|
70
70
|
|
71
71
|
desc "Upload and re-run #{description_name} job"
|
72
72
|
task :upload_rerun do
|
73
|
-
capistrano_nomad_upload_rerun_jobs([name], namespace: namespace)
|
73
|
+
capistrano_nomad_upload_rerun_jobs([name], namespace: namespace, is_detached: false)
|
74
74
|
end
|
75
75
|
|
76
76
|
desc "Deploy #{description_name} job"
|
77
77
|
task :deploy do
|
78
|
-
capistrano_nomad_deploy_jobs([name], namespace: namespace)
|
79
|
-
end
|
80
|
-
|
81
|
-
desc "Restart #{description_name} job"
|
82
|
-
task :restart do
|
83
|
-
capistrano_nomad_restart_jobs([name], namespace: namespace)
|
78
|
+
capistrano_nomad_deploy_jobs([name], namespace: namespace, is_detached: false)
|
84
79
|
end
|
85
80
|
|
86
81
|
desc "Stop #{description_name} job"
|
@@ -88,6 +83,11 @@ def nomad_job(name, attributes = {})
|
|
88
83
|
capistrano_nomad_stop_jobs([name], namespace: namespace)
|
89
84
|
end
|
90
85
|
|
86
|
+
desc "Restart #{description_name} job"
|
87
|
+
task :restart do
|
88
|
+
capistrano_nomad_restart_jobs([name], namespace: namespace)
|
89
|
+
end
|
90
|
+
|
91
91
|
desc "Purge #{description_name} job"
|
92
92
|
task :purge do
|
93
93
|
capistrano_nomad_purge_jobs([name], namespace: namespace, is_detached: false)
|
@@ -98,9 +98,32 @@ def nomad_job(name, attributes = {})
|
|
98
98
|
capistrano_nomad_display_job_status(name, namespace: namespace)
|
99
99
|
end
|
100
100
|
|
101
|
-
desc "Open console to #{description_name} job. Specify task
|
101
|
+
desc "Open console to #{description_name} job. Specify task with TASK, command with CMD"
|
102
102
|
task :console do
|
103
|
-
|
103
|
+
command = ENV["CMD"].presence || "/bin/bash"
|
104
|
+
|
105
|
+
capistrano_nomad_exec_within_job(name, command, namespace: namespace, task: ENV["TASK"])
|
106
|
+
end
|
107
|
+
|
108
|
+
desc "Display stdout and stderr of #{description_name} job. Specify task with TASK"
|
109
|
+
task :logs do
|
110
|
+
capistrano_nomad_tail_job_logs(name, namespace: namespace, stdout: true)
|
111
|
+
capistrano_nomad_tail_job_logs(name, namespace: namespace, stderr: true)
|
112
|
+
end
|
113
|
+
|
114
|
+
desc "Display stdout of #{description_name} job. Specify task with TASK"
|
115
|
+
task :stdout do
|
116
|
+
capistrano_nomad_tail_job_logs(name, namespace: namespace, stdout: true)
|
117
|
+
end
|
118
|
+
|
119
|
+
desc "Display stderr of #{description_name} job. Specify task with TASK"
|
120
|
+
task :stderr do
|
121
|
+
capistrano_nomad_tail_job_logs(name, namespace: namespace, stderr: true)
|
122
|
+
end
|
123
|
+
|
124
|
+
desc "Follow logs of #{description_name} job. Specify task with TASK"
|
125
|
+
task :follow do
|
126
|
+
capistrano_nomad_display_job_logs(name, namespace: namespace, f: true)
|
104
127
|
end
|
105
128
|
end
|
106
129
|
end
|
@@ -116,3 +139,7 @@ def nomad_job(name, attributes = {})
|
|
116
139
|
end
|
117
140
|
end
|
118
141
|
end
|
142
|
+
|
143
|
+
def nomad_template_helpers(&block)
|
144
|
+
CapistranoNomadErbNamespace.class_eval(&block)
|
145
|
+
end
|
@@ -10,16 +10,6 @@ class CapistranoNomadErbNamespace
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
# Use default value passed in unless `nomad_job_task_cpu_resource` is set
|
14
|
-
def build_nomad_job_task_cpu_resource(default:)
|
15
|
-
nomad_job_task_cpu_resource == "null" ? default : nomad_job_task_cpu_resource
|
16
|
-
end
|
17
|
-
|
18
|
-
# Use default value passed in unless `nomad_job_task_memory_resource` is set
|
19
|
-
def build_nomad_job_task_memory_resource(default:)
|
20
|
-
nomad_job_task_memory_resource == "null" ? default : nomad_job_task_memory_resource
|
21
|
-
end
|
22
|
-
|
23
13
|
# rubocop:disable Style/MissingRespondToMissing
|
24
14
|
def method_missing(name, *args)
|
25
15
|
instance_variable = "@#{name}"
|
@@ -123,25 +113,52 @@ def capistrano_nomad_capture_nomad_command(*args)
|
|
123
113
|
output
|
124
114
|
end
|
125
115
|
|
126
|
-
def
|
127
|
-
|
128
|
-
task ||= name
|
116
|
+
def capistrano_nomad_find_job_task_details(name, namespace: nil, task: nil)
|
117
|
+
task = task.presence || name
|
129
118
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
119
|
+
# Find alloc id that contains task
|
120
|
+
allocs_output = capistrano_nomad_capture_nomad_command(
|
121
|
+
:job,
|
122
|
+
:allocs,
|
123
|
+
{ namespace: namespace, t: "'{{range .}}{{ .ID }},{{ .TaskGroup }}|{{end}}'" },
|
124
|
+
name,
|
125
|
+
)
|
126
|
+
alloc_id = allocs_output.split("|").map { |s| s.split(",") }.find { |_, t| t == task.to_s }&.first
|
127
|
+
|
128
|
+
# Can't continue if we can't choose an alloc id
|
129
|
+
return unless alloc_id
|
130
|
+
|
131
|
+
tasks_output = capistrano_nomad_capture_nomad_command(
|
132
|
+
:alloc,
|
133
|
+
:status,
|
134
|
+
{ namespace: namespace, t: "'{{range $key, $value := .TaskStates}}{{ $key }},{{ .State }}|{{end}}'" },
|
135
|
+
alloc_id,
|
136
|
+
)
|
137
|
+
tasks_by_score = tasks_output.split("|").each_with_object({}) do |task_output, hash|
|
138
|
+
task, state = task_output.split(",")
|
139
|
+
|
140
|
+
score = 0
|
141
|
+
score += 5 if state == "running"
|
142
|
+
score += 5 unless task.match?(/connect-proxy/)
|
143
|
+
|
144
|
+
hash[task] = score
|
145
|
+
end
|
146
|
+
task = tasks_by_score.max_by { |_, v| v }.first
|
147
|
+
|
148
|
+
{
|
149
|
+
alloc_id: alloc_id,
|
150
|
+
name: task,
|
151
|
+
}
|
152
|
+
end
|
138
153
|
|
139
|
-
|
154
|
+
def capistrano_nomad_exec_within_job(name, command, namespace: nil, task: nil)
|
155
|
+
on(roles(:manager)) do
|
156
|
+
if (task_details = capistrano_nomad_find_job_task_details(name, namespace: namespace, task: task))
|
140
157
|
capistrano_nomad_execute_nomad_command(
|
141
158
|
:alloc,
|
142
159
|
:exec,
|
143
|
-
{ namespace: namespace, task:
|
144
|
-
alloc_id,
|
160
|
+
{ namespace: namespace, task: task_details[:name] },
|
161
|
+
task_details[:alloc_id],
|
145
162
|
command,
|
146
163
|
)
|
147
164
|
else
|
@@ -175,7 +192,7 @@ def capistrano_nomad_upload_file(local_path:, remote_path:, erb_vars: {})
|
|
175
192
|
}
|
176
193
|
|
177
194
|
# Add global ERB vars
|
178
|
-
final_erb_vars.merge!(fetch(:
|
195
|
+
final_erb_vars.merge!(fetch(:nomad_template_vars) || {})
|
179
196
|
|
180
197
|
# Add job-specific ERB vars
|
181
198
|
final_erb_vars.merge!(erb_vars)
|
@@ -269,11 +286,11 @@ def capistrano_nomad_plan_jobs(names, *args)
|
|
269
286
|
end
|
270
287
|
end
|
271
288
|
|
272
|
-
def capistrano_nomad_run_jobs(names, namespace: nil)
|
289
|
+
def capistrano_nomad_run_jobs(names, namespace: nil, is_detached: true)
|
273
290
|
names.each do |name|
|
274
291
|
run_options = {
|
275
292
|
namespace: namespace,
|
276
|
-
detach:
|
293
|
+
detach: is_detached,
|
277
294
|
|
278
295
|
# Don't reset counts since they may have been scaled
|
279
296
|
preserve_counts: true,
|
@@ -292,44 +309,52 @@ def capistrano_nomad_run_jobs(names, namespace: nil)
|
|
292
309
|
end
|
293
310
|
|
294
311
|
# Remove job and run again
|
295
|
-
def capistrano_nomad_rerun_jobs(names)
|
312
|
+
def capistrano_nomad_rerun_jobs(names, **options)
|
313
|
+
general_options = options.slice!(:is_detached)
|
314
|
+
|
296
315
|
names.each do |name|
|
297
316
|
# Wait for jobs to be purged before running again
|
298
|
-
capistrano_nomad_purge_jobs([name], is_detached: false)
|
317
|
+
capistrano_nomad_purge_jobs([name], **general_options.merge(is_detached: false))
|
299
318
|
|
300
|
-
capistrano_nomad_run_jobs([name])
|
319
|
+
capistrano_nomad_run_jobs([name], **general_options.merge(options))
|
301
320
|
end
|
302
321
|
end
|
303
322
|
|
304
|
-
def capistrano_nomad_upload_plan_jobs(names,
|
305
|
-
capistrano_nomad_upload_jobs(names,
|
306
|
-
capistrano_nomad_plan_jobs(names,
|
323
|
+
def capistrano_nomad_upload_plan_jobs(names, **options)
|
324
|
+
capistrano_nomad_upload_jobs(names, **options)
|
325
|
+
capistrano_nomad_plan_jobs(names, **options)
|
307
326
|
end
|
308
327
|
|
309
|
-
def capistrano_nomad_upload_run_jobs(names,
|
310
|
-
|
311
|
-
|
328
|
+
def capistrano_nomad_upload_run_jobs(names, **options)
|
329
|
+
general_options = options.slice!(:is_detached)
|
330
|
+
|
331
|
+
capistrano_nomad_upload_jobs(names, **general_options)
|
332
|
+
capistrano_nomad_run_jobs(names, **general_options.merge(options))
|
312
333
|
end
|
313
334
|
|
314
|
-
def capistrano_nomad_upload_rerun_jobs(names,
|
315
|
-
|
316
|
-
|
335
|
+
def capistrano_nomad_upload_rerun_jobs(names, **options)
|
336
|
+
general_options = options.slice!(:is_detached)
|
337
|
+
|
338
|
+
capistrano_nomad_upload_jobs(names, **general_options)
|
339
|
+
capistrano_nomad_rerun_jobs(names, **general_options.merge(options))
|
317
340
|
end
|
318
341
|
|
319
|
-
def capistrano_nomad_deploy_jobs(names,
|
320
|
-
|
321
|
-
|
342
|
+
def capistrano_nomad_deploy_jobs(names, **options)
|
343
|
+
general_options = options.slice!(:is_detached)
|
344
|
+
|
345
|
+
capistrano_nomad_assemble_jobs_docker_images(names, **general_options)
|
346
|
+
capistrano_nomad_upload_run_jobs(names, **general_options.merge(options))
|
322
347
|
end
|
323
348
|
|
324
|
-
def capistrano_nomad_stop_jobs(names,
|
349
|
+
def capistrano_nomad_stop_jobs(names, **options)
|
325
350
|
names.each do |name|
|
326
|
-
capistrano_nomad_execute_nomad_command(:job, :stop,
|
351
|
+
capistrano_nomad_execute_nomad_command(:job, :stop, options, name)
|
327
352
|
end
|
328
353
|
end
|
329
354
|
|
330
|
-
def capistrano_nomad_restart_jobs(names,
|
355
|
+
def capistrano_nomad_restart_jobs(names, **options)
|
331
356
|
names.each do |name|
|
332
|
-
capistrano_nomad_execute_nomad_command(:job, :restart,
|
357
|
+
capistrano_nomad_execute_nomad_command(:job, :restart, options, name)
|
333
358
|
end
|
334
359
|
end
|
335
360
|
|
@@ -339,6 +364,29 @@ def capistrano_nomad_purge_jobs(names, namespace: nil, is_detached: true)
|
|
339
364
|
end
|
340
365
|
end
|
341
366
|
|
342
|
-
def capistrano_nomad_display_job_status(name,
|
343
|
-
capistrano_nomad_execute_nomad_command(:status,
|
367
|
+
def capistrano_nomad_display_job_status(name, **options)
|
368
|
+
capistrano_nomad_execute_nomad_command(:status, options, name)
|
369
|
+
end
|
370
|
+
|
371
|
+
def capistrano_nomad_display_job_logs(name, namespace: nil, **options)
|
372
|
+
if (task_details = capistrano_nomad_find_job_task_details(name, namespace: namespace, task: ENV["TASK"]))
|
373
|
+
capistrano_nomad_execute_nomad_command(
|
374
|
+
:alloc,
|
375
|
+
:logs,
|
376
|
+
options.merge(namespace: namespace, task: task_details[:name]),
|
377
|
+
task_details[:alloc_id],
|
378
|
+
)
|
379
|
+
else
|
380
|
+
# If task can't be determined choose a random allocation
|
381
|
+
capistrano_nomad_execute_nomad_command(
|
382
|
+
:alloc,
|
383
|
+
:logs,
|
384
|
+
options.merge(namespace: namespace, job: true),
|
385
|
+
name,
|
386
|
+
)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def capistrano_nomad_tail_job_logs(*args, **options)
|
391
|
+
capistrano_nomad_display_job_logs(*args, **options.merge(tail: true, n: 50))
|
344
392
|
end
|
@@ -55,7 +55,7 @@ namespace :nomad do
|
|
55
55
|
end
|
56
56
|
|
57
57
|
desc "Create missing and remove unused namespaces"
|
58
|
-
task :
|
58
|
+
task :modify_namespaces do
|
59
59
|
output = capistrano_nomad_capture_nomad_command(:namespace, :list, t: "'{{range .}}{{ .Name }}|{{end}}'")
|
60
60
|
current_namespaces = output.split("|").compact.map(&:to_sym)
|
61
61
|
desired_namespaces = fetch(:nomad_jobs).keys
|
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.6.
|
4
|
+
version: 0.6.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Hu
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|