kettle-dev 1.0.2 → 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 +29 -1
- 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 +276 -44
- data/lib/kettle/dev/ci_helpers.rb +71 -39
- data/lib/kettle/dev/rakelib/template.rake +52 -4
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +8 -16
- metadata.gz.sig +3 -3
- 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
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,30 @@ 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
|
+
|
28
|
+
## [1.0.3] - 2025-08-24
|
29
|
+
- TAG: [v1.0.3][1.0.3t]
|
30
|
+
- COVERAGE: 100.00% -- 98/98 lines in 7 files
|
31
|
+
- BRANCH COVERAGE: 100.00% -- 30/30 branches in 7 files
|
32
|
+
- 94.59% documented
|
33
|
+
### Added
|
34
|
+
- template task now copies .git-hooks files necessary for git hooks to work
|
35
|
+
### Fixed
|
36
|
+
- kettle-release now uses the host project's root, instead of this gem's installed root.
|
37
|
+
- Added .git-hooks files necessary for git hooks to work
|
38
|
+
|
15
39
|
## [1.0.2] - 2025-08-24
|
16
40
|
- TAG: [v1.0.2][1.0.2t]
|
17
41
|
- COVERAGE: 100.00% -- 98/98 lines in 7 files
|
@@ -59,7 +83,11 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
|
|
59
83
|
- Selecting will run the selected workflow via `act`
|
60
84
|
- This may move to its own gem in the future.
|
61
85
|
|
62
|
-
[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
|
89
|
+
[1.0.3]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.2...v1.0.3
|
90
|
+
[1.0.3t]: https://gitlab.com/kettle-rb/kettle-dev/-/tags/v1.0.3
|
63
91
|
[1.0.2]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.1...v1.0.2
|
64
92
|
[1.0.2t]: https://gitlab.com/kettle-rb/kettle-dev/-/tags/v1.0.2
|
65
93
|
[1.0.1]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.0...v1.0.1
|
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,10 +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
|
-
|
16
|
-
|
15
|
+
$stdout.sync = true
|
17
16
|
require "rubygems"
|
18
|
-
|
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
|
19
23
|
|
20
24
|
require "open3"
|
21
25
|
require "shellwords"
|
@@ -31,7 +35,8 @@ module Kettle
|
|
31
35
|
module Dev
|
32
36
|
class ReleaseCLI
|
33
37
|
def initialize
|
34
|
-
|
38
|
+
# Use the host project's root, not the installed gem's directory
|
39
|
+
@root = Kettle::Dev::CIHelpers.project_root
|
35
40
|
end
|
36
41
|
|
37
42
|
def run
|
@@ -89,10 +94,13 @@ module Kettle
|
|
89
94
|
|
90
95
|
# Checksums (commits, but does not push)
|
91
96
|
run_cmd!("bin/gem_checksums")
|
97
|
+
validate_checksums!(version, stage: "after build + gem_checksums")
|
92
98
|
|
93
99
|
# Release: expect PEM password + RubyGems MFA OTP
|
94
100
|
puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
|
95
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")
|
96
104
|
|
97
105
|
puts "\nRelease complete. Don't forget to push the checksums commit if needed."
|
98
106
|
end
|
@@ -104,53 +112,89 @@ module Kettle
|
|
104
112
|
def monitor_workflows_after_push!
|
105
113
|
root = Kettle::Dev::CIHelpers.project_root
|
106
114
|
workflows = Kettle::Dev::CIHelpers.workflows_list(root)
|
107
|
-
|
108
|
-
puts "No workflows detected under .github/workflows; skipping CI checks."
|
109
|
-
return
|
110
|
-
end
|
115
|
+
gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
|
111
116
|
|
112
|
-
owner, repo = Kettle::Dev::CIHelpers.repo_info
|
113
117
|
branch = Kettle::Dev::CIHelpers.current_branch
|
114
|
-
|
115
|
-
|
116
|
-
|
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)
|
117
127
|
end
|
118
128
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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}")
|
133
154
|
end
|
134
|
-
elsif Kettle::Dev::CIHelpers.failed?(run)
|
135
|
-
# Fail fast with link to the failed run
|
136
|
-
puts
|
137
|
-
url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"
|
138
|
-
abort("Workflow failed: #{wf} -> #{url}")
|
139
155
|
end
|
156
|
+
break if passed.size == total
|
157
|
+
idx = (idx + 1) % total
|
158
|
+
sleep(1)
|
140
159
|
end
|
160
|
+
pbar.finish unless pbar.finished?
|
161
|
+
puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
|
162
|
+
end
|
141
163
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
146
189
|
end
|
147
|
-
|
148
|
-
|
190
|
+
|
191
|
+
abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
|
149
192
|
end
|
150
193
|
|
151
194
|
def run_cmd!(cmd)
|
152
195
|
puts "$ #{cmd}"
|
153
|
-
|
196
|
+
# Ensure current ENV (including SOURCE_DATE_EPOCH) is propagated explicitly
|
197
|
+
success = system(ENV, cmd)
|
154
198
|
abort("Command failed: #{cmd}") unless success
|
155
199
|
end
|
156
200
|
|
@@ -193,11 +237,45 @@ module Kettle
|
|
193
237
|
end
|
194
238
|
|
195
239
|
def push!
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
201
279
|
end
|
202
280
|
end
|
203
281
|
|
@@ -227,6 +305,56 @@ module Kettle
|
|
227
305
|
ok ? out.split(/\s+/).reject(&:empty?) : []
|
228
306
|
end
|
229
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
|
+
|
230
358
|
def has_remote?(name)
|
231
359
|
list_remotes.include?(name)
|
232
360
|
end
|
@@ -272,7 +400,7 @@ module Kettle
|
|
272
400
|
return
|
273
401
|
end
|
274
402
|
|
275
|
-
#
|
403
|
+
# Ensure local trunk is in sync with origin/trunk
|
276
404
|
run_cmd!("git fetch origin #{Shellwords.escape(trunk)}")
|
277
405
|
if trunk_behind_remote?(trunk, "origin")
|
278
406
|
puts "Local #{trunk} is behind origin/#{trunk}. Rebasing..."
|
@@ -285,6 +413,56 @@ module Kettle
|
|
285
413
|
else
|
286
414
|
puts "Local #{trunk} is up to date with origin/#{trunk}."
|
287
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
|
288
466
|
end
|
289
467
|
|
290
468
|
def merge_feature_into_trunk_and_push!(trunk, feature)
|
@@ -318,6 +496,60 @@ module Kettle
|
|
318
496
|
puts "Found signing cert: #{cert_path}"
|
319
497
|
puts "When prompted during build/release, enter the PEM password for ~/.ssh/gem-private_key.pem"
|
320
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
|
321
553
|
end
|
322
554
|
end
|
323
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
|
@@ -358,11 +358,18 @@ namespace :kettle do
|
|
358
358
|
puts "WARNING: Could not determine env file changes: #{e.class}: #{e.message}"
|
359
359
|
end
|
360
360
|
|
361
|
-
# Handle .git-hooks
|
361
|
+
# Handle .git-hooks files
|
362
|
+
# - Two template files (.txt) are offered with a location prompt (local/global/skip).
|
363
|
+
# - Two hook scripts are always copied to both local and global locations by default;
|
364
|
+
# if they already exist, ask before overwriting.
|
362
365
|
source_hooks_dir = File.join(gem_checkout_root, ".git-hooks")
|
363
366
|
if Dir.exist?(source_hooks_dir)
|
364
367
|
goalie_src = File.join(source_hooks_dir, "commit-subjects-goalie.txt")
|
365
368
|
footer_src = File.join(source_hooks_dir, "footer-template.erb.txt")
|
369
|
+
hook_ruby_src = File.join(source_hooks_dir, "commit-msg")
|
370
|
+
hook_sh_src = File.join(source_hooks_dir, "prepare-commit-msg")
|
371
|
+
|
372
|
+
# First: templates (.txt) — keep the existing single question which applies only to these
|
366
373
|
if File.file?(goalie_src) && File.file?(footer_src)
|
367
374
|
puts
|
368
375
|
puts "Git hooks templates found:"
|
@@ -394,19 +401,60 @@ namespace :kettle do
|
|
394
401
|
FileUtils.mkdir_p(dest_dir)
|
395
402
|
[[goalie_src, "commit-subjects-goalie.txt"], [footer_src, "footer-template.erb.txt"]].each do |src, base|
|
396
403
|
dest = File.join(dest_dir, base)
|
397
|
-
|
404
|
+
# Use helpers to allow create/replace prompts for these files (question applies to them)
|
405
|
+
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true)
|
398
406
|
# Ensure readable (0644). These are data/templates, not executables.
|
399
407
|
begin
|
400
|
-
File.chmod(0o644, dest)
|
408
|
+
File.chmod(0o644, dest) if File.exist?(dest)
|
401
409
|
rescue StandardError
|
402
410
|
# ignore permission issues
|
403
411
|
end
|
404
|
-
puts "Copied #{base} -> #{dest}"
|
405
412
|
end
|
406
413
|
else
|
407
414
|
puts "Skipping copy of .git-hooks templates."
|
408
415
|
end
|
409
416
|
end
|
417
|
+
|
418
|
+
# Second: hook scripts — copy only to the local project; prompt only on overwrite
|
419
|
+
# Per requirements: do not install hook scripts to global ~/.git-hooks
|
420
|
+
hook_dests = [File.join(project_root, ".git-hooks")]
|
421
|
+
hook_pairs = [[hook_ruby_src, "commit-msg", 0o755], [hook_sh_src, "prepare-commit-msg", 0o755]]
|
422
|
+
hook_pairs.each do |src, base, mode|
|
423
|
+
next unless File.file?(src)
|
424
|
+
hook_dests.each do |dstdir|
|
425
|
+
begin
|
426
|
+
FileUtils.mkdir_p(dstdir)
|
427
|
+
dest = File.join(dstdir, base)
|
428
|
+
# Create without prompt if missing; if exists, ask to replace
|
429
|
+
if File.exist?(dest)
|
430
|
+
# ask to overwrite
|
431
|
+
if helpers.ask("Overwrite existing #{dest}?", true)
|
432
|
+
content = File.read(src)
|
433
|
+
helpers.write_file(dest, content)
|
434
|
+
begin
|
435
|
+
File.chmod(mode, dest)
|
436
|
+
rescue StandardError
|
437
|
+
# ignore permission issues
|
438
|
+
end
|
439
|
+
puts "Replaced #{dest}"
|
440
|
+
else
|
441
|
+
puts "Kept existing #{dest}"
|
442
|
+
end
|
443
|
+
else
|
444
|
+
content = File.read(src)
|
445
|
+
helpers.write_file(dest, content)
|
446
|
+
begin
|
447
|
+
File.chmod(mode, dest)
|
448
|
+
rescue StandardError
|
449
|
+
# ignore permission issues
|
450
|
+
end
|
451
|
+
puts "Installed #{dest}"
|
452
|
+
end
|
453
|
+
rescue StandardError => e
|
454
|
+
puts "WARNING: Could not install hook #{base} to #{dstdir}: #{e.class}: #{e.message}"
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
410
458
|
end
|
411
459
|
end
|
412
460
|
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,12 +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
|
206
|
+
- checksums/kettle-dev-1.0.4.gem.sha256
|
207
|
+
- checksums/kettle-dev-1.0.4.gem.sha512
|
212
208
|
files:
|
213
209
|
- ".devcontainer/devcontainer.json"
|
214
210
|
- ".envrc"
|
@@ -260,12 +256,8 @@ files:
|
|
260
256
|
- RUBOCOP.md
|
261
257
|
- Rakefile
|
262
258
|
- SECURITY.md
|
263
|
-
- checksums/kettle-dev-1.0.
|
264
|
-
- checksums/kettle-dev-1.0.
|
265
|
-
- checksums/kettle-dev-1.0.1.gem.sha256
|
266
|
-
- checksums/kettle-dev-1.0.1.gem.sha512
|
267
|
-
- checksums/kettle-dev-1.0.2.gem.sha256
|
268
|
-
- checksums/kettle-dev-1.0.2.gem.sha512
|
259
|
+
- checksums/kettle-dev-1.0.4.gem.sha256
|
260
|
+
- checksums/kettle-dev-1.0.4.gem.sha512
|
269
261
|
- exe/kettle-commit-msg
|
270
262
|
- exe/kettle-readme-backers
|
271
263
|
- exe/kettle-release
|
@@ -298,10 +290,10 @@ licenses:
|
|
298
290
|
- MIT
|
299
291
|
metadata:
|
300
292
|
homepage_uri: https://kettle-dev.galtzo.com/
|
301
|
-
source_code_uri: https://github.com/galtzo-floss/kettle-dev/tree/v1.0.
|
302
|
-
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
|
303
295
|
bug_tracker_uri: https://github.com/galtzo-floss/kettle-dev/issues
|
304
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.
|
296
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.4
|
305
297
|
funding_uri: https://github.com/sponsors/pboling
|
306
298
|
wiki_uri: https://github.com/galtzo-floss/kettle-dev/wiki
|
307
299
|
news_uri: https://www.railsbling.com/tags/kettle-dev
|
metadata.gz.sig
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
��
|
2
|
-
|
3
|
-
|
1
|
+
0"O[KE౸�B��b�����$�f��k��2�:��6�İ�@�zn�ZG�/#胸�P����*�̞Rz*qa��v��r�Z����i`�.�-�
|
2
|
+
2�[�G��X�h]�*A�5�/�Z��y����æ�H�E�v�`��=���_�Gڼ�N�ۋ/���H9n,�?C���҈��E�_Tˤ-�Q� �js��APn�ʆM�l�K�B0�i�;��Q�
|
3
|
+
�./T���C�I�]A9�4�P]5������u7�z
|
@@ -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
|