kettle-dev 1.0.3 → 1.0.4

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: d8deda6f16a7ffb26b4122b469c1b74a79e3213146eabe11d694f5685759daed
4
+ data.tar.gz: 39576c04b2224fc95d9a9c12f37a04de6db556f3aab6f92f8733eb5742a0ff2d
5
5
  SHA512:
6
- metadata.gz: cdb45f3c3440b061c0ccbc21a9a315a7d87be88ce8f2389a0b530a21615e7908ea397ea4208a34f3e4eeb4e2af2a7b6c9db466af25348ae927840be46a456d55
7
- data.tar.gz: d9a60711d6d6c09b4203531551b733ae0c148da55507169cb9cbc0f002cc7e2f69b4c17b53ae0f6e3d8cff6b4302c5cb47209672d2969aa73247b3c14bb50752
6
+ metadata.gz: 97f743744b2a0ecaf4b7107886508e94b45a0c77152e3c1e14944ad0f3799a04cb26a8ad7bec59f3372a9c5f4a1a245b3630e7d518a889936e9adae186a39ee8
7
+ data.tar.gz: 3e7b3a47b47925c57f349932a8205a705ea44c288428ad35026241957aed25ec5c7bed835775a49db8616c0736e40f790b4ea6dcfacc195b09dfdc59960c2e3e
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,19 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
12
12
  ### Fixed
13
13
  ### Security
14
14
 
15
+ ## [1.0.4] - 2025-08-24
16
+ - TAG: [v1.0.4][1.0.4t]
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
+ ### Added
21
+ - 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.
22
+ - 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).
23
+ ### Fixed
24
+ - 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).
25
+ - kettle-release now enforces CI checks and aborts if CI cannot be verified; supports GitHub Actions and GitLab pipelines, including releases from trunk/main.
26
+ - kettle-release no longer requires bundler/setup, preventing silent exits when invoked from a dependent project; adds robust output flushing.
27
+
15
28
  ## [1.0.3] - 2025-08-24
16
29
  - TAG: [v1.0.3][1.0.3t]
17
30
  - COVERAGE: 100.00% -- 98/98 lines in 7 files
@@ -23,7 +36,6 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
23
36
  - kettle-release now uses the host project's root, instead of this gem's installed root.
24
37
  - Added .git-hooks files necessary for git hooks to work
25
38
 
26
-
27
39
  ## [1.0.2] - 2025-08-24
28
40
  - TAG: [v1.0.2][1.0.2t]
29
41
  - COVERAGE: 100.00% -- 98/98 lines in 7 files
@@ -71,7 +83,9 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
71
83
  - Selecting will run the selected workflow via `act`
72
84
  - This may move to its own gem in the future.
73
85
 
74
- [Unreleased]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.3...HEAD
86
+ [Unreleased]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.4...HEAD
87
+ [1.0.4]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.3...v1.0.4
88
+ [1.0.4t]: https://gitlab.com/kettle-rb/kettle-dev/-/tags/v1.0.4
75
89
  [1.0.3]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.2...v1.0.3
76
90
  [1.0.3t]: https://gitlab.com/kettle-rb/kettle-dev/-/tags/v1.0.3
