kettle-dev 1.0.11 → 1.0.12

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: 913b6449dd3d5d61bc20384f02a12cb6296f9cbc9e6fdc1255c1fabeb9332034
4
- data.tar.gz: ccb5f23873871c1e152d6394b4b5b96ea73ecb5306032710c637e948e7012741
3
+ metadata.gz: 18dc37569a34008ec953339ce7fd69e285bf007f2f1dd035de13856fb2d621ec
4
+ data.tar.gz: b3afeca255b2fb10fd86c4e48a47679b8652ff7ab9f75c4b8637c7738f83e28b
5
5
  SHA512:
6
- metadata.gz: a86a6391cdb16fff03bc84ef00a50c62a1d86d775f6527bda5693e5bc395ab45be775480037436c9a3460cc557584fc16ea35e85bdb939ff983b5db10df69fcc
7
- data.tar.gz: d492c8141e8a4899a1c742efc3e09afa38c97fdf3c667c92d0357f5ff1e0c460b9164ad1373a03d4696bad6b8406202c247c5d471cafccf5f29ea6ac2bb81912
6
+ metadata.gz: d12f7394b9aba54d138cbddc74598e03908280f6e4102c0b98a9d326e00909cc5d1cac16f1a5bced75ee694c2606e026bc772088b005abf1a69d9a1d23bf0c93
7
+ data.tar.gz: 5fc72941ddcc1c21e9e56fd0fffd2eb0b603eff7d1fe60d6cdf55108b8e4c314320be1ed243e5064d89cb604e74e42bdc6756764c42dcf920bc204514fbe539b
checksums.yaml.gz.sig CHANGED
Binary file
data/.junie/guidelines.md CHANGED
@@ -128,6 +128,7 @@ Notes
128
128
  - NEVER run vanilla rubocop, as it won't handle the linting config properly. Always run rubocop_gradual:autocorrect or rubocop_gradual.
129
129
  - Running only a subset of specs is supported but in order to bypass the hard failure due to coverage thresholds, you need to run with K_SOUP_COV_MIN_HARD=false.
130
130
  - When adding code that writes to STDOUT, remember most specs silence output unless tagged with :check_output or DEBUG=true.
131
+ - Completion criteria after changes: Only consider your change “done” when the relevant examples pass, as verified by .rspec_status. Do not rely on STDOUT impressions; consult .rspec_status (and example IDs) to confirm green results for the affected files/examples. If you ran a subset, re-run the full suite before finalizing to restore coverage thresholds.
131
132
  - Coverage reports: NEVER review the HTML report. Use JSON (preferred), XML, LCOV, or RCOV. For this project, always run tests with K_SOUP_COV_FORMATTERS set to "json".
132
133
  - Do NOT modify .envrc in tasks; when running tests locally or in scripts, manually prefix each run, e.g.: K_SOUP_COV_FORMATTERS="json" bin/rspec
133
134
  - For all the kettle-soup-cover options, see .envrc and find the K_SOUP_COV_* env vars.
data/Appraisals CHANGED
@@ -26,6 +26,7 @@ appraise "unlocked_deps" do
26
26
  eval_gemfile "modular/coverage.gemfile"
27
27
  eval_gemfile "modular/documentation.gemfile"
28
28
  eval_gemfile "modular/style.gemfile"
29
+ eval_gemfile "modular/optional.gemfile"
29
30
  end
30
31
 
31
32
  # Used for head (nightly) releases of ruby, truffleruby, and jruby.
data/Appraisals.example CHANGED
@@ -25,6 +25,7 @@ appraise "unlocked_deps" do
25
25
  eval_gemfile "modular/coverage.gemfile"
26
26
  eval_gemfile "modular/documentation.gemfile"
27
27
  eval_gemfile "modular/style.gemfile"
28
+ eval_gemfile "modular/optional.gemfile"
28
29
  end
29
30
 
30
31
  # Used for head (nightly) releases of ruby, truffleruby, and jruby.
@@ -92,6 +93,7 @@ appraise "coverage" do
92
93
  gem "mutex_m", "~> 0.2"
93
94
  gem "stringio", "~> 3.0"
94
95
  eval_gemfile "modular/coverage.gemfile"
96
+ eval_gemfile "modular/optional.gemfile"
95
97
  end
96
98
 
97
99
  # Only run linter on latest Ruby version (but, in support of oldest supported Ruby version)
data/CHANGELOG.md CHANGED
@@ -24,6 +24,30 @@ Please file a bug if you notice a violation of semantic versioning.
24
24
  ### Fixed
25
25
  ### Security
26
26
 
27
+ ## [1.0.12] - 2025-08-28
28
+ - TAG: [v1.0.12][1.0.12t]
29
+ - COVERAGE: 97.80% -- 1957/2001 lines in 19 files
30
+ - BRANCH COVERAGE: 79.98% -- 763/954 branches in 19 files
31
+ - 78.70% documented
32
+ ### Added
33
+ - CIMonitor to consolidate workflow / pipeline monitoring logic for GH/GL across kettle-release and rake tasks, with handling for:
34
+ - minutes exhausted
35
+ - blocked
36
+ - not configured
37
+ - normal failures
38
+ - pending
39
+ - queued
40
+ - running
41
+ - success
42
+ - Ability to restart kettle-release from any failed step, so manual fixed can be applied.
43
+ - Example (after intermittent failure of CI): `bundle exec kettle-release start_step=10`
44
+ ### Fixed
45
+ - added optional.gemfile.example, and handling for it in templating
46
+ - kettle-changelog: ensure a blank line at end of file
47
+ - add sleep(0.2) to ci:act to prevent race condition with stdout flushing
48
+ - kettle-release: ensure SKIP_GEM_SIGNING works as expected with values of "true" or "false"
49
+ - ensure it doesn't abort the process in CI
50
+
27
51
  ## [1.0.11] - 2025-08-28
