kettle-family 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a7c89b89e309502af9e996022c4c93d5b02b0118279464d197537e76bd2c65a
4
- data.tar.gz: f80c4406bda1cd8e3a0d42c47da1e4fa645303b71f1cd68a5381053c041e0351
3
+ metadata.gz: b3ec93e3425d765de198975bb390031d33f4a408c31994a18999e3178aec2cee
4
+ data.tar.gz: c22c0cec6c2dc9a387281adb456d7303bc838e687be27b0c69ecc59b7f8038ae
5
5
  SHA512:
6
- metadata.gz: 5160695320d1ce2a37cbb6e4cfa8dd45026a6784d660a932678aa601b5c4b5a22903530109537cf9e9d58f6658f20d0e2ea8d36ead281f0258a1743da7c1e565
7
- data.tar.gz: 324f032033f94e3d7891d9935efd8ca1583c081eac9e14d265c2dcb3d258ffdbb310e8fb0e89587ccb55db73d265ced7cd10e681954a778cbcd981668f375a19
6
+ metadata.gz: 7520e01212b4faebaed857c69a7bd0c068be891e8fa79c4ccb7485f9cd8fdd4daa824c5943cc030da765992422a1bf252fe76fde2e68b3b4f0feeaf179beb30a
7
+ data.tar.gz: '0683aae4dfdbbde5b004719856f32b65f7810acd81446145456a7810e4625f85a516f1d53d08d0c1622f65d7d8a2902c0dfb2ff57b35950f19bc76355cdded56'
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,55 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [0.1.4] - 2026-06-16
34
+
35
+ - TAG: [v0.1.4][0.1.4t]
36
+ - COVERAGE: 93.72% -- 1060/1131 lines in 19 files
37
+ - BRANCH COVERAGE: 76.12% -- 322/423 branches in 19 files
38
+ - 40.14% documented
39
+
40
+ ### Added
41
+
42
+ - Added configurable readiness checks, root/shared changelog support, release
43
+ environment overrides, and an optional family changelog release phase for
44
+ monorepo gem families whose members share root release metadata.
45
+
46
+ ### Fixed
47
+
48
+ - Fixed the Ruby 3.2 CI appraisal so root changelog release-state checks have
49
+ Prism available.
50
+
51
+ ## [0.1.3] - 2026-06-14
52
+
53
+ - TAG: [v0.1.3][0.1.3t]
54
+ - COVERAGE: 94.34% -- 917/972 lines in 19 files
55
+ - BRANCH COVERAGE: 78.36% -- 268/342 branches in 19 files
56
+ - 44.00% documented
57
+
58
+ ### Changed
59
+
60
+ - Runtime dependency `kettle-dev` now requires 2.2.8 or newer.
61
+ - `kettle-family release-state` now expands configured
62
+ `release.target_branches` and reports release state for each branch
63
+ independently.
64
+
65
+ - Project licensing changed from MIT to AGPL-3.0-only.
66
+ - `kettle-family release-state` now invokes `kettle-changelog` from the active
67
+ toolchain instead of depending on `kettle-dev` as a published runtime
68
+ dependency.
69
+
70
+ ### Fixed
71
+
72
+ - Fixed release-state checks to use the active `kettle-dev` API instead of each
73
+ member's potentially stale bundle.
74
+ - Fixed default discovery excludes so top-level `vendor/`, `tmp/`, `spec/`, and
75
+ `test/` directories are ignored.
76
+
77
+ ### Added
78
+
79
+ - Added `kettle-family metadata` to report each family member's version, Ruby
80
+ requirement, licenses, and authors.
81
+
33
82
  ## [0.1.2] - 2026-06-13
34
83
 
35
84
  - TAG: [v0.1.2][0.1.2t]
@@ -91,7 +140,11 @@ Please file a bug if you notice a violation of semantic versioning.
91
140
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
92
141
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
93
142
 
94
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.2...HEAD
143
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.4...HEAD
144
+ [0.1.4]: https://github.com/kettle-dev/kettle-family/compare/v0.1.3...v0.1.4
145
+ [0.1.4t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.4
146
+ [0.1.3]: https://github.com/kettle-dev/kettle-family/compare/v0.1.2...v0.1.3
147
+ [0.1.3t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.3
95
148
  [0.1.2]: https://github.com/kettle-dev/kettle-family/compare/v0.1.1...v0.1.2
96
149
  [0.1.2t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.2
97
150
  [0.1.1]: https://github.com/kettle-dev/kettle-family/compare/v0.1.0...v0.1.1
data/CONTRIBUTING.md CHANGED
@@ -109,14 +109,14 @@ Git diff driver setup
109
109
  - Git hosting forges generally ignore external diff drivers, so pull request views may still show raw textual diffs even when local `git diff` uses semantic drivers.
110
110
 
111
111
  ```console
112
- K_JEM_TEMPLATING=true bundle exec kettle-jem install
112
+ K_JEM_TEMPLATING=true kettle-jem install
113
113
  ```
114
114
 
115
115
  Troubleshooting Git diffs
116
116
  - Use `git diff --no-ext-diff` to compare against Git's built-in diff output.
117
117
  - Use `git diff --no-textconv` when a textconv projection obscures the raw file bytes you need to inspect.
118
118
  - If Git reports a missing `smorg-*` executable, rerun `bundle install` and the setup command above, then check `git config --local --get-regexp '^diff\.smorg-'`.
119
- - To remove managed local entries, run `K_JEM_TEMPLATING=true bundle exec kettle-jem install --undo`; remove global command registrations with `git config --global --unset-all diff.smorg-ruby.command`.
119
+ - To remove managed local entries, run `K_JEM_TEMPLATING=true kettle-jem install --undo`; remove global command registrations with `git config --global --unset-all diff.smorg-ruby.command`.
120
120
 
121
121
  For a quick starting point, this repository’s `mise.toml` defines the shared defaults, and `.env.local` can override them locally. Copy `.env.local.example` to `.env.local`, use `KEY=value` lines, and either activate `mise` in your shell or run commands through `mise exec -C /path/to/project -- ...`.
122
122
 
data/LICENSE.md CHANGED
@@ -3,7 +3,9 @@
3
3
  This project is made available under the following license.
4
4
  Choose the option that best fits your use case:
5
5
 
6
- - [MIT](MIT.md)
6
+ - [AGPL-3.0-only](AGPL-3.0-only.md)
7
+
8
+ If none of the above licenses fit your use case, please [contact us](mailto:floss@galtzo.com) to discuss a custom commercial license.
7
9
 
8
10
  ## Copyright Notice
9
11
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # 👩‍👩‍👧‍👧 Kettle::Family
4
4
 
5
- [![Version][👽versioni]][👽version] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf]
5
+ [![Version][👽versioni]][👽version] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: AGPL-3.0-only][📄license-img]][📄license] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf]
6
6
 
