toys-ci 0.0.0 → 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 +4 -4
- data/.yardopts +10 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +21 -0
- data/README.md +91 -7
- data/lib/toys/ci/mixin.rb +344 -0
- data/lib/toys/ci/template.rb +532 -0
- data/lib/toys/ci/version.rb +11 -0
- data/lib/toys-ci.rb +34 -7
- data/toys/.toys.rb +3 -0
- metadata +40 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33be1026b282eba5ffd250b26046e6de597db1ca2330ffb076c38e906a204bd0
|
|
4
|
+
data.tar.gz: 0d3ef0fc6fc1b0a348ee9754f2ed62a3b7880f7785c539d7411e838961a60702
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9cde4a096e1d9119ad8995f63cf66073a5ac53ff5f7974ace7c52d979f3177235e78515f6040f541ef29583977c2ab1bcbdb79562938bcf43ad9a966545c108
|
|
7
|
+
data.tar.gz: dbbb802c658ca28eba170292ded6f02178293373bcb8ca7ebaad4aa0673d32948dd065493e0819adf363968c4886cae9e02976a6925d673c4332eb47d99fa6ac
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Daniel Azuma and the Toys contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,9 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Toys-CI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
reserve the gem "toys-ci".
|
|
5
|
-
The actual gem is planned for release in the near future.
|
|
3
|
+
Toys-CI is a framework for generating CI Toys tools.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
Continuous Integration (CI) in a project typically involves running a variety
|
|
8
|
+
of jobs, such as dependency installation, code linting, unit testing, build
|
|
9
|
+
verification, and so forth. A large code base, or a monorepo that includes a
|
|
10
|
+
number of smaller components, may require a large number of these jobs. Often
|
|
11
|
+
it is desirable to have a "coordinator" job that decides which CI jobs to run
|
|
12
|
+
in what order, and collects and summarizes their results.
|
|
13
|
+
|
|
14
|
+
Toys-CI is a framework for generating simple CI coordinator tools. It provides
|
|
15
|
+
a declarative interface for configuring the tool and specifying individual jobs
|
|
16
|
+
to run. The generated tool runs jobs specified using command line arguments,
|
|
17
|
+
and produces a final report of the results.
|
|
18
|
+
|
|
19
|
+
### Basic example
|
|
20
|
+
|
|
21
|
+
Given the following `.toys.rb`:
|
|
22
|
+
|
|
23
|
+
# Create a "test" tool that runs minitest-based tests
|
|
24
|
+
expand :minitest, bundler: true
|
|
25
|
+
|
|
26
|
+
# Create a "rubocop" tool that checks the code base
|
|
27
|
+
expand :rubocop, bundler: true
|
|
28
|
+
|
|
29
|
+
# Create a "ci" tool that runs the above tools and
|
|
30
|
+
# summarizes their results
|
|
31
|
+
tool "ci" do
|
|
32
|
+
load_gem "toys-ci"
|
|
33
|
+
|
|
34
|
+
expand(Toys::CI::Template) do |ci|
|
|
35
|
+
ci.only_flag = true
|
|
36
|
+
ci.tool_job("Run Rubocop", ["rubocop"], flag: :rubocop)
|
|
37
|
+
ci.tool_job("Run tests", ["test"], flag: :tests)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Now you can:
|
|
42
|
+
|
|
43
|
+
$ toys ci # Runs both jobs
|
|
44
|
+
$ toys ci --only --tests # Runs only the tests
|
|
45
|
+
|
|
46
|
+
### Key features
|
|
47
|
+
|
|
48
|
+
* Two interfaces: a high level Template interface that generates an entire tool
|
|
49
|
+
for you including configuration flags, and a low-level Mixin interface that
|
|
50
|
+
provides convenient methods that you can call to write your own CI tool.
|
|
51
|
+
|
|
52
|
+
* The high-level interface generates flags that allow selection of individual
|
|
53
|
+
jobs and groups of jobs to execute.
|
|
54
|
+
|
|
55
|
+
* Optionally analyzes diffs and skips jobs that do not need executing because
|
|
56
|
+
no relevant changes have occurred.
|
|
57
|
+
|
|
58
|
+
* Customize your tool, including implementing additional flags and
|
|
59
|
+
functionality, using the power of the Toys framework.
|
|
60
|
+
|
|
61
|
+
### System requirements
|
|
62
|
+
|
|
63
|
+
Toys-CI requires Ruby 2.7 or later, and Toys 0.20 or later. We recommend the
|
|
64
|
+
latest version of Ruby, JRuby, or TruffleRuby. The Ruby provided by the
|
|
65
|
+
standard `setup-ruby` GitHub Action is sufficient.
|
|
66
|
+
|
|
67
|
+
### Learning more
|
|
68
|
+
|
|
69
|
+
For more information on the underlying Toys framework, see the
|
|
70
|
+
[Toys README](https://dazuma.github.io/toys/gems/toys/latest) and the
|
|
71
|
+
[Toys User Guide](https://dazuma.github.io/toys/gems/toys/latest/file.guide.html).
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Copyright 2026 Daniel Azuma and the Toys contributors
|
|
76
|
+
|
|
77
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
78
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
79
|
+
in the Software without restriction, including without limitation the rights
|
|
80
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
81
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
82
|
+
furnished to do so, subject to the following conditions:
|
|
83
|
+
|
|
84
|
+
The above copyright notice and this permission notice shall be included in
|
|
85
|
+
all copies or substantial portions of the Software.
|
|
86
|
+
|
|
87
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
88
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
89
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
90
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
91
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
93
|
+
IN THE SOFTWARE.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toys-core"
|
|
4
|
+
|
|
5
|
+
module Toys
|
|
6
|
+
module CI
|
|
7
|
+
##
|
|
8
|
+
# A mixin that provides methods useful for implementing CI tools.
|
|
9
|
+
#
|
|
10
|
+
# This mixin is a lower-level mechanism that depends on you to write your
|
|
11
|
+
# own run method and define any needed flags. For a more batteries-included
|
|
12
|
+
# experience, consider {Toys::CI::Template}, which does that work for you.
|
|
13
|
+
#
|
|
14
|
+
# To implement a CI tool using this mixin, you will:
|
|
15
|
+
#
|
|
16
|
+
# * Include this mixin
|
|
17
|
+
# * Call {toys_ci_init} to initialize the tool
|
|
18
|
+
# * Make calls to various `toys_ci_*_job` methods to run CI jobs
|
|
19
|
+
# * Call {toys_ci_report_results} to report final results
|
|
20
|
+
#
|
|
21
|
+
# This mixin adds various public and private methods, and several instance
|
|
22
|
+
# variables to the tool. All added method and instance variable names begin
|
|
23
|
+
# with `toys_ci_`, so avoid that prefix for any other methods and variables
|
|
24
|
+
# you are using in your tool.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
#
|
|
28
|
+
# # Define the "test" tool
|
|
29
|
+
# expand :minitest, bundler: true
|
|
30
|
+
#
|
|
31
|
+
# # Define the "rubocop" tool
|
|
32
|
+
# expand :rubocop, bundler: true
|
|
33
|
+
#
|
|
34
|
+
# # Define a "ci" tool that runs both the above, controlled by
|
|
35
|
+
# # flags "--tests", "--rubocop", and/or "--all".
|
|
36
|
+
# tool "ci" do
|
|
37
|
+
# # Activate the toys-ci gem and pull in the mixin
|
|
38
|
+
# load_gem "toys-ci"
|
|
39
|
+
# include Toys::CI::Mixin
|
|
40
|
+
#
|
|
41
|
+
# flag :tests, desc: "Run tests"
|
|
42
|
+
# flag :rubocop, desc: "Run rubocop"
|
|
43
|
+
# flag :all, desc: "Run all CI tasks"
|
|
44
|
+
#
|
|
45
|
+
# def run
|
|
46
|
+
# toys_ci_init
|
|
47
|
+
# toys_ci_tool_job("Tests", ["test"]) if tests || all
|
|
48
|
+
# toys_ci_tool_job("Rubocop", ["rubocop"]) if rubocop || all
|
|
49
|
+
# toys_ci_report_results
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
module Mixin
|
|
54
|
+
include ::Toys::Mixin
|
|
55
|
+
|
|
56
|
+
on_include do
|
|
57
|
+
include :exec unless include?(:exec)
|
|
58
|
+
include :terminal unless include?(:terminal)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# Initialize the CI tool. This must be called first before any other
|
|
63
|
+
# `toys_ci_` methods.
|
|
64
|
+
#
|
|
65
|
+
# @param fail_fast [boolean] If true, CI will terminate once any job ends
|
|
66
|
+
# in failure. Otherwise, all jobs will be run.
|
|
67
|
+
# @param limit_by_changes_since [String,nil] If set to a git ref, finds
|
|
68
|
+
# all the changed files since that ref, and skips CI jobs that
|
|
69
|
+
# declare trigger paths that do not match. Optional. By default, no
|
|
70
|
+
# jobs are skipped for this reason.
|
|
71
|
+
#
|
|
72
|
+
# @return [self]
|
|
73
|
+
#
|
|
74
|
+
def toys_ci_init(fail_fast: false, limit_by_changes_since: nil)
|
|
75
|
+
@toys_ci_fail_fast = fail_fast
|
|
76
|
+
@toys_ci_changed_paths = toys_ci_find_changes_since(limit_by_changes_since)
|
|
77
|
+
@toys_ci_successful_jobs = []
|
|
78
|
+
@toys_ci_failed_jobs = []
|
|
79
|
+
@toys_ci_skipped_jobs = []
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Look for environment variables set by a GitHub workflow, and attempt to
|
|
85
|
+
# extract a suitable change base. Returns a git SHA, or nil if one could
|
|
86
|
+
# not be obtained from the current environment.
|
|
87
|
+
#
|
|
88
|
+
# This may read the `GITHUB_EVENT_NAME` and `GITHUB_EVENT_PATH`
|
|
89
|
+
# environment variables, and may also read the event payload file if
|
|
90
|
+
# found. However, the exact logic is not specified.
|
|
91
|
+
#
|
|
92
|
+
# The result can be passed to the `:limit_by_changes_since` argument of
|
|
93
|
+
# {#toys_ci_init}.
|
|
94
|
+
#
|
|
95
|
+
# @return [String] The git SHA for the change base
|
|
96
|
+
# @return [nil] if no change base can be determined
|
|
97
|
+
#
|
|
98
|
+
def toys_ci_github_event_base_sha
|
|
99
|
+
event_path = ::ENV["GITHUB_EVENT_PATH"].to_s
|
|
100
|
+
if event_path.empty?
|
|
101
|
+
logger.info("GITHUB_EVENT_PATH is empty or unset; cannot determine event payload file")
|
|
102
|
+
return nil
|
|
103
|
+
end
|
|
104
|
+
event_payload = toys_ci_read_event_payload_file(event_path)
|
|
105
|
+
return nil unless event_payload
|
|
106
|
+
|
|
107
|
+
event_name = ::ENV["GITHUB_EVENT_NAME"]
|
|
108
|
+
case event_name
|
|
109
|
+
when "push"
|
|
110
|
+
logger.info("Getting change base from push event")
|
|
111
|
+
event_payload["before"]
|
|
112
|
+
when "pull_request"
|
|
113
|
+
logger.info("Getting change base from pull_request event")
|
|
114
|
+
event_payload.dig("pull_request", "base", "sha")
|
|
115
|
+
else
|
|
116
|
+
logger.info("Did not find a change base from event #{event_name.inspect}")
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Run a CI job implemented by a tool, and record the results.
|
|
123
|
+
#
|
|
124
|
+
# @param name [String] A user-visible name for the job. Required.
|
|
125
|
+
# @param tool [Array<String>] The Toys tool to run. Required.
|
|
126
|
+
# @param trigger_paths [Array<String>,String,nil] An array of file or
|
|
127
|
+
# directory paths, relative to the repo root, that must have changes
|
|
128
|
+
# in order to trigger the job. If not specified, the job is always
|
|
129
|
+
# triggered.
|
|
130
|
+
# @param env [Hash{String=>String}] Environment variables to set during
|
|
131
|
+
# the run. Optional.
|
|
132
|
+
# @param chdir [String] The working directory for the run. Optional.
|
|
133
|
+
#
|
|
134
|
+
# @return [:success] If the job succeeded
|
|
135
|
+
# @return [:failure] If the job failed
|
|
136
|
+
# @return [:skipped] If the job was skipped because it did not match the
|
|
137
|
+
# trigger paths
|
|
138
|
+
#
|
|
139
|
+
def toys_ci_tool_job(name, tool, trigger_paths: nil, env: nil, chdir: nil)
|
|
140
|
+
toys_ci_job(name, trigger_paths: trigger_paths) do
|
|
141
|
+
opts = {name: name}
|
|
142
|
+
opts[:env] = env if env
|
|
143
|
+
opts[:chdir] = chdir if chdir
|
|
144
|
+
exec_separate_tool(tool, **opts).success?
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
##
|
|
149
|
+
# Run a CI job implemented by an external process, and record the results.
|
|
150
|
+
#
|
|
151
|
+
# @param name [String] A user-visible name for the job. Required.
|
|
152
|
+
# @param cmd [Array<String>] The command to run. Required.
|
|
153
|
+
# @param trigger_paths [Array<String>,String,nil] An array of file or
|
|
154
|
+
# directory paths, relative to the repo root, that must have changes
|
|
155
|
+
# in order to trigger the job. If not specified, the job is always
|
|
156
|
+
# triggered.
|
|
157
|
+
# @param env [Hash{String=>String}] Environment variables to set during
|
|
158
|
+
# the run. Optional.
|
|
159
|
+
# @param chdir [String] The working directory for the run. Optional.
|
|
160
|
+
#
|
|
161
|
+
# @return [:success] If the job succeeded
|
|
162
|
+
# @return [:failure] If the job failed
|
|
163
|
+
# @return [:skipped] If the job was skipped because it did not match the
|
|
164
|
+
# trigger paths
|
|
165
|
+
#
|
|
166
|
+
def toys_ci_cmd_job(name, cmd, trigger_paths: nil, env: nil, chdir: nil)
|
|
167
|
+
toys_ci_job(name, trigger_paths: trigger_paths) do
|
|
168
|
+
opts = {name: name}
|
|
169
|
+
opts[:env] = env if env
|
|
170
|
+
opts[:chdir] = chdir if chdir
|
|
171
|
+
exec(cmd, **opts).success?
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
##
|
|
176
|
+
# Run a CI job implemented by a block, and record the results.
|
|
177
|
+
#
|
|
178
|
+
# @param name [String] A user-visible name for the job. Required.
|
|
179
|
+
# @param trigger_paths [Array<String>,String,nil] An array of file or
|
|
180
|
+
# directory paths, relative to the repo root, that must have changes
|
|
181
|
+
# in order to trigger the job. If not specified, the job is always
|
|
182
|
+
# triggered.
|
|
183
|
+
# @param block [Proc] The block to run. It will be run with `self` set to
|
|
184
|
+
# the tool context, and should return true or false indicating
|
|
185
|
+
# success or failure.
|
|
186
|
+
#
|
|
187
|
+
# @return [:success] If the job succeeded
|
|
188
|
+
# @return [:failure] If the job failed
|
|
189
|
+
# @return [:skipped] If the job was skipped because it did not match the
|
|
190
|
+
# trigger paths
|
|
191
|
+
#
|
|
192
|
+
def toys_ci_job(name, trigger_paths: nil, &block)
|
|
193
|
+
unless defined?(@toys_ci_successful_jobs)
|
|
194
|
+
raise ::Toys::ToolDefinitionError, "You must call toys_ci_init before running a job"
|
|
195
|
+
end
|
|
196
|
+
return :skipped unless toys_ci_check_trigger_paths(trigger_paths, name)
|
|
197
|
+
puts("**** RUNNING: #{name}", :cyan, :bold)
|
|
198
|
+
result =
|
|
199
|
+
begin
|
|
200
|
+
instance_exec(&block)
|
|
201
|
+
rescue ::StandardError => e
|
|
202
|
+
trace = e.backtrace
|
|
203
|
+
write("#{trace.first}: ")
|
|
204
|
+
puts("#{e.message} (#{e.class})", :bold)
|
|
205
|
+
Array(trace[1..]).each { |line| puts " from #{line}" }
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
toys_ci_job_result(name, result)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
# Print out a final report of the results, including a summary of the
|
|
213
|
+
# failed jobs. By default, this will also exit and never return. You can
|
|
214
|
+
# instead get the exit value by passing `exit: false`.
|
|
215
|
+
#
|
|
216
|
+
# @param exit [boolean] Whether to exit. Default is true.
|
|
217
|
+
#
|
|
218
|
+
# @return [Integer] The exit value
|
|
219
|
+
#
|
|
220
|
+
def toys_ci_report_results(exit: true) # rubocop:disable Metrics/MethodLength
|
|
221
|
+
unless defined?(@toys_ci_successful_jobs)
|
|
222
|
+
raise ::Toys::ToolDefinitionError, "You must call toys_ci_init before reporting job results"
|
|
223
|
+
end
|
|
224
|
+
success_count = @toys_ci_successful_jobs.size
|
|
225
|
+
failure_count = @toys_ci_failed_jobs.size
|
|
226
|
+
skipped_count = @toys_ci_skipped_jobs.size
|
|
227
|
+
total_job_count = success_count + failure_count + skipped_count
|
|
228
|
+
result =
|
|
229
|
+
if total_job_count.zero?
|
|
230
|
+
puts("**** CI: NO JOBS REQUESTED", :red, :bold)
|
|
231
|
+
puts("Try passing --help to see how to activate CI jobs.")
|
|
232
|
+
2
|
|
233
|
+
elsif failure_count.positive?
|
|
234
|
+
puts("**** CI: SKIPPED #{skipped_count} OF #{total_job_count} JOBS", :bold) unless skipped_count.zero?
|
|
235
|
+
puts("**** CI: FAILED #{failure_count} OF #{success_count + failure_count} RUNNABLE JOBS:", :red, :bold)
|
|
236
|
+
@toys_ci_failed_jobs.each { |name| puts(name, :red) }
|
|
237
|
+
1
|
|
238
|
+
elsif success_count.positive?
|
|
239
|
+
puts("**** CI: SKIPPED #{skipped_count} OF #{total_job_count} JOBS", :bold) unless skipped_count.zero?
|
|
240
|
+
puts("**** CI: ALL #{success_count} RUNNABLE JOBS SUCCEEDED", :green, :bold)
|
|
241
|
+
0
|
|
242
|
+
else
|
|
243
|
+
puts("**** CI: ALL #{skipped_count} JOBS SKIPPED", :yellow, :bold)
|
|
244
|
+
0
|
|
245
|
+
end
|
|
246
|
+
self.exit(result) if exit
|
|
247
|
+
result
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
##
|
|
251
|
+
# @return [Array<String>] The names of the failed jobs so far
|
|
252
|
+
#
|
|
253
|
+
attr_reader :toys_ci_failed_jobs
|
|
254
|
+
|
|
255
|
+
##
|
|
256
|
+
# @return [Array<String>] The names of the successful jobs so far
|
|
257
|
+
#
|
|
258
|
+
attr_reader :toys_ci_successful_jobs
|
|
259
|
+
|
|
260
|
+
##
|
|
261
|
+
# @return [Array<String>] The names of the skipped jobs so far
|
|
262
|
+
#
|
|
263
|
+
attr_reader :toys_ci_skipped_jobs
|
|
264
|
+
|
|
265
|
+
##
|
|
266
|
+
# @private
|
|
267
|
+
# Read a GitHub event payload file
|
|
268
|
+
#
|
|
269
|
+
def toys_ci_read_event_payload_file(path)
|
|
270
|
+
require "json"
|
|
271
|
+
::JSON.parse(::File.read(path))
|
|
272
|
+
rescue ::SystemCallError, ::JSON::ParserError => e
|
|
273
|
+
logger.error("Failed to read GitHub event payload from #{path.inspect}:")
|
|
274
|
+
logger.error(e.inspect)
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
##
|
|
279
|
+
# @private
|
|
280
|
+
# Find all the changed files since the given ref.
|
|
281
|
+
#
|
|
282
|
+
def toys_ci_find_changes_since(ref)
|
|
283
|
+
return nil unless ref
|
|
284
|
+
result = exec(["git", "rev-parse", ref], out: :capture)
|
|
285
|
+
unless result.success?
|
|
286
|
+
logger.error("Unable to find git ref #{ref.inspect}")
|
|
287
|
+
exit(1)
|
|
288
|
+
end
|
|
289
|
+
sha = result.captured_out.strip
|
|
290
|
+
logger.info("Filtering by changes since SHA: #{sha}")
|
|
291
|
+
result = exec(["git", "diff", "--name-only", sha], out: :capture)
|
|
292
|
+
unless result.success?
|
|
293
|
+
logger.error("Unable to get diff since SHA #{sha}")
|
|
294
|
+
exit(1)
|
|
295
|
+
end
|
|
296
|
+
result.captured_out.split("\n")
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
##
|
|
300
|
+
# @private
|
|
301
|
+
# Go through the given trigger paths and see if any match the changes
|
|
302
|
+
#
|
|
303
|
+
# @return [boolean] True to run the job, or false to skip
|
|
304
|
+
#
|
|
305
|
+
def toys_ci_check_trigger_paths(trigger_paths, name)
|
|
306
|
+
return true if !@toys_ci_changed_paths || !trigger_paths
|
|
307
|
+
Array(trigger_paths).each do |trigger_path|
|
|
308
|
+
trigger_dir = trigger_path.end_with?("/") ? trigger_path : "#{trigger_path}/"
|
|
309
|
+
return true if @toys_ci_changed_paths.any? do |changed_path|
|
|
310
|
+
changed_path == trigger_path || changed_path.start_with?(trigger_dir)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
@toys_ci_skipped_jobs << name
|
|
314
|
+
puts("**** SKIPPING BECAUSE NO CHANGES FOUND: #{name}", :cyan, :bold)
|
|
315
|
+
false
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
##
|
|
319
|
+
# @private
|
|
320
|
+
# Report the result of a single job
|
|
321
|
+
#
|
|
322
|
+
# @param name [String] The name of the job
|
|
323
|
+
# @param result [boolean] The result of the job
|
|
324
|
+
# @return [:success] If the result was success
|
|
325
|
+
# @return [:failure] If the result was failure
|
|
326
|
+
#
|
|
327
|
+
def toys_ci_job_result(name, result)
|
|
328
|
+
if result
|
|
329
|
+
@toys_ci_successful_jobs << name
|
|
330
|
+
puts("**** SUCCEEDED: #{name}", :green, :bold)
|
|
331
|
+
:success
|
|
332
|
+
else
|
|
333
|
+
@toys_ci_failed_jobs << name
|
|
334
|
+
puts("**** FAILED: #{name}", :red, :bold)
|
|
335
|
+
if @toys_ci_fail_fast
|
|
336
|
+
puts("TERMINATING CI", :red, :bold)
|
|
337
|
+
exit(1)
|
|
338
|
+
end
|
|
339
|
+
:failure
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toys-core"
|
|
4
|
+
|
|
5
|
+
module Toys
|
|
6
|
+
module CI
|
|
7
|
+
##
|
|
8
|
+
# A template that can be used to implement a CI tool.
|
|
9
|
+
#
|
|
10
|
+
# This template generates flags and implementation methods in the current
|
|
11
|
+
# tool to implement CI. In particular, it generates the `run` method
|
|
12
|
+
# itself. If you need more control over the CI tool's implementation,
|
|
13
|
+
# consider using {Toys::CI::Mixin} which provides a lower-level interface.
|
|
14
|
+
#
|
|
15
|
+
# To implement a CI tool using this template, simply expand the template
|
|
16
|
+
# and provide the necessary configuration, including specifying at least
|
|
17
|
+
# one CI task to run. The generated tool will use {Toys::CI::Mixin} under
|
|
18
|
+
# the hood.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
#
|
|
22
|
+
# # Define the "test" tool
|
|
23
|
+
# expand :minitest, bundler: true
|
|
24
|
+
#
|
|
25
|
+
# # Define the "rubocop" tool
|
|
26
|
+
# expand :rubocop, bundler: true
|
|
27
|
+
#
|
|
28
|
+
# # Define a "ci" tool that runs both the above, controlled by
|
|
29
|
+
# # flags "--tests", "--rubocop", and/or "--all".
|
|
30
|
+
# tool "ci" do
|
|
31
|
+
# # Activate the toys-ci gem and expand the template
|
|
32
|
+
# load_gem "toys-ci"
|
|
33
|
+
# expand Toys::CI::Template do |ci|
|
|
34
|
+
# ci.all_flag = true
|
|
35
|
+
# ci.tool_job("Tests", ["test"], flag: :tests)
|
|
36
|
+
# ci.tool_job("Rubocop", ["rubocop"], flag: :rubocop)
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
class Template
|
|
41
|
+
include ::Toys::Template
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Add a job implemented by a tool call.
|
|
45
|
+
#
|
|
46
|
+
# @param name [String] A user-visible name for the job. Required.
|
|
47
|
+
# @param tool [Array<String>] The Toys tool to run. Required.
|
|
48
|
+
# @param flag [Symbol] A flag key to control whether the job will run.
|
|
49
|
+
# If provided, it will be used to generate two flags that can be
|
|
50
|
+
# passed to the CI job. For example, passing `:unit_tests` will
|
|
51
|
+
# generate the flags `--unit-tests` and `--no-unit-tests` and set the
|
|
52
|
+
# context value `:unit_tests`. If not provided, no flag is generated
|
|
53
|
+
# and runnability will be determined by the "all" or "only" flag.
|
|
54
|
+
# @param override_flags [String,Array<String>,nil] Generated flags to use
|
|
55
|
+
# instead of the ones implied by the `:flag` argument. These must be
|
|
56
|
+
# specified without the leading `--`, and will automatically have
|
|
57
|
+
# `--[no-]` prepended. Optional. If not specified or nil, the flags
|
|
58
|
+
# fall back to those implied by the `:flag` argument.
|
|
59
|
+
# @param trigger_paths [Array<String>,String,nil] An array of file or
|
|
60
|
+
# directory paths, relative to the repo root, that must have changes
|
|
61
|
+
# in order to trigger the job. If not specified, the job is always
|
|
62
|
+
# triggered.
|
|
63
|
+
# @param env [Hash{String=>String}] Environment variables to set during
|
|
64
|
+
# the run. Optional.
|
|
65
|
+
# @param chdir [String] The working directory for the run. Optional.
|
|
66
|
+
#
|
|
67
|
+
def tool_job(name, tool, flag: nil, override_flags: nil, trigger_paths: nil, env: nil, chdir: nil)
|
|
68
|
+
if override_flags && !flag
|
|
69
|
+
raise ::Toys::ToolDefinitionError, "override_flags is meaningless without a flag"
|
|
70
|
+
end
|
|
71
|
+
@jobs << ToolJob.new(name, flag, Array(override_flags), trigger_paths, tool, env, chdir)
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Add a job implemented by an external process.
|
|
77
|
+
#
|
|
78
|
+
# @param name [String] A user-visible name for the job. Required.
|
|
79
|
+
# @param cmd [Array<String>] The command to run. Required.
|
|
80
|
+
# @param flag [Symbol] A flag key to control whether the job will run.
|
|
81
|
+
# If provided, it will be used to generate two flags that can be
|
|
82
|
+
# passed to the CI job. For example, passing `:unit_tests` will
|
|
83
|
+
# generate the flags `--unit-tests` and `--no-unit-tests` and set the
|
|
84
|
+
# context value `:unit_tests`. If not provided, no flag is generated
|
|
85
|
+
# and runnability will be determined by the "all" or "only" flag.
|
|
86
|
+
# @param override_flags [String,Array<String>,nil] Generated flags to use
|
|
87
|
+
# instead of the ones implied by the `:flag` argument. These must be
|
|
88
|
+
# specified without the leading `--`, and will automatically have
|
|
89
|
+
# `--[no-]` prepended. Optional. If not specified or nil, the flags
|
|
90
|
+
# fall back to those implied by the `:flag` argument.
|
|
91
|
+
# @param trigger_paths [Array<String>,String,nil] An array of file or
|
|
92
|
+
# directory paths, relative to the repo root, that must have changes
|
|
93
|
+
# in order to trigger the job. If not specified, the job is always
|
|
94
|
+
# triggered.
|
|
95
|
+
# @param env [Hash{String=>String}] Environment variables to set during
|
|
96
|
+
# the run. Optional.
|
|
97
|
+
# @param chdir [String] The working directory for the run. Optional.
|
|
98
|
+
#
|
|
99
|
+
def cmd_job(name, cmd, flag: nil, override_flags: nil, trigger_paths: nil, env: nil, chdir: nil)
|
|
100
|
+
if override_flags && !flag
|
|
101
|
+
raise ::Toys::ToolDefinitionError, "override_flags is meaningless without a flag"
|
|
102
|
+
end
|
|
103
|
+
@jobs << CmdJob.new(name, flag, Array(override_flags), trigger_paths, cmd, env, chdir)
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# Add a job implemented by a block.
|
|
109
|
+
#
|
|
110
|
+
# The block should perform a CI job and return a boolean indicating
|
|
111
|
+
# whether or not the job succeeded. It will execute in the tool execution
|
|
112
|
+
# context, with `self` set to the `Toys::Context`.
|
|
113
|
+
#
|
|
114
|
+
# @param name [String] A user-visible name for the job. Required.
|
|
115
|
+
# @param block [Proc] A block that runs this job. Required.
|
|
116
|
+
# @param flag [Symbol,nil] A flag key to control whether the job will run.
|
|
117
|
+
# If provided, it will be used to generate two flags that can be
|
|
118
|
+
# passed to the CI job. For example, passing `:unit_tests` will
|
|
119
|
+
# generate the flags `--unit-tests` and `--no-unit-tests` and set the
|
|
120
|
+
# context value `:unit_tests`. If not provided, no flag is generated
|
|
121
|
+
# and runnability will be determined by the "all" or "only" flag.
|
|
122
|
+
# @param override_flags [String,Array<String>,nil] Generated flags to use
|
|
123
|
+
# instead of the ones implied by the `:flag` argument. These must be
|
|
124
|
+
# specified without the leading `--`, and will automatically have
|
|
125
|
+
# `--[no-]` prepended. Optional. If not specified or nil, the flags
|
|
126
|
+
# fall back to those implied by the `:flag` argument.
|
|
127
|
+
# @param trigger_paths [Array<String>,String,nil] An array of file or
|
|
128
|
+
# directory paths, relative to the repo root, that must have changes
|
|
129
|
+
# in order to trigger the job. If not specified, the job is always
|
|
130
|
+
# triggered.
|
|
131
|
+
#
|
|
132
|
+
def job(name, flag: nil, override_flags: nil, trigger_paths: nil, &block)
|
|
133
|
+
if override_flags && !flag
|
|
134
|
+
raise ::Toys::ToolDefinitionError, "override_flags is meaningless without a flag"
|
|
135
|
+
end
|
|
136
|
+
@jobs << BlockJob.new(name, flag, Array(override_flags), trigger_paths, block)
|
|
137
|
+
self
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
##
|
|
141
|
+
# Define a collection of jobs that can be enabled/disabled as a group.
|
|
142
|
+
#
|
|
143
|
+
# @param name [String] A user-visible name for the collection. Required.
|
|
144
|
+
# @param flag [Symbol] A flag key to control the collection. Required.
|
|
145
|
+
# Used to define two flags that can be passed to the CI job. For
|
|
146
|
+
# example, passing `:unit_tests` will generate the flags
|
|
147
|
+
# `--unit-tests` and `--no-unit-tests` and set the context value
|
|
148
|
+
# `:unit_tests`.
|
|
149
|
+
# @param override_flags [String,Array<String>,nil] Generated flags to use
|
|
150
|
+
# instead of the ones implied by the `:flag` argument. These must be
|
|
151
|
+
# specified without the leading `--`, and will automatically have
|
|
152
|
+
# `--[no-]` prepended. Optional. If not specified or nil, the flags
|
|
153
|
+
# fall back to those implied by the `:flag` argument.
|
|
154
|
+
# @param job_flags [Array<Symbol>] The individual job flags that will be
|
|
155
|
+
# controlled as a group by this collection. Must be nonempty.
|
|
156
|
+
#
|
|
157
|
+
def collection(name, flag, job_flags, override_flags: nil)
|
|
158
|
+
if job_flags.empty?
|
|
159
|
+
raise ::Toys::ToolDefinitionError, "You must provide at least one entry in job_flags"
|
|
160
|
+
end
|
|
161
|
+
@collections << Collection.new(name, flag, Array(override_flags), job_flags)
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
##
|
|
166
|
+
# Provide a block that will be run at the beginning of the CI job.
|
|
167
|
+
# The block will run in the tool execution context, with `self` set to
|
|
168
|
+
# the `Toys::Context`.
|
|
169
|
+
#
|
|
170
|
+
# @param block [Proc] The block to execute.
|
|
171
|
+
#
|
|
172
|
+
def before_run(&block)
|
|
173
|
+
@prerun = block
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
##
|
|
177
|
+
# Create a flag that will enable all jobs. All jobs will otherwise be
|
|
178
|
+
# disabled by default. This setting is mutually exclusive with
|
|
179
|
+
# {#only_flag=} and {#jobs_disabled_by_default=}.
|
|
180
|
+
#
|
|
181
|
+
# The value can either be the flag key as a symbol, `true` to use the
|
|
182
|
+
# default (which is `:all`), or `false` to disable such a flag.
|
|
183
|
+
# For example, passing `true` will define the flag `--all` which will set
|
|
184
|
+
# the context key `:all`.
|
|
185
|
+
#
|
|
186
|
+
# @param value [Symbol,boolean]
|
|
187
|
+
#
|
|
188
|
+
def all_flag=(value)
|
|
189
|
+
@all_flag = value
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
##
|
|
193
|
+
# Create a flag that will disable all jobs. All jobs will otherwise be
|
|
194
|
+
# enabled by default. This setting is mutually exclusive with
|
|
195
|
+
# {#all_flag=} and {#jobs_disabled_by_default=}.
|
|
196
|
+
#
|
|
197
|
+
# The value can either be the flag key as a symbol, `true` to use the
|
|
198
|
+
# default (which is `:only`), or `false` to disable such a flag.
|
|
199
|
+
# For example, passing `true` will define the flag `--only` which will
|
|
200
|
+
# set the context key `:only`.
|
|
201
|
+
#
|
|
202
|
+
# @param value [Symbol,boolean]
|
|
203
|
+
#
|
|
204
|
+
def only_flag=(value)
|
|
205
|
+
@only_flag = value
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
##
|
|
209
|
+
# If set to true, all jobs are disabled by default unless explicitly
|
|
210
|
+
# enabled by their individual flags.
|
|
211
|
+
#
|
|
212
|
+
# This setting is mutually exclusive with {#all_flag=} and {#only_flag=}.
|
|
213
|
+
# If you set up one of those flags, the default enabling behavior is also
|
|
214
|
+
# set implicitly.
|
|
215
|
+
#
|
|
216
|
+
# @param value [boolean]
|
|
217
|
+
#
|
|
218
|
+
def jobs_disabled_by_default=(value)
|
|
219
|
+
@jobs_disabled_by_default = value
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
##
|
|
223
|
+
# Create flags that will enable and disable fail-fast. The flag should be
|
|
224
|
+
# specified by symbol, and the actual flag will be set accordingly. You
|
|
225
|
+
# can also use the value `true` which will set the default `:fail_fast`.
|
|
226
|
+
# For example, passing `true` will define the flags `--fail-fast` and
|
|
227
|
+
# `--no-fail-fast`.
|
|
228
|
+
#
|
|
229
|
+
# @param value [Symbol,boolean]
|
|
230
|
+
#
|
|
231
|
+
def fail_fast_flag=(value)
|
|
232
|
+
@fail_fast_flag = value
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
##
|
|
236
|
+
# Set the default value of fail-fast. You can also create flags that can
|
|
237
|
+
# override this value using {fail_fast_flag=}. Default is false.
|
|
238
|
+
#
|
|
239
|
+
# @param value [boolean]
|
|
240
|
+
#
|
|
241
|
+
def fail_fast_default=(value)
|
|
242
|
+
@fail_fast_default = value
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
##
|
|
246
|
+
# Create a flag that can be used to specify the base ref for the change
|
|
247
|
+
# directly. This can be used to filter CI jobs based on what has changed.
|
|
248
|
+
#
|
|
249
|
+
# A change base ref provided in this way will override any obtained from
|
|
250
|
+
# other means, such as from the GitHub environment using
|
|
251
|
+
# {#use_github_base_ref_flag=}.
|
|
252
|
+
#
|
|
253
|
+
# @param value [Symbol,boolean] If a symbol, it is used as the flag
|
|
254
|
+
# key for a flag that specifies the base ref. You can also pass
|
|
255
|
+
# `true` to use the default, `:base_ref`.
|
|
256
|
+
#
|
|
257
|
+
def base_ref_flag=(value)
|
|
258
|
+
@base_ref_flag = value
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
##
|
|
262
|
+
# Create a flag that enables obtaining the change base ref from the
|
|
263
|
+
# GitHub workflow environment. This can be used to filter CI jobs based
|
|
264
|
+
# on what has changed in a GitHub Actions workflow. The flag should be
|
|
265
|
+
# specified by symbol, and the actual flag will be set accordingly. You
|
|
266
|
+
# can also use the value `true` which will set the default
|
|
267
|
+
# `:use_github_base_ref`. For example, passing `true` will define the
|
|
268
|
+
# flags `--use-github-base-ref` and `--no-use-github-base-ref`.
|
|
269
|
+
#
|
|
270
|
+
# @param value [Symbol,boolean]
|
|
271
|
+
#
|
|
272
|
+
def use_github_base_ref_flag=(value)
|
|
273
|
+
@use_github_base_ref_flag = value
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# @private
|
|
277
|
+
def initialize
|
|
278
|
+
@jobs = []
|
|
279
|
+
@collections = []
|
|
280
|
+
@all_flag = nil
|
|
281
|
+
@only_flag = nil
|
|
282
|
+
@jobs_disabled_by_default = nil
|
|
283
|
+
@fail_fast_flag = nil
|
|
284
|
+
@fail_fast_default = false
|
|
285
|
+
@base_ref_flag = nil
|
|
286
|
+
@use_github_base_ref_flag = nil
|
|
287
|
+
@prerun = nil
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# @private
|
|
291
|
+
BlockJob = ::Struct.new(:name, :flag, :override_flags, :trigger_paths, :block)
|
|
292
|
+
|
|
293
|
+
# @private
|
|
294
|
+
ToolJob = ::Struct.new(:name, :flag, :override_flags, :trigger_paths, :tool, :env, :chdir)
|
|
295
|
+
|
|
296
|
+
# @private
|
|
297
|
+
CmdJob = ::Struct.new(:name, :flag, :override_flags, :trigger_paths, :cmd, :env, :chdir)
|
|
298
|
+
|
|
299
|
+
# @private
|
|
300
|
+
Collection = ::Struct.new(:name, :flag, :override_flags, :job_flags)
|
|
301
|
+
|
|
302
|
+
# @private
|
|
303
|
+
attr_reader :jobs
|
|
304
|
+
|
|
305
|
+
# @private
|
|
306
|
+
attr_reader :collections
|
|
307
|
+
|
|
308
|
+
# @private
|
|
309
|
+
attr_reader :prerun
|
|
310
|
+
|
|
311
|
+
# @private
|
|
312
|
+
attr_reader :jobs_disabled_by_default
|
|
313
|
+
|
|
314
|
+
# @private
|
|
315
|
+
attr_reader :fail_fast_default
|
|
316
|
+
|
|
317
|
+
# @private
|
|
318
|
+
def all_flag?
|
|
319
|
+
!@all_flag.nil? && @all_flag != false
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# @private
|
|
323
|
+
def only_flag?
|
|
324
|
+
!@only_flag.nil? && @only_flag != false
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @private
|
|
328
|
+
def fail_fast_flag?
|
|
329
|
+
!@fail_fast_flag.nil? && @fail_fast_flag != false
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @private
|
|
333
|
+
def base_ref_flag?
|
|
334
|
+
!@base_ref_flag.nil? && @base_ref_flag != false
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# @private
|
|
338
|
+
def use_github_base_ref_flag?
|
|
339
|
+
!@use_github_base_ref_flag.nil? && @use_github_base_ref_flag != false
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# @private
|
|
343
|
+
def all_flag(desired_format = :symbol)
|
|
344
|
+
format_flag(@all_flag, :all, desired_format)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# @private
|
|
348
|
+
def only_flag(desired_format = :symbol)
|
|
349
|
+
format_flag(@only_flag, :only, desired_format)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# @private
|
|
353
|
+
def fail_fast_flag(desired_format = :symbol)
|
|
354
|
+
format_flag(@fail_fast_flag, :fail_fast, desired_format)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# @private
|
|
358
|
+
def base_ref_flag(desired_format = :symbol)
|
|
359
|
+
format_flag(@base_ref_flag, :base_ref, desired_format)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# @private
|
|
363
|
+
def use_github_base_ref_flag(desired_format = :symbol)
|
|
364
|
+
format_flag(@use_github_base_ref_flag, :use_github_base_ref, desired_format)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# @private
|
|
368
|
+
def format_flag(raw_flag, default_symbol, desired_format)
|
|
369
|
+
value = raw_flag == true ? default_symbol : raw_flag
|
|
370
|
+
desired_format == :hyphenated ? value.to_s.tr("_", "-") : value
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
on_expand do |template|
|
|
374
|
+
if template.all_flag? && !template.only_flag? && template.jobs_disabled_by_default.nil?
|
|
375
|
+
template.jobs_disabled_by_default = true
|
|
376
|
+
flag(template.all_flag) do
|
|
377
|
+
flags("--#{template.all_flag(:hyphenated)}")
|
|
378
|
+
desc("Run all jobs unless explicitly disabled by their flags." \
|
|
379
|
+
" (If not set, only explicitly enabled jobs run.)")
|
|
380
|
+
end
|
|
381
|
+
elsif !template.all_flag? && template.only_flag? && template.jobs_disabled_by_default.nil?
|
|
382
|
+
template.jobs_disabled_by_default = false
|
|
383
|
+
flag(template.only_flag) do
|
|
384
|
+
flags("--#{template.only_flag(:hyphenated)}")
|
|
385
|
+
desc("Run only jobs explicitly enabled by their flags." \
|
|
386
|
+
" (If not set, all jobs run unless explicitly disabled.)")
|
|
387
|
+
end
|
|
388
|
+
elsif !template.all_flag? && !template.only_flag?
|
|
389
|
+
template.jobs_disabled_by_default ||= false
|
|
390
|
+
else
|
|
391
|
+
raise ::Toys::ToolDefinitionError, "all_flag, only_flag, and jobs_disabled_by_default are mutually exclusive"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
if template.fail_fast_flag?
|
|
395
|
+
flag(template.fail_fast_flag) do
|
|
396
|
+
flags("--[no-]#{template.fail_fast_flag(:hyphenated)}")
|
|
397
|
+
default(template.fail_fast_default)
|
|
398
|
+
desc("Terminate CI as soon as any job fails (default is #{template.fail_fast_default})")
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
if template.base_ref_flag?
|
|
403
|
+
flag(template.base_ref_flag) do
|
|
404
|
+
flags("--#{template.base_ref_flag(:hyphenated)} REF")
|
|
405
|
+
desc("Filter jobs that do not match changes since this git ref")
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
if template.use_github_base_ref_flag?
|
|
410
|
+
flag(template.use_github_base_ref_flag) do
|
|
411
|
+
flags("--[no-]#{template.use_github_base_ref_flag(:hyphenated)}")
|
|
412
|
+
desc("Look up the change base from GitHub to determine which jobs to filter")
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
flag_desc_suffix =
|
|
417
|
+
if template.all_flag?
|
|
418
|
+
"(Jobs run by default if --#{template.all_flag(:hyphenated)} is set.)"
|
|
419
|
+
elsif template.only_flag?
|
|
420
|
+
"(Jobs run by default unless --#{template.only_flag(:hyphenated)} is set.)"
|
|
421
|
+
elsif template.jobs_disabled_by_default
|
|
422
|
+
"(Jobs do not run by default.)"
|
|
423
|
+
else
|
|
424
|
+
"(All jobs run by default.)"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
flag_group(desc: "Jobs") do
|
|
428
|
+
template.jobs.each do |job|
|
|
429
|
+
next unless job.flag
|
|
430
|
+
flag(job.flag) do
|
|
431
|
+
if job.override_flags.empty?
|
|
432
|
+
hyphenated_flag = job.flag.to_s.tr("_", "-")
|
|
433
|
+
flags("--[no-]#{hyphenated_flag}")
|
|
434
|
+
else
|
|
435
|
+
job.override_flags.each { |override_flag| flags("--[no-]#{override_flag}") }
|
|
436
|
+
end
|
|
437
|
+
desc("Run or omit the job \"#{job.name}\". #{flag_desc_suffix}")
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
unless template.collections.empty?
|
|
443
|
+
flag_group(desc: "Collections") do
|
|
444
|
+
template.collections.each do |collection|
|
|
445
|
+
Array(collection.job_flags).each do |job_flag|
|
|
446
|
+
if template.jobs.none? { |job| job.flag == job_flag }
|
|
447
|
+
raise ::Toys::ToolDefinitionError,
|
|
448
|
+
"Collection \"#{collection.name}\" referenced nonexistent job flag: #{job_flag}"
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
flag(collection.flag) do
|
|
452
|
+
if collection.override_flags.empty?
|
|
453
|
+
hyphenated_flag = collection.flag.to_s.tr("_", "-")
|
|
454
|
+
flags("--[no-]#{hyphenated_flag}")
|
|
455
|
+
else
|
|
456
|
+
collection.override_flags.each { |override_flag| flags("--[no-]#{override_flag}") }
|
|
457
|
+
end
|
|
458
|
+
desc("Run or omit all \"#{collection.name}\". #{flag_desc_suffix}")
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
static :toys_ci_template, template
|
|
465
|
+
|
|
466
|
+
include ::Toys::CI::Mixin
|
|
467
|
+
|
|
468
|
+
def run
|
|
469
|
+
::Dir.chdir(context_directory) do
|
|
470
|
+
instance_exec(&toys_ci_template.prerun) if toys_ci_template.prerun
|
|
471
|
+
toys_ci_init(fail_fast: toys_ci_fail_fast_value, limit_by_changes_since: toys_ci_resolve_base_ref)
|
|
472
|
+
toys_ci_resolve_collections
|
|
473
|
+
toys_ci_run_all_jobs
|
|
474
|
+
toys_ci_report_results
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def toys_ci_resolve_base_ref
|
|
479
|
+
base_ref = toys_ci_template.base_ref_flag? ? self[toys_ci_template.base_ref_flag] : nil
|
|
480
|
+
if toys_ci_template.use_github_base_ref_flag? && self[toys_ci_template.use_github_base_ref_flag]
|
|
481
|
+
base_ref ||= toys_ci_github_event_base_sha
|
|
482
|
+
end
|
|
483
|
+
base_ref
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def toys_ci_fail_fast_value
|
|
487
|
+
if toys_ci_template.fail_fast_flag?
|
|
488
|
+
self[toys_ci_template.fail_fast_flag]
|
|
489
|
+
else
|
|
490
|
+
toys_ci_template.fail_fast_default
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def toys_ci_resolve_collections
|
|
495
|
+
toys_ci_template.collections.each do |collection|
|
|
496
|
+
value = self[collection.flag]
|
|
497
|
+
next if value.nil?
|
|
498
|
+
Array(collection.job_flags).each do |job_flag|
|
|
499
|
+
set(job_flag, value) if self[job_flag].nil?
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def toys_ci_run_all_jobs
|
|
505
|
+
toys_ci_template.jobs.each do |job|
|
|
506
|
+
next unless toys_ci_enabled_value(job.flag)
|
|
507
|
+
case job
|
|
508
|
+
when ToolJob
|
|
509
|
+
toys_ci_tool_job(job.name, job.tool, trigger_paths: job.trigger_paths, env: job.env, chdir: job.chdir)
|
|
510
|
+
when CmdJob
|
|
511
|
+
toys_ci_cmd_job(job.name, job.cmd, trigger_paths: job.trigger_paths, env: job.env, chdir: job.chdir)
|
|
512
|
+
when BlockJob
|
|
513
|
+
toys_ci_job(job.name, trigger_paths: job.trigger_paths, &job.block)
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def toys_ci_enabled_value(flag)
|
|
519
|
+
flag_value = self[flag] if flag
|
|
520
|
+
return flag_value unless flag_value.nil?
|
|
521
|
+
if toys_ci_template.all_flag?
|
|
522
|
+
self[toys_ci_template.all_flag]
|
|
523
|
+
elsif toys_ci_template.only_flag?
|
|
524
|
+
!self[toys_ci_template.only_flag]
|
|
525
|
+
else
|
|
526
|
+
!toys_ci_template.jobs_disabled_by_default
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
data/lib/toys-ci.rb
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Toys is a configurable command line tool. Write commands in config files
|
|
5
|
+
# using a simple DSL, and Toys will provide the command line executable and
|
|
6
|
+
# take care of all the details such as argument parsing, online help, and error
|
|
7
|
+
# reporting. Toys is designed for software developers, IT professionals, and
|
|
8
|
+
# other power users who want to write and organize scripts to automate their
|
|
9
|
+
# workflows. It can also be used as a Rake replacement, providing a more
|
|
10
|
+
# natural command line interface for your project's build tasks.
|
|
1
11
|
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
# The
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
12
|
+
module Toys
|
|
13
|
+
##
|
|
14
|
+
# The Toys CI system is a mixin and template useful for generating CI tools.
|
|
15
|
+
#
|
|
16
|
+
# In this system, a CI tool is a tool that calls some set of other tools or
|
|
17
|
+
# processes, each of which implements an individual job such as doing a build
|
|
18
|
+
# or running tests. The CI tool monitors and summarizes the results of those
|
|
19
|
+
# jobs.
|
|
20
|
+
#
|
|
21
|
+
# See {Toys::CI::Mixin} for a lower-level mixin that provides useful methods
|
|
22
|
+
# for implementing a CI tool, including methods for finding changed files,
|
|
23
|
+
# for running individual jobs, and for reporting results.
|
|
24
|
+
#
|
|
25
|
+
# See {Toys::CI::Template} for a higher-level template that generates a full
|
|
26
|
+
# CI tool, including flags for controlling how CI should behave and which
|
|
27
|
+
# jobs should be run.
|
|
28
|
+
#
|
|
29
|
+
module CI
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require "toys/ci/mixin"
|
|
34
|
+
require "toys/ci/template"
|
|
35
|
+
require "toys/ci/version"
|
data/toys/.toys.rb
ADDED
metadata
CHANGED
|
@@ -1,26 +1,55 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toys-ci
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Daniel Azuma
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: toys-core
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.20'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.20'
|
|
26
|
+
description: Toys-CI is a framework for generating simple CI coordinator tools. It
|
|
27
|
+
provides a declarative interface for configuring the tool and specifying individual
|
|
28
|
+
jobs to run. The generated tool runs jobs specified using command line arguments,
|
|
29
|
+
and produces a final report of the results.
|
|
30
|
+
email:
|
|
31
|
+
- dazuma@gmail.com
|
|
16
32
|
executables: []
|
|
17
33
|
extensions: []
|
|
18
34
|
extra_rdoc_files: []
|
|
19
35
|
files:
|
|
36
|
+
- ".yardopts"
|
|
37
|
+
- CHANGELOG.md
|
|
38
|
+
- LICENSE.md
|
|
20
39
|
- README.md
|
|
21
40
|
- lib/toys-ci.rb
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
- lib/toys/ci/mixin.rb
|
|
42
|
+
- lib/toys/ci/template.rb
|
|
43
|
+
- lib/toys/ci/version.rb
|
|
44
|
+
- toys/.toys.rb
|
|
45
|
+
homepage: https://github.com/dazuma/toys
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata:
|
|
49
|
+
changelog_uri: https://dazuma.github.io/toys/gems/toys-ci/v0.1.0/file.CHANGELOG.html
|
|
50
|
+
source_code_uri: https://github.com/dazuma/toys/tree/toys-ci/v0.1.0/toys-ci
|
|
51
|
+
bug_tracker_uri: https://github.com/dazuma/toys/issues
|
|
52
|
+
documentation_uri: https://dazuma.github.io/toys/gems/toys-ci/v0.1.0
|
|
24
53
|
rdoc_options: []
|
|
25
54
|
require_paths:
|
|
26
55
|
- lib
|
|
@@ -28,7 +57,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
28
57
|
requirements:
|
|
29
58
|
- - ">="
|
|
30
59
|
- !ruby/object:Gem::Version
|
|
31
|
-
version:
|
|
60
|
+
version: 2.7.0
|
|
32
61
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
33
62
|
requirements:
|
|
34
63
|
- - ">="
|
|
@@ -37,5 +66,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
37
66
|
requirements: []
|
|
38
67
|
rubygems_version: 4.0.3
|
|
39
68
|
specification_version: 4
|
|
40
|
-
summary:
|
|
69
|
+
summary: CI system using GitHub Actions and Toys
|
|
41
70
|
test_files: []
|