kettle-dev 1.0.3 → 1.0.5

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: 8e3927849812294208b1d9f6f582fbe820790c106e168b9fb0928069aedb2217
4
- data.tar.gz: ce1c09f75c7d044737f06c541da118c3925925858db156acd8bf97f71e7c13ca
3
+ metadata.gz: aaab564b37e2892996a6f419cb71a48aa2f7e0ac5536379f0f7de36710922060
4
+ data.tar.gz: 878eced8cbf467f399689ba5a93d93f8ffecea1eca451f7b1752c4c41b87ad9e
5
5
  SHA512:
6
- metadata.gz: cdb45f3c3440b061c0ccbc21a9a315a7d87be88ce8f2389a0b530a21615e7908ea397ea4208a34f3e4eeb4e2af2a7b6c9db466af25348ae927840be46a456d55
7
- data.tar.gz: d9a60711d6d6c09b4203531551b733ae0c148da55507169cb9cbc0f002cc7e2f69b4c17b53ae0f6e3d8cff6b4302c5cb47209672d2969aa73247b3c14bb50752
6
+ metadata.gz: 01c054122983c1c36bc0ec39c42a35732b9fb6225706e0034d6209923f0ac3704f874c23e7900617dc044a6b0245315bdbc8da72534a86f0eed87cd5156f94f3
7
+ data.tar.gz: a52bc544f7243a1b727e956cef403601689f3331c5cb3376a4b9135eb0c2ca3353f3e048709e2f153cae847a74eab55c5c21f7d432cb621e2416120314e8c560
checksums.yaml.gz.sig CHANGED
Binary file
data/.envrc CHANGED
@@ -20,7 +20,7 @@ export K_SOUP_COV_DO=true # Means you want code coverage
20
20
  export K_SOUP_COV_COMMAND_NAME="Test Coverage"
21
21
  # Available formats are html, xml, rcov, lcov, json, tty
22
22
  export K_SOUP_COV_FORMATTERS="html,xml,rcov,lcov,json,tty"
23
- export K_SOUP_COV_MIN_BRANCH=100 # Means you want to enforce X% branch coverage
23
+ export K_SOUP_COV_MIN_BRANCH=96 # Means you want to enforce X% branch coverage
24
24
  export K_SOUP_COV_MIN_LINE=100 # Means you want to enforce X% line coverage
25
25
  export K_SOUP_COV_MIN_HARD=true # Means you want the build to fail if the coverage thresholds are not met
26
26
  export K_SOUP_COV_MULTI_FORMATTERS=true
data/CHANGELOG.md CHANGED
@@ -12,6 +12,27 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
12
12
  ### Fixed
13
13
  ### Security
14
14
 
15
+ ## [1.0.5] - 2025-08-24
16
+ - TAG: [v1.0.5][1.0.5t]
17
+ - COVERAGE: 100.00% -- 130/130 lines in 7 files
18
+ - BRANCH COVERAGE: 96.00% -- 48/50 branches in 7 files
19
+ - 95.35% documented
20
+ ### Fixed
21
+ - kettle-release: will run regardless of how it is invoked (i.e. works as binstub)
22
+
23
+ ## [1.0.4] - 2025-08-24
24
+ - TAG: [v1.0.4][1.0.4t]
25
+ - COVERAGE: 100.00% -- 130/130 lines in 7 files
26
+ - BRANCH COVERAGE: 96.00% -- 48/50 branches in 7 files
27
+ - 95.35% documented
28
+ ### Added
29
+ - kettle-release: checks all remotes for a GitHub remote and syncs origin/trunk with it; prompts to rebase or --no-ff merge when histories diverge; pushes to both origin and the GitHub remote on merge; uses the GitHub remote for GitHub Actions CI checks, and also checks GitLab CI when a GitLab remote and .gitlab-ci.yml are present.
30
+ - kettle-release: push logic improved — if a remote named `all` exists, push the current branch to it (assumed to cover multiple push URLs). Otherwise push the current branch to `origin` and to any GitHub, GitLab, and Codeberg remotes (whatever their names are).
31
+ ### Fixed
32
+ - kettle-release now validates SHA256 checksums of the built gem against the recorded checksums and aborts on mismatch; helps ensure reproducible artifacts (honoring SOURCE_DATE_EPOCH).
33
+ - kettle-release now enforces CI checks and aborts if CI cannot be verified; supports GitHub Actions and GitLab pipelines, including releases from trunk/main.
34
+ - kettle-release no longer requires bundler/setup, preventing silent exits when invoked from a dependent project; adds robust output flushing.
35
+
15
36
  ## [1.0.3] - 2025-08-24
16
37
  - TAG: [v1.0.3][1.0.3t]
17
38
  - COVERAGE: 100.00% -- 98/98 lines in 7 files
@@ -23,7 +44,6 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
23
44
  - kettle-release now uses the host project's root, instead of this gem's installed root.
