makit 0.0.4 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/makit/apache.rb +32 -32
  3. data/lib/makit/cli/clean.rb +14 -14
  4. data/lib/makit/cli/clone.rb +59 -59
  5. data/lib/makit/cli/init.rb +38 -38
  6. data/lib/makit/cli/main.rb +33 -33
  7. data/lib/makit/cli/make.rb +54 -54
  8. data/lib/makit/cli/new.rb +37 -37
  9. data/lib/makit/cli/nuget_cache.rb +38 -38
  10. data/lib/makit/cli/pull.rb +31 -31
  11. data/lib/makit/cli/setup.rb +71 -71
  12. data/lib/makit/cli/work.rb +21 -21
  13. data/lib/makit/command_runner.rb +321 -318
  14. data/lib/makit/commands.rb +21 -21
  15. data/lib/makit/content/default_gitignore.rb +5 -5
  16. data/lib/makit/content/default_gitignore.txt +222 -0
  17. data/lib/makit/content/default_rakefile.rb +11 -11
  18. data/lib/makit/content/gem_rakefile.rb +14 -14
  19. data/lib/makit/data.rb +50 -50
  20. data/lib/makit/directories.rb +140 -140
  21. data/lib/makit/directory.rb +153 -153
  22. data/lib/makit/dotnet.rb +83 -83
  23. data/lib/makit/environment.rb +123 -123
  24. data/lib/makit/files.rb +47 -47
  25. data/lib/makit/git.rb +66 -66
  26. data/lib/makit/gitlab_runner.rb +60 -60
  27. data/lib/makit/humanize.rb +89 -89
  28. data/lib/makit/logging.rb +96 -96
  29. data/lib/makit/markdown.rb +75 -75
  30. data/lib/makit/mp/basic_object_mp.rb +16 -16
  31. data/lib/makit/mp/project_mp.rb +160 -156
  32. data/lib/makit/mp/string_mp.rb +107 -107
  33. data/lib/makit/nuget.rb +57 -57
  34. data/lib/makit/protoc.rb +61 -61
  35. data/lib/makit/serializer.rb +115 -115
  36. data/lib/makit/storage.rb +131 -131
  37. data/lib/makit/symbols.rb +149 -149
  38. data/lib/makit/tasks.rb +67 -67
  39. data/lib/makit/tree.rb +37 -37
  40. data/lib/makit/v1/makit.v1_pb.rb +4 -3
  41. data/lib/makit/v1/makit.v1_services_pb.rb +25 -25
  42. data/lib/makit/version.rb +12 -12
  43. data/lib/makit/wix.rb +95 -95
  44. data/lib/makit/zip.rb +17 -17
  45. data/lib/makit.rb +243 -243
  46. metadata +4 -3
