kettle-dev 2.1.1 → 2.2.0
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/CHANGELOG.md +67 -1
- data/README.md +46 -3
- data/SECURITY.md +1 -1
- data/exe/kettle-bump +41 -0
- data/exe/kettle-gha-sha-pins +40 -0
- data/exe/kettle-pre-release +4 -2
- data/exe/kettle-release +4 -0
- data/lib/kettle/dev/bump_cli.rb +209 -0
- data/lib/kettle/dev/ci_helpers.rb +14 -6
- data/lib/kettle/dev/ci_monitor.rb +41 -2
- data/lib/kettle/dev/gha_sha_pins_cli.rb +1186 -0
- data/lib/kettle/dev/pre_release_cli.rb +18 -6
- data/lib/kettle/dev/rakelib/spec_test.rake +22 -14
- data/lib/kettle/dev/release_cli.rb +7 -0
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +12 -0
- data/sig/kettle/dev/ci_helpers.rbs +5 -2
- data/sig/kettle/dev/ci_monitor.rbs +3 -0
- data.tar.gz.sig +1 -1
- metadata +14 -8
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8a1375214aa3c53580a46da791cc0813ab2303a56bf08d32c3928bb6773425a
|
|
4
|
+
data.tar.gz: 293ad0c152819ca21f1bf9a63cbf4f21078cff641d680ce7b955608c309d5d24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24dce616ec2536f3f3afd7dc6b16cffe9442aa7d81f86a25dcc2867166a10c55cc6e1285ea1e91261f4f176fc309435cd8240dca7e8eaf94bbff6efdaa0ea4c2
|
|
7
|
+
data.tar.gz: b4fe9b7736463c18bb8e97f2c91d55257c281502b6ccfb40e51e698b5616141dac2c48d6513262b97ee810fcec1e267c31908a0d02bfbe606d21427a0684cd0e
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,70 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [2.2.0] - 2026-06-08
|
|
34
|
+
|
|
35
|
+
- TAG: [v2.2.0][2.2.0t]
|
|
36
|
+
- COVERAGE: 91.51% -- 3710/4054 lines in 28 files
|
|
37
|
+
- BRANCH COVERAGE: 72.90% -- 1458/2000 branches in 28 files
|
|
38
|
+
- 67.51% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `kettle-pre-release` now runs `kettle-gha-sha-pins --check` before release
|
|
43
|
+
checks, failing with an outdated-actions summary and recommended
|
|
44
|
+
`kettle-gha-sha-pins --write --upgrade patch` command when workflow actions
|
|
45
|
+
need updated SHA pins.
|
|
46
|
+
- `kettle-release` now runs `kettle-pre-release` as a full-release gate before
|
|
47
|
+
release setup starts.
|
|
48
|
+
- `kettle-bump` now bumps a single gem's version file before `kettle-changelog`
|
|
49
|
+
release prep.
|
|
50
|
+
- `kettle-gha-sha-pins` now shows discovery, workflow scan, and action-resolution
|
|
51
|
+
progress on STDERR for human runs while keeping JSON output quiet by default.
|
|
52
|
+
- `kettle-gha-sha-pins` now times action resolution, caches one resolution plan
|
|
53
|
+
per action repository, and uses GitHub REST tag-ref lookups to avoid resolving
|
|
54
|
+
every release tag through individual commit requests.
|
|
55
|
+
|
|
56
|
+
- `kettle-gha-sha-pins` now keeps a 24-hour persistent action release cache at
|
|
57
|
+
the XDG state location and supports `--refresh-cache` to bypass stale entries
|
|
58
|
+
while preserving cached discoveries for other projects and actions.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- Minitest-only projects now keep `rake test` on the generated
|
|
63
|
+
`Rake::TestTask` path instead of synthesizing a `kettle-test` RSpec task when
|
|
64
|
+
tests live directly under `test/`.
|
|
65
|
+
|
|
66
|
+
- `kettle-release start_step=10` now waits for GitHub Actions runs for the
|
|
67
|
+
local HEAD SHA to appear before evaluating results, avoiding stale failures
|
|
68
|
+
from the previous branch run.
|
|
69
|
+
- Removed the duplicate `cgi` declaration from the `head` appraisal so
|
|
70
|
+
`kettle-release` can regenerate Appraisal gemfiles on Ruby 4.
|
|
71
|
+
|
|
72
|
+
- `kettle-gha-sha-pins` now treats version-equivalent but unresolved action
|
|
73
|
+
refs as invalid and converts them to release SHAs instead of accepting
|
|
74
|
+
stripped tag names such as `6.0.2` when only `v6.0.2` exists.
|
|
75
|
+
- `kettle-gha-sha-pins --write` now refreshes adjacent release-version comments
|
|
76
|
+
when the pinned SHA is current but the comment is stale.
|
|
77
|
+
- `kettle-gha-sha-pins` now uses `GH_TOKEN` or `gh auth token` as a fallback
|
|
78
|
+
when `GITHUB_TOKEN` is not set, avoiding false clean reports after
|
|
79
|
+
unauthenticated GitHub API rate limits are exhausted.
|
|
80
|
+
- Coverage workflow uploads now pin `codecov/codecov-action` and
|
|
81
|
+
`coverallsapp/github-action` to resolvable release SHAs.
|
|
82
|
+
|
|
83
|
+
- Ruby 2.4-2.6 CI appraisals now include `backports`, and Ruby 3.0-3.1
|
|
84
|
+
appraisals include `prism`, so `kettle-bump` and GitHub Action SHA pin specs
|
|
85
|
+
run on the supported legacy matrix.
|
|
86
|
+
|
|
87
|
+
- Legacy CI appraisals now skip `kettle-bump` specs when Prism is unavailable,
|
|
88
|
+
and the Ruby 3.2 appraisal includes `prism` so the specs run where supported.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- Retemplated project metadata with the latest `kettle-jem` stack, refreshing
|
|
93
|
+
README and security-policy generated details for the current release line.
|
|
94
|
+
|
|
95
|
+
- Runtime dependency `kettle-test` now requires 2.0.4 or newer.
|
|
96
|
+
|
|
33
97
|
## [2.1.1] - 2026-06-06
|
|
34
98
|
|
|
35
99
|
- TAG: [v2.1.1][2.1.1t]
|
|
@@ -1913,7 +1977,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
1913
1977
|
- Selecting will run the selected workflow via `act`
|
|
1914
1978
|
- This may move to its own gem in the future.
|
|
1915
1979
|
|
|
1916
|
-
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v2.
|
|
1980
|
+
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v2.2.0...HEAD
|
|
1981
|
+
[2.2.0]: https://github.com/kettle-rb/kettle-dev/compare/v2.1.1...v2.2.0
|
|
1982
|
+
[2.2.0t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v2.2.0
|
|
1917
1983
|
[2.1.1]: https://github.com/kettle-rb/kettle-dev/compare/v2.1.0...v2.1.1
|
|
1918
1984
|
[2.1.1t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v2.1.1
|
|
1919
1985
|
[2.1.0]: https://github.com/kettle-rb/kettle-dev/compare/v2.0.8...v2.1.0
|
data/README.md
CHANGED
|
@@ -303,6 +303,8 @@ Common local workflows:
|
|
|
303
303
|
- `bundle exec rake appraisal:generate` regenerates Appraisal gemfiles.
|
|
304
304
|
- `bundle exec rake appraisal:update` updates Appraisal locks and applies gradual RuboCop autocorrect.
|
|
305
305
|
- `bundle exec rake appraisal:reset` removes Appraisal lockfiles below `gemfiles/`.
|
|
306
|
+
- `kettle-bump patch` bumps the current gem's patch version before running
|
|
307
|
+
`kettle-changelog`.
|
|
306
308
|
|
|
307
309
|
GitHub Actions local runner helper:
|
|
308
310
|
|
|
@@ -373,6 +375,7 @@ What it does:
|
|
|
373
375
|
|
|
374
376
|
- Script: `exe/kettle-release` (run as `kettle-release`)
|
|
375
377
|
- Purpose: guided release helper that:
|
|
378
|
+
- Runs `kettle-pre-release` before the numbered release steps on full releases, aborting before release setup if any pre-release gate fails.
|
|
376
379
|
- Runs sanity checks (`bin/setup`, `bin/rake`), confirms version/changelog, optionally updates Appraisals, regenerates docs via `bin/rake yard`, commits “🔖 Prepare release vX.Y.Z”.
|
|
377
380
|
- Optionally runs your CI locally with `act` before any push:
|
|
378
381
|
- Enable with env: `K_RELEASE_LOCAL_CI="true"` (run automatically) or `K_RELEASE_LOCAL_CI="ask"` (prompt \[Y/n\]).
|
|
@@ -403,12 +406,29 @@ What it does:
|
|
|
403
406
|
19. Push tags to remotes (final)
|
|
404
407
|
- Examples:
|
|
405
408
|
- After intermittent CI failure, restart from monitoring: `bundle exec kettle-release start_step=10`
|
|
409
|
+
- After fixing a failed pre-release gate, rerun from the top: `bundle exec kettle-release`
|
|
406
410
|
- Tips:
|
|
407
411
|
- The commit message helper `exe/kettle-commit-msg` prefers project-local `.git-hooks` (then falls back to `~/.git-hooks`).
|
|
408
412
|
- The goalie file `commit-subjects-goalie.txt` controls when a footer is appended; customize `footer-template.erb.txt` as you like.
|
|
409
413
|
|
|
410
414
|
### Changelog generator
|
|
411
415
|
|
|
416
|
+
- Script: `exe/kettle-bump` (run as `kettle-bump`)
|
|
417
|
+
- Purpose: Bumps the current single gem's `lib/**/version.rb` before changelog
|
|
418
|
+
preparation. It accepts an exact version or `major`, `minor`, or `patch`.
|
|
419
|
+
- Usage:
|
|
420
|
+
- `kettle-bump patch`
|
|
421
|
+
- `kettle-bump 1.2.4 --from 1.2.3`
|
|
422
|
+
- `kettle-bump minor --dry-run`
|
|
423
|
+
- `kettle-bump patch --check`
|
|
424
|
+
- Behavior:
|
|
425
|
+
- Writes by default; use `--dry-run` to preview or `--check` to fail when a
|
|
426
|
+
bump would change files.
|
|
427
|
+
- Updates literal `spec.version = "..."` assignments in the gemspec when
|
|
428
|
+
they match the current version. Dynamic gemspec versions are left alone.
|
|
429
|
+
- Uses the same `K_CHANGELOG_VERSION_FILE` override as `kettle-changelog`
|
|
430
|
+
when a project needs to point at a specific version file.
|
|
431
|
+
|
|
412
432
|
- Script: `exe/kettle-changelog` (run as `kettle-changelog`)
|
|
413
433
|
- Purpose: Generates a new CHANGELOG.md section for the current version read from `lib/**/version.rb`, moves notes from the Unreleased section, and updates comparison links.
|
|
414
434
|
- Prerequisites:
|
|
@@ -424,6 +444,27 @@ What it does:
|
|
|
424
444
|
|
|
425
445
|
### Pre-release checks
|
|
426
446
|
|
|
447
|
+
- Script: `exe/kettle-gha-sha-pins` (run as `kettle-gha-sha-pins`)
|
|
448
|
+
- Purpose: Validate and optionally update GitHub Actions `uses:` refs to pinned
|
|
449
|
+
SHAs and current allowed release versions.
|
|
450
|
+
- Usage:
|
|
451
|
+
- `kettle-gha-sha-pins`
|
|
452
|
+
- `kettle-gha-sha-pins --check`
|
|
453
|
+
- `kettle-gha-sha-pins --write --upgrade patch`
|
|
454
|
+
- Behavior:
|
|
455
|
+
- Human output shows discovery, workflow scan, and action-resolution progress
|
|
456
|
+
on STDERR, including per-action timing via `ruby-progressbar`, then prints
|
|
457
|
+
the final report on STDOUT.
|
|
458
|
+
- Action metadata is resolved with the GitHub REST API and cached per
|
|
459
|
+
`owner/repo` action so duplicate uses of the same action reuse one
|
|
460
|
+
resolution plan.
|
|
461
|
+
- The outdated summary reports newer releases even when `--upgrade patch` or
|
|
462
|
+
`--upgrade minor` limits the write target to a safer release line.
|
|
463
|
+
- JSON output keeps progress disabled by default so STDOUT remains parseable.
|
|
464
|
+
Use `--progress` to force progress or `--no-progress` to suppress it.
|
|
465
|
+
- `--check` exits non-zero when workflow action pins are stale or mutable and
|
|
466
|
+
prints a recommended `kettle-gha-sha-pins --write --upgrade patch` command.
|
|
467
|
+
|
|
427
468
|
- Script: `exe/kettle-pre-release` (run as `kettle-pre-release`)
|
|
428
469
|
- Purpose: Run a suite of pre-release validations to catch avoidable mistakes (resumable by check number).
|
|
429
470
|
- Usage:
|
|
@@ -432,7 +473,9 @@ What it does:
|
|
|
432
473
|
- Options:
|
|
433
474
|
- `--check-num N` Start from check number N (default: 1)
|
|
434
475
|
- Checks:
|
|
435
|
-
- 1)
|
|
476
|
+
- 1) Validate GitHub Actions workflow action refs with `kettle-gha-sha-pins --check`; if pins are stale, it prints an outdated-actions summary, exits non-zero, and recommends `kettle-gha-sha-pins --write --upgrade patch`.
|
|
477
|
+
- 2) Normalize Markdown image URLs.
|
|
478
|
+
- 3) Validate that all image URLs referenced by Markdown files resolve (HTTP HEAD).
|
|
436
479
|
|
|
437
480
|
### Commit message helper (git hook)
|
|
438
481
|
|
|
@@ -811,7 +854,7 @@ Thanks for RTFM. ☺️
|
|
|
811
854
|
[📌gitmoji]: https://gitmoji.dev
|
|
812
855
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
813
856
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
814
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
857
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.054-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
815
858
|
[🔐security]: https://github.com/kettle-rb/kettle-dev/blob/main/SECURITY.md
|
|
816
859
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
817
860
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -839,7 +882,7 @@ Thanks for RTFM. ☺️
|
|
|
839
882
|
| Package | kettle-dev |
|
|
840
883
|
| Description | 🍲 Kettle::Dev is a meta tool from kettle-rb to streamline development and testing. Acts as a shim dependency, pulling in many other dependencies, to give you OOTB productivity with a RubyGem, or Ruby app project. Configures a complete set of Rake tasks, for all the libraries is brings in, so they arrive ready to go. Fund overlooked open source projects - bottom of stack, dev/test dependencies: floss-funding.dev |
|
|
841
884
|
| Homepage | https://github.com/kettle-rb/kettle-dev |
|
|
842
|
-
| Source | https://github.com/kettle-rb/kettle-dev/tree/v2.
|
|
885
|
+
| Source | https://github.com/kettle-rb/kettle-dev/tree/v2.2.0 |
|
|
843
886
|
| License | `AGPL-3.0-only` |
|
|
844
887
|
| Funding | https://github.com/sponsors/pboling, https://issuehunt.io/u/pboling, https://ko-fi.com/pboling, https://liberapay.com/pboling/donate, https://opencollective.com/kettle-rb, https://patreon.com/galtzo, https://polar.sh/pboling, https://thanks.dev/u/gh/pboling, https://tidelift.com/funding/github/rubygems/kettle-dev, https://www.buymeacoffee.com/pboling |
|
|
845
888
|
<!-- kettle-jem:metadata:end -->
|
data/SECURITY.md
CHANGED
data/exe/kettle-bump
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# vim: set syntax=ruby
|
|
5
|
+
|
|
6
|
+
# kettle-bump: Bump the current gem version before running kettle-changelog.
|
|
7
|
+
|
|
8
|
+
$stdout.sync = true
|
|
9
|
+
$stderr.sync = true
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
require "rubygems"
|
|
13
|
+
rescue LoadError
|
|
14
|
+
# Older Rubies always have rubygems; continue anyway
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
script_basename = File.basename(__FILE__)
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
require "kettle/dev"
|
|
21
|
+
puts "== #{script_basename} v#{Kettle::Dev::Version::VERSION} =="
|
|
22
|
+
rescue LoadError => e
|
|
23
|
+
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
24
|
+
warn("Hint: Ensure the host project has kettle-dev as a dependency and run bundle install.")
|
|
25
|
+
exit(1)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
exit(Kettle::Dev::BumpCLI.new(ARGV).run!)
|
|
30
|
+
rescue LoadError => e
|
|
31
|
+
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
32
|
+
warn(e.backtrace.join("\n")) if ENV["DEBUG"]
|
|
33
|
+
exit(1)
|
|
34
|
+
rescue SystemExit => e
|
|
35
|
+
warn("#{script_basename}: exited (status=#{e.status}, msg=#{e.message})") if e.status != 0
|
|
36
|
+
raise
|
|
37
|
+
rescue => e
|
|
38
|
+
warn("#{script_basename}: unexpected error: #{e.class}: #{e.message}")
|
|
39
|
+
warn(e.backtrace.join("\n")) if ENV["DEBUG"]
|
|
40
|
+
exit(1)
|
|
41
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# vim: set syntax=ruby
|
|
5
|
+
|
|
6
|
+
$stdout.sync = true
|
|
7
|
+
$stderr.sync = true
|
|
8
|
+
|
|
9
|
+
# Do not rely on Bundler; allow running in repos that do not depend on kettle-dev
|
|
10
|
+
begin
|
|
11
|
+
require "rubygems"
|
|
12
|
+
rescue LoadError
|
|
13
|
+
# Older Rubies always have rubygems; continue anyway
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
script_basename = File.basename(__FILE__)
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
require "kettle/dev"
|
|
20
|
+
rescue LoadError
|
|
21
|
+
repo_lib = File.expand_path("../lib", __dir__)
|
|
22
|
+
$LOAD_PATH.unshift(repo_lib) unless $LOAD_PATH.include?(repo_lib)
|
|
23
|
+
begin
|
|
24
|
+
require "kettle/dev"
|
|
25
|
+
rescue LoadError => e
|
|
26
|
+
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
27
|
+
warn("Hint: Install the kettle-dev gem (`gem install kettle-dev`) or add it to your Gemfile. Bundler is not required for this script.")
|
|
28
|
+
exit(1)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
cli = Kettle::Dev::GhaShaPinsCLI.new(ARGV)
|
|
34
|
+
status = cli.run!
|
|
35
|
+
Kettle::Dev::ExitAdapter.exit(status)
|
|
36
|
+
rescue => e
|
|
37
|
+
warn("#{script_basename}: unexpected error: #{e.class}: #{e.message}")
|
|
38
|
+
warn(e.backtrace.join("\n"))
|
|
39
|
+
Kettle::Dev::ExitAdapter.exit(1)
|
|
40
|
+
end
|
data/exe/kettle-pre-release
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
# kettle-pre-release: Run pre-release checks to catch avoidable mistakes.
|
|
7
7
|
# - Structured as a sequence of checks that can be resumed via check_num
|
|
8
|
-
# -
|
|
8
|
+
# - Checks GitHub Actions SHA pins and validates Markdown image links.
|
|
9
9
|
|
|
10
10
|
require "optparse"
|
|
11
11
|
|
|
@@ -45,7 +45,9 @@ parser = OptionParser.new do |opts|
|
|
|
45
45
|
puts opts
|
|
46
46
|
puts
|
|
47
47
|
puts "Checks:"
|
|
48
|
-
puts " 1) Validate
|
|
48
|
+
puts " 1) Validate GitHub Actions SHA pins"
|
|
49
|
+
puts " 2) Normalize Markdown image URLs"
|
|
50
|
+
puts " 3) Validate Markdown image links (HTTP HEAD)"
|
|
49
51
|
exit(0)
|
|
50
52
|
end
|
|
51
53
|
end
|
data/exe/kettle-release
CHANGED
|
@@ -47,6 +47,10 @@ if ARGV.include?("-h") || ARGV.include?("--help")
|
|
|
47
47
|
|
|
48
48
|
Automates the release flow for a Ruby gem in the host project.
|
|
49
49
|
|
|
50
|
+
Full releases run `kettle-pre-release` before step 1 and abort if any
|
|
51
|
+
pre-release gate fails. Resume with start_step > 1 only after the gate has
|
|
52
|
+
passed or after intentionally handling the failure.
|
|
53
|
+
|
|
50
54
|
Start steps (use start_step=<n> to begin at that step):
|
|
51
55
|
1. Verify Bundler >= 2.7 (always runs; start at 1 to do everything)
|
|
52
56
|
2. Detect version; RubyGems sanity check; confirm CHANGELOG/version; sync copyright years; update badges/headers
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Kettle
|
|
6
|
+
module Dev
|
|
7
|
+
# CLI for bumping the current project's gem version before changelog prep.
|
|
8
|
+
class BumpCLI
|
|
9
|
+
BUMP_TYPES = %w[major minor patch].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(argv = [], out: $stdout, err: $stderr, root: Kettle::Dev::CIHelpers.project_root)
|
|
12
|
+
@argv = argv.dup
|
|
13
|
+
@out = out
|
|
14
|
+
@err = err
|
|
15
|
+
@root = root
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run!
|
|
19
|
+
options = parse_options
|
|
20
|
+
return 0 if options.fetch(:help)
|
|
21
|
+
|
|
22
|
+
current_version = Kettle::Dev::Versioning.detect_version(root)
|
|
23
|
+
target_version = resolve_target_version(options.fetch(:target), current_version)
|
|
24
|
+
edits = collect_edits(
|
|
25
|
+
current_version: current_version,
|
|
26
|
+
target_version: target_version,
|
|
27
|
+
from_version: options[:from]
|
|
28
|
+
)
|
|
29
|
+
write_edits(edits) if options.fetch(:mode) == :execute
|
|
30
|
+
|
|
31
|
+
out.puts(summary(edits: edits, current_version: current_version, target_version: target_version, mode: options.fetch(:mode)))
|
|
32
|
+
(options.fetch(:mode) == :check && edits.any?) ? 1 : 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :argv, :out, :err, :root
|
|
38
|
+
|
|
39
|
+
def parse_options
|
|
40
|
+
options = {mode: :execute, help: false}
|
|
41
|
+
parser = OptionParser.new do |opts|
|
|
42
|
+
opts.banner = "Usage: kettle-bump VERSION|major|minor|patch [options]"
|
|
43
|
+
opts.on("--from VERSION", "Require the current version before bumping") { |value| options[:from] = validate_version(value) }
|
|
44
|
+
opts.on("--check", "Exit non-zero when the bump would change files") { options[:mode] = :check }
|
|
45
|
+
opts.on("--dry-run", "Print planned changes without writing files") { options[:mode] = :dry_run }
|
|
46
|
+
opts.on("--execute", "Write the bump to disk (default)") { options[:mode] = :execute }
|
|
47
|
+
opts.on("-h", "--help", "Show this help") do
|
|
48
|
+
out.puts(opts)
|
|
49
|
+
options[:help] = true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
parser.parse!(argv)
|
|
53
|
+
options[:target] = argv.shift
|
|
54
|
+
raise Kettle::Dev::Error, "kettle-bump requires VERSION, major, minor, or patch" unless options[:target] || options[:help]
|
|
55
|
+
raise Kettle::Dev::Error, "unexpected arguments: #{argv.join(" ")}" unless argv.empty?
|
|
56
|
+
|
|
57
|
+
options
|
|
58
|
+
rescue OptionParser::ParseError => error
|
|
59
|
+
raise Kettle::Dev::Error, error.message
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def resolve_target_version(target, current_version)
|
|
63
|
+
if BUMP_TYPES.include?(target)
|
|
64
|
+
bumped_version(target, current_version)
|
|
65
|
+
else
|
|
66
|
+
validate_version(target)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def bumped_version(type, current_version)
|
|
71
|
+
version = Gem::Version.new(current_version)
|
|
72
|
+
segments = version.segments
|
|
73
|
+
unless segments.all? { |segment| segment.is_a?(Integer) }
|
|
74
|
+
raise Kettle::Dev::Error, "cannot #{type}-bump non-numeric version #{current_version.inspect}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
major, minor, patch = (segments + [0, 0, 0])[0, 3]
|
|
78
|
+
case type
|
|
79
|
+
when "major"
|
|
80
|
+
"#{major + 1}.0.0"
|
|
81
|
+
when "minor"
|
|
82
|
+
"#{major}.#{minor + 1}.0"
|
|
83
|
+
when "patch"
|
|
84
|
+
"#{major}.#{minor}.#{patch + 1}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def validate_version(version)
|
|
89
|
+
Gem::Version.new(version).to_s
|
|
90
|
+
rescue ArgumentError => error
|
|
91
|
+
raise Kettle::Dev::Error, "invalid version #{version.inspect}: #{error.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def collect_edits(current_version:, target_version:, from_version:)
|
|
95
|
+
if from_version && current_version != from_version
|
|
96
|
+
raise Kettle::Dev::Error, "current version is #{current_version}, not --from #{from_version}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
version_file_edits(target_version) + gemspec_version_edits(current_version, target_version)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def version_file_edits(target_version)
|
|
103
|
+
Kettle::Dev::Versioning.version_file_candidates(root).filter_map do |path|
|
|
104
|
+
source = File.read(path)
|
|
105
|
+
node = version_string_node(source, path)
|
|
106
|
+
current = node.unescaped
|
|
107
|
+
next if current == target_version
|
|
108
|
+
|
|
109
|
+
replacement = quote_like(node.location.slice, target_version)
|
|
110
|
+
file_edit(path, source, node.location.start_offset, node.location.end_offset, replacement)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def gemspec_version_edits(current_version, target_version)
|
|
115
|
+
gemspec_path = gemspec_path_for_bump
|
|
116
|
+
return [] unless gemspec_path
|
|
117
|
+
|
|
118
|
+
source = File.read(gemspec_path)
|
|
119
|
+
parse_result = parse_source(source, gemspec_path)
|
|
120
|
+
each_node(parse_result.value).filter_map do |node|
|
|
121
|
+
next unless node.is_a?(Prism::CallNode) && node.name == :version=
|
|
122
|
+
|
|
123
|
+
version_node = node.arguments&.arguments&.first
|
|
124
|
+
next unless version_node.is_a?(Prism::StringNode)
|
|
125
|
+
next unless version_node.unescaped == current_version
|
|
126
|
+
next if version_node.unescaped == target_version
|
|
127
|
+
|
|
128
|
+
replacement = quote_like(version_node.location.slice, target_version)
|
|
129
|
+
file_edit(gemspec_path, source, version_node.location.start_offset, version_node.location.end_offset, replacement)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def gemspec_path_for_bump
|
|
134
|
+
paths = Dir.glob(File.join(root, "*.gemspec")).sort
|
|
135
|
+
return nil if paths.empty?
|
|
136
|
+
raise Kettle::Dev::Error, "multiple gemspecs found; kettle-bump supports single gems only" if paths.length > 1
|
|
137
|
+
|
|
138
|
+
paths.first
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def version_string_node(source, path)
|
|
142
|
+
parse_result = parse_source(source, path)
|
|
143
|
+
constant = each_node(parse_result.value).find do |node|
|
|
144
|
+
node.is_a?(Prism::ConstantWriteNode) && node.name == :VERSION && node.value.is_a?(Prism::StringNode)
|
|
145
|
+
end
|
|
146
|
+
raise Kettle::Dev::Error, "could not find string VERSION constant in #{path}" unless constant
|
|
147
|
+
|
|
148
|
+
constant.value
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def parse_source(source, path)
|
|
152
|
+
require_prism
|
|
153
|
+
parse_result = Prism.parse(source)
|
|
154
|
+
raise Kettle::Dev::Error, "could not parse #{path}" unless parse_result.success?
|
|
155
|
+
|
|
156
|
+
parse_result
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def require_prism
|
|
160
|
+
return if defined?(Prism)
|
|
161
|
+
|
|
162
|
+
require "prism"
|
|
163
|
+
rescue LoadError => error
|
|
164
|
+
raise Kettle::Dev::Error, "kettle-bump requires Prism; install the prism gem or run on Ruby 3.3+ (#{error.message})"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def each_node(root)
|
|
168
|
+
return enum_for(__method__, root) unless block_given?
|
|
169
|
+
|
|
170
|
+
queue = [root]
|
|
171
|
+
until queue.empty?
|
|
172
|
+
node = queue.shift
|
|
173
|
+
yield node
|
|
174
|
+
queue.concat(node.child_nodes.compact) if node.respond_to?(:child_nodes)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def file_edit(path, source, start_offset, end_offset, replacement)
|
|
179
|
+
{path: path, source: source, start_offset: start_offset, end_offset: end_offset, replacement: replacement}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def quote_like(original, value)
|
|
183
|
+
quote = original.start_with?("'") ? "'" : '"'
|
|
184
|
+
"#{quote}#{value}#{quote}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def write_edits(edits)
|
|
188
|
+
edits.group_by { |edit| edit.fetch(:path) }.each_value do |file_edits|
|
|
189
|
+
source = file_edits.first.fetch(:source)
|
|
190
|
+
file_edits.sort_by { |edit| -edit.fetch(:start_offset) }.each do |edit|
|
|
191
|
+
source[edit.fetch(:start_offset)...edit.fetch(:end_offset)] = edit.fetch(:replacement)
|
|
192
|
+
end
|
|
193
|
+
File.write(file_edits.first.fetch(:path), source)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def summary(edits:, current_version:, target_version:, mode:)
|
|
198
|
+
lines = ["kettle-bump: #{current_version} -> #{target_version}"]
|
|
199
|
+
if edits.empty?
|
|
200
|
+
lines << "No version changes needed."
|
|
201
|
+
else
|
|
202
|
+
verb = (mode == :execute) ? "updated" : "would update"
|
|
203
|
+
edits.map { |edit| edit.fetch(:path) }.uniq.each { |path| lines << "#{verb} #{Kettle::Dev.display_path(path)}" }
|
|
204
|
+
end
|
|
205
|
+
lines.join("\n")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -45,6 +45,13 @@ module Kettle
|
|
|
45
45
|
status.success? ? out.strip : nil
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Current git commit SHA, or nil when unavailable.
|
|
49
|
+
# @return [String, nil]
|
|
50
|
+
def current_head_sha
|
|
51
|
+
out, status = Open3.capture2("git", "rev-parse", "HEAD")
|
|
52
|
+
status.success? ? out.strip : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
48
55
|
# List workflow YAML basenames under .github/workflows at the given root.
|
|
49
56
|
# Excludes maintenance workflows defined by {#exclusions}.
|
|
50
57
|
# @param root [String] project root (defaults to {#project_root})
|
|
@@ -81,15 +88,14 @@ module Kettle
|
|
|
81
88
|
# @param branch [String, nil] branch to query; defaults to {#current_branch}
|
|
82
89
|
# @param token [String, nil] OAuth token for higher rate limits; defaults to {#default_token}
|
|
83
90
|
# @return [Hash{String=>String,Integer}, nil] minimal run info or nil on error/none
|
|
84
|
-
def latest_run(owner:, repo:, workflow_file:, branch: nil, token: default_token)
|
|
91
|
+
def latest_run(owner:, repo:, workflow_file:, branch: nil, token: default_token, require_head: false, head_sha: nil)
|
|
85
92
|
return unless owner && repo
|
|
86
93
|
|
|
87
94
|
b = branch || current_branch
|
|
88
95
|
return unless b
|
|
89
96
|
|
|
90
97
|
# Scope to the exact commit SHA when available to avoid picking up a previous run on the same branch.
|
|
91
|
-
|
|
92
|
-
sha = status.success? ? sha_out.strip : nil
|
|
98
|
+
sha = head_sha || current_head_sha
|
|
93
99
|
base_url = "https://api.github.com/repos/#{owner}/#{repo}/actions/workflows/#{workflow_file}/runs?branch=#{URI.encode_www_form_component(b)}&per_page=5"
|
|
94
100
|
uri = URI(base_url)
|
|
95
101
|
req = Net::HTTP::Get.new(uri)
|
|
@@ -102,9 +108,10 @@ module Kettle
|
|
|
102
108
|
runs = Array(data["workflow_runs"]) || []
|
|
103
109
|
# Try to match by head_sha first; fall back to first run (branch-scoped) if none matches yet.
|
|
104
110
|
run = if sha
|
|
105
|
-
runs.find { |r| r["head_sha"] == sha }
|
|
111
|
+
match = runs.find { |r| r["head_sha"] == sha }
|
|
112
|
+
require_head ? match : (match || runs.first)
|
|
106
113
|
else
|
|
107
|
-
runs.first
|
|
114
|
+
runs.first unless require_head
|
|
108
115
|
end
|
|
109
116
|
return unless run
|
|
110
117
|
|
|
@@ -112,7 +119,8 @@ module Kettle
|
|
|
112
119
|
"status" => run["status"],
|
|
113
120
|
"conclusion" => run["conclusion"],
|
|
114
121
|
"html_url" => run["html_url"],
|
|
115
|
-
"id" => run["id"]
|
|
122
|
+
"id" => run["id"],
|
|
123
|
+
"head_sha" => run["head_sha"]
|
|
116
124
|
}
|
|
117
125
|
rescue => e
|
|
118
126
|
Kettle::Dev.debug_error(e, __method__)
|
|
@@ -285,8 +285,13 @@ module Kettle
|
|
|
285
285
|
total = workflows.size
|
|
286
286
|
abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
|
|
287
287
|
|
|
288
|
+
head_sha = Kettle::Dev::CIHelpers.current_head_sha
|
|
289
|
+
abort("Could not determine local HEAD SHA for GitHub Actions checks.") unless head_sha && !head_sha.empty?
|
|
290
|
+
|
|
288
291
|
passed = {}
|
|
292
|
+
started = {}
|
|
289
293
|
puts "Ensuring GitHub Actions workflows pass on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'"
|
|
294
|
+
puts "Waiting for GitHub Actions runs to start for HEAD #{head_sha[0, 12]}."
|
|
290
295
|
pbar = if defined?(ProgressBar)
|
|
291
296
|
ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
|
|
292
297
|
end
|
|
@@ -300,11 +305,15 @@ module Kettle
|
|
|
300
305
|
end
|
|
301
306
|
end
|
|
302
307
|
sleep((initial_sleep && initial_sleep >= 0) ? initial_sleep : 3)
|
|
308
|
+
start_timeout = github_start_timeout
|
|
309
|
+
poll_interval = github_poll_interval
|
|
310
|
+
start_deadline = monotonic_time + start_timeout
|
|
303
311
|
idx = 0
|
|
304
312
|
loop do
|
|
305
313
|
wf = workflows[idx]
|
|
306
|
-
run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch)
|
|
314
|
+
run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch, require_head: true, head_sha: head_sha)
|
|
307
315
|
if run
|
|
316
|
+
started[wf] = true
|
|
308
317
|
if Kettle::Dev::CIHelpers.success?(run)
|
|
309
318
|
unless passed[wf]
|
|
310
319
|
passed[wf] = true
|
|
@@ -317,9 +326,14 @@ module Kettle
|
|
|
317
326
|
end
|
|
318
327
|
end
|
|
319
328
|
break if passed.size == total
|
|
329
|
+
if started.size < total && monotonic_time >= start_deadline
|
|
330
|
+
missing = (workflows - started.keys).join(", ")
|
|
331
|
+
puts
|
|
332
|
+
abort("Timed out after #{start_timeout}s waiting for GitHub Actions workflows to start for HEAD #{head_sha[0, 12]}: #{missing}. Confirm GitHub Actions started, then restart this tool from CI validation with: #{restart_hint}")
|
|
333
|
+
end
|
|
320
334
|
|
|
321
335
|
idx = (idx + 1) % total
|
|
322
|
-
sleep(
|
|
336
|
+
sleep(poll_interval)
|
|
323
337
|
end
|
|
324
338
|
pbar&.finish unless pbar&.finished?
|
|
325
339
|
puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
|
|
@@ -327,6 +341,31 @@ module Kettle
|
|
|
327
341
|
end
|
|
328
342
|
module_function :monitor_github_internal!
|
|
329
343
|
|
|
344
|
+
def monotonic_time
|
|
345
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
346
|
+
end
|
|
347
|
+
module_function :monotonic_time
|
|
348
|
+
|
|
349
|
+
def github_start_timeout
|
|
350
|
+
seconds = begin
|
|
351
|
+
Integer(ENV["K_RELEASE_CI_START_TIMEOUT"])
|
|
352
|
+
rescue
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
355
|
+
(seconds && seconds >= 0) ? seconds : 120
|
|
356
|
+
end
|
|
357
|
+
module_function :github_start_timeout
|
|
358
|
+
|
|
359
|
+
def github_poll_interval
|
|
360
|
+
seconds = begin
|
|
361
|
+
Float(ENV["K_RELEASE_CI_POLL_INTERVAL"])
|
|
362
|
+
rescue
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
(seconds && seconds >= 0) ? seconds : 1
|
|
366
|
+
end
|
|
367
|
+
module_function :github_poll_interval
|
|
368
|
+
|
|
330
369
|
def monitor_gitlab_internal!(restart_hint:)
|
|
331
370
|
root = Kettle::Dev::CIHelpers.project_root
|
|
332
371
|
gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
|