24
45
  - Added .git-hooks files necessary for git hooks to work
25
46
 
26
-
27
47
  ## [1.0.2] - 2025-08-24
28
48
  - TAG: [v1.0.2][1.0.2t]
29
49
  - COVERAGE: 100.00% -- 98/98 lines in 7 files
@@ -71,7 +91,11 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
71
91
  - Selecting will run the selected workflow via `act`
72
92
  - This may move to its own gem in the future.
73
93
 
74
- [Unreleased]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.3...HEAD
94
+ [Unreleased]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.5...HEAD
95
+ [1.0.5]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.4...v1.0.5
96
+ [1.0.5t]: https://gitlab.com/kettle-rb/kettle-dev/-/tags/v1.0.5
97
+ [1.0.4]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.3...v1.0.4
98
+ [1.0.4t]: https://gitlab.com/kettle-rb/kettle-dev/-/tags/v1.0.4
75
99
  [1.0.3]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.2...v1.0.3
76
100
  [1.0.3t]: https://gitlab.com/kettle-rb/kettle-dev/-/tags/v1.0.3
77
101
  [1.0.2]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.1...v1.0.2
data/CONTRIBUTING.md CHANGED
@@ -127,7 +127,9 @@ Run `kettle-release`.
127
127
  to create SHA-256 and SHA-512 checksums. This functionality is provided by the `stone_checksums`
128
128
  [gem][💎stone_checksums].
129
129
  - The script automatically commits but does not push the checksums
130
- 12. Run `bundle exec rake release` which will create a git tag for the version,
130
+ 12. Sanity check the SHA256, comparing with the output from the `bin/gem_checksums` command:
131
+ - `sha256sum pkg/<gem name>-<version>.gem`
132
+ 13. Run `bundle exec rake release` which will create a git tag for the version,
131
133
  push git commits and tags, and push the `.gem` file to [rubygems.org][💎rubygems]
132
134
 
133
135
  [🚎src-main]: https://gitlab.com/kettle-rb/kettle-dev
@@ -0,0 +1 @@
1
+ 772ba761ef205f134e3a096fbfde418b3d699093482ff345bd755f8f596f9de7
@@ -0,0 +1 @@
1
+ 5e791a2feaa44cfcede4c5e933f2d44725dd6c84d5b44511d8e26147a1540ed3455508fd9c2e1ed8d07093009863b2db38ff9532939404aed7f047091c495e36
@@ -0,0 +1 @@
1
+ 60434bcaca59d509c76b746ca2707c4203c6e78fe1af64073c3e871a55ad72aa
@@ -0,0 +1 @@
1
+ e818b9baeced996daebc4565812ff015ba552788220ff34773d2c5d7e3ff7e9a1adca4ae16379dd9551824c7e5a8bef877c66a14faa6795d3982b1faa0d35f07
data/exe/kettle-release CHANGED
@@ -12,18 +12,31 @@
12
12
  # - Runs bin/gem_checksums
13
13
  # - Runs `bundle exec rake release` (expects PEM password and RubyGems MFA OTP)
14
14
 
15
+ $stdout.sync = true
15
16
  require "rubygems"
16
- require "bundler/setup"
17
-
18
- require "open3"
19
- require "shellwords"
20
- require "time"
21
- require "fileutils"
22
- require "net/http"
23
- require "json"
24
- require "uri"
25
- require "kettle/dev/ci_helpers"
26
- require "ruby-progressbar"
17
+ # Ensure we run within the host project's Bundler context (do not override BUNDLE_GEMFILE)
18
+ begin
19
+ require "bundler/setup"
20
+ rescue LoadError
21
+ # Allow running outside of Bundler; runtime deps should still be available via rubygems
22
+ end
23
+
24
+ begin
25
+ require "open3"
26
+ require "shellwords"
27
+ require "time"
28
+ require "fileutils"
29
+ require "net/http"
30
+ require "json"
31
+ require "uri"
32
+ require "kettle/dev/ci_helpers"
33
+ require "ruby-progressbar"
34
+ rescue LoadError => e
35
+ warn("kettle-release: failed to load a required library: #{e.message}")
36
+ warn("Hint: Ensure the host project includes kettle-dev and its runtime deps, then run bundle install.")
37
+ warn(e.backtrace.join("\n")) if ENV["DEBUG"]
38
+ exit(1)
39
+ end
27
40
 
28
41
  module Kettle
29
42
  module Dev
@@ -88,10 +101,13 @@ module Kettle
88
101
 
89
102
  # Checksums (commits, but does not push)
90
103
  run_cmd!("bin/gem_checksums")
104
+ validate_checksums!(version, stage: "after build + gem_checksums")
91
105
 
92
106
  # Release: expect PEM password + RubyGems MFA OTP
93
107
  puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
94
108
  run_cmd!("bundle exec rake release")
