kettle-dev 2.2.13 → 2.2.14

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.
data/README.md CHANGED
@@ -10,9 +10,9 @@
10
10
 
11
11
  `if ci_badges.map(&:color).all? { it == "green"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job.
12
12
 
13
- [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
13
+ [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
14
14
 
15
- <details>
15
+ <details markdown="1">
16
16
  <summary>👣 How will this project approach the September 2025 hostile takeover of RubyGems? 🚑️</summary>
17
17
 
18
18
  I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo).
@@ -22,7 +22,7 @@ I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-ta
22
22
  ## 🌻 Synopsis <a href="https://discord.gg/3qme4XHNKN"><img alt="Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/galtzo-floss/avatar-128px.svg" width="8%" align="right"/></a> <a href="https://ruby-toolbox.com"><img alt="ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5" src="https://logos.galtzo.com/assets/images/ruby-lang/avatar-128px.svg" width="8%" align="right"/></a>
23
23
 
24
24
  Kettle::Dev is the development, CI, changelog, and release harness used by
25
- kettle-rb gems. It installs rake tasks when loaded from a project's `Rakefile`,
25
+ kettle-dev gems. It installs rake tasks when loaded from a project's `Rakefile`,
26
26
  and it ships command-line tools for changelog preparation, release automation,
27
27
  multi-forge git remotes, commit-message hooks, and Open Collective README
28
28
  updates.
@@ -42,14 +42,14 @@ require "kettle/dev"
42
42
  ```
43
43
 
44
44
  For RSpec projects, use the matching test harness from
