forge-ci 0.1.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 +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +273 -0
- data/exe/forge-ci +8 -0
- data/lib/forge_ci/cli.rb +203 -0
- data/lib/forge_ci/compiler.rb +537 -0
- data/lib/forge_ci/dag.rb +68 -0
- data/lib/forge_ci/errors.rb +101 -0
- data/lib/forge_ci/interpolator.rb +137 -0
- data/lib/forge_ci/ir.rb +117 -0
- data/lib/forge_ci/validator.rb +403 -0
- data/lib/forge_ci.rb +20 -0
- metadata +54 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ForgeCI
|
|
4
|
+
# Structural validation of the CircleCI v2.1 subset. Every problem is
|
|
5
|
+
# collected into +errors+ (an ErrorCollector) — nothing is raised — so the
|
|
6
|
+
# caller sees all problems in one pass. Validation runs on the
|
|
7
|
+
# expanded + interpolated config, i.e. after reusable commands/executors
|
|
8
|
+
# have been inlined and << >> references resolved.
|
|
9
|
+
module Validator
|
|
10
|
+
TOP_LEVEL_KEYS = %w[version orbs parameters commands executors jobs workflows].freeze
|
|
11
|
+
SUPPORTED_VERSIONS = %w[2 2.1].freeze
|
|
12
|
+
|
|
13
|
+
# Step types we know how to normalize into the IR.
|
|
14
|
+
STEP_TYPES = %w[
|
|
15
|
+
checkout run save_cache restore_cache
|
|
16
|
+
persist_to_workspace attach_workspace
|
|
17
|
+
store_artifacts store_test_results add_ssh_keys
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# Conditional wrappers evaluated at compile time.
|
|
21
|
+
CONDITIONAL_TYPES = %w[when unless].freeze
|
|
22
|
+
|
|
23
|
+
RUN_WHEN_VALUES = %w[on_success on_fail always].freeze
|
|
24
|
+
PARAMETER_TYPES = %w[string boolean integer enum].freeze
|
|
25
|
+
WORKFLOW_JOB_KEYS = %w[requires filters matrix context name].freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# --- top level ---------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def top_level(config, errors)
|
|
32
|
+
unknown = config.keys.map(&:to_s) - TOP_LEVEL_KEYS
|
|
33
|
+
unknown.each do |key|
|
|
34
|
+
errors.add([], "unknown top-level key #{key.inspect}; " \
|
|
35
|
+
"supported keys: #{TOP_LEVEL_KEYS.join(', ')}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
version = config["version"]
|
|
39
|
+
unless SUPPORTED_VERSIONS.include?(version.to_s)
|
|
40
|
+
errors.add(["version"], "unsupported version #{version.inspect}; expected \"2.1\"")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if config.key?("orbs") && !config["orbs"].nil? && config["orbs"] != {}
|
|
44
|
+
errors.add(["orbs"], "orb registry is not supported in forge-ci v1; " \
|
|
45
|
+
"inline the orb's steps/commands into your config instead")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless config["jobs"].is_a?(Hash) && !config["jobs"].empty?
|
|
49
|
+
errors.add(["jobs"], "no jobs defined; add at least one job under \"jobs:\"")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
%w[parameters commands executors].each do |key|
|
|
53
|
+
if config.key?(key) && !config[key].nil? && !config[key].is_a?(Hash)
|
|
54
|
+
errors.add([key], "\"#{key}:\" must be a mapping")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return unless config.key?("workflows") && !config["workflows"].is_a?(Hash)
|
|
59
|
+
|
|
60
|
+
errors.add(["workflows"], "\"workflows:\" must be a mapping of workflow name to definition")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# --- pipeline parameters ------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def parameters(params_def, errors)
|
|
66
|
+
return if params_def.nil?
|
|
67
|
+
|
|
68
|
+
unless params_def.is_a?(Hash)
|
|
69
|
+
errors.add(["parameters"], "\"parameters:\" must be a mapping of name to definition")
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
params_def.each do |name, definition|
|
|
74
|
+
path = ["parameters", name]
|
|
75
|
+
unless definition.is_a?(Hash)
|
|
76
|
+
errors.add(path, "parameter definition must be a mapping with \"type\" (and usually \"default\")")
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
type = definition["type"] || "string"
|
|
81
|
+
unless PARAMETER_TYPES.include?(type.to_s)
|
|
82
|
+
errors.add(path + ["type"], "unknown parameter type #{type.inspect}; " \
|
|
83
|
+
"expected one of: #{PARAMETER_TYPES.join(', ')}")
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if type.to_s == "enum" && !definition["enum"].is_a?(Array)
|
|
88
|
+
errors.add(path, "enum parameters require an \"enum:\" list of allowed values")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
next unless definition.key?("default")
|
|
92
|
+
|
|
93
|
+
default = definition["default"]
|
|
94
|
+
unless value_matches_type?(default, type.to_s)
|
|
95
|
+
errors.add(path + ["default"],
|
|
96
|
+
"default #{default.inspect} does not match declared type #{type.inspect}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if type.to_s == "enum" && definition["enum"].is_a?(Array) && !definition["enum"].include?(default)
|
|
100
|
+
errors.add(path + ["default"],
|
|
101
|
+
"default #{default.inspect} is not in the enum list #{definition['enum'].inspect}")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def value_matches_type?(value, type)
|
|
107
|
+
case type
|
|
108
|
+
when "string" then value.is_a?(String)
|
|
109
|
+
when "boolean" then value == true || value == false
|
|
110
|
+
when "integer" then value.is_a?(Integer)
|
|
111
|
+
when "enum" then true # membership checked separately
|
|
112
|
+
else false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# --- reusable commands / executors --------------------------------------
|
|
117
|
+
|
|
118
|
+
def command(name, definition, errors)
|
|
119
|
+
path = ["commands", name]
|
|
120
|
+
unless definition.is_a?(Hash)
|
|
121
|
+
errors.add(path, "command definition must be a mapping with \"steps\" (and optional \"parameters\")")
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
parameters(definition["parameters"], errors) if definition.key?("parameters")
|
|
125
|
+
unless definition["steps"].is_a?(Array)
|
|
126
|
+
errors.add(path + ["steps"], "command #{name.inspect} must define a \"steps:\" list")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def executor_definition(name, definition, errors)
|
|
131
|
+
path = ["executors", name]
|
|
132
|
+
unless definition.is_a?(Hash)
|
|
133
|
+
errors.add(path, "executor definition must be a mapping")
|
|
134
|
+
return
|
|
135
|
+
end
|
|
136
|
+
parameters(definition["parameters"], errors) if definition.key?("parameters")
|
|
137
|
+
docker = definition["docker"]
|
|
138
|
+
unless docker.is_a?(Array) && !docker.empty?
|
|
139
|
+
errors.add(path + ["docker"],
|
|
140
|
+
"executor #{name.inspect} must define a non-empty \"docker:\" image list (only Docker executors are supported in v1)")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# --- jobs ---------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def job(name, job, errors)
|
|
147
|
+
path = ["jobs", name]
|
|
148
|
+
unless job.is_a?(Hash)
|
|
149
|
+
errors.add(path, "job definition must be a mapping")
|
|
150
|
+
return
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
docker = job["docker"]
|
|
154
|
+
if docker.nil? || (docker.is_a?(Array) && docker.empty?)
|
|
155
|
+
errors.add(path, "job #{name.inspect} must declare a \"docker:\" executor " \
|
|
156
|
+
"(or reference one via \"executor:\")")
|
|
157
|
+
elsif !docker.is_a?(Array)
|
|
158
|
+
errors.add(path + ["docker"], "\"docker:\" must be a list of image definitions")
|
|
159
|
+
else
|
|
160
|
+
docker.each_with_index { |img, i| docker_image(img, path + ["docker", i], errors) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if job.key?("machine") || job.key?("macos")
|
|
164
|
+
errors.add(path, "only Docker executors are supported in v1; " \
|
|
165
|
+
"\"machine\"/\"macos\" executors are not available yet")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
steps = job["steps"]
|
|
169
|
+
if !steps.is_a?(Array) || steps.empty?
|
|
170
|
+
errors.add(path + ["steps"], "job #{name.inspect} must define a non-empty \"steps:\" list")
|
|
171
|
+
return
|
|
172
|
+
end
|
|
173
|
+
steps.each_with_index { |step, i| step(step, path + ["steps", i], errors) }
|
|
174
|
+
|
|
175
|
+
return unless job.key?("environment") && !job["environment"].is_a?(Hash)
|
|
176
|
+
|
|
177
|
+
errors.add(path + ["environment"], "\"environment:\" must be a mapping of variable names to values")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def docker_image(img, path, errors)
|
|
181
|
+
unless img.is_a?(Hash) && img["image"].is_a?(String) && !img["image"].empty?
|
|
182
|
+
errors.add(path, "each \"docker:\" entry needs an \"image:\" string (e.g. image: cimg/ruby:3.2)")
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
return unless img.key?("environment") && !img["environment"].is_a?(Hash)
|
|
186
|
+
|
|
187
|
+
errors.add(path + ["environment"], "\"environment:\" must be a mapping")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# --- steps ----------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
# Validates one step; expects pre-normalized steps (bare `run:` strings
|
|
193
|
+
# already expanded to {command: ...} by the compiler).
|
|
194
|
+
def step(step, path, errors)
|
|
195
|
+
unless step.is_a?(Hash) && step.size == 1
|
|
196
|
+
errors.add(path, "each step must be a mapping with exactly one key " \
|
|
197
|
+
"(e.g. \"- run:\", \"- checkout:\", or a reusable command name)")
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
type, cfg = step.first
|
|
202
|
+
type = type.to_s
|
|
203
|
+
|
|
204
|
+
if CONDITIONAL_TYPES.include?(type)
|
|
205
|
+
conditional_step(cfg, path, errors, type)
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
unless STEP_TYPES.include?(type)
|
|
210
|
+
errors.add(path, "unknown step type #{type.inspect}; " \
|
|
211
|
+
"supported steps: #{(STEP_TYPES + CONDITIONAL_TYPES).join(', ')}")
|
|
212
|
+
return
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
cfg = {} if cfg.nil?
|
|
216
|
+
unless cfg.is_a?(Hash)
|
|
217
|
+
errors.add(path + [type], "step #{type.inspect} expects a mapping of attributes")
|
|
218
|
+
return
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
send("validate_#{type}", cfg, path + [type], errors)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def conditional_step(cfg, path, errors, type)
|
|
225
|
+
unless cfg.is_a?(Hash) && cfg.key?("condition") && cfg["steps"].is_a?(Array)
|
|
226
|
+
errors.add(path + [type], "\"#{type}:\" needs \"condition:\" and a \"steps:\" list")
|
|
227
|
+
return
|
|
228
|
+
end
|
|
229
|
+
cfg["steps"].each_with_index { |s, i| step(s, path + [type, "steps", i], errors) }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def validate_checkout(cfg, path, errors)
|
|
233
|
+
return unless cfg.key?("path") && !cfg["path"].is_a?(String)
|
|
234
|
+
|
|
235
|
+
errors.add(path + ["path"], "\"path:\" must be a string")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def validate_run(cfg, path, errors)
|
|
239
|
+
if cfg["command"].nil? || (cfg["command"].is_a?(String) && cfg["command"].strip.empty?)
|
|
240
|
+
errors.add(path, "\"run:\" needs a \"command:\" (or use the \"run: <command>\" shorthand)")
|
|
241
|
+
end
|
|
242
|
+
if cfg.key?("when") && !RUN_WHEN_VALUES.include?(cfg["when"].to_s)
|
|
243
|
+
errors.add(path + ["when"], "\"when:\" must be one of: #{RUN_WHEN_VALUES.join(', ')}")
|
|
244
|
+
end
|
|
245
|
+
return unless cfg.key?("background") && ![true, false].include?(cfg["background"])
|
|
246
|
+
|
|
247
|
+
errors.add(path + ["background"], "\"background:\" must be true or false")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def validate_save_cache(cfg, path, errors)
|
|
251
|
+
validate_cache_keys(cfg, path, errors, "save_cache")
|
|
252
|
+
return unless cfg["paths"].nil? || !cfg["paths"].is_a?(Array) || cfg["paths"].empty?
|
|
253
|
+
|
|
254
|
+
errors.add(path, "\"save_cache:\" needs a non-empty \"paths:\" list of directories/files to cache")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def validate_restore_cache(cfg, path, errors)
|
|
258
|
+
validate_cache_keys(cfg, path, errors, "restore_cache")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def validate_cache_keys(cfg, path, errors, type)
|
|
262
|
+
keys = cfg["keys"] || (cfg.key?("key") ? [cfg["key"]] : nil)
|
|
263
|
+
if !keys.is_a?(Array) || keys.empty? || keys.any? { |k| !k.is_a?(String) || k.empty? }
|
|
264
|
+
errors.add(path, "\"#{type}:\" needs \"keys:\" as a non-empty list of key template strings")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def validate_persist_to_workspace(cfg, path, errors)
|
|
269
|
+
if cfg["root"].nil? || !cfg["root"].is_a?(String) || cfg["root"].empty?
|
|
270
|
+
errors.add(path, "\"persist_to_workspace:\" needs a \"root:\" directory path")
|
|
271
|
+
end
|
|
272
|
+
return unless cfg["paths"].nil? || !cfg["paths"].is_a?(Array) || cfg["paths"].empty?
|
|
273
|
+
|
|
274
|
+
errors.add(path, "\"persist_to_workspace:\" needs a non-empty \"paths:\" list")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def validate_attach_workspace(cfg, path, errors)
|
|
278
|
+
return if cfg["at"].is_a?(String) && !cfg["at"].empty?
|
|
279
|
+
|
|
280
|
+
errors.add(path, "\"attach_workspace:\" needs an \"at:\" directory path")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def validate_store_artifacts(cfg, path, errors)
|
|
284
|
+
return if cfg["path"].is_a?(String) && !cfg["path"].empty?
|
|
285
|
+
|
|
286
|
+
errors.add(path, "\"store_artifacts:\" needs a \"path:\" to the file or directory to upload")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def validate_store_test_results(cfg, path, errors)
|
|
290
|
+
return if cfg["path"].is_a?(String) && !cfg["path"].empty?
|
|
291
|
+
|
|
292
|
+
errors.add(path, "\"store_test_results:\" needs a \"path:\" to the JUnit XML results directory")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def validate_add_ssh_keys(cfg, path, errors)
|
|
296
|
+
return unless cfg.key?("fingerprints") && !cfg["fingerprints"].is_a?(Array)
|
|
297
|
+
|
|
298
|
+
errors.add(path + ["fingerprints"], "\"fingerprints:\" must be a list of SSH key fingerprints")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# --- workflows ------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
def workflow(name, wf, errors, known_jobs)
|
|
304
|
+
path = ["workflows", name]
|
|
305
|
+
unless wf.is_a?(Hash) && wf["jobs"].is_a?(Array)
|
|
306
|
+
errors.add(path, "workflow #{name.inspect} must define a \"jobs:\" list")
|
|
307
|
+
return
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
wf["jobs"].each_with_index do |entry, i|
|
|
311
|
+
job_name, cfg = CompilerEntry.parse(entry)
|
|
312
|
+
entry_path = path + ["jobs", i]
|
|
313
|
+
|
|
314
|
+
if job_name.nil?
|
|
315
|
+
errors.add(entry_path, "workflow job entry must be a job name or a mapping of job name to options " \
|
|
316
|
+
"(requires/filters/matrix/context)")
|
|
317
|
+
next
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
unless known_jobs.include?(job_name)
|
|
321
|
+
errors.add(entry_path, "workflow references unknown job #{job_name.inspect}; " \
|
|
322
|
+
"defined jobs: #{known_jobs.sort.join(', ')}")
|
|
323
|
+
next
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
unknown = cfg.keys.map(&:to_s) - WORKFLOW_JOB_KEYS
|
|
327
|
+
unknown.each do |key|
|
|
328
|
+
errors.add(entry_path + [job_name], "unknown workflow job option #{key.inspect}; " \
|
|
329
|
+
"supported: #{WORKFLOW_JOB_KEYS.join(', ')}")
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
if cfg.key?("requires")
|
|
333
|
+
unless cfg["requires"].is_a?(Array) && cfg["requires"].all? { |r| r.is_a?(String) }
|
|
334
|
+
errors.add(entry_path + [job_name, "requires"], "\"requires:\" must be a list of job names")
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
validate_filters(cfg["filters"], entry_path + [job_name, "filters"], errors) if cfg.key?("filters")
|
|
339
|
+
validate_matrix(cfg["matrix"], entry_path + [job_name, "matrix"], errors) if cfg.key?("matrix")
|
|
340
|
+
|
|
341
|
+
next unless cfg.key?("type")
|
|
342
|
+
|
|
343
|
+
errors.add(entry_path + [job_name], "\"type: approval\" jobs are not supported in v1; " \
|
|
344
|
+
"every workflow job runs its steps")
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def validate_filters(filters, path, errors)
|
|
349
|
+
unless filters.is_a?(Hash)
|
|
350
|
+
errors.add(path, "\"filters:\" must be a mapping")
|
|
351
|
+
return
|
|
352
|
+
end
|
|
353
|
+
branches = filters["branches"]
|
|
354
|
+
return if branches.nil?
|
|
355
|
+
|
|
356
|
+
unless branches.is_a?(Hash)
|
|
357
|
+
errors.add(path + ["branches"], "\"branches:\" must be a mapping with \"only\" and/or \"ignore\" lists")
|
|
358
|
+
return
|
|
359
|
+
end
|
|
360
|
+
%w[only ignore].each do |key|
|
|
361
|
+
next if branches[key].nil?
|
|
362
|
+
next if branches[key].is_a?(Array) && branches[key].all? { |b| b.is_a?(String) }
|
|
363
|
+
|
|
364
|
+
errors.add(path + ["branches", key], "\"#{key}:\" must be a list of branch names " \
|
|
365
|
+
"(plain names or /regex/ patterns)")
|
|
366
|
+
end
|
|
367
|
+
return unless filters.keys.map(&:to_s) != ["branches"]
|
|
368
|
+
|
|
369
|
+
# tags filters etc. are parsed but not honored in v1 — handled by the compiler as a warning.
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def validate_matrix(matrix, path, errors)
|
|
373
|
+
unless matrix.is_a?(Hash)
|
|
374
|
+
errors.add(path, "\"matrix:\" must be a mapping with \"parameters:\"")
|
|
375
|
+
return
|
|
376
|
+
end
|
|
377
|
+
params = matrix["parameters"]
|
|
378
|
+
if !params.is_a?(Hash) || params.empty? || !params.values.all? { |v| v.is_a?(Array) && !v.empty? }
|
|
379
|
+
errors.add(path + ["parameters"], "\"matrix.parameters:\" must map names to non-empty value lists")
|
|
380
|
+
end
|
|
381
|
+
return if !matrix.key?("alias") || matrix["alias"].is_a?(String)
|
|
382
|
+
|
|
383
|
+
errors.add(path + ["alias"], "\"matrix.alias:\" must be a string")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Parses a workflow "jobs:" entry: either a bare job name String, or a
|
|
387
|
+
# single-key mapping of job name => options hash. Returns [name, options].
|
|
388
|
+
module CompilerEntry
|
|
389
|
+
module_function
|
|
390
|
+
|
|
391
|
+
def parse(entry)
|
|
392
|
+
return [entry, {}] if entry.is_a?(String)
|
|
393
|
+
return [nil, {}] unless entry.is_a?(Hash) && entry.size == 1
|
|
394
|
+
|
|
395
|
+
name, cfg = entry.first
|
|
396
|
+
cfg = {} if cfg.nil?
|
|
397
|
+
return [nil, {}] unless cfg.is_a?(Hash)
|
|
398
|
+
|
|
399
|
+
[name.to_s, cfg]
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
data/lib/forge_ci.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "psych"
|
|
4
|
+
|
|
5
|
+
require_relative "forge_ci/errors"
|
|
6
|
+
require_relative "forge_ci/interpolator"
|
|
7
|
+
require_relative "forge_ci/dag"
|
|
8
|
+
require_relative "forge_ci/validator"
|
|
9
|
+
require_relative "forge_ci/ir"
|
|
10
|
+
require_relative "forge_ci/compiler"
|
|
11
|
+
|
|
12
|
+
# ForgeCI — an open-source, CircleCI-compatible CI toolkit.
|
|
13
|
+
#
|
|
14
|
+
# This gem (component 1 of the ForgeCI project) is the config compiler:
|
|
15
|
+
# it turns a `.circleci/config.yml`-style YAML document into a normalized,
|
|
16
|
+
# JSON-serializable intermediate representation (IR) that a scheduler and
|
|
17
|
+
# runner can execute. Pure Ruby, stdlib only, no network, no processes.
|
|
18
|
+
module ForgeCI
|
|
19
|
+
VERSION = "0.1.0"
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: forge-ci
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Azmi Muwahid
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Compiles CircleCI v2.1-style pipeline YAML into a normalized, JSON-serializable
|
|
13
|
+
intermediate representation. Pure Ruby, stdlib only. Component 1 of the ForgeCI
|
|
14
|
+
open-source CI project.
|
|
15
|
+
email:
|
|
16
|
+
- azmimuwahid@gmail.com
|
|
17
|
+
executables:
|
|
18
|
+
- forge-ci
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- MIT-LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- exe/forge-ci
|
|
25
|
+
- lib/forge_ci.rb
|
|
26
|
+
- lib/forge_ci/cli.rb
|
|
27
|
+
- lib/forge_ci/compiler.rb
|
|
28
|
+
- lib/forge_ci/dag.rb
|
|
29
|
+
- lib/forge_ci/errors.rb
|
|
30
|
+
- lib/forge_ci/interpolator.rb
|
|
31
|
+
- lib/forge_ci/ir.rb
|
|
32
|
+
- lib/forge_ci/validator.rb
|
|
33
|
+
homepage: https://github.com/azmi2409/forge-ci
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata: {}
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '3.0'
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 4.0.4
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: 'Open-source CircleCI-compatible CI: config compiler (v1)'
|
|
54
|
+
test_files: []
|