109
+ # Some release tasks rebuild the gem; re-validate to ensure reproducibility
110
+ validate_checksums!(version, stage: "after release")
95
111
 
96
112
  puts "\nRelease complete. Don't forget to push the checksums commit if needed."
97
113
  end
@@ -103,53 +119,89 @@ module Kettle
103
119
  def monitor_workflows_after_push!
104
120
  root = Kettle::Dev::CIHelpers.project_root
105
121
  workflows = Kettle::Dev::CIHelpers.workflows_list(root)
106
- if workflows.empty?
107
- puts "No workflows detected under .github/workflows; skipping CI checks."
108
- return
109
- end
122
+ gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
110
123
 
111
- owner, repo = Kettle::Dev::CIHelpers.repo_info
112
124
  branch = Kettle::Dev::CIHelpers.current_branch
113
- unless owner && repo && branch
114
- puts "Unable to determine repository or branch; skipping CI checks."
115
- return
125
+ abort("Could not determine current branch for CI checks.") unless branch
126
+
127
+ # Prefer an explicit GitHub remote if available; fall back to origin repo_info
128
+ gh_remote = preferred_github_remote
129
+ gh_owner = nil
130
+ gh_repo = nil
131
+ if gh_remote && !workflows.empty?
132
+ url = remote_url(gh_remote)
133
+ gh_owner, gh_repo = parse_github_owner_repo(url)
116
134
  end
117
135
 
118
- total = workflows.size
119
- passed = {}
120
- idx = 0
121
- puts "Ensuring CI workflows pass on branch #{branch} (#{owner}/#{repo})"
122
- pbar = ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
123
-
124
- loop do
125
- wf = workflows[idx]
126
- run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch)
127
- if run
128
- if Kettle::Dev::CIHelpers.success?(run)
129
- unless passed[wf]
130
- passed[wf] = true
131
- pbar.increment
136
+ checks_any = false
137
+
138
+ if gh_owner && gh_repo && !workflows.empty?
139
+ checks_any = true
140
+ total = workflows.size
141
+ abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
142
+
143
+ passed = {}
144
+ idx = 0
145
+ puts "Ensuring GitHub Actions workflows pass on #{branch} (#{gh_owner}/#{gh_repo}) via remote '#{gh_remote}'"
146
+ pbar = ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
147
+
148
+ loop do
149
+ wf = workflows[idx]
150
+ run = Kettle::Dev::CIHelpers.latest_run(owner: gh_owner, repo: gh_repo, workflow_file: wf, branch: branch)
151
+ if run
152
+ if Kettle::Dev::CIHelpers.success?(run)
153
+ unless passed[wf]
154
+ passed[wf] = true
155
+ pbar.increment
156
+ end
157
+ elsif Kettle::Dev::CIHelpers.failed?(run)
158
+ puts
159
+ url = run["html_url"] || "https://github.com/#{gh_owner}/#{gh_repo}/actions/workflows/#{wf}"
160
+ abort("Workflow failed: #{wf} -> #{url}")
132
161
  end
133
- elsif Kettle::Dev::CIHelpers.failed?(run)
134
- # Fail fast with link to the failed run
135
- puts
136
- url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"
137
- abort("Workflow failed: #{wf} -> #{url}")
138
162
  end
163
+ break if passed.size == total
164
+ idx = (idx + 1) % total
165
+ sleep(1)
139
166
  end
167
+ pbar.finish unless pbar.finished?
168
+ puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
169
+ end
140
170
 
141
- break if passed.size == total
142
-
143
- idx = (idx + 1) % total
144
- sleep(1)
171
+ # Additionally, check GitLab if configured
172
+ gl_remote = gitlab_remote_candidates.first
173
+ if gitlab_ci && gl_remote
174
+ owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
175
+ if owner && repo
176
+ checks_any = true
177
+ puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
178
+ pbar = ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
179
+ loop do
180
+ pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
181
+ if pipe
182
+ if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
183
+ pbar.increment unless pbar.finished?
184
+ break
185
+ elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
186
+ puts
187
+ url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
188
+ abort("Pipeline failed: #{url}")
189
+ end
190
+ end
191
+ sleep(1)
192
+ end
193
+ pbar.finish unless pbar.finished?
194
+ puts "\nGitLab pipeline passing."
195
+ end
145
196
  end
146
- pbar.finish unless pbar.finished?
147
- puts "\nAll workflows passing (#{passed.size}/#{total})."
197
+
198
+ abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
148
199
  end
149
200
 
150
201
  def run_cmd!(cmd)
151
202
  puts "$ #{cmd}"
152
- success = system(cmd)
203
+ # Ensure current ENV (including SOURCE_DATE_EPOCH) is propagated explicitly
204
+ success = system(ENV, cmd)
153
205
  abort("Command failed: #{cmd}") unless success
154
206
  end
155
207
 
@@ -192,11 +244,45 @@ module Kettle
192
244
  end