45
- [kettle-test](https://github.com/kettle-rb/kettle-test):
45
+ [kettle-test](https://github.com/kettle-dev/kettle-test):
46
46
 
47
47
  ```ruby
48
48
  require "kettle/test/rspec"
49
49
  ```
50
50
 
51
51
  Project setup and template refreshes are now owned by
52
- [kettle-jem](https://github.com/kettle-rb/kettle-jem), not kettle-dev:
52
+ [kettle-jem](https://github.com/kettle-dev/kettle-jem), not kettle-dev:
53
53
 
54
54
  ```console
55
55
  gem install kettle-jem
@@ -77,7 +77,7 @@ bin/kettle-release
77
77
  - Rake task loading from `require "kettle/dev"`.
78
78
  - RuboCop Gradual, Reek, YARD, appraisal, local CI, benchmark, and coverage task wiring.
79
79
  - `kettle-changelog` for moving Unreleased changelog notes into a versioned release section with coverage and documentation stats.
80
- - `kettle-release` for the canonical kettle-rb release flow.
80
+ - `kettle-release` for the canonical kettle-dev release flow.
81
81
  - `kettle-pre-release` for release readiness checks.
82
82
  - `kettle-dvcs` for normalizing GitHub, GitLab, Codeberg, and aggregate remotes.
83
83
  - `kettle-commit-msg` for shared commit-message hook behavior.
@@ -88,9 +88,9 @@ bin/kettle-release
88
88
 
89
89
  | Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] |
90
90
  |-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
91
- | Works with JRuby | [![JRuby 9.2 Compat][💎jruby-9.2i]][🚎jruby-9.2-wf] [![JRuby 9.3 Compat][💎jruby-9.3i]][🚎jruby-9.3-wf] <br/> [![JRuby 9.4 Compat][💎jruby-9.4i]][🚎jruby-9.4-wf] [![JRuby current Compat][💎jruby-c-i]][🚎10-j-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf]|
92
- | Works with Truffle Ruby | [![Truffle Ruby 22.3 Compat][💎truby-22.3i]][🚎truby-22.3-wf] [![Truffle Ruby 23.0 Compat][💎truby-23.0i]][🚎truby-23.0-wf] [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎truby-23.1-wf] <br/> [![Truffle Ruby 24.2 Compat][💎truby-24.2i]][🚎truby-24.2-wf] [![Truffle Ruby 25.0 Compat][💎truby-25.0i]][🚎truby-25.0-wf] [![Truffle Ruby current Compat][💎truby-c-i]][🚎9-t-wf]|
93
- | Works with MRI Ruby 4 | [![Ruby 4.0 Compat][💎ruby-4.0i]][🚎11-c-wf] [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf]|
91
+ | Works with JRuby | [![JRuby 9.2 Compat][💎jruby-9.2i]][🚎jruby-9.2-wf] [![JRuby 9.3 Compat][💎jruby-9.3i]][🚎jruby-9.3-wf] <br/> [![JRuby 9.4 Compat][💎jruby-9.4i]][🚎jruby-9.4-wf] [![JRuby 10.0 Compat][💎jruby-10.0i]][🚎jruby-10.0-wf] [![JRuby current Compat][💎jruby-c-i]][🚎10-j-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf]|
92
+ | Works with Truffle Ruby | [![Truffle Ruby 22.3 Compat][💎truby-22.3i]][🚎truby-22.3-wf] [![Truffle Ruby 23.0 Compat][💎truby-23.0i]][🚎truby-23.0-wf] [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎truby-23.1-wf] <br/> [![Truffle Ruby 24.2 Compat][💎truby-24.2i]][🚎truby-24.2-wf] [![Truffle Ruby 25.0 Compat][💎truby-25.0i]][🚎truby-25.0-wf] [![Truffle Ruby 33.0 Compat][💎truby-33.0i]][🚎truby-33.0-wf] [![Truffle Ruby current Compat][💎truby-c-i]][🚎9-t-wf] [![Truffle Ruby HEAD Compat][💎truby-headi]][🚎3-hd-wf]|
93
+ | Works with MRI Ruby 4 | [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf]|
94
94
  | Works with MRI Ruby 3 | [![Ruby 3.0 Compat][💎ruby-3.0i]][🚎ruby-3.0-wf] [![Ruby 3.1 Compat][💎ruby-3.1i]][🚎ruby-3.1-wf] [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎ruby-3.2-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎ruby-3.3-wf] [![Ruby 3.4 Compat][💎ruby-3.4i]][🚎ruby-3.4-wf]|
95
95
  | Works with MRI Ruby 2 | [![Ruby 2.4 Compat][💎ruby-2.4i]][🚎ruby-2.4-wf] [![Ruby 2.5 Compat][💎ruby-2.5i]][🚎ruby-2.5-wf] [![Ruby 2.6 Compat][💎ruby-2.6i]][🚎ruby-2.6-wf] [![Ruby 2.7 Compat][💎ruby-2.7i]][🚎ruby-2.7-wf]|
96
96
  | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
@@ -109,9 +109,24 @@ This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
109
109
  may be higher than the gem's runtime compatibility floor when legacy Rubies are
110
110
  not practical for the current toolchain.
111
111
 
112
- | 🚚 _Amazing_ test matrix was brought to you by | 🔎 appraisal2 🔎 and the color 💚 green 💚 |
113
- |------------------------------------------------|--------------------------------------------------------|
114
- | 👟 Check it out! | [github.com/appraisal-rb/appraisal2][💎appraisal2] ✨ |
112
+ <a href="https://github.com/kettle-dev"><img alt="kettle-dev Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/kettle-dev/avatar-128px.svg" width="14%" align="right"/></a>
113
+
114
+ The _amazing_ test matrix is powered by the kettle-dev stack.
115
+
116
+ <details markdown="1">
117
+ <summary>How kettle-dev manages complexity in tests</summary>
118
+
119
+ | Gem | Source | Role | Daily download rank |
120
+ |-----|--------|------|---------------------|
121
+ | [appraisal2](https://bestgems.org/gems/appraisal2) | [GitHub](https://github.com/appraisal-rb/appraisal2) | multi-dependency Appraisal matrix generation | [![Daily download rank for appraisal2](https://img.shields.io/gem/rd/appraisal2.svg?style=flat-square)](https://bestgems.org/gems/appraisal2) |
122
+ | [appraisal2-rubocop](https://bestgems.org/gems/appraisal2-rubocop) | [GitHub](https://github.com/appraisal-rb/appraisal2-rubocop) | RuboCop Appraisal generator integration | [![Daily download rank for appraisal2-rubocop](https://img.shields.io/gem/rd/appraisal2-rubocop.svg?style=flat-square)](https://bestgems.org/gems/appraisal2-rubocop) |
123
+ | [kettle-jem](https://bestgems.org/gems/kettle-jem) | [GitHub](https://github.com/kettle-dev/kettle-jem) | Appraisals & CI workflow templates | [![Daily download rank for kettle-jem](https://img.shields.io/gem/rd/kettle-jem.svg?style=flat-square)](https://bestgems.org/gems/kettle-jem) |
124
+ | [kettle-soup-cover](https://bestgems.org/gems/kettle-soup-cover) | [GitHub](https://github.com/kettle-dev/kettle-soup-cover) | SimpleCov coverage policy and reporting | [![Daily download rank for kettle-soup-cover](https://img.shields.io/gem/rd/kettle-soup-cover.svg?style=flat-square)](https://bestgems.org/gems/kettle-soup-cover) |
125
+ | [kettle-test](https://bestgems.org/gems/kettle-test) | [GitHub](https://github.com/kettle-dev/kettle-test) | standard test runner and coverage harness | [![Daily download rank for kettle-test](https://img.shields.io/gem/rd/kettle-test.svg?style=flat-square)](https://bestgems.org/gems/kettle-test) |
126
+ | [rubocop-lts](https://bestgems.org/gems/rubocop-lts) | [GitHub](https://github.com/rubocop-lts/rubocop-lts) | Ruby-version-aware linting | [![Daily download rank for rubocop-lts](https://img.shields.io/gem/rd/rubocop-lts.svg?style=flat-square)](https://bestgems.org/gems/rubocop-lts) |
127
+ | [turbo_tests2](https://bestgems.org/gems/turbo_tests2) | [GitHub](https://github.com/galtzo-floss/turbo_tests2) | parallel test execution | [![Daily download rank for turbo_tests2](https://img.shields.io/gem/rd/turbo_tests2.svg?style=flat-square)](https://bestgems.org/gems/turbo_tests2) |
128
+
129
+ </details>
115
130
 
116
131
  ### Federated DVCS
117
132
 
@@ -187,7 +202,7 @@ require "kettle/dev"
187
202
 
188
203
  ### RSpec
189
204
 
190
- This gem integrates tightly with [kettle-test](https://github.com/kettle-rb/kettle-test).
205
+ This gem integrates tightly with [kettle-test](https://github.com/kettle-dev/kettle-test).
191
206
 
192
207
  ```ruby
193
208
  require "kettle/test/rspec"
@@ -527,7 +542,7 @@ What it does:
527
542
  - Sponsors (Organizations): `<!-- <TAG>-ORGANIZATIONS:START --> … <!-- <TAG>-ORGANIZATIONS:END -->`
528
543
  - Handle resolution:
529
544
  1. `OPENCOLLECTIVE_HANDLE` environment variable, if set
530
- 2. `opencollective.yml` in the project root (e.g., `collective: "kettle-rb"` in this repo)
545
+ 2. `opencollective.yml` in the project root (e.g., `collective: "kettle-dev"` in this repo)
531
546
  - Usage:
532
547
  - `exe/kettle-readme-backers`
533
548
  - `OPENCOLLECTIVE_HANDLE=my-collective exe/kettle-readme-backers`
@@ -593,7 +608,7 @@ Made with [contributors-img][🖐contrib-rocks].
593
608
 
594
609
  Also see GitLab Contributors: [https://gitlab.com/kettle-dev/kettle-dev/-/graphs/main][🚎contributors-gl]
595
610
 
596
- <details>
611
+ <details markdown="1">
597
612
  <summary>⭐️ Star History</summary>
598
613
 
599
614
  <a href="https://star-history.com/kettle-dev/kettle-dev&Date">
@@ -690,12 +705,8 @@ Thanks for RTFM. ☺️
690
705
  [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
691
706
  [🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-pboling-blue?style=for-the-badge&logo=github
692
707
  [🖇sponsor]: https://github.com/sponsors/pboling
693
- [🖇polar-img]: https://img.shields.io/badge/polar-donate-a51611.svg?style=flat
694
- [🖇polar]: https://polar.sh/pboling
695
708
  [🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat
696
709
  [🖇kofi]: https://ko-fi.com/pboling
697
- [🖇patreon-img]: https://img.shields.io/badge/patreon-donate-a51611.svg?style=flat
698
- [🖇patreon]: https://patreon.com/galtzo
699
710
  [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat
700
711
  [🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug=pboling&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff
701
712
  [🖇buyme]: https://www.buymeacoffee.com/pboling
@@ -785,11 +796,13 @@ Thanks for RTFM. ☺️
785
796
  [🚎jruby-9.2-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/jruby-9.2.yml
786
797
  [🚎jruby-9.3-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/jruby-9.3.yml
787
798
  [🚎jruby-9.4-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/jruby-9.4.yml
799
+ [🚎jruby-10.0-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/jruby-10.0.yml
788
800
  [🚎truby-22.3-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-22.3.yml
789
801
  [🚎truby-23.0-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-23.0.yml
790
802
  [🚎truby-23.1-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-23.1.yml
791
803
  [🚎truby-24.2-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-24.2.yml
792
804
  [🚎truby-25.0-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-25.0.yml
805
+ [🚎truby-33.0-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-33.0.yml
793
806
  [🚎2-cov-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/coverage.yml
794
807
  [🚎2-cov-wfi]: https://github.com/kettle-dev/kettle-dev/actions/workflows/coverage.yml/badge.svg
795
808
  [🚎3-hd-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/heads.yml
@@ -817,7 +830,6 @@ Thanks for RTFM. ☺️
817
830
  [💎ruby-3.2i]: https://img.shields.io/badge/Ruby-3.2-CC342D?style=for-the-badge&logo=ruby&logoColor=white
818
831
  [💎ruby-3.3i]: https://img.shields.io/badge/Ruby-3.3-CC342D?style=for-the-badge&logo=ruby&logoColor=white
819
832
  [💎ruby-3.4i]: https://img.shields.io/badge/Ruby-3.4-CC342D?style=for-the-badge&logo=ruby&logoColor=white
820
- [💎ruby-4.0i]: https://img.shields.io/badge/Ruby-4.0-CC342D?style=for-the-badge&logo=ruby&logoColor=white
821
833
  [💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
822
834
  [💎ruby-headi]: https://img.shields.io/badge/Ruby-HEAD-CC342D?style=for-the-badge&logo=ruby&logoColor=blue
823
835
  [💎truby-22.3i]: https://img.shields.io/badge/Truffle_Ruby-22.3-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
@@ -825,10 +837,13 @@ Thanks for RTFM. ☺️
825
837
  [💎truby-23.1i]: https://img.shields.io/badge/Truffle_Ruby-23.1-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
826
838
  [💎truby-24.2i]: https://img.shields.io/badge/Truffle_Ruby-24.2-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
827
839
  [💎truby-25.0i]: https://img.shields.io/badge/Truffle_Ruby-25.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
840
+ [💎truby-33.0i]: https://img.shields.io/badge/Truffle_Ruby-33.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
828
841
  [💎truby-c-i]: https://img.shields.io/badge/Truffle_Ruby-current-34BCB1?style=for-the-badge&logo=ruby&logoColor=green
842
+ [💎truby-headi]: https://img.shields.io/badge/Truffle_Ruby-HEAD-34BCB1?style=for-the-badge&logo=ruby&logoColor=blue
829
843
  [💎jruby-9.2i]: https://img.shields.io/badge/JRuby-9.2-FBE742?style=for-the-badge&logo=ruby&logoColor=red
830
844
  [💎jruby-9.3i]: https://img.shields.io/badge/JRuby-9.3-FBE742?style=for-the-badge&logo=ruby&logoColor=red
831
845
  [💎jruby-9.4i]: https://img.shields.io/badge/JRuby-9.4-FBE742?style=for-the-badge&logo=ruby&logoColor=red
846
+ [💎jruby-10.0i]: https://img.shields.io/badge/JRuby-10.0-FBE742?style=for-the-badge&logo=ruby&logoColor=red
832
847
  [💎jruby-c-i]: https://img.shields.io/badge/JRuby-current-FBE742?style=for-the-badge&logo=ruby&logoColor=green
833
848
  [💎jruby-headi]: https://img.shields.io/badge/JRuby-HEAD-FBE742?style=for-the-badge&logo=ruby&logoColor=blue
834
849
  [🤝gh-issues]: https://github.com/kettle-dev/kettle-dev/issues
@@ -857,7 +872,7 @@ Thanks for RTFM. ☺️
857
872
  [📌gitmoji]: https://gitmoji.dev
858
873
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
859
874
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
860
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.266-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
875
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.411-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
861
876
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
862
877
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
863
878
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -885,7 +900,7 @@ Thanks for RTFM. ☺️
885
900
  | Package | kettle-dev |
886
901
  | 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 |
887
902
  | Homepage | https://github.com/kettle-dev/kettle-dev |
888
- | Source | https://github.com/kettle-dev/kettle-dev/tree/v2.2.4 |
903
+ | Source | https://github.com/kettle-dev/kettle-dev |
889
904
  | License | `AGPL-3.0-only` |
890
- | 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-dev, 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 |
905
+ | Funding | https://github.com/sponsors/pboling, https://ko-fi.com/pboling, https://liberapay.com/pboling/donate, https://opencollective.com/kettle-dev, https://thanks.dev/u/gh/pboling, https://tidelift.com/funding/github/rubygems/kettle-dev, https://www.buymeacoffee.com/pboling |
891
906
  <!-- kettle-jem:metadata:end -->
data/exe/kettle-changelog CHANGED
@@ -55,7 +55,7 @@ end
55
55
  begin
56
56
  if ARGV.include?("-h") || ARGV.include?("--help")
57
57
  puts <<~USAGE
58
- Usage: kettle-changelog [--version VERSION] [--update-prep] [--pending-release] [--release-state] [--json] [--no-strict] [--no-coverage-threshold]
58
+ Usage: kettle-changelog [--version VERSION] [--update-prep] [--pending-release] [--release-state] [--add-unreleased-entry --section SECTION --entry TEXT] [--json] [--no-strict] [--no-coverage-threshold]
59
59
 
60
60
  Detects the current version from lib/**/version.rb, the latest live release, and
61
61
  the most recent CHANGELOG.md release section, then prompts to confirm the selected plan:
@@ -70,6 +70,9 @@ begin
70
70
  --pending-release Query whether the changelog has release work pending; exits 0 for yes, 1 for no
71
71
  --release-state,
72
72
  --release-status Print changelog release state, including latest published release and pending sources
73
+ --add-unreleased-entry Add one entry to an existing section under ## [Unreleased]
74
+ --section SECTION Unreleased section to receive the entry (Added, Changed, Deprecated, Removed, Fixed, Security)
75
+ --entry TEXT Changelog entry text; "- " is added when omitted
73
76
  --json Print query output as JSON
74
77
  --no-strict Allow missing coverage and yard data (warnings only, no errors)
75
78
  --no-coverage-threshold Generate coverage without hard-failing below configured thresholds
@@ -128,11 +131,42 @@ begin
128
131
  update_prep = ARGV.delete("--update-prep")
129
132
  pending_release_query = ARGV.delete("--pending-release")
130
133
  release_state_query = ARGV.delete("--release-state") || ARGV.delete("--release-status")
134
+ add_unreleased_entry = ARGV.delete("--add-unreleased-entry") || ARGV.delete("--add-changelog-entry")
131
135
  json_output = ARGV.delete("--json")
132
136
  coverage_threshold_disabled = ARGV.include?("--no-coverage-threshold") || ARGV.include?("--no-coverage-thresholds")
133
137
  coverage_hard = !coverage_threshold_disabled && ENV.fetch("K_CHANGELOG_COVERAGE_HARD", "true").downcase != "false"
134
138
  version_override = extract_version_arg!(ARGV)
135
139
 
140
+ def extract_option_arg!(argv, name)
141
+ value = nil
142
+ if (idx = argv.index(name))
143
+ value = argv[idx + 1]
144
+ Kettle::Dev::ExitAdapter.abort("#{name} requires a value") if value.to_s.empty?
145
+ argv.slice!(idx, 2)
146
+ end
147
+ argv.delete_if do |arg|
148
+ if arg.start_with?("#{name}=")
149
+ value = arg.split("=", 2)[1]
150
+ true
151
+ else
152
+ false
153
+ end
154
+ end
155
+ value
156
+ end
157
+
158
+ if add_unreleased_entry
159
+ section = extract_option_arg!(ARGV, "--section")
160
+ entry = extract_option_arg!(ARGV, "--entry")
161
+ Kettle::Dev::ExitAdapter.abort("--section is required with --add-unreleased-entry") if section.to_s.empty?
162
+ Kettle::Dev::ExitAdapter.abort("--entry is required with --add-unreleased-entry") if entry.to_s.empty?
163
+
164
+ result = Kettle::Dev::ChangelogEntryAdder.new(section: section, entry: entry).run
165
+ message = (result == :changed) ? "CHANGELOG.md updated under Unreleased #{section}." : "CHANGELOG.md already contains that Unreleased #{section} entry."
166
+ puts(json_output ? JSON.pretty_generate({changed: result == :changed, section: section, entry: entry}) : message)
167
+ exit(0)
168
+ end
169
+
136
170
  cli = Kettle::Dev::ChangelogCLI.new(strict: strict_mode, enforce_coverage_thresholds: coverage_hard, update_prep: update_prep, version: version_override)
137
171
  if release_state_query
138
172
  state = cli.release_state
@@ -14,6 +14,8 @@ module Kettle
14
14
  # includes coverage and YARD stats, and updates link references.
15
15
  class ChangelogCLI
16
16
  UNRELEASED_SECTION_HEADING = "[Unreleased]:"
17
+ CHANGELOG_VERSION_PATTERN = /\d+\.\d+\.\d+(?:[.-][0-9A-Za-z]+)*/
18
+ CHANGELOG_VERSION_PATTERN_SOURCE = CHANGELOG_VERSION_PATTERN.source
17
19
  # Matches a Markdown link-reference definition line, e.g. `[key]: https://...`
18
20
  LINK_REF_DEF_RE = /^\s*\[[^\]]+\]:\s+\S+/
19
21
  # Matches an ATX heading at H4 or deeper (####, #####, ...)
@@ -435,7 +437,7 @@ module Kettle
435
437
 
436
438
  def detect_previous_version(after_text)
437
439
  # after_text begins with the first released section following Unreleased
438
- m = after_text.match(/^## \[(\d+\.\d+\.\d+)\]/)
440
+ m = after_text.match(/^## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]/o)
439
441
  return m[1] if m
440
442
 
441
443
  nil
@@ -445,7 +447,7 @@ module Kettle
445
447
  unreleased_block, before, after = extract_unreleased(changelog)
446
448
  abort("Could not find '## [Unreleased]' section in CHANGELOG.md") if unreleased_block.nil?
447
449
 
448
- release_heading = after.to_s.match(/\A## \[(\d+\.\d+\.\d+)\][^\n]*\n/)
450
+ release_heading = after.to_s.match(/\A## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\][^\n]*\n/o)
449
451
  abort("Could not find a prepared release section after '## [Unreleased]' in CHANGELOG.md") unless release_heading
450
452
 
451
453
  prepared_version = release_heading[1]
@@ -790,16 +792,16 @@ module Kettle
790
792
  non_t_tag_refs = {}
791
793
  lines[scan_start..-1].to_a.each do |l|
792
794
  # Case A: explicit tag ref key like [1.2.3t]: ...
793
- if (m = l.match(/^\[(\d+\.\d+\.\d+)t\]:\s+(\S+)/))
795
+ if (m = l.match(/^\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})t\]:\s+(\S+)/o))
794
796
  t_versions[m[1]] = true
795
797
  next
796
798
  end
797
799
  # Case B: non-t ref that nevertheless points to a tag URL (GitHub or GitLab)
798
- if (m2 = l.match(/^\[(\d+\.\d+\.\d+)\]:\s+(\S+)/))
800
+ if (m2 = l.match(/^\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]:\s+(\S+)/o))
799
801
  url = m2[2]
800
802
  # Accept only when the URL clearly points to a tag for the SAME version
801
803
  # Support both GitHub and GitLab style tag URLs
802
- if (murl = url.match(%r{/(?:releases/)?tags?/v(\d+\.\d+\.\d+)}i))
804
+ if (murl = url.match(%r{/(?:releases/)?tags?/v(#{CHANGELOG_VERSION_PATTERN_SOURCE})}io))
803
805
  version_in_url = murl[1]
804
806
  if version_in_url == m2[1]
805
807
  non_t_tag_refs[m2[1]] = url
@@ -817,7 +819,7 @@ module Kettle
817
819
  while i < lines.length
818
820
  line = lines[i]
819
821
  # Case 1: Heading contains legacy tag suffix we should convert
820
- m = line.match(/^## \[(\d+\.\d+\.\d+)\](.*)\(\[tag\]\[(\d+\.\d+\.\d+)t\]\)\s*$/i)
822
+ m = line.match(/^## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\](.*)\(\[tag\]\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})t\]\)\s*$/io)
821
823
  if m && m[1] == m[3]
822
824
  ver = m[1]
823
825
  middle = m[2]
@@ -837,7 +839,7 @@ module Kettle
837
839
  end
838
840
 
839
841
  # Case 2: Heading does NOT contain suffix, but a matching tag ref exists; ensure a TAG list item
840
- if (m2 = line.match(/^## \[(\d+\.\d+\.\d+)\](.*)$/))
842
+ if (m2 = line.match(/^## \[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\](.*)$/o))
841
843
  ver2 = m2[1]
842
844
  # Skip Unreleased heading and non-release headings
843
845
  unless ver2.nil?
@@ -858,7 +860,7 @@ module Kettle
858
860
  # Footer duplication: if we are in the footer block and encounter a non-t tag-ref
859
861
  # without a matching t-ref, emit the t-ref immediately after with the same URL.
860
862
  if i >= scan_start
861
- if (mref = line.match(/^\[(\d+\.\d+\.\d+)\]:\s+(\S+)/))
863
+ if (mref = line.match(/^\[(#{CHANGELOG_VERSION_PATTERN_SOURCE})\]:\s+(\S+)/o))
862
864
  vref = mref[1]
863
865
  mref[2]
864
866
  if non_t_tag_refs[vref] && !t_versions[vref]
@@ -949,9 +951,9 @@ module Kettle
949
951
  compares = {}
950
952
  tags = {}
951
953
  by_key.each do |k, v|
952
- if k =~ /^(\d+\.\d+\.\d+)$/
954
+ if k =~ /^(#{CHANGELOG_VERSION_PATTERN_SOURCE})$/o
953
955
  compares[$1] = v
954
- elsif k =~ /^(\d+\.\d+\.\d+)t$/
956
+ elsif k =~ /^(#{CHANGELOG_VERSION_PATTERN_SOURCE})t$/o
955
957
  tags[$1] = v
956
958
  end
957
959
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Dev
5
+ class ChangelogEntryAdder
6
+ SECTIONS = %w[Added Changed Deprecated Removed Fixed Security].freeze
7
+
8
+ def initialize(section:, entry:, root: Kettle::Dev::CIHelpers.project_root)
9
+ @root = root
10
+ @section = section.to_s
11
+ @entry = normalize_entry(entry)
12
+ @changelog_path = File.join(@root, "CHANGELOG.md")
13
+ end
14
+
15
+ def run
16
+ raise Error, "unsupported changelog section #{@section.inspect}" unless SECTIONS.include?(@section)
17
+ raise Error, "missing CHANGELOG.md in #{Kettle::Dev.display_path(@root)}" unless File.file?(@changelog_path)
18
+
19
+ require_markly_crispr!
20
+ source = File.read(@changelog_path)
21
+ context = Ast::Crispr::Markdown::Markly.document_context(content: source, source_label: @changelog_path)
22
+ sections = context.structural_owners(owner_scope: :heading_sections)
23
+ unreleased = find_unreleased_heading!(sections)
24
+ target = find_unreleased_section!(sections, unreleased)
25
+ slice = target_slice(source, target)
26
+ return :unchanged if entry_present?(slice)
27
+
28
+ updated = insert_entry(source, target)
29
+ File.write(@changelog_path, updated)
30
+ :changed
31
+ end
32
+
33
+ private
34
+
35
+ def normalize_entry(entry)
36
+ text = entry.to_s.strip
37
+ raise Error, "changelog entry must not be empty" if text.empty?
38
+
39
+ text.start_with?("- ") ? text : "- #{text}"
40
+ end
41
+
42
+ def require_markly_crispr!
43
+ require "ast/crispr/markdown/markly"
44
+ rescue LoadError => error
45
+ raise Error, "kettle-changelog add-unreleased-entry requires ast-crispr-markdown-markly (#{error.message})"
46
+ end
47
+
48
+ def find_heading!(sections, heading_text:, level:)
49
+ matches = sections.select { |owner| owner.heading_text.to_s.strip == heading_text && owner.level == level }
50
+ raise Error, "expected exactly one #{heading(level, heading_text)} section in CHANGELOG.md, found #{matches.length}" unless matches.length == 1
51
+
52
+ matches.first
53
+ end
54
+
55
+ def find_unreleased_heading!(sections)
56
+ matches = sections.select do |owner|
57
+ owner.level == 2 && owner.heading_source.to_s.strip == "## [Unreleased]"
58
+ end
59
+ raise Error, "expected exactly one ## [Unreleased] section in CHANGELOG.md, found #{matches.length}" unless matches.length == 1
60
+
61
+ matches.first
62
+ end
63
+
64
+ def find_unreleased_section!(sections, unreleased)
65
+ matches = sections.select do |owner|
66
+ owner.heading_text.to_s.strip == @section &&
67
+ owner.level == 3 &&
68
+ owner.location.start_line > unreleased.location.start_line &&
69
+ owner.location.end_line <= unreleased.location.end_line
70
+ end
71
+ raise Error, "expected exactly one #{heading(3, @section)} section under ## [Unreleased] in CHANGELOG.md, found #{matches.length}" unless matches.length == 1
72
+
73
+ matches.first
74
+ end
75
+
76
+ def heading(level, text)
77
+ "#{"#" * level} #{text}"
78
+ end
79
+
80
+ def target_slice(source, target)
81
+ lines = source.lines
82
+ lines[(target.location.start_line - 1)...target.location.end_line].to_a.join
83
+ end
84
+
85
+ def entry_present?(slice)
86
+ slice.lines.any? { |line| line.chomp == @entry }
87
+ end
88
+
89
+ def insert_entry(source, target)
90
+ lines = source.lines
91
+ insertion_index = insertion_index(lines, target)
92
+ payload = entry_payload(lines, insertion_index, target)
93
+ lines.insert(insertion_index, payload)
94
+ lines.join
95
+ end
96
+
97
+ def insertion_index(lines, target)
98
+ first_content_index = target.location.start_line
99
+ index = target.location.end_line
100
+ while index > first_content_index && lines.fetch(index - 1).strip.empty?
101
+ index -= 1
102
+ end
103
+ index
104
+ end
105
+
106
+ def entry_payload(lines, insertion_index, target)
107
+ previous_line = lines[insertion_index - 1].to_s
108
+ next_line = lines[insertion_index].to_s
109
+ before = previous_line.strip.empty? ? "" : "\n"
110
+ after = (next_line.empty? || next_line.strip.empty?) ? "" : "\n"
111
+ "#{before}#{@entry}\n#{after}"
112
+ end
113
+ end
114
+ end
115
+ end
@@ -32,6 +32,9 @@ module Kettle
32
32
  COMMENT_REASON = "update_version_comment"
33
33
  DEFAULT_UPGRADE_LEVEL = "patch"
34
34
  DEFAULT_CACHE_TTL_SECONDS = 24 * 60 * 60
35
+ DEFAULT_HTTP_OPEN_TIMEOUT_SECONDS = 5
36
+ DEFAULT_HTTP_READ_TIMEOUT_SECONDS = 10
37
+ DEFAULT_HTTP_REFRESH_TIMEOUT_SECONDS = 20
35
38
  VALID_UPGRADE_LEVELS = %w[major minor patch].freeze
36
39
  VERSION_COMMENT_SUFFIX_RE = /\A\s+#\s*v?(?<version>\d+(?:\.\d+\.\d+(?:[-.]?[0-9A-Za-z.-]+)?)?)/
37
40
  VERSION_COMMENT_REPLACEMENT_RE = /\A(?<prefix>\s+#\s*)v?\d+(?:\.\d+\.\d+(?:[-.]?[0-9A-Za-z.-]+)?)?/
@@ -1100,12 +1103,15 @@ module Kettle
1100
1103
 
1101
1104
  # Lightweight GitHub API client for commit and release SHA resolution.
1102
1105
  class GitHubClient
1103
- def initialize(token:, api_base:, user_agent:, persistent_cache: nil, refresh_cache: false)
1106
+ def initialize(token:, api_base:, user_agent:, persistent_cache: nil, refresh_cache: false, open_timeout: DEFAULT_HTTP_OPEN_TIMEOUT_SECONDS, read_timeout: DEFAULT_HTTP_READ_TIMEOUT_SECONDS, refresh_timeout: DEFAULT_HTTP_REFRESH_TIMEOUT_SECONDS)
1104
1107
  @token = token
1105
1108
  @api_base = api_base
1106
1109
  @user_agent = user_agent
1107
1110
  @persistent_cache = persistent_cache
1108
1111
  @refresh_cache = refresh_cache
1112
+ @open_timeout = open_timeout
1113
+ @read_timeout = read_timeout
1114
+ @refresh_timeout = refresh_timeout
1109
1115
  @commit_cache = {}
1110
1116
  @release_cache = {}
1111
1117
  end
@@ -1114,58 +1120,31 @@ module Kettle
1114
1120
  return [] if repo_ref.to_s.empty?
1115
1121
  return @release_cache[repo_ref] if @release_cache.key?(repo_ref)
1116
1122
 
1123
+ stale = nil
1117
1124
  unless @refresh_cache
1118
1125
  cached = @persistent_cache&.versions_for_repo(repo_ref, fresh: true)
1119
1126
  if cached
1120
1127
  @release_cache[repo_ref] = cached
1121
1128
  return cached
1122
1129
  end
1130
+ stale = @persistent_cache&.versions_for_repo(repo_ref, fresh: false)
1123
1131
  end
1124
1132
 
1125
- data = request_json("/repos/#{repo_ref}/releases?per_page=100")
1126
- unless data.is_a?(Array)
1127
- fallback = @persistent_cache&.versions_for_repo(repo_ref, fresh: false)
1128
- return fallback if fallback
1133
+ releases = nil
1134
+ Timeout.timeout(@refresh_timeout) do
1135
+ data = request_json("/repos/#{repo_ref}/releases?per_page=100")
1136
+ return cached_versions(repo_ref, stale) unless data.is_a?(Array)
1129
1137
 
1130
- return []
1131
- end
1132
-
1133
- tag_shas = tag_ref_shas(repo_ref)
1134
- releases = data.filter_map do |release|
1135
- next unless release.is_a?(Hash)
1136
-
1137
- tag = release["tag_name"].to_s
1138
- parsed = parse_release_version_text(tag)
1139
- next unless parsed
1138
+ tag_shas = tag_ref_shas(repo_ref)
1139
+ return cached_versions(repo_ref, stale) unless tag_shas
1140
1140
 
1141
- {
1142
- tag: tag,
1143
- version_obj: parsed,
1144
- version: parsed.to_s,
1145
- sha: tag_shas[tag]
1146
- }
1141
+ releases = build_release_versions(data, tag_shas)
1147
1142
  end
1148
- released_tags = releases.each_with_object({}) { |release, memo| memo[release[:tag]] = true }
1149
- tag_versions = tag_shas.filter_map do |tag, sha|
1150
- next if released_tags[tag]
1151
-
1152
- parsed = parse_release_version_text(tag)
1153
- next unless parsed
1154
-
1155
- {
1156
- tag: tag,
1157
- version_obj: parsed,
1158
- version: parsed.to_s,
1159
- sha: sha
1160
- }
1161
- end
1162
- releases.concat(tag_versions)
1163
-
1164
- releases.sort_by! { |release| release[:version_obj] }
1165
- releases.reverse!
1166
1143
  @persistent_cache&.write_versions(repo_ref, releases)
1167
1144
  @release_cache[repo_ref] = releases
1168
1145
  releases
1146
+ rescue Timeout::Error
1147
+ cached_versions(repo_ref, stale)
1169
1148
  end
1170
1149
 
1171
1150
  def commit_sha(repo_ref, ref)
@@ -1203,6 +1182,48 @@ module Kettle
1203
1182
 
1204
1183
  private
1205
1184
 
1185
+ def cached_versions(repo_ref, stale)
1186
+ versions = stale || []
1187
+ @release_cache[repo_ref] = versions
1188
+ versions
1189
+ end
1190
+
1191
+ def build_release_versions(data, tag_shas)
1192
+ releases = data.filter_map do |release|
1193
+ next unless release.is_a?(Hash)
1194
+
1195
+ tag = release["tag_name"].to_s
1196
+ parsed = parse_release_version_text(tag)
1197
+ next unless parsed
1198
+
1199
+ {
1200
+ tag: tag,
1201
+ version_obj: parsed,
1202
+ version: parsed.to_s,
1203
+ sha: tag_shas[tag]
1204
+ }
1205
+ end
1206
+ released_tags = releases.each_with_object({}) { |release, memo| memo[release[:tag]] = true }
1207
+ tag_versions = tag_shas.filter_map do |tag, sha|
1208
+ next if released_tags[tag]
1209
+
1210
+ parsed = parse_release_version_text(tag)
1211
+ next unless parsed
1212
+
1213
+ {
1214
+ tag: tag,
1215
+ version_obj: parsed,
1216
+ version: parsed.to_s,
1217
+ sha: sha
1218
+ }
1219
+ end
1220
+ releases.concat(tag_versions)
1221
+
1222
+ releases.sort_by! { |release| release[:version_obj] }
1223
+ releases.reverse!
1224
+ releases
1225
+ end
1226
+
1206
1227
  def parse_release_version_text(value)
1207
1228
  normalized = value.to_s.sub(/\A[vV]/, "")
1208
1229
  return nil unless normalized.match?(/\A(?:\d+|\d+\.\d+\.\d+(?:[-.]?[0-9A-Za-z.-]+)?)\z/)
@@ -1214,13 +1235,15 @@ module Kettle
1214
1235
 
1215
1236
  def tag_ref_shas(repo_ref)
1216
1237
  data = request_json("/repos/#{repo_ref}/git/matching-refs/tags/")
1217
- return {} unless data.is_a?(Array)
1238
+ return nil unless data.is_a?(Array)
1218
1239
 
1219
1240
  data.each_with_object({}) do |entry, memo|
1220
1241
  ref = entry["ref"].to_s
1221
1242
  next unless ref.start_with?("refs/tags/")
1222
1243
 
1223
1244
  tag = ref.sub(%r{\Arefs/tags/}, "")
1245
+ next unless parse_release_version_text(tag)
1246
+
1224
1247
  object = entry["object"]
1225
1248
  next unless object.is_a?(Hash)
1226
1249
 
@@ -1229,8 +1252,7 @@ module Kettle
1229
1252
  when "commit"
1230
1253
  memo[tag] = sha
1231
1254
  when "tag"
1232
- dereferenced_sha = annotated_tag_commit_sha(repo_ref, sha)
1233
- memo[tag] = dereferenced_sha if dereferenced_sha
1255
+ memo[tag] = nil
1234
1256
  end
1235
1257
  end
1236
1258
  end
@@ -1259,9 +1281,7 @@ module Kettle
1259
1281
  request["X-GitHub-Api-Version"] = "2022-11-28"
1260
1282
  request["Authorization"] = "Bearer #{@token}" if @token && !@token.empty?
1261
1283
 
1262
- response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
1263
- http.request(request)
1264
- end
1284
+ response = http_request(uri, request)
1265
1285
 
1266
1286
  break unless response.code.to_i.between?(300, 399)
1267
1287
  redirects -= 1
@@ -1280,6 +1300,17 @@ module Kettle
1280
1300
  rescue JSON::ParserError
1281
1301
  nil
1282
1302
  end
1303
+ rescue IOError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout
1304
+ nil
1305
+ end
1306
+
1307
+ def http_request(uri, request)
1308
+ http = Net::HTTP.new(uri.host, uri.port)
1309
+ http.use_ssl = uri.scheme == "https"
1310
+ http.open_timeout = @open_timeout
1311
+ http.read_timeout = @read_timeout
1312
+ http.ssl_timeout = @open_timeout if http.respond_to?(:ssl_timeout=)
1313
+ http.start { |connection| connection.request(request) }
1283
1314
  end
1284
1315
 
1285
1316
  def uri_encode(value)