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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29f82722f9217011683b155e6ac979418a9b78392cd0e045a34af00e0c263f98
4
- data.tar.gz: d987f1334cab11e82de0727352411366c5736ca75a085bec4641dc3b15402af3
3
+ metadata.gz: c5e469a0e3234d66985a729cb66222e4f57ea4f627804a7ff4d96f39dcafc2c0
4
+ data.tar.gz: 0ae35f6c3627d2fdf6fd68b27278563d346a560d419259f115b685c41d1693c0
5
5
  SHA512:
6
- metadata.gz: 3f01f9168e6861420e52191d50b96c38391196426b2082e046fbb78ffe50bbb30e141a1bb280c4158c59c557758a9bc1e5541134054ae2371676f3bef030648b
7
- data.tar.gz: 9399e4a82bc56f565015f9bdfafd031b008ec1c121c35a1b52a22ca5781993050388f5a328354b742ec7bf42367187e1dd9ec50a61e469764295384900f55746
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
- ## Publishing
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
- ```bash
103
- gem push local_ci_plus-0.1.0.gem
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 = ".ci_state"
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(" Cannot combine --parallel with --fail-fast", :error)
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(" Cannot combine --parallel with --continue", :error)
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 !success && fail_fast?
105
- save_failed_step(title)
106
- abort colorize("\n❌ #{title} failed (fail-fast enabled)", :error)
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 #{title} interrupted", :error)
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 #{title} terminated", :error)
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 #{title} passed in #{elapsed}", type: :success
164
+ echo "\n#{status_marker(:success)} #{title} passed in #{elapsed}", type: :success
160
165
  clear_state
161
166
  else
162
- echo "\n #{title} failed in #{elapsed}", type: :error
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 " #{step_title} failed", type: :error
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 Running #{total} steps in parallel:", type: :subtitle
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
- return echo(text, type: type) if plain?
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 load_failed_step
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
- File.read(state_file_path).strip
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
- if File.exist?(File.join(destination_root, "bin/ci"))
10
- unless ci_requires_local_ci_plus?
11
- inject_into_file "bin/ci", "require \"local_ci_plus\"\n", after: "require_relative \"../config/boot\"\n"
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")).include?("require \"local_ci_plus\"")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LocalCiPlus
4
- VERSION = "0.1.0"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: local_ci_plus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Watermasysk