193
245
 
194
246
  def push!
195
- puts "$ git push"
196
- success = system("git push")
197
- unless success
198
- warn("Normal push failed; retrying with force push...")
199
- run_cmd!("git push -f")
247
+ branch = current_branch
248
+ abort("Could not determine current branch to push.") unless branch
249
+
250
+ if has_remote?("all")
251
+ puts "$ git push all #{branch}"
252
+ success = system("git push all #{Shellwords.escape(branch)}")
253
+ unless success
254
+ warn("Normal push to 'all' failed; retrying with force push...")
255
+ run_cmd!("git push -f all #{Shellwords.escape(branch)}")
256
+ end
257
+ return
258
+ end
259
+
260
+ # Build the list of remotes to push to
261
+ remotes = []
262
+ remotes << "origin" if has_remote?("origin")
263
+ remotes |= github_remote_candidates
264
+ remotes |= gitlab_remote_candidates
265
+ remotes |= codeberg_remote_candidates
266
+ remotes.uniq!
267
+
268
+ if remotes.empty?
269
+ # Fallback to default behavior if we couldn't detect any remotes
270
+ puts "$ git push #{branch}"
271
+ success = system("git push #{Shellwords.escape(branch)}")
272
+ unless success
273
+ warn("Normal push failed; retrying with force push...")
274
+ run_cmd!("git push -f #{Shellwords.escape(branch)}")
275
+ end
276
+ return
277
+ end
278
+
279
+ remotes.each do |remote|
280
+ puts "$ git push #{remote} #{branch}"
281
+ success = system("git push #{Shellwords.escape(remote)} #{Shellwords.escape(branch)}")
282
+ unless success
283
+ warn("Push to #{remote} failed; retrying with force push...")
284
+ run_cmd!("git push -f #{Shellwords.escape(remote)} #{Shellwords.escape(branch)}")
285
+ end
200
286
  end
201
287
  end
202
288
 
@@ -226,6 +312,56 @@ module Kettle
226
312
  ok ? out.split(/\s+/).reject(&:empty?) : []
227
313
  end
228
314
 
315
+ def remotes_with_urls
316
+ out, ok = git_output(["remote", "-v"])
317
+ return {} unless ok
318
+ urls = {}
319
+ out.each_line do |line|
320
+ if line =~ /(\S+)\s+(\S+)\s+\((fetch|push)\)/
321
+ name = Regexp.last_match(1)
322
+ url = Regexp.last_match(2)
323
+ kind = Regexp.last_match(3)
324
+ # prefer fetch URL when available
325
+ urls[name] = url if kind == "fetch" || !urls.key?(name)
326
+ end
327
+ end
328
+ urls
329
+ end
330
+
331
+ def remote_url(name)
332
+ remotes_with_urls[name]
333
+ end
334
+
335
+ def github_remote_candidates
336
+ remotes_with_urls.select { |n, u| u.include?("github.com") }.keys
337
+ end
338
+
339
+ def gitlab_remote_candidates
340
+ remotes_with_urls.select { |n, u| u.include?("gitlab.com") }.keys
341
+ end
342
+
343
+ def codeberg_remote_candidates
344
+ remotes_with_urls.select { |n, u| u.include?("codeberg.org") }.keys
345
+ end
346
+
347
+ def preferred_github_remote
348
+ cands = github_remote_candidates
349
+ return if cands.empty?
350
+ # Prefer a remote literally named 'github', otherwise the first
351
+ cands.find { |n| n == "github" } || cands.first
352
+ end
353
+
354
+ def parse_github_owner_repo(url)
355
+ return [nil, nil] unless url
356
+ if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
357
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
358
+ elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
359
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
360
+ else
361
+ [nil, nil]
362
+ end
363
+ end
364
+
229
365
  def has_remote?(name)
230
366
  list_remotes.include?(name)
231
367
  end
@@ -271,7 +407,7 @@ module Kettle
271
407
  return
272
408
  end
273
409
 
274
- # Default behavior: ensure local trunk is not behind origin/trunk; if it is, rebase flows
410
+ # Ensure local trunk is in sync with origin/trunk
275
411
  run_cmd!("git fetch origin #{Shellwords.escape(trunk)}")
276
412
  if trunk_behind_remote?(trunk, "origin")
277
413
  puts "Local #{trunk} is behind origin/#{trunk}. Rebasing..."
@@ -284,6 +420,56 @@ module Kettle
284
420
  else
285
421
  puts "Local #{trunk} is up to date with origin/#{trunk}."
286
422
  end
