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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.envrc +1 -1
- data/CHANGELOG.md +16 -2
- data/CONTRIBUTING.md +3 -1
- data/checksums/kettle-dev-1.0.4.gem.sha256 +1 -0
- data/checksums/kettle-dev-1.0.4.gem.sha512 +1 -0
- data/exe/kettle-release +274 -41
- data/lib/kettle/dev/ci_helpers.rb +71 -39
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +8 -20
- metadata.gz.sig +0 -0
- data/checksums/kettle-dev-1.0.0.gem.sha256 +0 -1
- data/checksums/kettle-dev-1.0.0.gem.sha512 +0 -1
- data/checksums/kettle-dev-1.0.1.gem.sha256 +0 -1
- data/checksums/kettle-dev-1.0.1.gem.sha512 +0 -1
- data/checksums/kettle-dev-1.0.2.gem.sha256 +0 -1
- data/checksums/kettle-dev-1.0.2.gem.sha512 +0 -1
- data/checksums/kettle-dev-1.0.3.gem.sha256 +0 -1
- data/checksums/kettle-dev-1.0.3.gem.sha512 +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8deda6f16a7ffb26b4122b469c1b74a79e3213146eabe11d694f5685759daed
|
4
|
+
data.tar.gz: 39576c04b2224fc95d9a9c12f37a04de6db556f3aab6f92f8733eb5742a0ff2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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=
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
#
|
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
|
data/lib/kettle/dev/version.rb
CHANGED
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.
|
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.
|
207
|
-
- checksums/kettle-dev-1.0.
|
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.
|
266
|
-
- checksums/kettle-dev-1.0.
|
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.
|
306
|
-
changelog_uri: https://github.com/galtzo-floss/kettle-dev/blob/v1.0.
|
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.
|
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
|