7
7
  `if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know][✉️discord-invite], as I may have missed the [discord notification][✉️discord-invite].
8
8
 
@@ -51,7 +51,7 @@ while RubyGems MFA prompts remain interactive.
51
51
  | 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] |
52
52
  | Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ!][🧮kloc-img]][🧮kloc] |
53
53
  | Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] |
54
- | Compliance | [![License: MIT][📄license-img]][📄license] [![Apache license compatibility: Category A][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
54
+ | Compliance | [![License: AGPL-3.0-only][📄license-img]][📄license] [![Apache license compatibility: Category X][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
55
55
  | Style | [![Enforced Code Style Linter][💎rlts-img]][💎rlts] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] [![Gitmoji Commits][📌gitmoji-img]][📌gitmoji] [![Compatibility appraised by: appraisal2][💎appraisal2-img]][💎appraisal2] |
56
56
  | Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]][💖🖇linkedin] [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]][💖🐘ruby-mast] [![Follow Me on Bluesky][💖🦋bluesky-img]][💖🦋bluesky] [![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact] [![My technical writing][💖💁🏼‍♂️devto-img]][💖💁🏼‍♂️devto] |
57
57
  | `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]][💖✌️wellfound] [![Find Me on CrunchBase][💖💲crunchbase-img]][💖💲crunchbase] [![My LinkTree][💖🌳linktree-img]][💖🌳linktree] [![More About Me][💖💁🏼‍♂️aboutme-img]][💖💁🏼‍♂️aboutme] [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] |
@@ -142,6 +142,36 @@ members:
142
142
  - "**/vendor/**"
143
143
  ```
144
144
 
145
+ Monorepo families whose member gems share release metadata from the repository
146
+ root can configure readiness and changelog ownership explicitly:
147
+
148
+ ```yaml
149
+ check:
150
+ required_files:
151
+ - Gemfile
152
+ - Rakefile
153
+ - README.md
154
+ - LICENSE.md
155
+ required_bins:
156
+ - bin/rake
157
+ - bin/rspec
158
+ root_required_files:
159
+ - CHANGELOG.md
160
+ - SECURITY.md
161
+
162
+ changelog:
163
+ mode: root
164
+ path: CHANGELOG.md
165
+ version_file: gems/tree_haver/lib/tree_haver/version.rb
166
+
167
+ release:
168
+ env:
169
+ KETTLE_RB_DEV: false
170
+ family_changelog:
171
+ enabled: true
172
+ command: bundle exec kettle-changelog
173
+ ```
174
+
145
175
  For a flat repository that releases from multiple long-lived branches, list the
146
176
  release branches under `release.target_branches`. The branch list is processed
147
177
  in order. Each branch must be clean enough for `git checkout`, and each branch
@@ -232,63 +262,6 @@ If you intentionally need to continue after CI failures, opt in explicitly:
232
262
  kettle-family release --publish --execute --continue-ci-failures
233
263
  ```
234
264
 
