local_ci_plus 0.1.0 → 0.5.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/README.md +11 -27
- data/lib/local_ci_plus/continuous_integration.rb +93 -22
- data/lib/local_ci_plus/generators/update_generator.rb +22 -6
- data/lib/local_ci_plus/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5e469a0e3234d66985a729cb66222e4f57ea4f627804a7ff4d96f39dcafc2c0
|
|
4
|
+
data.tar.gz: 0ae35f6c3627d2fdf6fd68b27278563d346a560d419259f115b685c41d1693c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02d9a9b3b8af2921728c5e3c7625247e72ceee8d43e2512d7f8de851a956ae1c24a2cace9691655db01600e505e834d37d0792161b4a401a4b2cad3e629b2b12
|
|
7
|
+
data.tar.gz: '08442d855fa89ee0739d90e62145a19720ea6c44f961c1ba3af33dee3289928650e9fc32987699f0bec2a28187a81a235ef98ab0ef79ef7bb6f11d435a467d51'
|
data/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
local_ci_plus improves Rails local CI for both developers and agents with parallel execution, fail-fast,
|
|
4
4
|
resume, and plain output.
|
|
5
5
|
|
|
6
|
+
https://github.com/user-attachments/assets/cf8d8c01-ddee-4154-bdb3-0a77cb5c1f53
|
|
7
|
+
|
|
6
8
|
## Installation
|
|
7
9
|
|
|
8
10
|
Add to your Gemfile:
|
|
@@ -20,7 +22,7 @@ bundle install
|
|
|
20
22
|
## Usage
|
|
21
23
|
|
|
22
24
|
`local_ci_plus` overrides `ActiveSupport::ContinuousIntegration` when it is loaded, so the default Rails `bin/ci`
|
|
23
|
-
continues to work without changes.
|
|
25
|
+
continues to work without changes. In plain/non-TTY mode, output is ASCII-only.
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
28
|
bin/ci
|
|
@@ -61,6 +63,10 @@ Compatibility:
|
|
|
61
63
|
--parallel cannot be combined with --fail-fast or --continue
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
### State file
|
|
67
|
+
|
|
68
|
+
By default, the first failing step is stored in `tmp/ci_state`. Set `CI_STATE_FILE` to override the path. To reset the resume point, delete the file.
|
|
69
|
+
|
|
64
70
|
## Development
|
|
65
71
|
|
|
66
72
|
Run tests:
|
|
@@ -75,35 +81,13 @@ Run linting:
|
|
|
75
81
|
bundle exec standardrb
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
### One-time setup (RubyGems)
|
|
81
|
-
|
|
82
|
-
1. Create a RubyGems account if you do not have one.
|
|
83
|
-
2. Add your API key:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
mkdir -p ~/.gem
|
|
87
|
-
printf "---\n:rubygems_api_key: YOUR_KEY\n" > ~/.gem/credentials
|
|
88
|
-
chmod 0600 ~/.gem/credentials
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Initial release
|
|
92
|
-
|
|
93
|
-
1. Update `local_ci_plus.gemspec` with the real `authors`, `email`, `homepage`, and `license`.
|
|
94
|
-
2. Build the gem:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
gem build local_ci_plus.gemspec
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
3. Push to RubyGems:
|
|
84
|
+
Or run them both:
|
|
101
85
|
|
|
102
|
-
```
|
|
103
|
-
|
|
86
|
+
```base
|
|
87
|
+
bin/ci --parallel
|
|
104
88
|
```
|
|
105
89
|
|
|
106
|
-
### Updates
|
|
90
|
+
### Publishing Updates
|
|
107
91
|
|
|
108
92
|
1. Bump the version in `lib/local_ci_plus/version.rb`.
|
|
109
93
|
2. Build and push:
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# - bin/ci -fc: Both options combined
|
|
7
7
|
# - bin/ci -p (--parallel): Run all steps concurrently
|
|
8
8
|
|
|
9
|
+
require "fileutils"
|
|
9
10
|
require "tempfile"
|
|
10
11
|
|
|
11
12
|
module LocalCiPlus
|
|
@@ -20,7 +21,8 @@ module LocalCiPlus
|
|
|
20
21
|
pending: "\033[1;34m" # Blue
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
STATE_FILE = "
|
|
24
|
+
STATE_FILE = File.join("tmp", "ci_state")
|
|
25
|
+
STATE_FILE_ENV = "CI_STATE_FILE"
|
|
24
26
|
|
|
25
27
|
attr_reader :results
|
|
26
28
|
|
|
@@ -49,10 +51,10 @@ module LocalCiPlus
|
|
|
49
51
|
|
|
50
52
|
def validate_mode_compatibility!
|
|
51
53
|
if parallel? && fail_fast?
|
|
52
|
-
abort colorize("
|
|
54
|
+
abort colorize("#{status_marker(:error)} Cannot combine --parallel with --fail-fast", :error)
|
|
53
55
|
end
|
|
54
56
|
if parallel? && continue_mode?
|
|
55
|
-
abort colorize("
|
|
57
|
+
abort colorize("#{status_marker(:error)} Cannot combine --parallel with --continue", :error)
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -83,7 +85,6 @@ module LocalCiPlus
|
|
|
83
85
|
if @skipping
|
|
84
86
|
if title == @skip_until
|
|
85
87
|
@skipping = false
|
|
86
|
-
clear_state
|
|
87
88
|
else
|
|
88
89
|
heading title, "skipped (resuming from: #{@skip_until})", type: :skip
|
|
89
90
|
results << [true, title]
|
|
@@ -101,9 +102,13 @@ module LocalCiPlus
|
|
|
101
102
|
success = system(*command)
|
|
102
103
|
results << [success, title]
|
|
103
104
|
|
|
104
|
-
if
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
if success
|
|
106
|
+
clear_state if recorded_failed_step == title
|
|
107
|
+
else
|
|
108
|
+
record_failed_step(title)
|
|
109
|
+
if fail_fast?
|
|
110
|
+
abort colorize("\n#{status_marker(:error)} #{title} failed (fail-fast enabled)", :error)
|
|
111
|
+
end
|
|
107
112
|
end
|
|
108
113
|
end
|
|
109
114
|
end
|
|
@@ -134,19 +139,19 @@ module LocalCiPlus
|
|
|
134
139
|
end
|
|
135
140
|
|
|
136
141
|
def report(title, &block)
|
|
142
|
+
ci = self.class.new
|
|
143
|
+
ci.instance_variable_set(:@skip_until, @skip_until)
|
|
144
|
+
ci.instance_variable_set(:@skipping, @skipping)
|
|
145
|
+
|
|
137
146
|
prev_int = Signal.trap("INT") {
|
|
138
|
-
interrupt_parallel! if parallel?
|
|
139
|
-
abort colorize("\n
|
|
147
|
+
ci.interrupt_parallel! if ci.parallel?
|
|
148
|
+
abort colorize("\n#{status_marker(:error)} #{title} interrupted", :error)
|
|
140
149
|
}
|
|
141
150
|
prev_term = Signal.trap("TERM") {
|
|
142
|
-
interrupt_parallel! if parallel?
|
|
143
|
-
abort colorize("\n
|
|
151
|
+
ci.interrupt_parallel! if ci.parallel?
|
|
152
|
+
abort colorize("\n#{status_marker(:error)} #{title} terminated", :error)
|
|
144
153
|
}
|
|
145
154
|
|
|
146
|
-
ci = self.class.new
|
|
147
|
-
ci.instance_variable_set(:@skip_until, @skip_until)
|
|
148
|
-
ci.instance_variable_set(:@skipping, @skipping)
|
|
149
|
-
|
|
150
155
|
elapsed = timing do
|
|
151
156
|
ci.instance_eval(&block)
|
|
152
157
|
ci.run_parallel_steps! if ci.parallel? && ci.parallel_steps.any?
|
|
@@ -156,14 +161,14 @@ module LocalCiPlus
|
|
|
156
161
|
@skipping = ci.instance_variable_get(:@skipping)
|
|
157
162
|
|
|
158
163
|
if ci.success?
|
|
159
|
-
echo "\n
|
|
164
|
+
echo "\n#{status_marker(:success)} #{title} passed in #{elapsed}", type: :success
|
|
160
165
|
clear_state
|
|
161
166
|
else
|
|
162
|
-
echo "\n
|
|
167
|
+
echo "\n#{status_marker(:error)} #{title} failed in #{elapsed}", type: :error
|
|
163
168
|
|
|
164
169
|
if ci.multiple_results?
|
|
165
170
|
ci.failures.each do |success, step_title|
|
|
166
|
-
echo "
|
|
171
|
+
echo " #{failure_bullet} #{step_title} failed", type: :error
|
|
167
172
|
end
|
|
168
173
|
end
|
|
169
174
|
end
|
|
@@ -216,7 +221,7 @@ module LocalCiPlus
|
|
|
216
221
|
completed = []
|
|
217
222
|
completed_by_index = {}
|
|
218
223
|
|
|
219
|
-
echo "\n
|
|
224
|
+
echo "\n#{status_marker(:pending)} Running #{total} steps in parallel:", type: :subtitle
|
|
220
225
|
unless plain?
|
|
221
226
|
@parallel_steps.each do |step|
|
|
222
227
|
echo format_parallel_line(step[:title], :pending), type: :pending
|
|
@@ -339,6 +344,41 @@ module LocalCiPlus
|
|
|
339
344
|
failed_jobs = completed.reject { |j| j[:success] }
|
|
340
345
|
return if failed_jobs.empty?
|
|
341
346
|
|
|
347
|
+
if plain?
|
|
348
|
+
separator = "-" * 60
|
|
349
|
+
echo "\n#{separator}", type: :error
|
|
350
|
+
echo "Failed step output:", type: :error
|
|
351
|
+
echo separator, type: :error
|
|
352
|
+
|
|
353
|
+
failed_jobs.each do |job|
|
|
354
|
+
echo "\n-- #{job[:title]} (exit #{job[:exit_code]})", type: :error
|
|
355
|
+
echo " Command: #{job[:command].join(" ")}", type: :subtitle
|
|
356
|
+
|
|
357
|
+
stdout_content = truncated_file_content(job[:stdout_file])
|
|
358
|
+
stderr_content = truncated_file_content(job[:stderr_file])
|
|
359
|
+
|
|
360
|
+
if stdout_content.empty? && stderr_content.empty?
|
|
361
|
+
echo " (no output)", type: :subtitle
|
|
362
|
+
else
|
|
363
|
+
unless stdout_content.empty?
|
|
364
|
+
echo " -- stdout --", type: :subtitle
|
|
365
|
+
stdout_content.each_line { |line| echo " #{line.chomp}", type: :subtitle }
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
unless stderr_content.empty?
|
|
369
|
+
echo " -- stderr --", type: :error
|
|
370
|
+
stderr_content.each_line { |line| echo " #{line.chomp}", type: :error }
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
echo "-- end", type: :error
|
|
375
|
+
|
|
376
|
+
cleanup_job_files!(job)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
return
|
|
380
|
+
end
|
|
381
|
+
|
|
342
382
|
echo "\n" + ("─" * 60), type: :error
|
|
343
383
|
echo "Failed step output:", type: :error
|
|
344
384
|
echo ("─" * 60), type: :error
|
|
@@ -392,8 +432,22 @@ module LocalCiPlus
|
|
|
392
432
|
{pending: "•", success: "✅", error: "❌"}[status]
|
|
393
433
|
end
|
|
394
434
|
|
|
435
|
+
def status_marker(type)
|
|
436
|
+
return {pending: "WAIT", success: "OK", error: "FAIL"}[type] if plain?
|
|
437
|
+
|
|
438
|
+
{pending: "⏳", success: "✅", error: "❌"}[type]
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def failure_bullet
|
|
442
|
+
plain? ? "->" : "↳"
|
|
443
|
+
end
|
|
444
|
+
|
|
395
445
|
def update_parallel_line(index, text, type)
|
|
396
|
-
|
|
446
|
+
if plain?
|
|
447
|
+
echo(text, type: type)
|
|
448
|
+
$stdout.flush
|
|
449
|
+
return
|
|
450
|
+
end
|
|
397
451
|
|
|
398
452
|
lines_up = @parallel_steps.size - index
|
|
399
453
|
print "\033[s"
|
|
@@ -401,19 +455,36 @@ module LocalCiPlus
|
|
|
401
455
|
print "\r\033[2K"
|
|
402
456
|
print colorize(text, type)
|
|
403
457
|
print "\033[u"
|
|
458
|
+
$stdout.flush
|
|
404
459
|
end
|
|
405
460
|
|
|
406
461
|
def state_file_path
|
|
462
|
+
custom_path = ENV[STATE_FILE_ENV]
|
|
463
|
+
return File.expand_path(custom_path, Dir.pwd) if custom_path && !custom_path.empty?
|
|
464
|
+
|
|
407
465
|
File.join(Dir.pwd, STATE_FILE)
|
|
408
466
|
end
|
|
409
467
|
|
|
410
468
|
def save_failed_step(title)
|
|
469
|
+
FileUtils.mkdir_p(File.dirname(state_file_path))
|
|
411
470
|
File.write(state_file_path, title)
|
|
412
471
|
end
|
|
413
472
|
|
|
414
|
-
def
|
|
473
|
+
def record_failed_step(title)
|
|
474
|
+
return if recorded_failed_step
|
|
475
|
+
|
|
476
|
+
save_failed_step(title)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def recorded_failed_step
|
|
415
480
|
return nil unless File.exist?(state_file_path)
|
|
416
|
-
|
|
481
|
+
|
|
482
|
+
content = File.read(state_file_path).strip
|
|
483
|
+
content.empty? ? nil : content
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def load_failed_step
|
|
487
|
+
recorded_failed_step
|
|
417
488
|
end
|
|
418
489
|
|
|
419
490
|
def clear_state
|
|
@@ -6,19 +6,35 @@ module LocalCiPlus
|
|
|
6
6
|
module Generators
|
|
7
7
|
class UpdateGenerator < Rails::Generators::Base
|
|
8
8
|
def update_ci_runner
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
end
|
|
13
|
-
else
|
|
9
|
+
ci_path = File.join(destination_root, "bin/ci")
|
|
10
|
+
|
|
11
|
+
unless File.exist?(ci_path)
|
|
14
12
|
say_status :skipped, "bin/ci does not exist", :yellow
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
return if ci_requires_local_ci_plus?
|
|
17
|
+
|
|
18
|
+
lines = File.read(ci_path).lines
|
|
19
|
+
boot_index = lines.find_index { |line| line.match?(boot_require_regex) }
|
|
20
|
+
|
|
21
|
+
unless boot_index
|
|
22
|
+
say_status :skipped, "bin/ci does not require config/boot", :yellow
|
|
23
|
+
return
|
|
15
24
|
end
|
|
25
|
+
|
|
26
|
+
lines.insert(boot_index + 1, "require \"local_ci_plus\"\n")
|
|
27
|
+
File.write(ci_path, lines.join)
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
private
|
|
19
31
|
|
|
20
32
|
def ci_requires_local_ci_plus?
|
|
21
|
-
File.read(File.join(destination_root, "bin/ci")).
|
|
33
|
+
File.read(File.join(destination_root, "bin/ci")).match?(/require\s+["']local_ci_plus["']/)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def boot_require_regex
|
|
37
|
+
/^\s*require_relative\s+\(?\s*["']\.\.\/config\/boot["']/
|
|
22
38
|
end
|
|
23
39
|
end
|
|
24
40
|
end
|