423
+
424
+ # If there is a GitHub remote that is not origin, ensure origin/#{trunk} incorporates it
425
+ gh_remote = preferred_github_remote
426
+ if gh_remote && gh_remote != "origin"
427
+ puts "GitHub remote detected: #{gh_remote}. Fetching #{trunk}..."
428
+ run_cmd!("git fetch #{gh_remote} #{Shellwords.escape(trunk)}")
429
+
430
+ # Compare origin/trunk vs github/trunk to see if they differ
431
+ left, right = ahead_behind_counts("origin/#{trunk}", "#{gh_remote}/#{trunk}")
432
+ if left.zero? && right.zero?
433
+ puts "origin/#{trunk} and #{gh_remote}/#{trunk} are already in sync."
434
+ return
435
+ end
436
+
437
+ checkout!(trunk)
438
+ run_cmd!("git pull --rebase origin #{Shellwords.escape(trunk)}")
439
+
440
+ if left.positive? && right.positive?
441
+ # Histories have diverged -> let user choose
442
+ puts "origin/#{trunk} and #{gh_remote}/#{trunk} have diverged (#{left} ahead of GH, #{right} behind GH)."
443
+ puts "Choose how to reconcile:"
444
+ puts " [r] Rebase local/#{trunk} on top of #{gh_remote}/#{trunk} (push to origin)"
445
+ puts " [m] Merge --no-ff #{gh_remote}/#{trunk} into #{trunk} (push to origin and #{gh_remote})"
446
+ puts " [a] Abort"
447
+ print("> ")
448
+ choice = $stdin.gets&.strip&.downcase
449
+ case choice
450
+ when "r"
451
+ run_cmd!("git rebase #{Shellwords.escape("#{gh_remote}/#{trunk}")}")
452
+ run_cmd!("git push origin #{Shellwords.escape(trunk)}")
453
+ puts "Rebased #{trunk} onto #{gh_remote}/#{trunk} and pushed to origin."
454
+ when "m"
455
+ run_cmd!("git merge --no-ff #{Shellwords.escape("#{gh_remote}/#{trunk}")}")
456
+ run_cmd!("git push origin #{Shellwords.escape(trunk)}")
457
+ run_cmd!("git push #{Shellwords.escape(gh_remote)} #{Shellwords.escape(trunk)}")
458
+ puts "Merged #{gh_remote}/#{trunk} into #{trunk} and pushed to origin and #{gh_remote}."
459
+ else
460
+ abort("Aborted by user. Please reconcile trunks and re-run.")
461
+ end
462
+ elsif right.positive? && left.zero?
463
+ # One side can be fast-forwarded
464
+ puts "Fast-forwarding #{trunk} to include #{gh_remote}/#{trunk}..."
465
+ run_cmd!("git merge --ff-only #{Shellwords.escape("#{gh_remote}/#{trunk}")}")
466
+ run_cmd!("git push origin #{Shellwords.escape(trunk)}")
467
+ # origin is behind GH -> fast-forward merge
468
+ elsif left.positive? && right.zero?
469
+ # origin ahead of GH -> nothing required for origin, optionally inform user
470
+ puts "origin/#{trunk} is ahead of #{gh_remote}/#{trunk}; no action required before push."
471
+ end
472
+ end
287
473
  end
288
474
 
289
475
  def merge_feature_into_trunk_and_push!(trunk, feature)
@@ -317,10 +503,100 @@ module Kettle
317
503
  puts "Found signing cert: #{cert_path}"
318
504
  puts "When prompted during build/release, enter the PEM password for ~/.ssh/gem-private_key.pem"
319
505
  end
506
+
507
+ # --- Checksum validation ---
508
+ # Validate that the sha256 of the built gem in pkg/ matches the recorded
509
+ # checksum stored under checksums/<gem>.gem.sha256. Abort with guidance if not.
510
+ # @param version [String]
511
+ # @param stage [String] human-readable context (e.g., "after release")
512
+ def validate_checksums!(version, stage: "")
513
+ gem_path = gem_file_for_version(version)
514
+ unless gem_path && File.file?(gem_path)
515
+ abort("Unable to locate built gem for version #{version} in pkg/. Did the build succeed?")
516
+ end
517
+ actual = compute_sha256(gem_path)
518
+ checks_path = File.join(@root, "checksums", "#{File.basename(gem_path)}.sha256")
519
+ unless File.file?(checks_path)
520
+ abort("Expected checksum file not found: #{checks_path}. Did bin/gem_checksums run?")
521
+ end
522
+ expected = File.read(checks_path).strip
523
+ if actual != expected
524
+ abort(<<~MSG)
525
+ SHA256 mismatch #{stage}:
526
+ gem: #{gem_path}
527
+ sha256sum: #{actual}
528
+ file: #{checks_path}
529
+ file: #{expected}
530
+ Ensure SOURCE_DATE_EPOCH is set consistently and that the artifact used by release is identical to the one checksummed.
531
+ You can retry: export SOURCE_DATE_EPOCH=$EPOCHSECONDS; bundle exec rake build && bin/gem_checksums && bundle exec rake release
532
+ MSG
533
+ else
534
+ puts "Checksum OK #{stage}: #{File.basename(gem_path)}"
535
+ end
536
+ end
537
+
538
+ # Find the gem file in pkg/ that matches the given version
539
+ def gem_file_for_version(version)
540
+ pkg = File.join(@root, "pkg")
541
+ pattern = File.join(pkg, "*.gem")
542
+ gems = Dir[pattern].select { |p| File.basename(p).include?("-#{version}.gem") }
543
+ gems.sort.last
544
+ end
545
+
546
+ # Compute sha256 using system utilities (sha256sum or shasum -a 256),
547
+ # falling back to Ruby Digest if neither is available.
548
+ def compute_sha256(path)
549
+ if system("which sha256sum > /dev/null 2>&1")
550
+ out, _ = Open3.capture2e("sha256sum", path)
551
+ out.split.first
552
+ elsif system("which shasum > /dev/null 2>&1")
553
+ out, _ = Open3.capture2e("shasum", "-a", "256", path)
554
+ out.split.first
555
+ else
556
+ require "digest"
557
+ Digest::SHA256.file(path).hexdigest
558
+ end
559
+ end
320
560
  end
321
561
  end
322
562
  end
323
563
 
324
- if __FILE__ == $PROGRAM_NAME
564
+ # Always execute when this file is loaded (e.g., via a Bundler binstub).
565
+ # Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
566
+ if ARGV.include?("-h") || ARGV.include?("--help")
567
+ puts <<~USAGE
568
+ Usage: kettle-release
569
+
570
+ Automates the release flow for a Ruby gem in the host project:
571
+ - Runs bin/setup and bin/rake sanity checks
572
+ - Prompts to confirm version and changelog updates
573
+ - Commits a release prep change
574
+ - Ensures trunk is up-to-date, pushes branch, and monitors CI (GitHub/GitLab)
575
+ - Merges feature into trunk upon CI success
576
+ - Exports SOURCE_DATE_EPOCH, builds, records checksums, and releases
577
+
578
+ Environment:
579
+ SKIP_GEM_SIGNING=true # skip gem signing during build/release
580
+ GEM_CERT_USER=<user> # selects certs/<user>.pem for signing
581
+ GITHUB_TOKEN / GH_TOKEN # optional, to query GitHub Actions
582
+ GITLAB_TOKEN / GL_TOKEN # optional, to query GitLab pipelines
583
+ DEBUG=true # print backtraces on errors
584
+ USAGE
585
+ exit 0
586
+ end
587
+
588
+ begin
325
589
  Kettle::Dev::ReleaseCLI.new.run
590
+ rescue LoadError => e
591
+ warn("kettle-release: could not load dependency: #{e.message}")
592
+ warn(e.backtrace.join("\n")) if ENV["DEBUG"]
593
+ exit(1)
594
+ rescue SystemExit => e
595
+ # Preserve exit status, but ensure at least a newline so shells don't show an empty line only.
596
+ warn("kettle-release exited (status=#{e.status})") if e.status != 0
597
+ raise
598
+ rescue StandardError => e
599
+ warn("kettle-release: unexpected error: #{e.class}: #{e.message}")
600
+ warn(e.backtrace.join("\n"))
601
+ exit(1)
326
602
  end
@@ -16,11 +16,6 @@ module Kettle
16
16
  module_function
17
17
 
18
18
  # Determine the project root directory.
19
- #
20
- # Prefers the directory Rake was invoked from (Rake.application.original_dir)
21
- # so that tasks shipped with this gem operate relative to the host project.
22
- # Falls back to the current working directory when Rake context is absent.
23
- #
24
19
  # @return [String] absolute path to the project root
25
20
  def project_root
26
21
  # Too difficult to test every possible branch here, so ignoring
@@ -33,10 +28,8 @@ module Kettle
33
28
  end
34
29
 
35
30
  # Parse the GitHub owner/repo from the configured origin remote.
36
- #
37
31
  # Supports SSH (git@github.com:owner/repo(.git)) and HTTPS
38
32
  # (https://github.com/owner/repo(.git)) forms.
39
- #
40
33
  # @return [Array(String, String), nil] [owner, repo] or nil when unavailable
41
34
  def repo_info
42
35
  out, status = Open3.capture2("git", "config", "--get", "remote.origin.url")
@@ -57,9 +50,7 @@ module Kettle
57
50
  end
58
51
 
59
52
  # List workflow YAML basenames under .github/workflows at the given root.
60
- #
61
53
  # Excludes maintenance workflows defined by {#exclusions}.
62
- #
63
54
  # @param root [String] project root (defaults to {#project_root})
64
55
  # @return [Array<String>] sorted list of basenames (e.g., "ci.yml")
65
56
  def workflows_list(root = project_root)
@@ -75,35 +66,6 @@ module Kettle
75
66
  end
76
67
 
77
68
  # List of workflow files to exclude from interactive menus and checks.
78
- #
79
- # For reference...
80
- #
81
- # A list of all worlflows,
82
- # with each marked relative to if they exist in this repo,
83
- # or at the top of the README marked.
84
- #
85
- # - ancient (+)
86
- # - auto-assign.yml (-)
87
- # - codeql-analysis.yml (-)
88
- # - coverage.yml (+)
89
- # - current.yml (+)
90
- # - danger.yml (x)
91
- # - dependency-review.yml (-)
92
- # - discord-notifier.yml (-)
93
- # - heads.yml (+)
94
- # - jruby.yml (+)
95
- # - legacy.yml (+)
96
- # - locked_deps.yml (+)
97
- # - opencollective.yml (-)
98
- # - style.yml (+)
99
- # - supported.yml (+)
100
- # - truffle.yml (+)
101
- # - unlocked_deps.yml (+)
102
- # - unsupported.yml (+)
103
- #
104
- # All those marked as (-) or (x) are excluded from interactive menus and checks.
105
- # The (x) exist because they may be common in other repos.
106
- #
107
69
  # @return [Array<String>]
108
70
  def exclusions
109
71
  %w[
@@ -117,7 +79,6 @@ module Kettle
117
79
  end
118
80
 
119
81
  # Fetch latest workflow run info for a given workflow and branch via GitHub API.
120
- #
121
82
  # @param owner [String]
122
83
  # @param repo [String]
123
84
  # @param workflow_file [String] the workflow basename (e.g., "ci.yml")
@@ -166,6 +127,77 @@ module Kettle
166
127
  def default_token
167
128
  ENV["GITHUB_TOKEN"] || ENV["GH_TOKEN"]
168
129
  end
130
+
131
+ # --- GitLab support ---
132
+
133
+ # Raw origin URL string from git config
134
+ # @return [String, nil]
135
+ def origin_url
136
+ out, status = Open3.capture2("git", "config", "--get", "remote.origin.url")
137
+ status.success? ? out.strip : nil
138
+ end
139
+
140
+ # Parse GitLab owner/repo from origin if pointing to gitlab.com
141
+ # @return [Array(String, String), nil]
142
+ def repo_info_gitlab
143
+ url = origin_url
144
+ return unless url
145
+ if url =~ %r{git@gitlab.com:(.+?)/(.+?)(\.git)?$}
146
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
147
+ elsif url =~ %r{https://gitlab.com/(.+?)/(.+?)(\.git)?$}
148
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
149
+ end
150
+ end
151
+
152
+ # Default GitLab token from environment
153
+ # @return [String, nil]
154
+ def default_gitlab_token
155
+ ENV["GITLAB_TOKEN"] || ENV["GL_TOKEN"]
156
+ end
157
+
158
+ # Fetch the latest pipeline for a branch on GitLab
159
+ # @param owner [String]
160
+ # @param repo [String]
161
+ # @param branch [String, nil]
162
+ # @param host [String]
163
+ # @param token [String, nil]
164
+ # @return [Hash{String=>String,Integer}, nil]
165
+ def gitlab_latest_pipeline(owner:, repo:, branch: nil, host: "gitlab.com", token: default_gitlab_token)
166
+ return unless owner && repo
167
+ b = branch || current_branch
168
+ return unless b
169
+ project = URI.encode_www_form_component("#{owner}/#{repo}")
170
+ uri = URI("https://#{host}/api/v4/projects/#{project}/pipelines?ref=#{URI.encode_www_form_component(b)}&per_page=1")
171
+ req = Net::HTTP::Get.new(uri)
172
+ req["User-Agent"] = "kettle-dev/ci-helpers"
173
+ req["PRIVATE-TOKEN"] = token if token && !token.empty?
174
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
175
+ return unless res.is_a?(Net::HTTPSuccess)
176
+ data = JSON.parse(res.body)
177
+ pipe = data&.first
178
+ return unless pipe
179
+ {
180
+ "status" => pipe["status"],
181
+ "web_url" => pipe["web_url"],
182
+ "id" => pipe["id"],
183
+ }
184
+ rescue StandardError
185
+ nil
186
+ end
187
+
188
+ # Whether a GitLab pipeline has succeeded
189
+ # @param pipeline [Hash, nil]
190
+ # @return [Boolean]
191
+ def gitlab_success?(pipeline)
192
+ pipeline && pipeline["status"] == "success"
193
+ end
194
+
195
+ # Whether a GitLab pipeline has failed
196
+ # @param pipeline [Hash, nil]
197
+ # @return [Boolean]
198
+ def gitlab_failed?(pipeline)
199
+ pipeline && pipeline["status"] == "failed"
200
+ end
169
201
  end
170
202
  end
171
203
  end
@@ -6,7 +6,7 @@ module Kettle
6
6
  module Version
7
7
  # The gem version.
8
8
  # @return [String]
9
- VERSION = "1.0.3"
9
+ VERSION = "1.0.5"
10
10
  end
11
11
  end
12
12
  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.3
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -203,14 +203,10 @@ extra_rdoc_files:
203
203
  - REEK
204
204
  - RUBOCOP.md
205
205
  - SECURITY.md
206
- - checksums/kettle-dev-1.0.0.gem.sha256
207
- - checksums/kettle-dev-1.0.0.gem.sha512
208
- - checksums/kettle-dev-1.0.1.gem.sha256
209
- - checksums/kettle-dev-1.0.1.gem.sha512
210
- - checksums/kettle-dev-1.0.2.gem.sha256
211
- - checksums/kettle-dev-1.0.2.gem.sha512
212
- - checksums/kettle-dev-1.0.3.gem.sha256
213
- - checksums/kettle-dev-1.0.3.gem.sha512
206
+ - checksums/kettle-dev-1.0.4.gem.sha256
207
+ - checksums/kettle-dev-1.0.4.gem.sha512
208
+ - checksums/kettle-dev-1.0.5.gem.sha256
209
+ - checksums/kettle-dev-1.0.5.gem.sha512
214
210
  files:
215
211
  - ".devcontainer/devcontainer.json"
216
212
  - ".envrc"
@@ -262,14 +258,10 @@ files:
262
258
  - RUBOCOP.md
263
259
  - Rakefile
264
260
  - SECURITY.md
265
- - checksums/kettle-dev-1.0.0.gem.sha256
266
- - checksums/kettle-dev-1.0.0.gem.sha512
267
- - checksums/kettle-dev-1.0.1.gem.sha256
268
- - checksums/kettle-dev-1.0.1.gem.sha512
269
- - checksums/kettle-dev-1.0.2.gem.sha256
270
- - checksums/kettle-dev-1.0.2.gem.sha512
271
- - checksums/kettle-dev-1.0.3.gem.sha256
272
- - checksums/kettle-dev-1.0.3.gem.sha512
261
+ - checksums/kettle-dev-1.0.4.gem.sha256
262
+ - checksums/kettle-dev-1.0.4.gem.sha512
263
+ - checksums/kettle-dev-1.0.5.gem.sha256
264
+ - checksums/kettle-dev-1.0.5.gem.sha512
273
265
  - exe/kettle-commit-msg
274
266
  - exe/kettle-readme-backers
275
267
  - exe/kettle-release
@@ -302,10 +294,10 @@ licenses:
302
294
  - MIT
303
295
  metadata:
304
296
  homepage_uri: https://kettle-dev.galtzo.com/
305
- source_code_uri: https://github.com/galtzo-floss/kettle-dev/tree/v1.0.3
306
- changelog_uri: https://github.com/galtzo-floss/kettle-dev/blob/v1.0.3/CHANGELOG.md
297
+ source_code_uri: https://github.com/galtzo-floss/kettle-dev/tree/v1.0.5
298
+ changelog_uri: https://github.com/galtzo-floss/kettle-dev/blob/v1.0.5/CHANGELOG.md
307
299
  bug_tracker_uri: https://github.com/galtzo-floss/kettle-dev/issues
308
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.3
300
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.5
309
301
  funding_uri: https://github.com/sponsors/pboling
310
302
  wiki_uri: https://github.com/galtzo-floss/kettle-dev/wiki
311
303
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file
@@ -1 +0,0 @@
1
- b4c6725b40f3e0906cd314309dfa6a9f4a8fda0394dacd99f16aa32376275ab9
@@ -1 +0,0 @@
1
- 1273b5c26da368293af8c2d0b87efabf5f8af66e89fbeea1a20e26c960dedb130eb1388c151cba85682110cf4fea577680c3a1e7171606b0f913dce7750e5f45
@@ -1 +0,0 @@
1
- 5b92c8a76f54954791e4154bb22594dbc9dd4f0e06d9d4c5db15a32aa2718ede
@@ -1 +0,0 @@
1
- 5cac18505a8f780d3897f1d900a662c896537056441b1a8178531355a4b10198bee0bdc62216ab2036b640adada34911bc30b813f715a14a464d797718d7a065
@@ -1 +0,0 @@
1
- 2338f9fc4e14a03c39c6509d11fdcfad85faaa7615d855703cb511bc9f95351c
@@ -1 +0,0 @@
1
- ccc6a5c3cd36a8c40d78458166adfbd7ad452b62055579ee4333089a56313170bf97d0aca1f8c3d4f15287bb3d396cfbfcce2174665feb7a77f0b6b03ac8096c
@@ -1 +0,0 @@
1
- 996386236fb02c4837bcab0d180f39604e8f0c93535531e6aded6cfbf804ad0a
@@ -1 +0,0 @@
1
- 67fde9c262cbf74e7ea89e18ba3b7a737aad2cc7a596660931e2e42e73bbc8bfd9a778b6f54d79902ef6a48783fa0f90277b7dcb656a7e3c9d63c2192baaae85