235
- ## 🦷 FLOSS Funding
236
-
237
- While kettle-dev tools are free software and will always be, the project would benefit immensely from some funding.
238
- Raising a monthly budget of... "dollars" would make the project more sustainable.
239
-
240
- We welcome both individual and corporate sponsors! We also offer a
241
- wide array of funding channels to account for your preferences.
242
- Currently, [Open Collective][🖇osc] is our preferred funding platform.
243
-
244
- **If you're working in a company that's making significant use of kettle-dev tools we'd
245
- appreciate it if you suggest to your company to become a kettle-dev sponsor.**
246
-
247
- You can support the development of kettle-dev tools via
248
- [GitHub Sponsors][🖇sponsor],
249
- [Liberapay][⛳liberapay],
250
- [PayPal][🖇paypal],
251
- [Open Collective][🖇osc]
252
- and [Tidelift][🏙️entsup-tidelift].
253
-
254
- | 📍 NOTE |
255
- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
256
- | If doing a sponsorship in the form of donation is problematic for your company <br/> from an accounting standpoint, we'd recommend the use of Tidelift, <br/> where you can get a support-like subscription instead. |
257
-
258
- ### Open Collective for Individuals
259
-
260
- Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/kettle-dev#backer)]
261
-
262
- NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
263
-
264
- <!-- OPENCOLLECTIVE-INDIVIDUALS:START -->
265
- No backers yet. Be the first!
266
- <!-- OPENCOLLECTIVE-INDIVIDUALS:END -->
267
-
268
- ### Open Collective for Organizations
269
-
270
- Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/kettle-dev#sponsor)]
271
-
272
- NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
273
-
274
- <!-- OPENCOLLECTIVE-ORGANIZATIONS:START -->
275
- No sponsors yet. Be the first!
276
- <!-- OPENCOLLECTIVE-ORGANIZATIONS:END -->
277
-
278
- [kettle-readme-backers]: https://github.com/kettle-dev/kettle-family/blob/main/exe/kettle-readme-backers
279
-
280
- ### Another way to support open-source
281
-
282
- I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats).
283
-
284
- If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`.
285
-
286
- I’m developing a new library, [floss_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look.
287
-
288
- **[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags**
289
-
290
- [![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 to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon]
291
-
292
265
  ## 🔐 Security
293
266
 
294
267
  See [SECURITY.md][🔐security].
@@ -374,8 +347,10 @@ See [CHANGELOG.md][📌changelog] for a list of releases.
374
347
 
375
348
  ## 📄 License
376
349
 
377
- The gem is available as open source under the terms of
378
- the [MIT](MIT.md) [![License: MIT][📄license-img]][📄license-ref].
350
+ The gem is available under the following license: [AGPL-3.0-only](AGPL-3.0-only.md).
351
+ See [LICENSE.md][📄license] for details.
352
+
353
+ If none of the available licenses suit your use case, please [contact us](mailto:floss@galtzo.com) to discuss a custom commercial license.
379
354
 
380
355
  ### © Copyright
381
356
 
@@ -534,8 +509,6 @@ Thanks for RTFM. ☺️
534
509
  [🚎13-🔒️-wfi]: https://github.com/kettle-dev/kettle-family/actions/workflows/locked_deps.yml/badge.svg
535
510
  [🚎14-🔓️-wf]: https://github.com/kettle-dev/kettle-family/actions/workflows/unlocked_deps.yml
536
511
  [🚎14-🔓️-wfi]: https://github.com/kettle-dev/kettle-family/actions/workflows/unlocked_deps.yml/badge.svg
537
- [🚎15-🪪-wf]: https://github.com/kettle-dev/kettle-family/actions/workflows/license-eye.yml
538
- [🚎15-🪪-wfi]: https://github.com/kettle-dev/kettle-family/actions/workflows/license-eye.yml/badge.svg
539
512
  [💎ruby-3.2i]: https://img.shields.io/badge/Ruby-3.2-CC342D?style=for-the-badge&logo=ruby&logoColor=white
540
513
  [💎ruby-3.3i]: https://img.shields.io/badge/Ruby-3.3-CC342D?style=for-the-badge&logo=ruby&logoColor=white
541
514
  [💎ruby-3.4i]: https://img.shields.io/badge/Ruby-3.4-CC342D?style=for-the-badge&logo=ruby&logoColor=white
@@ -573,15 +546,15 @@ Thanks for RTFM. ☺️
573
546
  [📌gitmoji]: https://gitmoji.dev
574
547
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
575
548
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
576
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.892-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
549
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.131-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
577
550
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
578
551
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
579
552
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
580
553
  [📄license]: LICENSE.md
581
- [📄license-ref]: MIT.md
582
- [📄license-img]: https://img.shields.io/badge/License-MIT-259D6C.svg
583
- [📄license-compat]: https://www.apache.org/legal/resolved.html#category-a
584
- [📄license-compat-img]: https://img.shields.io/badge/Apache_Compatible:_Category_A-%E2%9C%93-259D6C.svg?style=flat&logo=Apache
554
+ [📄license-ref]: AGPL-3.0-only.md
555
+ [📄license-img]: https://img.shields.io/badge/License-AGPL--3.0--only-259D6C.svg
556
+ [📄license-compat]: https://www.apache.org/legal/resolved.html#category-x
557
+ [📄license-compat-img]: https://img.shields.io/badge/Apache_Incompatible:_Category_X-%E2%9C%97-C0392B.svg?style=flat&logo=Apache
585
558
 
586
559
  [📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
587
560
  [📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
@@ -601,7 +574,7 @@ Thanks for RTFM. ☺️
601
574
  | Package | kettle-family |
602
575
  | Description | 👩‍👩‍👧‍👧 Kettle::Family provides scripts and conventions for coordinating related Ruby gems as one family. |
603
576
  | Homepage | https://github.com/kettle-dev/kettle-family |
604
- | Source | https://github.com/kettle-dev/kettle-family/tree/v0.1.0 |
605
- | License | `MIT` |
577
+ | Source | https://github.com/kettle-dev/kettle-family |
578
+ | License | `AGPL-3.0-only` |
606
579
  | 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-family, https://www.buymeacoffee.com/pboling |
607
580
  <!-- kettle-jem:metadata:end -->
@@ -3,25 +3,37 @@
3
3
  module Kettle
4
4
  module Family
5
5
  class ChangelogCheck
6
- def self.call(member:)
7
- new(member: member).call
6
+ def self.call(member:, config: nil)
7
+ new(member: member, config: config).call
8
8
  end
9
9
 
10
- def initialize(member:)
10
+ def initialize(member:, config: nil)
11
11
  @member = member
12
+ @config = config
12
13
  end
13
14
 
14
15
  def call
15
16
  diagnostics = []
16
- changelog = File.join(member.root, "CHANGELOG.md")
17
- diagnostics << "missing CHANGELOG.md" unless File.file?(changelog)
18
- diagnostics << "CHANGELOG.md missing Unreleased section" if File.file?(changelog) && !File.read(changelog).include?("## [Unreleased]")
17
+ changelog = changelog_path
18
+ diagnostics << "missing #{relative_changelog_path}" unless File.file?(changelog)
19
+ diagnostics << "#{relative_changelog_path} missing Unreleased section" if File.file?(changelog) && !File.read(changelog).include?("## [Unreleased]")
19
20
  result(diagnostics)
20
21
  end
21
22
 
22
23
  private
23
24
 
24
- attr_reader :member
25
+ attr_reader :member, :config
26
+
27
+ def changelog_path
28
+ config ? config.changelog_full_path(member) : File.join(member.root, "CHANGELOG.md")
29
+ end
30
+
31
+ def relative_changelog_path
32
+ return "CHANGELOG.md" unless config
33
+
34
+ base = config.shared_changelog? ? config.root : member.root
35
+ changelog_path.delete_prefix("#{base}/")
36
+ end
25
37
 
26
38
  def result(diagnostics)
27
39
  CommandResult.new(
@@ -6,7 +6,7 @@ require "optparse"
6
6
  module Kettle
7
7
  module Family
8
8
  class CLI
9
- COMMANDS = %w[discover plan report check test lint docs template bump-version release branch-lanes release-state].freeze
9
+ COMMANDS = %w[discover plan report metadata check test lint docs template bump-version release branch-lanes release-state].freeze
10
10
  WORKFLOW_COMMANDS = %w[check test lint docs template release].freeze
11
11
 
12
12
  def self.call(argv, out: $stdout, err: $stderr)
@@ -53,6 +53,7 @@ module Kettle
53
53
  discover Discover family members and print selected order
54
54
  plan Alias for discover while execution workflows are built
55
55
  report Print family discovery and configuration report
56
+ metadata Print version, Ruby floor, license, and author metadata
56
57
  check Run internal read-only readiness checks
57
58
  test Plan or execute configured test command per member
58
59
  lint Plan or execute configured lint command per member
@@ -139,7 +140,7 @@ module Kettle
139
140
  def build_report(command, options)
140
141
  config = Config.load(root: options[:root], path: options[:config])
141
142
  members = Discovery.new(config: config).members
142
- ordered = if command == "release-state"
143
+ ordered = if %w[metadata release-state].include?(command)
143
144
  members.sort_by(&:name)
144
145
  else
145
146
  Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
@@ -168,7 +169,7 @@ module Kettle
168
169
  def command_results(command:, config:, members:, options:)
169
170
  return bump_version_results(members: members, options: options) if command == "bump-version"
170
171
  return branch_lane_results(config: config, members: members) if command == "branch-lanes"
171
- return release_state_results(members: members) if command == "release-state"
172
+ return release_state_results(config: config, members: members) if command == "release-state"
172
173
  return [] unless WORKFLOW_COMMANDS.include?(command)
173
174
 
174
175
  Workflow.new(
@@ -207,8 +208,8 @@ module Kettle
207
208
  BranchLaneAudit.new(config: config, members: members).results
208
209
  end
209
210
 
210
- def release_state_results(members:)
211
- ReleaseStateCheck.new(members: members).results
211
+ def release_state_results(config:, members:)
212
+ ReleaseStateCheck.new(config: config, members: members).results
212
213
  end
213
214
 
214
215
  def write_report(report, options)
@@ -6,7 +6,16 @@ module Kettle
6
6
  module Family
7
7
  class Config
8
8
  DEFAULT_PATHS = [".kettle-family.yml", ".structuredmerge/kettle-family.yml"].freeze
9
- DEFAULT_MEMBER_EXCLUDES = ["**/vendor/**", "**/tmp/**", "**/spec/**", "**/test/**"].freeze
9
+ DEFAULT_MEMBER_EXCLUDES = [
10
+ "vendor/**",
11
+ "**/vendor/**",
12
+ "tmp/**",
13
+ "**/tmp/**",
14
+ "spec/**",
15
+ "**/spec/**",
16
+ "test/**",
17
+ "**/test/**"
18
+ ].freeze
10
19
 
11
20
  attr_reader :data, :path, :root
12
21
 
@@ -81,6 +90,64 @@ module Kettle
81
90
  fetch_path("commands", name)
82
91
  end
83
92
 
93
+ def check_required_files
94
+ fetch_path("check", "required_files") || ReadinessCheck::REQUIRED_FILES
95
+ end
96
+
97
+ def check_required_bins
98
+ fetch_path("check", "required_bins") || ReadinessCheck::REQUIRED_BINS
99
+ end
100
+
101
+ def check_root_required_files
102
+ fetch_path("check", "root_required_files") || []
103
+ end
104
+
105
+ def check_member_required_dirs
106
+ fetch_path("check", "member_required_dirs") || []
107
+ end
108
+
109
+ def check_forbidden_tracked_member_dirs
110
+ fetch_path("check", "forbidden_tracked_member_dirs") || []
111
+ end
112
+
113
+ def check_forbidden_tracked_member_dirs_except
114
+ fetch_path("check", "forbidden_tracked_member_dirs_except") || []
115
+ end
116
+
117
+ def check_readme_links
118
+ fetch_path("check", "readme_links") || {}
119
+ end
120
+
121
+ def changelog_mode
122
+ fetch_path("changelog", "mode") || "member"
123
+ end
124
+
125
+ def shared_changelog?
126
+ changelog_mode == "root"
127
+ end
128
+
129
+ def changelog_path
130
+ fetch_path("changelog", "path") || "CHANGELOG.md"
131
+ end
132
+
133
+ def changelog_version_file
134
+ fetch_path("changelog", "version_file")
135
+ end
136
+
137
+ def changelog_workdir(_member = nil)
138
+ shared_changelog? ? root : nil
139
+ end
140
+
141
+ def changelog_full_path(member)
142
+ File.expand_path(changelog_path, shared_changelog? ? root : member.root)
143
+ end
144
+
145
+ def changelog_env
146
+ return {} unless changelog_version_file
147
+
148
+ {"K_CHANGELOG_VERSION_FILE" => changelog_version_file.to_s}
149
+ end
150
+
84
151
  def template_command
85
152
  fetch_path("template", "command") || command_for("template")
86
153
  end
@@ -109,6 +176,18 @@ module Kettle
109
176
  fetch_path("release", "publish_command") || command_for("release_publish") || "bundle exec kettle-release"
110
177
  end
111
178
 
179
+ def release_env
180
+ stringify_env(fetch_path("release", "env") || {})
181
+ end
182
+
183
+ def release_family_changelog?
184
+ fetch_path("release", "family_changelog", "enabled") == true
185
+ end
186
+
187
+ def release_family_changelog_command
188
+ fetch_path("release", "family_changelog", "command") || "bundle exec kettle-changelog"
189
+ end
190
+
112
191
  def release_tag_command
113
192
  fetch_path("release", "tag_command") || command_for("release_tag") || "git tag"
114
193
  end
@@ -151,6 +230,10 @@ module Kettle
151
230
  value
152
231
  end
153
232
  end
233
+
234
+ def stringify_env(value)
235
+ stringify_keys(value).to_h { |key, item| [key.to_s, item.to_s] }
236
+ end
154
237
  end
155
238
  end
156
239
  end
@@ -63,17 +63,39 @@ module Kettle
63
63
  gemspec_path: path,
64
64
  version_file: version_file(File.dirname(path)),
65
65
  version: spec.version.to_s,
66
- dependencies: spec.dependencies.map(&:name).sort
66
+ dependencies: spec.dependencies.map(&:name).sort,
67
+ required_ruby_version: required_ruby_version(spec),
68
+ licenses: licenses(spec),
69
+ authors: authors(spec)
67
70
  )
68
71
  end
69
72
 
73
+ def required_ruby_version(spec)
74
+ value = spec.required_ruby_version&.to_s&.strip
75
+ value.empty? ? nil : value
76
+ end
77
+
78
+ def licenses(spec)
79
+ values = Array(spec.licenses).compact.map(&:to_s).map(&:strip).reject(&:empty?)
80
+ values = [spec.license.to_s.strip] if values.empty? && spec.respond_to?(:license) && !spec.license.to_s.strip.empty?
81
+ values
82
+ end
83
+
84
+ def authors(spec)
85
+ Array(spec.authors).compact.map(&:to_s).map(&:strip).reject(&:empty?)
86
+ end
87
+
70
88
  def version_file(root)
71
89
  candidates = Dir.glob(File.join(root, "lib", "**", "version.rb"))
72
90
  candidates.min
73
91
  end
74
92
 
75
93
  def load_gemspec(path)
76
- spec = Gem::Specification.load(path)
94
+ # Some legacy gemspecs use root-relative Kernel.load calls, and RubyGems
95
+ # evaluates gemspecs relative to the current process directory.
96
+ # rubocop:disable ThreadSafety/DirChdir
97
+ spec = Dir.chdir(File.dirname(path)) { Gem::Specification.load(path) }
98
+ # rubocop:enable ThreadSafety/DirChdir
77
99
  raise Error, "could not load gemspec #{path}" unless spec
78
100
 
79
101
  spec
@@ -8,7 +8,10 @@ module Kettle
8
8
  :gemspec_path,
9
9
  :version_file,
10
10
  :version,
11
- :dependencies
11
+ :dependencies,
12
+ :required_ruby_version,
13
+ :licenses,
14
+ :authors
12
15
  ) do
13
16
  def to_h
14
17
  {
@@ -17,7 +20,10 @@ module Kettle
17
20
  "gemspec_path" => gemspec_path,
18
21
  "version_file" => version_file,
19
22
  "version" => version,
20
- "dependencies" => dependencies
23
+ "dependencies" => dependencies,
24
+ "required_ruby_version" => required_ruby_version,
25
+ "licenses" => Array(licenses),
26
+ "authors" => Array(authors)
21
27
  }
22
28
  end
23
29
  end
@@ -6,28 +6,33 @@ module Kettle
6
6
  REQUIRED_FILES = %w[Gemfile Rakefile README.md CHANGELOG.md LICENSE.md].freeze
7
7
  REQUIRED_BINS = %w[bin/rake bin/rspec].freeze
8
8
 
9
- def self.call(member:)
10
- new(member: member).call
9
+ def self.call(member:, config: nil)
10
+ new(member: member, config: config).call
11
11
  end
12
12
 
13
- def initialize(member:)
13
+ def initialize(member:, config: nil)
14
14
  @member = member
15
+ @config = config
15
16
  end
16
17
 
17
18
  def call
18
19
  diagnostics = []
19
20
  diagnostics.concat(missing_required_files)
20
21
  diagnostics.concat(missing_required_bins)
22
+ diagnostics.concat(missing_root_required_files)
23
+ diagnostics.concat(missing_member_required_dirs)
24
+ diagnostics.concat(forbidden_tracked_member_dirs)
25
+ diagnostics.concat(missing_readme_links)
21
26
  diagnostics.concat(local_path_lockfile_entries)
22
27
  result(diagnostics)
23
28
  end
24
29
 
25
30
  private
26
31
 
27
- attr_reader :member
32
+ attr_reader :member, :config
28
33
 
29
34
  def missing_required_files
30
- REQUIRED_FILES.filter_map do |path|
35
+ required_files.filter_map do |path|
31
36
  next if File.file?(File.join(member.root, path))
32
37
 
33
38
  "missing required file #{path}"
@@ -35,7 +40,7 @@ module Kettle
35
40
  end
36
41
 
37
42
  def missing_required_bins
38
- REQUIRED_BINS.filter_map do |path|
43
+ required_bins.filter_map do |path|
39
44
  full_path = File.join(member.root, path)
40
45
  next if File.file?(full_path) && File.executable?(full_path)
41
46
 
@@ -43,6 +48,52 @@ module Kettle
43
48
  end
44
49
  end
45
50
 
51
+ def missing_root_required_files
52
+ return [] unless config
53
+
54
+ config.check_root_required_files.filter_map do |path|
55
+ next if File.file?(File.join(config.root, path))
56
+
57
+ "missing root required file #{path}"
58
+ end
59
+ end
60
+
61
+ def missing_member_required_dirs
62
+ return [] unless config
63
+
64
+ config.check_member_required_dirs.filter_map do |path|
65
+ next if Dir.exist?(File.join(member.root, path))
66
+
67
+ "missing required directory #{path}"
68
+ end
69
+ end
70
+
71
+ def forbidden_tracked_member_dirs
72
+ return [] unless config
73
+ return [] if config.check_forbidden_tracked_member_dirs_except.include?(member.name)
74
+
75
+ config.check_forbidden_tracked_member_dirs.filter_map do |path|
76
+ full_path = File.join(member.root, path)
77
+ next unless Dir.exist?(full_path) && tracked_path?(full_path)
78
+
79
+ "forbidden tracked directory #{path}"
80
+ end
81
+ end
82
+
83
+ def missing_readme_links
84
+ return [] unless config
85
+
86
+ readme = File.join(member.root, "README.md")
87
+ return [] unless File.file?(readme)
88
+
89
+ content = File.read(readme)
90
+ config.check_readme_links.filter_map do |label, target|
91
+ next if content.include?("/#{target}") || content.include?("../../#{target}") || content.include?("../#{target}")
92
+
93
+ "README.md missing link to root #{label}"
94
+ end
95
+ end
96
+
46
97
  def local_path_lockfile_entries
47
98
  lockfile = File.join(member.root, "Gemfile.lock")
48
99
  return [] unless File.file?(lockfile)
@@ -54,6 +105,21 @@ module Kettle
54
105
  end
55
106
  end
56
107
 
108
+ def required_files
109
+ config ? config.check_required_files : REQUIRED_FILES
110
+ end
111
+
112
+ def required_bins
113
+ config ? config.check_required_bins : REQUIRED_BINS
114
+ end
115
+
116
+ def tracked_path?(path)
117
+ return false unless config
118
+
119
+ relative = path.delete_prefix("#{config.root}/")
120
+ system("git", "-C", config.root, "ls-files", "--error-unmatch", relative, out: File::NULL, err: File::NULL)
121
+ end
122
+
57
123
  def result(diagnostics)
58
124
  CommandResult.new(
59
125
  member_name: member.name,
@@ -1,40 +1,205 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "fileutils"
4
5
  require "open3"
6
+ require "rbconfig"
7
+ require "securerandom"
5
8
 
6
9
  module Kettle
7
10
  module Family
8
11
  class ReleaseStateCheck
9
- COMMAND = ["bundle", "exec", "kettle-changelog", "--release-state", "--json"].freeze
10
-
11
- def initialize(members:)
12
+ def initialize(members:, config: nil)
12
13
  @members = members
14
+ @config = config
13
15
  end
14
16
 
15
17
  def results
18
+ return branch_results unless release_target_branches.empty?
19
+ return [check_family_changelog] if shared_changelog?
20
+
16
21
  members.map { |member| check_member(member) }
17
22
  end
18
23
 
19
24
  private
20
25
 
21
- attr_reader :members
26
+ attr_reader :members, :config
27
+
28
+ def branch_results
29
+ root = git_root
30
+ selected_names = members.map(&:name)
31
+ release_target_branches.each_with_object([]) do |branch, memo|
32
+ with_branch_worktree(root: root, branch: branch) do |worktree_root|
33
+ if shared_changelog?
34
+ memo << check_family_changelog(branch: branch, worktree_root: worktree_root)
35
+ next
36
+ end
37
+
38
+ branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
39
+ memo.concat(branch_members.map { |member| check_member(member, branch: branch) })
40
+ end
41
+ rescue Error => error
42
+ memo << error_result(branch: branch, error: error)
43
+ end
44
+ end
22
45
 
23
- def check_member(member)
46
+ def check_member(member, branch: nil)
24
47
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
- stdout, stderr, status = Open3.capture3(*COMMAND, chdir: member.root)
48
+ command = release_state_command
49
+ stdout, stderr, status = Open3.capture3(release_state_env, *command, chdir: release_state_workdir(member))
26
50
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
27
51
  success = status.success?
28
52
  state = success ? JSON.parse(stdout) : {}
29
- result(member: member, stdout: stdout, stderr: stderr, status: status.exitstatus, elapsed: elapsed, success: success, state: state)
53
+ result(member: member, command: command, stdout: stdout, stderr: stderr, status: status.exitstatus, elapsed: elapsed, success: success, state: state, branch: branch)
30
54
  rescue JSON::ParserError => error
31
- result(member: member, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}")
55
+ result(member: member, command: command || release_state_command, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}", branch: branch)
56
+ end
57
+
58
+ def release_state_command
59
+ [RbConfig.ruby, "-S", "kettle-changelog", "--release-state", "--json"]
60
+ end
61
+
62
+ def check_family_changelog(branch: nil, worktree_root: nil)
63
+ member = family_member(root: worktree_root ? branch_config_root(worktree_root) : config.root)
64
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+ state = family_changelog_state(member.root)
66
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
67
+ result(
68
+ member: member,
69
+ command: ["internal", "release-state", "root-changelog"],
70
+ stdout: "",
71
+ stderr: "",
72
+ status: 0,
73
+ elapsed: elapsed,
74
+ success: true,
75
+ state: state,
76
+ branch: branch
77
+ )
78
+ rescue Error => error
79
+ result(
80
+ member: member,
81
+ command: ["internal", "release-state", "root-changelog"],
82
+ stdout: "",
83
+ stderr: error.message,
84
+ status: 1,
85
+ elapsed: 0.0,
86
+ success: false,
87
+ state: {},
88
+ reason: "release state check failed",
89
+ branch: branch
90
+ )
91
+ end
92
+
93
+ def family_member(root:)
94
+ Member.new(
95
+ name: config.family_name,
96
+ root: root,
97
+ gemspec_path: nil,
98
+ version_file: nil,
99
+ version: nil,
100
+ dependencies: []
101
+ )
102
+ end
103
+
104
+ def release_state_workdir(member)
105
+ return member.root unless config
106
+ return member.root if config.shared_changelog?
107
+
108
+ config.changelog_workdir(member) || member.root
109
+ end
110
+
111
+ def release_state_env
112
+ config ? config.changelog_env : {}
113
+ end
114
+
115
+ def family_changelog_state(root)
116
+ changelog = File.expand_path(config.changelog_path, root)
117
+ raise Error, "missing root changelog #{config.changelog_path}" unless File.file?(changelog)
118
+
119
+ version = root_changelog_version(root)
120
+ content = File.read(changelog)
121
+ latest_changelog_version = latest_changelog_version(content)
122
+ unreleased_entries = unreleased_entries?(content)
123
+ prepared_release_pending = !version.to_s.empty? && latest_changelog_version == version
124
+ {
125
+ "gem_name" => config.family_name,
126
+ "version" => version,
127
+ "latest_released" => nil,
128
+ "latest_changelog_version" => latest_changelog_version,
129
+ "unreleased_entries" => unreleased_entries,
130
+ "prepared_release_pending" => prepared_release_pending,
131
+ "pending_release" => unreleased_entries || prepared_release_pending
132
+ }
133
+ end
134
+
135
+ def root_changelog_version(root)
136
+ version_file = config.changelog_version_file
137
+ return nil unless version_file
138
+
139
+ path = File.expand_path(version_file, root)
140
+ raise Error, "missing changelog version file #{version_file}" unless File.file?(path)
141
+
142
+ version_string_node(File.read(path), path).unescaped
32
143
  end
33
144
 
34
- def result(member:, stdout:, stderr:, status:, elapsed:, success:, state:, reason: nil)
145
+ def version_string_node(source, path)
146
+ require_prism
147
+ parse_result = Prism.parse(source)
148
+ raise Error, "could not parse #{path}" unless parse_result.success?
149
+
150
+ constant = each_node(parse_result.value).find do |node|
151
+ node.is_a?(Prism::ConstantWriteNode) && node.name == :VERSION && node.value.is_a?(Prism::StringNode)
152
+ end
153
+ raise Error, "could not find string VERSION constant in #{path}" unless constant
154
+
155
+ constant.value
156
+ end
157
+
158
+ def latest_changelog_version(content)
159
+ content.each_line.filter_map do |line|
160
+ match = line.match(/\A## \[([^\]]+)\]/)
161
+ next unless match
162
+
163
+ version = match[1]
164
+ next if version == "Unreleased"
165
+
166
+ version
167
+ end.first
168
+ end
169
+
170
+ def unreleased_entries?(content)
171
+ lines = content.lines
172
+ start = lines.index { |line| line.start_with?("## [Unreleased]") }
173
+ return false unless start
174
+
175
+ following = lines[(start + 1)..] || []
176
+ block = following.take_while { |line| !line.start_with?("## [") }
177
+ block.any? { |line| line.match?(/\S/) && !line.match?(/\A###? /) }
178
+ end
179
+
180
+ def require_prism
181
+ return if defined?(Prism)
182
+
183
+ require "prism"
184
+ rescue LoadError => error
185
+ raise Error, "root changelog release-state requires Prism; install the prism gem or run on a Ruby engine that provides it (#{error.message})"
186
+ end
187
+
188
+ def each_node(root)
189
+ return enum_for(__method__, root) unless block_given?
190
+
191
+ queue = [root]
192
+ until queue.empty?
193
+ node = queue.shift
194
+ yield node
195
+ queue.concat(node.child_nodes.compact) if node.respond_to?(:child_nodes)
196
+ end
197
+ end
198
+
199
+ def result(member:, command:, stdout:, stderr:, status:, elapsed:, success:, state:, reason: nil, branch: nil)
35
200
  ReleaseStateResult.new(
36
201
  member_name: member.name,
37
- command: COMMAND,
202
+ command: command,
38
203
  workdir: member.root,
39
204
  status: status,
40
205
  success: success,
@@ -42,9 +207,89 @@ module Kettle
42
207
  stderr: stderr,
43
208
  elapsed_seconds: elapsed,
44
209
  state: state,
45
- reason: reason || (success ? nil : "release state check failed")
210
+ reason: reason || (success ? nil : "release state check failed"),
211
+ branch: branch
212
+ )
213
+ end
214
+
215
+ def error_result(branch:, error:)
216
+ ReleaseStateResult.new(
217
+ member_name: branch,
218
+ command: ["internal", "release-state", branch],
219
+ workdir: config.root,
220
+ status: 1,
221
+ success: false,
222
+ stdout: "",
223
+ stderr: error.message,
224
+ elapsed_seconds: 0.0,
225
+ state: {},
226
+ reason: "branch release state failed",
227
+ branch: branch
46
228
  )
47
229
  end
230
+
231
+ def release_target_branches
232
+ return [] unless config
233
+
234
+ config.release_target_branches
235
+ end
236
+
237
+ def shared_changelog?
238
+ config&.shared_changelog?
239
+ end
240
+
241
+ def git_root
242
+ stdout, stderr, status = Open3.capture3("git", "rev-parse", "--show-toplevel", chdir: config.root)
243
+ raise Error, "could not determine git root for #{config.root}: #{stderr}" unless status.success?
244
+
245
+ File.realpath(stdout.strip)
246
+ end
247
+
248
+ def with_branch_worktree(root:, branch:)
249
+ base = File.join(root, "tmp", "kettle-family-release-state")
250
+ FileUtils.mkdir_p(base)
251
+ worktree_root = File.join(base, "worktree-#{Process.pid}-#{SecureRandom.hex(8)}")
252
+ add_branch_worktree(root: root, branch: branch, worktree_root: worktree_root)
253
+ yield worktree_root
254
+ ensure
255
+ remove_branch_worktree(root: root, worktree_root: worktree_root)
256
+ end
257
+
258
+ def add_branch_worktree(root:, branch:, worktree_root:)
259
+ _stdout, stderr, status = Open3.capture3("git", "worktree", "add", "--detach", worktree_root, branch, chdir: root)
260
+ raise Error, "could not add worktree for #{branch}: #{stderr}" unless status.success?
261
+ end
262
+
263
+ def remove_branch_worktree(root:, worktree_root:)
264
+ return unless worktree_root && Dir.exist?(worktree_root)
265
+
266
+ Open3.capture3("git", "worktree", "remove", "--force", worktree_root, chdir: root)
267
+ end
268
+
269
+ def discover_branch_members(worktree_root:, selected_names:)
270
+ branch_config = Config.load(root: branch_config_root(worktree_root))
271
+ Discovery.new(config: branch_config).members
272
+ .sort_by(&:name)
273
+ .select { |member| selected_names.include?(member.name) }
274
+ end
275
+
276
+ def branch_config_root(worktree_root)
277
+ File.join(worktree_root, relative_config_root)
278
+ end
279
+
280
+ def relative_config_root
281
+ @relative_config_root ||= begin
282
+ root = git_root
283
+ config_root = File.realpath(config.root)
284
+ if config_root == root
285
+ "."
286
+ elsif config_root.start_with?("#{root}/")
287
+ config_root.delete_prefix("#{root}/")
288
+ else
289
+ raise Error, "configured root #{config.root} is outside git root #{root}"
290
+ end
291
+ end
292
+ end
48
293
  end
49
294
  end
50
295
  end
@@ -3,9 +3,9 @@
3
3
  module Kettle
4
4
  module Family
5
5
  class ReleaseStateResult
6
- attr_reader :member_name, :phase, :command, :workdir, :status, :success, :stdout, :stderr, :elapsed_seconds, :skipped, :reason, :state
6
+ attr_reader :member_name, :phase, :command, :workdir, :status, :success, :stdout, :stderr, :elapsed_seconds, :skipped, :reason, :state, :branch
7
7
 
8
- def initialize(member_name:, command:, workdir:, status:, success:, stdout:, stderr:, elapsed_seconds:, state:, reason: nil)
8
+ def initialize(member_name:, command:, workdir:, status:, success:, stdout:, stderr:, elapsed_seconds:, state:, reason: nil, branch: nil)
9
9
  @member_name = member_name
10
10
  @phase = "release_state"
11
11
  @command = command
@@ -18,6 +18,7 @@ module Kettle
18
18
  @skipped = false
19
19
  @reason = reason
20
20
  @state = state
21
+ @branch = branch
21
22
  end
22
23
 
23
24
  def ok?
@@ -37,6 +38,7 @@ module Kettle
37
38
  "elapsed_seconds" => elapsed_seconds,
38
39
  "skipped" => skipped,
39
40
  "reason" => reason,
41
+ "branch" => branch,
40
42
  "release_state" => state
41
43
  }
42
44
  end
@@ -64,6 +64,7 @@ module Kettle
64
64
  private
65
65
 
66
66
  def append_results(lines)
67
+ return append_metadata_results(lines) if command == "metadata"
67
68
  return if results.empty?
68
69
  return append_release_state_results(lines) if command == "release-state"
69
70
 
@@ -82,9 +83,24 @@ module Kettle
82
83
  "failed"
83
84
  end
84
85
 
86
+ def append_metadata_results(lines)
87
+ lines << "metadata:"
88
+ rows = [["gem", "version", "ruby", "licenses", "authors"]]
89
+ selected_members.each do |member|
90
+ rows << [
91
+ member.name.to_s,
92
+ member.version.to_s,
93
+ blank_as_none(member.required_ruby_version),
94
+ blank_as_none(Array(member.licenses).join(", ")),
95
+ blank_as_none(Array(member.authors).join(", "))
96
+ ]
97
+ end
98
+ lines.concat(format_table(rows).map { |line| " #{line}" })
99
+ end
100
+
85
101
  def append_release_state_results(lines)
86
102
  lines << "release state:"
87
- rows = [["gem", "version.rb", "latest released", "latest changelog", "unreleased", "prepared", "pending"]]
103
+ rows = release_state_header
88
104
  results.each do |result|
89
105
  rows << release_state_row(result)
90
106
  end
@@ -101,7 +117,7 @@ module Kettle
101
117
 
102
118
  def release_state_row(result)
103
119
  state = result.state || {}
104
- [
120
+ row = [
105
121
  state.fetch("gem_name", result.member_name).to_s,
106
122
  state.fetch("version", "unknown").to_s,
107
123
  state.fetch("latest_released", nil).to_s.empty? ? "unknown" : state.fetch("latest_released").to_s,
@@ -110,6 +126,20 @@ module Kettle
110
126
  yes_no(state.fetch("prepared_release_pending", nil)),
111
127
  yes_no(state.fetch("pending_release", nil))
112
128
  ]
129
+ return row unless release_state_has_branches?
130
+
131
+ [result.branch.to_s.empty? ? "current" : result.branch.to_s, *row]
132
+ end
133
+
134
+ def release_state_header
135
+ header = [["gem", "version.rb", "latest released", "latest changelog", "unreleased", "prepared", "pending"]]
136
+ return header unless release_state_has_branches?
137
+
138
+ [["branch", *header.first]]
139
+ end
140
+
141
+ def release_state_has_branches?
142
+ results.any? { |result| !result.branch.to_s.empty? }
113
143
  end
114
144
 
115
145
  def format_table(rows)
@@ -131,6 +161,11 @@ module Kettle
131
161
  end
132
162
  end
133
163
 
164
+ def blank_as_none(value)
165
+ text = value.to_s.strip
166
+ text.empty? ? "(none)" : text
167
+ end
168
+
134
169
  def resume_hint
135
170
  failed = results.find { |result| !result.ok? }
136
171
  resume_hint_for(failed) if failed
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "0.1.2"
6
+ VERSION = "0.1.4"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -54,14 +54,14 @@ module Kettle
54
54
  attr_reader :command, :config, :members, :execute, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :local_ci, :continue_ci_failures
55
55
 
56
56
  def check_results
57
- members.map { |member| ReadinessCheck.call(member: member) }
57
+ members.map { |member| ReadinessCheck.call(member: member, config: config) }
58
58
  end
59
59
 
60
60
  def release_results
61
61
  prompt_for_gem_signing_password if execute && publish && gem_signing_required?
62
62
  return branch_target_release_results unless config.release_target_branches.empty?
63
63
 
64
- release_member_results(members)
64
+ release_member_results(members, include_family_changelog: true)
65
65
  end
66
66
 
67
67
  def branch_target_release_results
@@ -72,14 +72,18 @@ module Kettle
72
72
  break memo unless memo.last.ok?
73
73
 
74
74
  branch_members = rediscovered_selected_members(selected_names)
75
- memo.concat(release_member_results(branch_members))
75
+ memo.concat(release_member_results(branch_members, include_family_changelog: true))
76
76
  break memo unless memo.last&.ok?
77
77
  end
78
78
  end
79
79
 
80
- def release_member_results(release_members)
80
+ def release_member_results(release_members, include_family_changelog: false)
81
81
  runner = command_runner
82
- release_members.each_with_object([]) do |member, memo|
82
+ results = []
83
+ append_family_changelog_result(runner: runner, memo: results) if include_family_changelog
84
+ return results unless results.all?(&:ok?)
85
+
86
+ release_members.each_with_object(results) do |member, memo|
83
87
  if skip_already_released?(member)
84
88
  memo << already_released_result(member)
85
89
  next
@@ -121,8 +125,19 @@ module Kettle
121
125
  end
122
126
 
123
127
  def append_release_internal_checks(member:, memo:)
124
- memo << ReadinessCheck.call(member: member)
125
- memo << ChangelogCheck.call(member: member) if memo.last.ok?
128
+ memo << ReadinessCheck.call(member: member, config: config)
129
+ memo << ChangelogCheck.call(member: member, config: config) if memo.last.ok?
130
+ end
131
+
132
+ def append_family_changelog_result(runner:, memo:)
133
+ return unless config.release_family_changelog?
134
+
135
+ memo << runner.call(
136
+ member: family_member,
137
+ phase: "family_changelog",
138
+ command: config.release_family_changelog_command,
139
+ env: release_env.merge(config.changelog_env)
140
+ )
126
141
  end
127
142
 
128
143
  def release_phase
@@ -155,7 +170,9 @@ module Kettle
155
170
  end
156
171
 
157
172
  def release_env
158
- continue_ci_failures ? {"K_RELEASE_CI_CONTINUE" => "true"} : {}
173
+ env = config.release_env.dup
174
+ env["K_RELEASE_CI_CONTINUE"] = "true" if continue_ci_failures
175
+ env
159
176
  end
160
177
 
161
178
  def append_release_git_phases(member:, runner:, memo:)
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-family
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -60,7 +60,7 @@ dependencies:
60
60
  version: '1.1'
61
61
  - - ">="
62
62
  - !ruby/object:Gem::Version
63
- version: 1.1.11
63
+ version: 1.1.12
64
64
  type: :runtime
65
65
  prerelease: false
66
66
  version_requirements: !ruby/object:Gem::Requirement
@@ -70,7 +70,7 @@ dependencies:
70
70
  version: '1.1'
71
71
  - - ">="
72
72
  - !ruby/object:Gem::Version
73
- version: 1.1.11
73
+ version: 1.1.12
74
74
  - !ruby/object:Gem::Dependency
75
75
  name: kettle-dev
76
76
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +80,7 @@ dependencies:
80
80
  version: '2.2'
81
81
  - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: 2.2.7
83
+ version: 2.2.9
84
84
  type: :development
85
85
  prerelease: false
86
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  version: '2.2'
91
91
  - - ">="
92
92
  - !ruby/object:Gem::Version
93
- version: 2.2.7
93
+ version: 2.2.9
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: bundler-audit
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -188,7 +188,7 @@ dependencies:
188
188
  version: '3.1'
189
189
  - - ">="
190
190
  - !ruby/object:Gem::Version
191
- version: 3.1.2
191
+ version: 3.1.3
192
192
  type: :development
193
193
  prerelease: false
194
194
  version_requirements: !ruby/object:Gem::Requirement
@@ -198,7 +198,7 @@ dependencies:
198
198
  version: '3.1'
199
199
  - - ">="
200
200
  - !ruby/object:Gem::Version
201
- version: 3.1.2
201
+ version: 3.1.3
202
202
  - !ruby/object:Gem::Dependency
203
203
  name: ruby-progressbar
204
204
  requirement: !ruby/object:Gem::Requirement
@@ -242,7 +242,7 @@ dependencies:
242
242
  version: '2.0'
243
243
  - - ">="
244
244
  - !ruby/object:Gem::Version
245
- version: 2.0.1
245
+ version: 2.0.2
246
246
  type: :development
247
247
  prerelease: false
248
248
  version_requirements: !ruby/object:Gem::Requirement
@@ -252,7 +252,7 @@ dependencies:
252
252
  version: '2.0'
253
253
  - - ">="
254
254
  - !ruby/object:Gem::Version
255
- version: 2.0.1
255
+ version: 2.0.2
256
256
  description: "\U0001F469‍\U0001F469‍\U0001F467‍\U0001F467 Kettle::Family provides
257
257
  scripts and conventions for coordinating related Ruby gems as one family."
258
258
  email:
@@ -305,13 +305,13 @@ files:
305
305
  - sig/kettle/family/version.rbs
306
306
  homepage: https://github.com/kettle-dev/kettle-family
307
307
  licenses:
308
- - MIT
308
+ - AGPL-3.0-only
309
309
  metadata:
310
310
  homepage_uri: https://kettle-family.galtzo.com
311
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.2
312
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.2/CHANGELOG.md
311
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.4
312
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.4/CHANGELOG.md
313
313
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
314
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.2
314
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.4
315
315
  funding_uri: https://github.com/sponsors/pboling
316
316
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
317
317
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file