data/lib/makit.rb CHANGED
@@ -1,243 +1,243 @@
1
- # frozen_string_literal: true
2
-
3
- require "rake/clean"
4
- require "logger"
5
- require "json"
6
- require "yaml"
7
-
8
- %w[makit makit/v1 makit/cli makit/content makit/mp].each do |dir|
9
- Dir[File.join(__dir__, dir, "*.rb")].each do |file|
10
- require_relative file
11
- end
12
- end
13
-
14
- module Makit
15
- class Error < StandardError; end
16
-
17
- # Constants
18
- #PROJECT = Makit::V1::Project.create
19
-
20
- STARTTIME = Time.now
21
- IS_GIT_REPO = Dir.exist? ".git"
22
- DETACHED = `git status`.include?("detached")
23
- IS_READ_ONLY = !IS_GIT_REPO || DETACHED
24
- RUNTIME_IDENTIFIER = Makit::Environment::get_runtime_identifier
25
- DEVICE = Socket.gethostname
26
- LOGGER = Makit::Logging::MultiLogger.create_logger
27
-
28
- # Git Commit and Branch/Tag constants
29
- ENV["COMMIT_SHA"] = Makit::Git::commitsha
30
- ENV["COMMIT_BRANCH"] = Makit::Git::branch
31
-
32
- RUNNER = CommandRunner.new
33
- #RUNNER.log_to_artifacts = true
34
- # Variables
35
- log_level = Logger::INFO
36
- package_type = Makit::V1::PackageType::GEM
37
- commands = Makit::Commands.new
38
-
39
- # methods
40
- #
41
- # initialize a git repository
42
- def self.init(directory)
43
- if !Dir.exist?(directory)
44
- FileUtils.mkdir_p(directory)
45
- end
46
- raise Makit::Error.new("directory does not exist: #{directory}") if !Dir.exist?(directory)
47
- Dir.chdir(directory) do
48
- File.write(".gitignore", Makit::Content::GITIGNORE) unless File.exist?(".gitignore")
49
- init = Makit::RUNNER.execute "git init"
50
- if init.exit_code != 0
51
- raise Makit::Error.new("failed to initialize local repository: #{directory}\n#{Makit::Humanize.get_command_summary(init)}")
52
- end
53
- init
54
- end
55
- end
56
-
57
- # clone a git repository to a local directory in Directories::CLONE
58
- # returns the Makit::V1::Command for 'git clone ...'
59
- def self.clone(git_repository)
60
- commands = []
61
- # make sure a local clone of the repository exists
62
- clone_dir = Directories::get_clone_directory(git_repository)
63
- if (!Dir.exist?(clone_dir))
64
- commands << Makit::RUNNER.execute("git clone #{git_repository} #{clone_dir}")
65
- end
66
- commands
67
- end
68
-
69
- # pull the latest changes from the remote repository to a local clone in Directories::CLONE
70
- def self.pull(git_repository)
71
- clone_dir = Directories::get_clone_directory(git_repository)
72
- raise Makit::Error.new("clone directory does not exist: #{clone_dir}") if !Dir.exist?(clone_dir)
73
- Dir.chdir(clone_dir) do
74
- request = Makit::V1::CommandRequest.new(
75
- name: "git",
76
- arguments: ["pull"],
77
- directory: clone_dir,
78
- )
79
- pull_command = Makit::RUNNER.execute(request)
80
- raise Makit::Error.new(Makit::Humanize::get_command_details(pull_command)) if pull_command.exit_code != 0
81
- return pull_command
82
- end
83
- end
84
-
85
- def self.clone_or_pull(git_repository)
86
- commands = []
87
- clone_dir = Directories::get_clone_directory(git_repository)
88
- if Dir.exist?(clone_dir)
89
- commands << pull(git_repository)
90
- else
91
- commands << clone(git_repository)
92
- end
93
- commands
94
- end
95
-
96
- # log information about a specific repository
97
- # return an array of GitLogEntry objects
98
- def self.log(git_repository, limit, skip)
99
- entries = []
100
- clone_dir = Directories::get_clone_directory(git_repository)
101
- raise Makit::Error.new("clone directory does not exist: #{clone_dir}") if !Dir.exist?(clone_dir)
102
- Dir.chdir(clone_dir) do
103
- log_command = Makit::RUNNER.execute("git log -n #{limit} --skip #{skip} --date=iso")
104
- if log_command.exit_code != 0
105
- lines = log_command.stderr.split("\n")
106
- # iterate over the lines, generating a GitLogEntry for each commit
107
- lines.each do |line|
108
- if line.start_with?("commit")
109
- commit = line.split(" ")[1]
110
- entries << GitLogEntry.new(commit)
111
- end
112
- if line.start_with?("Author:")
113
- entries.last.author = line.split(" ")[1..-1].join(" ")
114
- end
115
- if line.start_with?("Date:")
116
- entries.last.date = line.split(" ")[1..-1].join(" ")
117
- end
118
- if line.start_with?(" ")
119
- entries.last.message += line[4..-1]
120
- end
121
- end
122
- end
123
- end
124
- entries
125
- end
126
-
127
- # work on a local clone of a git repository
128
- # if the repository does not exist, clone it
129
- # if the repository exists, pull the latest changes
130
- # if a build command can be found, execute it and return the result Makit::V1::WorkResult
131
- def self.work(repository)
132
- commands = []
133
- work_dir = Makit::Directories::get_work_directory(repository)
134
- commands << clone_or_pull(repository)
135
- clone_dir = Makit::Directories::get_clone_directory(repository)
136
- if !Dir.exist?(work_dir)
137
- # make the parent directory for work_dir if it does not exist
138
- FileUtils.mkdir_p(File.dirname(work_dir)) unless Dir.exist?(File.dirname(work_dir))
139
- Makit::RUNNER::execute "git clone #{clone_dir} #{work_dir}"
140
- end
141
- Dir.chdir(work_dir) do
142
- # if there is no .gitignore file, create one
143
- File.write(".gitignore", Makit::Content::GITIGNORE) unless File.exist?(".gitignore")
144
- end
145
- nil?
146
- end
147
-
148
- def self.enable_monkey_patch
149
- %w[makit/mp].each do |dir|
150
- Dir[File.join(__dir__, dir, "*.rb")].each do |file|
151
- require_relative file
152
- end
153
- end
154
- end
155
- # Given a git repository URL and a commit id, create a new MakeResult object.
156
- def self.make(url, commit, force = false)
157
- log_filename = File.join(Directories::get_log_directory(url), commit, +"#{RUNTIME_IDENTIFIER}.#{DEVICE}.json")
158
- if File.exist?(log_filename) && !force && commit != "latest"
159
- begin
160
- # deserialize the log file to a Makite::V1::MakeResult object
161
- make_result = Makit::V1::MakeResult.decode_json(File.read(log_filename))
162
- return make_result
163
- rescue => e
164
- # if deserialization fails, delete the log file and continue
165
- FileUtils.rm(log_filename)
166
- end
167
- else
168
- commands = []
169
- begin
170
- clone_or_pull(url).each do |command|
171
- commands << command
172
- end
173
- # make sure a local clone of the repository exists
174
- clone_dir = Directories::get_clone_directory(url)
175
- raise Makit::Error.new("clone directory does not exist: #{clone_dir}") if !Dir.exist?(clone_dir)
176
-
177
- if (commit == "latest")
178
- Dir.chdir(clone_dir) do
179
- git_log = Makit::RUNNER.execute("git log -n 1 --date=iso")
180
-
181
- commands << git_log
182
- # assert that the commit is valid
183
- commit = git_log.output.match(/^commit ([0-9a-f]{40})$/i)[1]
184
- raise Makit::Error.new("invalid commit: #{commit}") if commit.nil? || commit.empty? || !commit.match?(/\A[0-9a-f]{40}\z/i)
185
- log_filename = File.join(Directories::get_log_directory(url), commit, +"#{RUNTIME_IDENTIFIER}.#{DEVICE}.json")
186
- end
187
- end
188
-
189
- # clone a fresh copy of the repository to a make directory
190
- make_dir = Directories::get_make_commit_directory(url, commit)
191
- FileUtils.rm_rf(make_dir) if Dir.exist?(make_dir)
192
- commands << Makit::RUNNER.execute("git clone #{clone_dir} #{make_dir}")
193
- raise Makit::Error.new("failed to clone repository: #{url} to #{make_dir}") if !Dir.exist?(make_dir)
194
- Dir.chdir(make_dir) do
195
- commands << Makit::RUNNER.execute("git reset --hard #{commit}")
196
- commands << Makit::RUNNER.execute("git log -n 1")
197
-
198
- commands << Makit::RUNNER.execute("bundle install") if File.exist? "Gemfile"
199
- if File.exist? ("Rakefile")
200
- commands << Makit::RUNNER.execute("rake default")
201
- else
202
- commands << Makit::RUNNER.execute("rake default") if File.exist? "rakefile.rb"
203
- end
204
-
205
- make_result = Makit::V1::MakeResult.new(
206
- repository: url,
207
- commit: commit,
208
- branch: "?",
209
- tag: "?",
210
- device: DEVICE,
211
- runtime_identifier: RUNTIME_IDENTIFIER,
212
- )
213
- commands.flatten.each do |command|
214
- make_result.commands << command
215
- end
216
-
217
- # save the MakeResult object to a log file as pretty printed json
218
- FileUtils.mkdir_p(File.dirname(log_filename)) unless Dir.exist?(File.dirname(log_filename))
219
- File.write(log_filename, make_result.to_json)
220
-
221
- return make_result
222
- end
223
- rescue => e
224
- message = "error raised attempting to make repository: #{url} commit: #{commit}\n\n"
225
- message += "#{e.message}\n"
226
- backtrace = e.backtrace.join("\n")
227
- message += "#{backtrace}\n\n"
228
- message += "commands:\n"
229
- commands.flatten.each do |command|
230
- message += Makit::Humanize::get_command_details(command)
231
- end
232
- raise Makit::Error.new(message)
233
- end
234
- end
235
- end
236
- end
237
-
238
- if !File.exist?(".gitignore")
239
- Makit::LOGGER.info("added .gitignore file")
240
- File.open(".gitignore", "w") do |file|
241
- file.puts Makit::Content::GITIGNORE
242
- end
243
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/clean"
4
+ require "logger"
5
+ require "json"
6
+ require "yaml"
7
+
8
+ %w[makit makit/v1 makit/cli makit/content makit/mp].each do |dir|
9
+ Dir[File.join(__dir__, dir, "*.rb")].each do |file|
10
+ require_relative file
11
+ end
12
+ end
13
+
14
+ module Makit
15
+ class Error < StandardError; end
16
+
17
+ # Constants
18
+ #PROJECT = Makit::V1::Project.create
19
+
20
+ STARTTIME = Time.now
21
+ IS_GIT_REPO = Dir.exist? ".git"
22
+ DETACHED = `git status`.include?("detached")
23
+ IS_READ_ONLY = !IS_GIT_REPO || DETACHED
24
+ RUNTIME_IDENTIFIER = Makit::Environment::get_runtime_identifier
25
+ DEVICE = Socket.gethostname
26
+ LOGGER = Makit::Logging::MultiLogger.create_logger
27
+
28
+ # Git Commit and Branch/Tag constants
29
+ ENV["COMMIT_SHA"] = Makit::Git::commitsha
30
+ ENV["COMMIT_BRANCH"] = Makit::Git::branch
31
+
32
+ RUNNER = CommandRunner.new
33
+ #RUNNER.log_to_artifacts = true
34
+ # Variables
35
+ log_level = Logger::INFO
36
+ package_type = Makit::V1::PackageType::GEM
37
+ commands = Makit::Commands.new
38
+
39
+ # methods
40
+ #
41
+ # initialize a git repository
42
+ def self.init(directory)
43
+ if !Dir.exist?(directory)
44
+ FileUtils.mkdir_p(directory)
45
+ end
46
+ raise Makit::Error.new("directory does not exist: #{directory}") if !Dir.exist?(directory)
47
+ Dir.chdir(directory) do
48
+ File.write(".gitignore", Makit::Content::GITIGNORE) unless File.exist?(".gitignore")
49
+ init = Makit::RUNNER.execute "git init"
50
+ if init.exit_code != 0
51
+ raise Makit::Error.new("failed to initialize local repository: #{directory}\n#{Makit::Humanize.get_command_summary(init)}")
52
+ end
53
+ init
54
+ end
55
+ end
56
+
57
+ # clone a git repository to a local directory in Directories::CLONE
58
+ # returns the Makit::V1::Command for 'git clone ...'
59
+ def self.clone(git_repository)
60
+ commands = []
61
+ # make sure a local clone of the repository exists
62
+ clone_dir = Directories::get_clone_directory(git_repository)
63
+ if (!Dir.exist?(clone_dir))
64
+ commands << Makit::RUNNER.execute("git clone #{git_repository} #{clone_dir}")
65
+ end
66
+ commands
67
+ end
68
+
69
+ # pull the latest changes from the remote repository to a local clone in Directories::CLONE
70
+ def self.pull(git_repository)
71
+ clone_dir = Directories::get_clone_directory(git_repository)
72
+ raise Makit::Error.new("clone directory does not exist: #{clone_dir}") if !Dir.exist?(clone_dir)
73
+ Dir.chdir(clone_dir) do
74
+ request = Makit::V1::CommandRequest.new(
75
+ name: "git",
76
+ arguments: ["pull"],
77
+ directory: clone_dir,
78
+ )
79
+ pull_command = Makit::RUNNER.execute(request)
80
+ raise Makit::Error.new(Makit::Humanize::get_command_details(pull_command)) if pull_command.exit_code != 0
81
+ return pull_command
82
+ end
83
+ end
84
+
85
+ def self.clone_or_pull(git_repository)
86
+ commands = []
87
+ clone_dir = Directories::get_clone_directory(git_repository)
88
+ if Dir.exist?(clone_dir)
89
+ commands << pull(git_repository)
90
+ else
91
+ commands << clone(git_repository)
92
+ end
93
+ commands
94
+ end
95
+
96
+ # log information about a specific repository
97
+ # return an array of GitLogEntry objects
98
+ def self.log(git_repository, limit, skip)
99
+ entries = []
100
+ clone_dir = Directories::get_clone_directory(git_repository)
101
+ raise Makit::Error.new("clone directory does not exist: #{clone_dir}") if !Dir.exist?(clone_dir)
102
+ Dir.chdir(clone_dir) do
103
+ log_command = Makit::RUNNER.execute("git log -n #{limit} --skip #{skip} --date=iso")
104
+ if log_command.exit_code != 0
105
+ lines = log_command.stderr.split("\n")
106
+ # iterate over the lines, generating a GitLogEntry for each commit
107
+ lines.each do |line|
108
+ if line.start_with?("commit")
109
+ commit = line.split(" ")[1]
110
+ entries << GitLogEntry.new(commit)
111
+ end
112
+ if line.start_with?("Author:")
113
+ entries.last.author = line.split(" ")[1..-1].join(" ")
114
+ end
115
+ if line.start_with?("Date:")
116
+ entries.last.date = line.split(" ")[1..-1].join(" ")
117
+ end
118
+ if line.start_with?(" ")
119
+ entries.last.message += line[4..-1]
120
+ end
121
+ end
122
+ end
123
+ end
124
+ entries
125
+ end
126
+
127
+ # work on a local clone of a git repository
128
+ # if the repository does not exist, clone it
129
+ # if the repository exists, pull the latest changes
130
+ # if a build command can be found, execute it and return the result Makit::V1::WorkResult
131
+ def self.work(repository)
132
+ commands = []
133
+ work_dir = Makit::Directories::get_work_directory(repository)
134
+ commands << clone_or_pull(repository)
135
+ clone_dir = Makit::Directories::get_clone_directory(repository)
136
+ if !Dir.exist?(work_dir)
137
+ # make the parent directory for work_dir if it does not exist
138
+ FileUtils.mkdir_p(File.dirname(work_dir)) unless Dir.exist?(File.dirname(work_dir))
139
+ Makit::RUNNER::execute "git clone #{clone_dir} #{work_dir}"
140
+ end
141
+ Dir.chdir(work_dir) do
142
+ # if there is no .gitignore file, create one
143
+ File.write(".gitignore", Makit::Content::GITIGNORE) unless File.exist?(".gitignore")
144
+ end
145
+ nil?
146
+ end
147
+
148
+ def self.enable_monkey_patch
149
+ %w[makit/mp].each do |dir|
150
+ Dir[File.join(__dir__, dir, "*.rb")].each do |file|
151
+ require_relative file
152
+ end
153
+ end
154
+ end
155
+ # Given a git repository URL and a commit id, create a new MakeResult object.
156
+ def self.make(url, commit, force = false)
157
+ log_filename = File.join(Directories::get_log_directory(url), commit, +"#{RUNTIME_IDENTIFIER}.#{DEVICE}.json")
158
+ if File.exist?(log_filename) && !force && commit != "latest"
159
+ begin
160
+ # deserialize the log file to a Makite::V1::MakeResult object
161
+ make_result = Makit::V1::MakeResult.decode_json(File.read(log_filename))
162
+ return make_result
163
+ rescue => e
164
+ # if deserialization fails, delete the log file and continue
165
+ FileUtils.rm(log_filename)
166
+ end
167
+ else
168
+ commands = []
169
+ begin
170
+ clone_or_pull(url).each do |command|
171
+ commands << command
172
+ end
173
+ # make sure a local clone of the repository exists
174
+ clone_dir = Directories::get_clone_directory(url)
175
+ raise Makit::Error.new("clone directory does not exist: #{clone_dir}") if !Dir.exist?(clone_dir)
176
+
177
+ if (commit == "latest")
178
+ Dir.chdir(clone_dir) do
179
+ git_log = Makit::RUNNER.execute("git log -n 1 --date=iso")
180
+
181
+ commands << git_log
182
+ # assert that the commit is valid
183
+ commit = git_log.output.match(/^commit ([0-9a-f]{40})$/i)[1]
184
+ raise Makit::Error.new("invalid commit: #{commit}") if commit.nil? || commit.empty? || !commit.match?(/\A[0-9a-f]{40}\z/i)
185
+ log_filename = File.join(Directories::get_log_directory(url), commit, +"#{RUNTIME_IDENTIFIER}.#{DEVICE}.json")
186
+ end
187
+ end
188
+
189
+ # clone a fresh copy of the repository to a make directory
190
+ make_dir = Directories::get_make_commit_directory(url, commit)
191
+ FileUtils.rm_rf(make_dir) if Dir.exist?(make_dir)
192
+ commands << Makit::RUNNER.execute("git clone #{clone_dir} #{make_dir}")
193
+ raise Makit::Error.new("failed to clone repository: #{url} to #{make_dir}") if !Dir.exist?(make_dir)
194
+ Dir.chdir(make_dir) do
195
+ commands << Makit::RUNNER.execute("git reset --hard #{commit}")
196
+ commands << Makit::RUNNER.execute("git log -n 1")
197
+
198
+ commands << Makit::RUNNER.execute("bundle install") if File.exist? "Gemfile"
199
+ if File.exist? ("Rakefile")
200
+ commands << Makit::RUNNER.execute("rake default")
201
+ else
202
+ commands << Makit::RUNNER.execute("rake default") if File.exist? "rakefile.rb"
203
+ end
204
+
205
+ make_result = Makit::V1::MakeResult.new(
206
+ repository: url,
207
+ commit: commit,
208
+ branch: "?",
209
+ tag: "?",
210
+ device: DEVICE,
211
+ runtime_identifier: RUNTIME_IDENTIFIER,
212
+ )
213
+ commands.flatten.each do |command|
214
+ make_result.commands << command
215
+ end
216
+
217
+ # save the MakeResult object to a log file as pretty printed json
218
+ FileUtils.mkdir_p(File.dirname(log_filename)) unless Dir.exist?(File.dirname(log_filename))
219
+ File.write(log_filename, make_result.to_json)
220
+
221
+ return make_result
222
+ end
223
+ rescue => e
224
+ message = "error raised attempting to make repository: #{url} commit: #{commit}\n\n"
225
+ message += "#{e.message}\n"
226
+ backtrace = e.backtrace.join("\n")
227
+ message += "#{backtrace}\n\n"
228
+ message += "commands:\n"
229
+ commands.flatten.each do |command|
230
+ message += Makit::Humanize::get_command_details(command)
231
+ end
232
+ raise Makit::Error.new(message)
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ if !File.exist?(".gitignore")
239
+ Makit::LOGGER.info("added .gitignore file")
240
+ File.open(".gitignore", "w") do |file|
241
+ file.puts Makit::Content::GITIGNORE
242
+ end
243
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: makit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lou Parslow
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-06 00:00:00.000000000 Z
11
+ date: 2024-11-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clamp
@@ -158,6 +158,7 @@ files:
158
158
  - lib/makit/command_runner.rb
159
159
  - lib/makit/commands.rb
160
160
  - lib/makit/content/default_gitignore.rb
161
+ - lib/makit/content/default_gitignore.txt
161
162
  - lib/makit/content/default_rakefile.rb
162
163
  - lib/makit/content/gem_rakefile.rb
163
164
  - lib/makit/data.rb
@@ -209,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
209
210
  - !ruby/object:Gem::Version
210
211
  version: '0'
211
212
  requirements: []
212
- rubygems_version: 3.5.16
213
+ rubygems_version: 3.5.20
213
214
  signing_key:
214
215
  specification_version: 4
215
216
  summary: CI/CD tools for Ruby developers.