28
52
  - TAG: [v1.0.11][1.0.11t]
29
53
  - COVERAGE: 97.90% -- 1959/2001 lines in 19 files
@@ -190,7 +214,7 @@ Please file a bug if you notice a violation of semantic versioning.
190
214
  - Selecting will run the selected workflow via `act`
191
215
  - This may move to its own gem in the future.
192
216
 
193
- [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.11...HEAD
217
+ [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.12...HEAD
194
218
  [1.0.0]: https://github.com/kettle-rb/kettle-dev/compare/a427c302df09cfe4253a7c8d400333f9a4c1a208...v1.0.0
195
219
  [1.0.0t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.0
196
220
  [1.0.1]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.0...v1.0.1
@@ -215,4 +239,5 @@ Please file a bug if you notice a violation of semantic versioning.
215
239
  [1.0.10t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.10
216
240
  [1.0.11]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.10...v1.0.11
217
241
  [1.0.11t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.11
218
-
242
+ [1.0.12]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.11...v1.0.12
243
+ [1.0.12t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.12
data/Gemfile.example ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+ git_source(:gitlab) { |repo_name| "https://gitlab.com/#{repo_name}" }
7
+
8
+ #### IMPORTANT #######################################################
9
+ # Gemfile is for local development ONLY; Gemfile is NOT loaded in CI #
10
+ ####################################################### IMPORTANT ####
11
+
12
+ # Include dependencies from <gem name>.gemspec
13
+ gemspec
14
+
15
+ platform :mri do
16
+ # Debugging - Ensure ENV["DEBUG"] == "true" to use debuggers within spec suite
17
+ # Use binding.break, binding.b, or debugger in code
18
+ gem "debug", ">= 1.0.0" # ruby >= 2.7
19
+ gem "gem_bench", "~> 2.0", ">= 2.0.5"
20
+
21
+ # Dev Console - Binding.pry - Irb replacement
22
+ gem "pry", "~> 0.14" # ruby >= 2.0
23
+ end
24
+
25
+ # Code Coverage
26
+ eval_gemfile "gemfiles/modular/coverage.gemfile"
27
+
28
+ # Linting
29
+ eval_gemfile "gemfiles/modular/style.gemfile"
30
+
31
+ # Documentation
32
+ eval_gemfile "gemfiles/modular/documentation.gemfile"
33
+
34
+ # Optional
35
+ eval_gemfile "gemfiles/modular/optional.gemfile"
data/README.md CHANGED
@@ -697,7 +697,7 @@ Thanks for RTFM. ☺️
697
697
  [📌gitmoji]:https://gitmoji.dev
698
698
  [📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20😜%20😍-34495e.svg?style=flat-square
699
699
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
700
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.725-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
700
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.001-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
701
701
  [🔐security]: SECURITY.md
702
702
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
703
703
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/exe/kettle-changelog CHANGED
@@ -106,6 +106,9 @@ module Kettle
106
106
 
107
107
  updated = update_link_refs(updated, owner, repo, prev_version, version)
108
108
 
109
+ # Ensure exactly one trailing newline at EOF
110
+ updated = updated.rstrip + "\n"
111
+
109
112
  File.write(@changelog_path, updated)
110
113
  puts "CHANGELOG.md updated with v#{version} section."
111
114
  end
data/exe/kettle-release CHANGED
@@ -25,9 +25,12 @@ require "kettle/dev"
25
25
  # Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
26
26
  if ARGV.include?("-h") || ARGV.include?("--help")
27
27
  puts <<~USAGE
28
- Usage: kettle-release
28
+ Usage: kettle-release [start_step=<number>]
29
29
 
30
30
  Automates the release flow for a Ruby gem in the host project:
31
+
32
+ Options:
33
+ start_step=<number> # skip directly to the numbered step (e.g., 10 for CI validation)
31
34
  - Runs bin/setup and bin/rake sanity checks
32
35
  - Prompts to confirm version and changelog updates
33
36
  - Commits a release prep change
@@ -46,8 +49,11 @@ if ARGV.include?("-h") || ARGV.include?("--help")
46
49
  end
47
50
 
48
51
  puts "== kettle-release v#{Kettle::Dev::Version::VERSION} =="
52
+ # Parse start_step=<n> from ARGV
53
+ start_step_arg = ARGV.find { |a| a.start_with?("start_step=") }
54
+ start_step = start_step_arg ? start_step_arg.split("=", 2)[1].to_i : 1
49
55
  begin
50
- Kettle::Dev::ReleaseCLI.new.run
56
+ Kettle::Dev::ReleaseCLI.new(start_step: start_step).run
51
57
  rescue LoadError => e
52
58
  warn("kettle-release: could not load dependency: #{e.message}")
53
59
  warn(e.backtrace.join("\n")) if ENV["DEBUG"]
@@ -1,5 +1 @@
1
- # Optional dependencies are not dependended on directly, but will be used if present.
2
- # git gem is not a direct dependency for two reasons:
3
- # 1. it is incompatible with Truffleruby v23
4
- # 2. it depends on activesupport, which is too heavy
5
- gem "git", ">= 1.19.1" # ruby >= 2.3
1
+ # Optional dependencies are not dependended on directly, but may be used if present.
@@ -177,10 +177,29 @@ module Kettle
177
177
  data = JSON.parse(res.body)
178
178
  pipe = data&.first
179
179
  return unless pipe
180
+ # Attempt to enrich with failure_reason by querying the single pipeline endpoint
181
+ begin
182
+ if pipe["id"]
183
+ detail_uri = URI("https://#{host}/api/v4/projects/#{project}/pipelines/#{pipe["id"]}")
184
+ dreq = Net::HTTP::Get.new(detail_uri)
185
+ dreq["User-Agent"] = "kettle-dev/ci-helpers"
186
+ dreq["PRIVATE-TOKEN"] = token if token && !token.empty?
187
+ dres = Net::HTTP.start(detail_uri.hostname, detail_uri.port, use_ssl: true) { |http| http.request(dreq) }
188
+ if dres.is_a?(Net::HTTPSuccess)
189
+ det = JSON.parse(dres.body)
190
+ pipe["failure_reason"] = det["failure_reason"] if det.is_a?(Hash)
191
+ pipe["status"] = det["status"] if det["status"]
192
+ pipe["web_url"] = det["web_url"] if det["web_url"]
193
+ end
194
+ end
195
+ rescue StandardError
196
+ # ignore enrichment errors; fall back to basic fields
197
+ end
180
198
  {
181
199
  "status" => pipe["status"],
182
200
  "web_url" => pipe["web_url"],
183
201
  "id" => pipe["id"],
202
+ "failure_reason" => pipe["failure_reason"],
184
203
  }
185
204
  rescue StandardError
186
205
  nil
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ # External stdlib
4
+ require "uri"
5
+ require "json"
6
+ require "net/http"
7
+
8
+ # Internal
9
+ require "kettle/dev/ci_helpers"
10
+ require "kettle/dev/exit_adapter"
11
+ require "kettle/dev/git_adapter"
12
+
13
+ module Kettle
14
+ module Dev
15
+ # CIMonitor centralizes CI monitoring logic (GitHub Actions and GitLab pipelines)
16
+ # so it can be reused by both kettle-release and Rake tasks (e.g., ci:act).
17
+ #
18
+ # Public API is intentionally small and based on environment/project introspection
19
+ # via CIHelpers, matching the behavior historically implemented in ReleaseCLI.
20
+ module CIMonitor
21
+ module_function
22
+
23
+ # Abort helper (delegates through ExitAdapter so specs can trap exits)
24
+ def abort(msg)
25
+ Kettle::Dev::ExitAdapter.abort(msg)
26
+ end
27
+ module_function :abort
28
+
29
+ # Monitor both GitHub and GitLab CI for the current project/branch.
30
+ # This mirrors ReleaseCLI behavior.
31
+ #
32
+ # @param restart_hint [String] guidance command shown on failure
33
+ # @return [void]
34
+ def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10")
35
+ checks_any = false
36
+ checks_any |= monitor_github_internal!(restart_hint: restart_hint)
37
+ checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint)
38
+ abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
39
+ end
40
+
41
+ # Monitor only the GitLab pipeline for current project/branch.
42
+ # Used by ci:act after running 'act'.
43
+ #
44
+ # @param restart_hint [String] guidance command shown on failure
45
+ # @return [Boolean] true if check performed (gitlab configured), false otherwise
46
+ def monitor_gitlab!(restart_hint: "bundle exec rake ci:act")
47
+ monitor_gitlab_internal!(restart_hint: restart_hint)
48
+ end
49
+
50
+ # -- internals --
51
+
52
+ def monitor_github_internal!(restart_hint:)
53
+ root = Kettle::Dev::CIHelpers.project_root
54
+ workflows = Kettle::Dev::CIHelpers.workflows_list(root)
55
+ gh_remote = preferred_github_remote
56
+ return false unless gh_remote && !workflows.empty?
57
+
58
+ branch = Kettle::Dev::CIHelpers.current_branch
59
+ abort("Could not determine current branch for CI checks.") unless branch
60
+
61
+ url = remote_url(gh_remote)
62
+ owner, repo = parse_github_owner_repo(url)
63
+ return false unless owner && repo
64
+
65
+ total = workflows.size
66
+ abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
67
+
68
+ passed = {}
69
+ puts "Ensuring GitHub Actions workflows pass on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'"
70
+ pbar = if defined?(ProgressBar)
71
+ ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
72
+ end
73
+ idx = 0
74
+ loop do
75
+ wf = workflows[idx]
76
+ run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch)
77
+ if run
78
+ if Kettle::Dev::CIHelpers.success?(run)
79
+ unless passed[wf]
80
+ passed[wf] = true
81
+ pbar&.increment
82
+ end
83
+ elsif Kettle::Dev::CIHelpers.failed?(run)
84
+ puts
85
+ wf_url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"
86
+ abort("Workflow failed: #{wf} -> #{wf_url} Fix the workflow, then restart this tool from CI validation with: #{restart_hint}")
87
+ end
88
+ end
89
+ break if passed.size == total
90
+ idx = (idx + 1) % total
91
+ sleep(1)
92
+ end
93
+ pbar&.finish unless pbar&.finished?
94
+ puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
95
+ true
96
+ end
97
+ module_function :monitor_github_internal!
98
+
99
+ def monitor_gitlab_internal!(restart_hint:)
100
+ root = Kettle::Dev::CIHelpers.project_root
101
+ gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
102
+ gl_remote = gitlab_remote_candidates.first
103
+ return false unless gitlab_ci && gl_remote
104
+
105
+ branch = Kettle::Dev::CIHelpers.current_branch
106
+ abort("Could not determine current branch for CI checks.") unless branch
107
+
108
+ owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
109
+ return false unless owner && repo
110
+
111
+ puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
112
+ pbar = if defined?(ProgressBar)
113
+ ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
114
+ end
115
+ loop do
116
+ pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
117
+ if pipe
118
+ if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
119
+ pbar&.increment unless pbar&.finished?
120
+ break
121
+ elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
122
+ # Special-case: if failure is due to exhausted minutes/insufficient quota, treat as unknown and continue
123
+ reason = (pipe["failure_reason"] || "").to_s
124
+ if reason =~ /insufficient|quota|minute/i
125
+ puts "\nGitLab reports pipeline cannot run due to quota/minutes exhaustion. Result is unknown; continuing."
126
+ pbar&.finish unless pbar&.finished?
127
+ break
128
+ else
129
+ puts
130
+ url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
131
+ abort("Pipeline failed: #{url} Fix the pipeline, then restart this tool from CI validation with: #{restart_hint}")
132
+ end
133
+ elsif pipe["status"] == "blocked"
134
+ # Blocked pipeline (e.g., awaiting approvals) — treat as unknown and continue
135
+ puts "\nGitLab pipeline is blocked. Result is unknown; continuing."
136
+ pbar&.finish unless pbar&.finished?
137
+ break
138
+ end
139
+ end
140
+ sleep(1)
141
+ end
142
+ pbar&.finish unless pbar&.finished?
143
+ puts "\nGitLab pipeline passing."
144
+ true
145
+ end
146
+ module_function :monitor_gitlab_internal!
147
+
148
+ # -- tiny wrappers around GitAdapter-like helpers used by ReleaseCLI --
149
+ def remotes_with_urls
150
+ Kettle::Dev::GitAdapter.new.remotes_with_urls
151
+ end
152
+ module_function :remotes_with_urls
153
+
154
+ def remote_url(name)
155
+ Kettle::Dev::GitAdapter.new.remote_url(name)
156
+ end
157
+ module_function :remote_url
158
+
159
+ def github_remote_candidates
160
+ remotes_with_urls.select { |n, u| u.include?("github.com") }.keys
161
+ end
162
+ module_function :github_remote_candidates
163
+
164
+ def gitlab_remote_candidates
165
+ remotes_with_urls.select { |n, u| u.include?("gitlab.com") }.keys
166
+ end
167
+ module_function :gitlab_remote_candidates
168
+
169
+ def preferred_github_remote
170
+ cands = github_remote_candidates
171
+ return if cands.empty?
172
+ explicit = cands.find { |n| n == "github" } || cands.find { |n| n == "gh" }
173
+ return explicit if explicit
174
+ return "origin" if cands.include?("origin")
175
+ cands.first
176
+ end
177
+ module_function :preferred_github_remote
178
+
179
+ def parse_github_owner_repo(url)
180
+ return [nil, nil] unless url
181
+ if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
182
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
183
+ elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
184
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
185
+ else
186
+ [nil, nil]
187
+ end
188
+ end
189
+ module_function :parse_github_owner_repo
190
+ end
191
+ end
192
+ end
@@ -25,6 +25,10 @@ module Kettle
25
25
  $stdin.gets(*args)
26
26
  end
27
27
 
28
+ def tty?
29
+ $stdin.tty?
30
+ end
31
+
28
32
  # Read one line from standard input, raising EOFError on end-of-file.
29
33
  # Provided for convenience symmetry with IO#readline when needed.
30
34
  #
@@ -30,124 +30,177 @@ module Kettle
30
30
 
31
31
  public
32
32
 
33
- def initialize
33
+ def initialize(start_step: 1)
34
34
  @root = Kettle::Dev::CIHelpers.project_root
35
35
  @git = Kettle::Dev::GitAdapter.new
36
+ @start_step = (start_step || 1).to_i
37
+ @start_step = 1 if @start_step < 1
36
38
  end
37
39
 
38
40
  def run
41
+ # 1. Ensure Bundler version ✓
39
42
  ensure_bundler_2_7_plus!
40
43
 
41
- version = detect_version
42
- puts "Detected version: #{version.inspect}"
43
-
44
- latest_overall = nil
45
- latest_for_series = nil
46
- begin
47
- gem_name = detect_gem_name
48
- latest_overall, latest_for_series = latest_released_versions(gem_name, version)
49
- rescue StandardError => e
50
- warn("Warning: failed to check RubyGems for latest version (#{e.class}: #{e.message}). Proceeding.")
51
- end
52
-
53
- if latest_overall
54
- msg = "Latest released: #{latest_overall}"
55
- if latest_for_series && latest_for_series != latest_overall
56
- msg += " | Latest for series #{Gem::Version.new(version).segments[0, 2].join(".")}.x: #{latest_for_series}"
57
- elsif latest_for_series
58
- msg += " (matches current series)"
59
- end
60
- puts msg
61
-
62
- cur = Gem::Version.new(version)
63
- overall = Gem::Version.new(latest_overall)
64
- cur_series = cur.segments[0, 2]
65
- overall_series = overall.segments[0, 2]
66
- target = if (cur_series <=> overall_series) == -1
67
- latest_for_series
68
- else
69
- latest_overall
44
+ version = nil
45
+ committed = nil
46
+ trunk = nil
47
+ feature = nil
48
+
49
+ # 2. Version detection and sanity checks + prompt
50
+ if @start_step <= 2
51
+ version = detect_version
52
+ puts "Detected version: #{version.inspect}"
53
+
54
+ latest_overall = nil
55
+ latest_for_series = nil
56
+ begin
57
+ gem_name = detect_gem_name
58
+ latest_overall, latest_for_series = latest_released_versions(gem_name, version)
59
+ rescue StandardError => e
60
+ warn("Warning: failed to check RubyGems for latest version (#{e.class}: #{e.message}). Proceeding.")
70
61
  end
71
- if target
72
- bump = Kettle::Dev::Versioning.classify_bump(target, version)
73
- case bump
74
- when :same
75
- series = cur_series.join(".")
76
- warn("version.rb (#{version}) matches the latest released version for series #{series} (#{target}).")
77
- abort("Aborting: version bump required. Bump PATCH/MINOR/MAJOR/EPIC.")
78
- when :downgrade
79
- series = cur_series.join(".")
80
- warn("version.rb (#{version}) is lower than the latest released version for series #{series} (#{target}).")
81
- abort("Aborting: version must be bumped above #{target}.")
62
+
63
+ if latest_overall
64
+ msg = "Latest released: #{latest_overall}"
65
+ if latest_for_series && latest_for_series != latest_overall
66
+ msg += " | Latest for series #{Gem::Version.new(version).segments[0, 2].join(".")}.x: #{latest_for_series}"
67
+ elsif latest_for_series
68
+ msg += " (matches current series)"
69
+ end
70
+ puts msg
71
+
72
+ cur = Gem::Version.new(version)
73
+ overall = Gem::Version.new(latest_overall)
74
+ cur_series = cur.segments[0, 2]
75
+ overall_series = overall.segments[0, 2]
76
+ target = if (cur_series <=> overall_series) == -1
77
+ latest_for_series
82
78
  else
83
- label = {epic: "EPIC", major: "MAJOR", minor: "MINOR", patch: "PATCH"}[bump] || bump.to_s.upcase
84
- puts "Proposed bump type: #{label} (from #{target} -> #{version})"
79
+ latest_overall
80
+ end
81
+ if target
82
+ bump = Kettle::Dev::Versioning.classify_bump(target, version)
83
+ case bump
84
+ when :same
85
+ series = cur_series.join(".")
86
+ warn("version.rb (#{version}) matches the latest released version for series #{series} (#{target}).")
87
+ abort("Aborting: version bump required. Bump PATCH/MINOR/MAJOR/EPIC.")
88
+ when :downgrade
89
+ series = cur_series.join(".")
90
+ warn("version.rb (#{version}) is lower than the latest released version for series #{series} (#{target}).")
91
+ abort("Aborting: version must be bumped above #{target}.")
92
+ else
93
+ label = {epic: "EPIC", major: "MAJOR", minor: "MINOR", patch: "PATCH"}[bump] || bump.to_s.upcase
94
+ puts "Proposed bump type: #{label} (from #{target} -> #{version})"
95
+ end
96
+ else
97
+ puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
85
98
  end
86
99
  else
87
100
  puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
88
101
  end
89
- else
90
- puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
91
- end
92
102
 
93
- puts "Have you updated lib/**/version.rb and CHANGELOG.md for v#{version}? [y/N]"
94
- print("> ")
95
- ans = Kettle::Dev::InputAdapter.gets&.strip
96
- abort("Aborted: please update version.rb and CHANGELOG.md, then re-run.") unless ans&.downcase&.start_with?("y")
103
+ puts "Have you updated lib/**/version.rb and CHANGELOG.md for v#{version}? [y/N]"
104
+ print("> ")
105
+ ans = Kettle::Dev::InputAdapter.gets&.strip
106
+ abort("Aborted: please update version.rb and CHANGELOG.md, then re-run.") unless ans&.downcase&.start_with?("y")
107
+ end
97
108
 
98
- run_cmd!("bin/setup")
99
- run_cmd!("bin/rake")
109
+ # 3. bin/setup
110
+ run_cmd!("bin/setup") if @start_step <= 3
111
+ # 4. bin/rake
112
+ run_cmd!("bin/rake") if @start_step <= 4
113
+
114
+ # 5. appraisal:update (optional)
115
+ if @start_step <= 5
116
+ appraisals_path = File.join(@root, "Appraisals")
117
+ if File.file?(appraisals_path)
118
+ puts "Appraisals detected at #{appraisals_path}. Running: bin/rake appraisal:update"
119
+ run_cmd!("bin/rake appraisal:update")
120
+ else
121
+ puts "No Appraisals file found; skipping appraisal:update"
122
+ end
123
+ end
100
124
 
101
- appraisals_path = File.join(@root, "Appraisals")
102
- if File.file?(appraisals_path)
103
- puts "Appraisals detected at #{appraisals_path}. Running: bin/rake appraisal:update"
104
- run_cmd!("bin/rake appraisal:update")
105
- else
106
- puts "No Appraisals file found; skipping appraisal:update"
125
+ # 6. git user + commit release prep
126
+ if @start_step <= 6
127
+ ensure_git_user!
128
+ version ||= detect_version
129
+ committed = commit_release_prep!(version)
107
130
  end
108
131
 
109
- ensure_git_user!
110
- committed = commit_release_prep!(version)
132
+ # 7. optional local CI via act
133
+ maybe_run_local_ci_before_push!(committed) if @start_step <= 7
111
134
 
112
- maybe_run_local_ci_before_push!(committed)
135
+ # 8. ensure trunk synced
136
+ if @start_step <= 8
137
+ trunk = detect_trunk_branch
138
+ feature = current_branch
139
+ puts "Trunk branch detected: #{trunk}"
140
+ ensure_trunk_synced_before_push!(trunk, feature)
141
+ end
113
142
 
114
- trunk = detect_trunk_branch
115
- feature = current_branch
116
- puts "Trunk branch detected: #{trunk}"
117
- ensure_trunk_synced_before_push!(trunk, feature)
143
+ # 9. push branches
144
+ push! if @start_step <= 9
118
145
 
119
- push!
146
+ # 10. monitor CI after push
147
+ monitor_workflows_after_push! if @start_step <= 10
120
148
 
121
- monitor_workflows_after_push!
149
+ # 11. merge feature into trunk and push
150
+ if @start_step <= 11
151
+ trunk ||= detect_trunk_branch
152
+ feature ||= current_branch
153
+ merge_feature_into_trunk_and_push!(trunk, feature)
154
+ end
122
155
 
123
- merge_feature_into_trunk_and_push!(trunk, feature)
156
+ # 12. checkout trunk and pull
157
+ if @start_step <= 12
158
+ trunk ||= detect_trunk_branch
159
+ checkout!(trunk)
160
+ pull!(trunk)
161
+ end
124
162
 
125
- checkout!(trunk)
126
- pull!(trunk)
127
-
128
- # Strong reminder for local runs: skip signing when testing a release flow
129
- if ENV["SKIP_GEM_SIGNING"].to_s.strip == ""
130
- puts "TIP: For local dry-runs or testing the release workflow, set SKIP_GEM_SIGNING=true to avoid PEM password prompts."
131
- # Prompt on CI to allow an explicit abort when signing would otherwise hang
132
- if ENV.fetch("CI", "false").casecmp("true").zero?
133
- print("Proceed with signing enabled? This may hang waiting for a PEM password. [y/N]: ")
134
- ans = Kettle::Dev::InputAdapter.gets&.strip
135
- unless ans&.downcase&.start_with?("y")
136
- abort("Aborted. Re-run with SKIP_GEM_SIGNING=true bundle exec kettle-release (or set it in your environment).")
163
+ # 13. signing guidance and checks
164
+ if @start_step <= 13
165
+ if ENV.fetch("SKIP_GEM_SIGNING", "false").casecmp("false").zero?
166
+ puts "TIP: For local dry-runs or testing the release workflow, set SKIP_GEM_SIGNING=true to avoid PEM password prompts."
167
+ if Kettle::Dev::InputAdapter.tty?
168
+ # In CI, avoid interactive prompts when no TTY is present (e.g., act or GitHub Actions "CI validation").
169
+ # Non-interactive CI runs should not abort here; later signing checks are either stubbed in tests
170
+ # or will be handled explicitly by ensure_signing_setup_or_skip!.
171
+ print("Proceed with signing enabled? This may hang waiting for a PEM password. [y/N]: ")
172
+ ans = Kettle::Dev::InputAdapter.gets&.strip
173
+ unless ans&.downcase&.start_with?("y")
174
+ abort("Aborted. Re-run with SKIP_GEM_SIGNING=true bundle exec kettle-release (or set it in your environment).")
175
+ end
176
+ else
177
+ warn("Non-interactive shell detected (non-TTY); skipping interactive signing confirmation.")
137
178
  end
138
179
  end
180
+
181
+ ensure_signing_setup_or_skip!
139
182
  end
140
183
 
141
- ensure_signing_setup_or_skip!
142
- puts "Running build (you may be prompted for the signing key password)..."
143
- run_cmd!("bundle exec rake build")
184
+ # 14. build
185
+ if @start_step <= 14
186
+ puts "Running build (you may be prompted for the signing key password)..."
187
+ run_cmd!("bundle exec rake build")
188
+ end
144
189
 
145
- run_cmd!("bin/gem_checksums")
146
- validate_checksums!(version, stage: "after build + gem_checksums")
190
+ # 15. checksums validate
191
+ if @start_step <= 15
192
+ run_cmd!("bin/gem_checksums")
193
+ version ||= detect_version
194
+ validate_checksums!(version, stage: "after build + gem_checksums")
195
+ end
147
196
 
148
- puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
149
- run_cmd!("bundle exec rake release")
150
- validate_checksums!(version, stage: "after release")
197
+ # 16. release and validate
198
+ if @start_step <= 16
199
+ puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
200
+ run_cmd!("bundle exec rake release")
201
+ version ||= detect_version
202
+ validate_checksums!(version, stage: "after release")
203
+ end
151
204
 
152
205
  puts "\nRelease complete. Don't forget to push the checksums commit if needed."
153
206
  end
@@ -155,87 +208,9 @@ module Kettle
155
208
  private
156
209
 
157
210
  def monitor_workflows_after_push!
158
- root = Kettle::Dev::CIHelpers.project_root
159
- workflows = Kettle::Dev::CIHelpers.workflows_list(root)
160
- gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
161
-
162
- branch = Kettle::Dev::CIHelpers.current_branch
163
- abort("Could not determine current branch for CI checks.") unless branch
164
-
165
- gh_remote = preferred_github_remote
166
- gh_owner = nil
167
- gh_repo = nil
168
- if gh_remote && !workflows.empty?
169
- url = remote_url(gh_remote)
170
- gh_owner, gh_repo = parse_github_owner_repo(url)
171
- end
172
-
173
- checks_any = false
174
-
175
- if gh_owner && gh_repo && !workflows.empty?
176
- checks_any = true
177
- total = workflows.size
178
- abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
179
-
180
- passed = {}
181
- idx = 0
182
- puts "Ensuring GitHub Actions workflows pass on #{branch} (#{gh_owner}/#{gh_repo}) via remote '#{gh_remote}'"
183
- pbar = if defined?(ProgressBar)
184
- ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
185
- end
186
-
187
- loop do
188
- wf = workflows[idx]
189
- run = Kettle::Dev::CIHelpers.latest_run(owner: gh_owner, repo: gh_repo, workflow_file: wf, branch: branch)
190
- if run
191
- if Kettle::Dev::CIHelpers.success?(run)
192
- unless passed[wf]
193
- passed[wf] = true
194
- pbar&.increment
195
- end
196
- elsif Kettle::Dev::CIHelpers.failed?(run)
197
- puts
198
- url = run["html_url"] || "https://github.com/#{gh_owner}/#{gh_repo}/actions/workflows/#{wf}"
199
- abort("Workflow failed: #{wf} -> #{url}")
200
- end
201
- end
202
- break if passed.size == total
203
- idx = (idx + 1) % total
204
- sleep(1)
205
- end
206
- pbar&.finish unless pbar&.finished?
207
- puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
208
- end
209
-
210
- gl_remote = gitlab_remote_candidates.first
211
- if gitlab_ci && gl_remote
212
- owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
213
- if owner && repo
214
- checks_any = true
215
- puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
216
- pbar = if defined?(ProgressBar)
217
- ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
218
- end
219
- loop do
220
- pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
221
- if pipe
222
- if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
223
- pbar&.increment unless pbar&.finished?
224
- break
225
- elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
226
- puts
227
- url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
228
- abort("Pipeline failed: #{url}")
229
- end
230
- end
231
- sleep(1)
232
- end
233
- pbar&.finish unless pbar&.finished?
234
- puts "\nGitLab pipeline passing."
235
- end
236
- end
237
-
238
- abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
211
+ # Delegate to shared CI monitor to keep logic DRY across release flow and rake tasks
212
+ require "kettle/dev/ci_monitor"
213
+ Kettle::Dev::CIMonitor.monitor_all!(restart_hint: "bundle exec kettle-release start_step=10")
239
214
  end
240
215
 
241
216
  def run_cmd!(cmd)
@@ -608,8 +583,8 @@ module Kettle
608
583
  end
609
584
 
610
585
  def ensure_signing_setup_or_skip!
611
- # Treat any non-empty value as an explicit skip signal (more robust across Ruby versions and ENV adapters)
612
- return if ENV["SKIP_GEM_SIGNING"].to_s.strip != ""
586
+ # Treat any non-/true/i value as an explicit skip signal
587
+ return if ENV.fetch("SKIP_GEM_SIGNING", "").casecmp("true").zero?
613
588
 
614
589
  user = ENV.fetch("GEM_CERT_USER", ENV["USER"])
615
590
  cert_path = File.join(@root, "certs", "#{user}.pem")
@@ -132,6 +132,13 @@ module Kettle
132
132
  end
133
133
  fetch_and_print_status.call(file)
134
134
  run_act_for.call(file_path)
135
+ # After running locally, check upstream GitLab pipeline status if configured
136
+ begin
137
+ require "kettle/dev/ci_monitor"
138
+ Kettle::Dev::CIMonitor.monitor_gitlab!(restart_hint: "bundle exec rake ci:act")
139
+ rescue LoadError
140
+ # ignore if not available
141
+ end
135
142
  return
136
143
  end
137
144
 
@@ -186,6 +193,10 @@ module Kettle
186
193
  print(prompt)
187
194
  $stdout.flush
188
195
 
196
+ # We need to sleep a bit here to ensure the terminal is ready for both
197
+ # input and writing status updates to each workflow's line
198
+ sleep(0.2) unless Kettle::Dev::IS_CI
199
+
189
200
  selected = nil
190
201
  input_thread = Thread.new do
191
202
  begin
@@ -330,6 +341,13 @@ module Kettle
330
341
  abort("ci:act aborted: workflow not found: #{file_path}") unless File.file?(file_path)
331
342
  fetch_and_print_status.call(chosen_file)
332
343
  run_act_for.call(file_path)
344
+ # After running locally, check upstream GitLab pipeline status if configured
345
+ begin
346
+ require "kettle/dev/ci_monitor"
347
+ Kettle::Dev::CIMonitor.monitor_gitlab!(restart_hint: "bundle exec rake ci:act")
348
+ rescue LoadError
349
+ # ignore if not available
350
+ end
333
351
  end
334
352
  end
335
353
  end
@@ -96,7 +96,7 @@ module Kettle
96
96
  )
97
97
 
98
98
  # 4) gemfiles/modular/*.gemfile (from gem's gemfiles/modular)
99
- [%w[coverage.gemfile], %w[documentation.gemfile], %w[style.gemfile]].each do |base|
99
+ [%w[coverage.gemfile], %w[documentation.gemfile], %w[style.gemfile], %w[optional.gemfile]].each do |base|
100
100
  src = helpers.prefer_example(File.join(gem_checkout_root, "gemfiles/modular", base[0]))
101
101
  dest = File.join(project_root, "gemfiles/modular", base[0])
102
102
  if File.basename(src).sub(/\.example\z/, "") == "style.gemfile"
@@ -6,7 +6,7 @@ module Kettle
6
6
  module Version
7
7
  # The gem version.
8
8
  # @return [String]
9
- VERSION = "1.0.11"
9
+ VERSION = "1.0.12"
10
10
  end
11
11
  end
12
12
  end
@@ -31,7 +31,7 @@ module Kettle
31
31
  ?branch: String?,
32
32
  ?host: String,
33
33
  ?token: String?
34
- ) -> { "status" => String, "web_url" => String, "id" => Integer }?
34
+ ) -> { "status" => String, "web_url" => String, "id" => Integer, "failure_reason" => String? }?
35
35
  def self.gitlab_success?: ({ "status" => String }?) -> bool
36
36
  def self.gitlab_failed?: ({ "status" => String }?) -> bool
37
37
  end
@@ -0,0 +1,8 @@
1
+ module Kettle
2
+ module Dev
3
+ module CIMonitor
4
+ def self.monitor_all!: (?restart_hint: String) -> void
5
+ def self.monitor_gitlab!: (?restart_hint: String) -> bool
6
+ end
7
+ end
8
+ end
@@ -1,7 +1,7 @@
1
1
  module Kettle
2
2
  module Dev
3
3
  class ReleaseCLI
4
- def initialize: () -> void
4
+ def initialize: (?start_step: Integer) -> void
5
5
  def run: () -> void
6
6
  end
7
7
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.11
4
+ version: 1.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -270,6 +270,7 @@ files:
270
270
  - CODE_OF_CONDUCT.md
271
271
  - CONTRIBUTING.md
272
272
  - Gemfile
273
+ - Gemfile.example
273
274
  - LICENSE.txt
274
275
  - README.md
275
276
  - README.md.example
@@ -288,6 +289,7 @@ files:
288
289
  - lib/kettle-dev.rb
289
290
  - lib/kettle/dev.rb
290
291
  - lib/kettle/dev/ci_helpers.rb
292
+ - lib/kettle/dev/ci_monitor.rb
291
293
  - lib/kettle/dev/commit_msg.rb
292
294
  - lib/kettle/dev/exit_adapter.rb
293
295
  - lib/kettle/dev/git_adapter.rb
@@ -317,6 +319,7 @@ files:
317
319
  - sig/kettle-dev.rbs
318
320
  - sig/kettle/dev.rbs
319
321
  - sig/kettle/dev/ci_helpers.rbs
322
+ - sig/kettle/dev/ci_monitor.rbs
320
323
  - sig/kettle/dev/commit_msg.rbs
321
324
  - sig/kettle/dev/exit_adapter.rbs
322
325
  - sig/kettle/dev/git_adapter.rbs
@@ -336,10 +339,10 @@ licenses:
336
339
  - MIT
337
340
  metadata:
338
341
  homepage_uri: https://kettle-dev.galtzo.com/
339
- source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.0.11
340
- changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.0.11/CHANGELOG.md
342
+ source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.0.12
343
+ changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.0.12/CHANGELOG.md
341
344
  bug_tracker_uri: https://github.com/kettle-rb/kettle-dev/issues
342
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.11
345
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.12
343
346
  funding_uri: https://github.com/sponsors/pboling
344
347
  wiki_uri: https://github.com/kettle-rb/kettle-dev/wiki
345
348
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file