77
91
  [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
data/exe/kettle-release CHANGED
@@ -12,8 +12,14 @@
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
+ # 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
17
23
 
18
24
  require "open3"
19
25
  require "shellwords"
@@ -88,10 +94,13 @@ module Kettle
88
94
 
89
95
  # Checksums (commits, but does not push)
90
96
  run_cmd!("bin/gem_checksums")
97
+ validate_checksums!(version, stage: "after build + gem_checksums")
91
98
 
92
99
  # Release: expect PEM password + RubyGems MFA OTP
93
100
  puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
94
101
  run_cmd!("bundle exec rake release")
102
+ # Some release tasks rebuild the gem; re-validate to ensure reproducibility
103
+ validate_checksums!(version, stage: "after release")
95
104
 
96
105
  puts "\nRelease complete. Don't forget to push the checksums commit if needed."
97
106
  end
@@ -103,53 +112,89 @@ module Kettle
103
112
  def monitor_workflows_after_push!
104
113
  root = Kettle::Dev::CIHelpers.project_root
105
114
  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
115
+ gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
110
116
 
111
- owner, repo = Kettle::Dev::CIHelpers.repo_info
112
117
  branch = Kettle::Dev::CIHelpers.current_branch
113
- unless owner && repo && branch
114
- puts "Unable to determine repository or branch; skipping CI checks."
115
- return
118
+ abort("Could not determine current branch for CI checks.") unless branch
119
+
120
+ # Prefer an explicit GitHub remote if available; fall back to origin repo_info
121
+ gh_remote = preferred_github_remote
122
+ gh_owner = nil
123
+ gh_repo = nil
124
+ if gh_remote && !workflows.empty?
125
+ url = remote_url(gh_remote)
126
+ gh_owner, gh_repo = parse_github_owner_repo(url)
116
127
  end
117
128
 
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
129
+ checks_any = false
130
+
131
+ if gh_owner && gh_repo && !workflows.empty?
132
+ checks_any = true
133
+ total = workflows.size
134
+ abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
135
+
136
+ passed = {}
137
+ idx = 0
138
+ puts "Ensuring GitHub Actions workflows pass on #{branch} (#{gh_owner}/#{gh_repo}) via remote '#{gh_remote}'"
139
+ pbar = ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
140
+
141
+ loop do
142
+ wf = workflows[idx]
143
+ run = Kettle::Dev::CIHelpers.latest_run(owner: gh_owner, repo: gh_repo, workflow_file: wf, branch: branch)
144
+ if run
145
+ if Kettle::Dev::CIHelpers.success?(run)
146
+ unless passed[wf]
147
+ passed[wf] = true
148
+ pbar.increment
149
+ end
150
+ elsif Kettle::Dev::CIHelpers.failed?(run)
151
+ puts
152
+ url = run["html_url"] || "https://github.com/#{gh_owner}/#{gh_repo}/actions/workflows/#{wf}"
153
+ abort("Workflow failed: #{wf} -> #{url}")
132
154
  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
155
  end
156
+ break if passed.size == total
157
+ idx = (idx + 1) % total
158
+ sleep(1)
139
159
  end
160
+ pbar.finish unless pbar.finished?
161
+ puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
162
+ end
140
163
 
141
- break if passed.size == total
142
-
143
- idx = (idx + 1) % total
144
- sleep(1)
164
+ # Additionally, check GitLab if configured
165
+ gl_remote = gitlab_remote_candidates.first
166
+ if gitlab_ci && gl_remote
167
+ owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
168
+ if owner && repo
169
+ checks_any = true
170
+ puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
171
+ pbar = ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
172
+ loop do
173
+ pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
174
+ if pipe
175
+ if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
176
+ pbar.increment unless pbar.finished?
177
+ break
178
+ elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
179
+ puts
180
+ url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
181
+ abort("Pipeline failed: #{url}")
182
+ end
183
+ end
184
+ sleep(1)
185
+ end
186
+ pbar.finish unless pbar.finished?
187
+ puts "\nGitLab pipeline passing."
188
+ end
145
189
  end
146
- pbar.finish unless pbar.finished?
147
- puts "\nAll workflows passing (#{passed.size}/#{total})."
190
+
191
+ abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
148
192
  end
149
193
 
150
194
  def run_cmd!(cmd)
151
195
  puts "$ #{cmd}"
152
- success = system(cmd)
196
+ # Ensure current ENV (including SOURCE_DATE_EPOCH) is propagated explicitly
197
+ success = system(ENV, cmd)
153
198
  abort("Command failed: #{cmd}") unless success
154
199
  end
155
200
 
@@ -192,11 +237,45 @@ module Kettle
192
237
  end
193
238
 
194
239
  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")
240
+ branch = current_branch
241
+ abort("Could not determine current branch to push.") unless branch
242
+
243
+ if has_remote?("all")
244
+ puts "$ git push all #{branch}"
245
+ success = system("git push all #{Shellwords.escape(branch)}")
246
+ unless success
247
+ warn("Normal push to 'all' failed; retrying with force push...")
248
+ run_cmd!("git push -f all #{Shellwords.escape(branch)}")
249
+ end
250
+ return
251
+ end
252
+
253
+ # Build the list of remotes to push to
254
+ remotes = []
255
+ remotes << "origin" if has_remote?("origin")
256
+ remotes |= github_remote_candidates
257
+ remotes |= gitlab_remote_candidates
258
+ remotes |= codeberg_remote_candidates
259
+ remotes.uniq!
260
+
261
+ if remotes.empty?
262
+ # Fallback to default behavior if we couldn't detect any remotes
263
+ puts "$ git push #{branch}"
264
+ success = system("git push #{Shellwords.escape(branch)}")
265
+ unless success
266
+ warn("Normal push failed; retrying with force push...")
267
+ run_cmd!("git push -f #{Shellwords.escape(branch)}")
268
+ end
269
+ return
270
+ end
271
+
272
+ remotes.each do |remote|
273
+ puts "$ git push #{remote} #{branch}"
274
+ success = system("git push #{Shellwords.escape(remote)} #{Shellwords.escape(branch)}")
275
+ unless success
276
+ warn("Push to #{remote} failed; retrying with force push...")
277
+ run_cmd!("git push -f #{Shellwords.escape(remote)} #{Shellwords.escape(branch)}")
278
+ end
200
279
  end
201
280
  end
202
281
 
@@ -226,6 +305,56 @@ module Kettle
226
305
  ok ? out.split(/\s+/).reject(&:empty?) : []
227
306
  end
228
307
 
308
+ def remotes_with_urls
309
+ out, ok = git_output(["remote", "-v"])
310
+ return {} unless ok
311
+ urls = {}
312
+ out.each_line do |line|
313
+ if line =~ /(\S+)\s+(\S+)\s+\((fetch|push)\)/
314
+ name = Regexp.last_match(1)
315
+ url = Regexp.last_match(2)
316
+ kind = Regexp.last_match(3)
317
+ # prefer fetch URL when available
318
+ urls[name] = url if kind == "fetch" || !urls.key?(name)
319
+ end
320
+ end
321
+ urls
322
+ end
323
+
324
+ def remote_url(name)
325
+ remotes_with_urls[name]
326
+ end
327
+
328
+ def github_remote_candidates
329
+ remotes_with_urls.select { |n, u| u.include?("github.com") }.keys
330
+ end
331
+
332
+ def gitlab_remote_candidates
333
+ remotes_with_urls.select { |n, u| u.include?("gitlab.com") }.keys
334
+ end
335
+
336
+ def codeberg_remote_candidates
337
+ remotes_with_urls.select { |n, u| u.include?("codeberg.org") }.keys
338
+ end
339
+
340
+ def preferred_github_remote
341
+ cands = github_remote_candidates
342
+ return if cands.empty?
343
+ # Prefer a remote literally named 'github', otherwise the first
344
+ cands.find { |n| n == "github" } || cands.first
345
+ end
346
+
347
+ def parse_github_owner_repo(url)
348
+ return [nil, nil] unless url
349
+ if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
350
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
351
+ elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
352
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
353
+ else
354
+ [nil, nil]
355
+ end
356
+ end
357
+
229
358
  def has_remote?(name)
230
359
  list_remotes.include?(name)
231
360
  end
@@ -271,7 +400,7 @@ module Kettle
271
400
  return
272
401
  end
273
402
 
274
- # Default behavior: ensure local trunk is not behind origin/trunk; if it is, rebase flows
403
+ # Ensure local trunk is in sync with origin/trunk
275
404
  run_cmd!("git fetch origin #{Shellwords.escape(trunk)}")
276
405
  if trunk_behind_remote?(trunk, "origin")
277
406
  puts "Local #{trunk} is behind origin/#{trunk}. Rebasing..."
@@ -284,6 +413,56 @@ module Kettle
284
413
  else
285
414
  puts "Local #{trunk} is up to date with origin/#{trunk}."
286
415
  end
416
+
417
+ # If there is a GitHub remote that is not origin, ensure origin/#{trunk} incorporates it
418
+ gh_remote = preferred_github_remote
419
+ if gh_remote && gh_remote != "origin"
420
+ puts "GitHub remote detected: #{gh_remote}. Fetching #{trunk}..."
421
+ run_cmd!("git fetch #{gh_remote} #{Shellwords.escape(trunk)}")
422
+
423
+ # Compare origin/trunk vs github/trunk to see if they differ
424
+ left, right = ahead_behind_counts("origin/#{trunk}", "#{gh_remote}/#{trunk}")
425
+ if left.zero? && right.zero?
426
+ puts "origin/#{trunk} and #{gh_remote}/#{trunk} are already in sync."
427
+ return
428
+ end
429
+
430
+ checkout!(trunk)
431
+ run_cmd!("git pull --rebase origin #{Shellwords.escape(trunk)}")
432
+
433
+ if left.positive? && right.positive?
434
+ # Histories have diverged -> let user choose
435
+ puts "origin/#{trunk} and #{gh_remote}/#{trunk} have diverged (#{left} ahead of GH, #{right} behind GH)."
436
+ puts "Choose how to reconcile:"
437
+ puts " [r] Rebase local/#{trunk} on top of #{gh_remote}/#{trunk} (push to origin)"
438
+ puts " [m] Merge --no-ff #{gh_remote}/#{trunk} into #{trunk} (push to origin and #{gh_remote})"
439
+ puts " [a] Abort"
440
+ print("> ")
441
+ choice = $stdin.gets&.strip&.downcase
442
+ case choice
443
+ when "r"
444
+ run_cmd!("git rebase #{Shellwords.escape("#{gh_remote}/#{trunk}")}")
445
+ run_cmd!("git push origin #{Shellwords.escape(trunk)}")
446
+ puts "Rebased #{trunk} onto #{gh_remote}/#{trunk} and pushed to origin."
447
+ when "m"
448
+ run_cmd!("git merge --no-ff #{Shellwords.escape("#{gh_remote}/#{trunk}")}")
449
+ run_cmd!("git push origin #{Shellwords.escape(trunk)}")
450
+ run_cmd!("git push #{Shellwords.escape(gh_remote)} #{Shellwords.escape(trunk)}")
451
+ puts "Merged #{gh_remote}/#{trunk} into #{trunk} and pushed to origin and #{gh_remote}."
452
+ else
453
+ abort("Aborted by user. Please reconcile trunks and re-run.")
454
+ end
455
+ elsif right.positive? && left.zero?
456
+ # One side can be fast-forwarded
457
+ puts "Fast-forwarding #{trunk} to include #{gh_remote}/#{trunk}..."
458
+ run_cmd!("git merge --ff-only #{Shellwords.escape("#{gh_remote}/#{trunk}")}")
459
+ run_cmd!("git push origin #{Shellwords.escape(trunk)}")
460
+ # origin is behind GH -> fast-forward merge
461
+ elsif left.positive? && right.zero?
462
+ # origin ahead of GH -> nothing required for origin, optionally inform user
463
+ puts "origin/#{trunk} is ahead of #{gh_remote}/#{trunk}; no action required before push."
464
+ end
465
+ end
287
466
  end
288
467
 
289
468
  def merge_feature_into_trunk_and_push!(trunk, feature)
@@ -317,6 +496,60 @@ module Kettle
317
496
  puts "Found signing cert: #{cert_path}"
318
497
  puts "When prompted during build/release, enter the PEM password for ~/.ssh/gem-private_key.pem"
319
498
  end
499
+
500
+ # --- Checksum validation ---
501
+ # Validate that the sha256 of the built gem in pkg/ matches the recorded
502
+ # checksum stored under checksums/<gem>.gem.sha256. Abort with guidance if not.
503
+ # @param version [String]
504
+ # @param stage [String] human-readable context (e.g., "after release")
505
+ def validate_checksums!(version, stage: "")
506
+ gem_path = gem_file_for_version(version)
507
+ unless gem_path && File.file?(gem_path)
508
+ abort("Unable to locate built gem for version #{version} in pkg/. Did the build succeed?")
509
+ end
510
+ actual = compute_sha256(gem_path)
511
+ checks_path = File.join(@root, "checksums", "#{File.basename(gem_path)}.sha256")
512
+ unless File.file?(checks_path)
513
+ abort("Expected checksum file not found: #{checks_path}. Did bin/gem_checksums run?")
514
+ end
515
+ expected = File.read(checks_path).strip
516
+ if actual != expected
517
+ abort(<<~MSG)
518
+ SHA256 mismatch #{stage}:
519
+ gem: #{gem_path}
520
+ sha256sum: #{actual}
521
+ file: #{checks_path}
522
+ file: #{expected}
523
+ Ensure SOURCE_DATE_EPOCH is set consistently and that the artifact used by release is identical to the one checksummed.
524
+ You can retry: export SOURCE_DATE_EPOCH=$EPOCHSECONDS; bundle exec rake build && bin/gem_checksums && bundle exec rake release
525
+ MSG
526
+ else
527
+ puts "Checksum OK #{stage}: #{File.basename(gem_path)}"
528
+ end
529
+ end
530
+
531
+ # Find the gem file in pkg/ that matches the given version
532
+ def gem_file_for_version(version)
533
+ pkg = File.join(@root, "pkg")
534
+ pattern = File.join(pkg, "*.gem")
535
+ gems = Dir[pattern].select { |p| File.basename(p).include?("-#{version}.gem") }
536
+ gems.sort.last
537
+ end
538
+
539
+ # Compute sha256 using system utilities (sha256sum or shasum -a 256),
540
+ # falling back to Ruby Digest if neither is available.
541
+ def compute_sha256(path)
542
+ if system("which sha256sum > /dev/null 2>&1")
543
+ out, _ = Open3.capture2e("sha256sum", path)
544
+ out.split.first
545
+ elsif system("which shasum > /dev/null 2>&1")
546
+ out, _ = Open3.capture2e("shasum", "-a", "256", path)
547
+ out.split.first
548
+ else
549
+ require "digest"
550
+ Digest::SHA256.file(path).hexdigest
551
+ end
552
+ end
320
553
  end
321
554
  end
322
555
  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.4"
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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -203,14 +203,8 @@ 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
214
208
  files:
215
209
  - ".devcontainer/devcontainer.json"
216
210
  - ".envrc"
@@ -262,14 +256,8 @@ files:
262
256
  - RUBOCOP.md
263
257
  - Rakefile
264
258
  - 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
259
+ - checksums/kettle-dev-1.0.4.gem.sha256
260
+ - checksums/kettle-dev-1.0.4.gem.sha512
273
261
  - exe/kettle-commit-msg
274
262
  - exe/kettle-readme-backers
275
263
  - exe/kettle-release
@@ -302,10 +290,10 @@ licenses:
302
290
  - MIT
303
291
  metadata:
304
292
  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
293
+ source_code_uri: https://github.com/galtzo-floss/kettle-dev/tree/v1.0.4
294
+ changelog_uri: https://github.com/galtzo-floss/kettle-dev/blob/v1.0.4/CHANGELOG.md
307
295
  bug_tracker_uri: https://github.com/galtzo-floss/kettle-dev/issues
308
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.3
296
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.4
309
297
  funding_uri: https://github.com/sponsors/pboling
310
298
  wiki_uri: https://github.com/galtzo-floss/kettle-dev/wiki
311
299
  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