capistrano-nomad 0.6.4 → 0.6.5
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 +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
|