kettle-family 1.3.36 → 1.3.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +51 -1
- data/README.md +2 -114
- data/lib/kettle/family/cli.rb +19 -2
- data/lib/kettle/family/command_runner.rb +6 -6
- data/lib/kettle/family/config.rb +45 -0
- data/lib/kettle/family/release_state_check.rb +25 -8
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +207 -24
- data.tar.gz.sig +0 -0
- metadata +12 -12
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f6f4ceee082895db3eb6f2b0c90735263adbcf3e8693640edd5d7b929a01244
|
|
4
|
+
data.tar.gz: 9a1c2c5c05bea72ade7f755329bec2e361aac45b171b92bdb76fa6812209a632
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4571f37dfb76b652fbd65cf68eeeda058e43b35a82879c2760937e0ec4f65cb76d7b9264bb5d0859ea631b927649a92e640c087eb964a19c7974d3d2e89bf9d0
|
|
7
|
+
data.tar.gz: 1ec331486c79ae31f99b8ef9d0937fda86b25db2dc350ded8a553c721bb056b25f01e185630fd683aa3c4ac87c61699f23b470badd571a0550b5a69eacc5af5a
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,52 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.3.38] - 2026-11-07
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.3.38][1.3.38t]
|
|
36
|
+
- COVERAGE: 93.15% -- 7697/8263 lines in 45 files
|
|
37
|
+
- BRANCH COVERAGE: 77.57% -- 3089/3982 branches in 45 files
|
|
38
|
+
- 39.53% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- [kc] template-root-outputs: Monorepo template runs can now update explicitly configured repository-root output paths through the template.root_outputs setting, alongside the nested member being templated. Those outputs are isolated, validated, synchronized, and committed with the template changes.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- [kc] kettle-jem/prepare: updated 13 project files:
|
|
47
|
+
- configuration (2)
|
|
48
|
+
- dependencies (11)
|
|
49
|
+
|
|
50
|
+
- [kc] kettle-jem/template: updated 7 project files:
|
|
51
|
+
- code and tests (2)
|
|
52
|
+
- dependencies (3)
|
|
53
|
+
- documentation (1)
|
|
54
|
+
- other (1)
|
|
55
|
+
|
|
56
|
+
## [1.3.37] - 2026-10-30
|
|
57
|
+
|
|
58
|
+
- TAG: [v1.3.37][1.3.37t]
|
|
59
|
+
- COVERAGE: 93.13% -- 7665/8230 lines in 45 files
|
|
60
|
+
- BRANCH COVERAGE: 77.52% -- 3076/3968 branches in 45 files
|
|
61
|
+
- 39.41% documented
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- Release preflight now blocks on bootstrap dependency drift: a new `bootstrap dependency drift` phase compares each selected member's committed Gemfile.lock against the newest published version satisfying the member's declared floor for bootstrap-managed gems (`release.drift_check.gems`, default `[nomono]`), failing with a `bundle update <gem>` / family template sweep remediation hint; registry lookup failures skip non-fatally instead of blocking.
|
|
66
|
+
|
|
67
|
+
- New family config release.version_series.independent_members: members that keep their own monotonic version series in a shared-version family. They are skipped by lockstep shared-version raises and exempt from the aggregate GitHub Release same-version invariant, while their built artifacts still attach to the aggregate release.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- kettle-family bump now raises shared-changelog members to the family's next shared version when their release is pending, including a misaligned cohort that previously only aligned to the current maximum and reported ok, leaving the unreleased-entries bump gate uncleared with no documented command able to clear it (#17).
|
|
72
|
+
|
|
73
|
+
- Independently versioned members (release.version_series.independent_members) can no longer be captured by the lockstep shared-version raise: they bump on their own series, are never force-marked pending by the shared cohort, their higher own-series version no longer drags the cohort line, and the aggregate GitHub Release no longer fails its same-version check on them (#18).
|
|
74
|
+
|
|
75
|
+
### Security
|
|
76
|
+
|
|
77
|
+
- Generated resume commands no longer embed secret env values: secret-shaped keys (at minimum `KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE`) are redacted to the shell-safe placeholder `<redacted-reauthorize>`; re-authorize the secret through the release secrets provider (1Password or the `KETTLE_RELEASE_SECRETS_BROKER` cache), or paste the real value over the placeholder, before resuming.
|
|
78
|
+
|
|
33
79
|
## [1.3.36] - 2026-10-28
|
|
34
80
|
|
|
35
81
|
- TAG: [v1.3.36][1.3.36t]
|
|
@@ -3320,7 +3366,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
3320
3366
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
3321
3367
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
3322
3368
|
|
|
3323
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.
|
|
3369
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.38...HEAD
|
|
3370
|
+
[1.3.38]: https://github.com/kettle-dev/kettle-family/compare/v1.3.37...v1.3.38
|
|
3371
|
+
[1.3.38t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.38
|
|
3372
|
+
[1.3.37]: https://github.com/kettle-dev/kettle-family/compare/v1.3.36...v1.3.37
|
|
3373
|
+
[1.3.37t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.37
|
|
3324
3374
|
[1.3.36]: https://github.com/kettle-dev/kettle-family/compare/v1.3.35...v1.3.36
|
|
3325
3375
|
[1.3.36t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.36
|
|
3326
3376
|
[1.3.35]: https://github.com/kettle-dev/kettle-family/compare/v1.3.34...v1.3.35
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
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
|
-
[![
|
|
13
|
+
[![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
15
|
<details markdown="1">
|
|
16
16
|
<summary>👣 How will this project approach the September 2025 hostile takeover of RubyGems? 🚑️</summary>
|
|
@@ -91,28 +91,6 @@ The _amazing_ test matrix is powered by the kettle-dev stack.
|
|
|
91
91
|
|
|
92
92
|
</details>
|
|
93
93
|
|
|
94
|
-
### Federated DVCS
|
|
95
|
-
|
|
96
|
-
<details markdown="1">
|
|
97
|
-
<summary>Find this repo on federated forges (Coming soon!)</summary>
|
|
98
|
-
|
|
99
|
-
| Federated [DVCS][💎d-in-dvcs] Repository | Status | Issues | PRs | Wiki | CI |
|
|
100
|
-
|-------------------------------------------------|-------------------------------------------|-------------------|------------------|-----------------|----------------|
|
|
101
|
-
| 🧪 [kettle-dev/kettle-family on GitLab][📜src-gl] | The Truth | [💚][🤝gl-issues] | [💚][🤝gl-pulls] | [💚][📜gl-wiki] | 🐭 Tiny Matrix |
|
|
102
|
-
| 🧊 [kettle-dev/kettle-family on CodeBerg][📜src-cb] | An Ethical Mirror ([Donate][🤝cb-donate]) | [💚][🤝cb-issues] | [💚][🤝cb-pulls] | ➖ | ⭕️ No Matrix |
|
|
103
|
-
| 🐙 [kettle-dev/kettle-family on GitHub][📜src-gh] | Another Mirror | [💚][🤝gh-issues] | [💚][🤝gh-pulls] | [💚][📜gh-wiki] | 💯 Full Matrix |
|
|
104
|
-
|
|
105
|
-
</details>
|
|
106
|
-
|
|
107
|
-
### Enterprise Support
|
|
108
|
-
|
|
109
|
-
For enterprise support from the maintainer, use [Upwork][👨🏼🏫expsup-upwork] or [CodeMentor][👨🏼🏫expsup-codementor].
|
|
110
|
-
|
|
111
|
-
Other support channels:
|
|
112
|
-
|
|
113
|
-
- [![Ruby Users Forum][✉️ruby-forum-img]][✉️ruby-forum]
|
|
114
|
-
- [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite]
|
|
115
|
-
|
|
116
94
|
## ✨ Installation
|
|
117
95
|
|
|
118
96
|
Install the gem and add to the application's Gemfile by executing:
|
|
@@ -955,36 +933,6 @@ We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you m
|
|
|
955
933
|
|
|
956
934
|
See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
|
|
957
935
|
|
|
958
|
-
### 🚀 Release Instructions
|
|
959
|
-
|
|
960
|
-
See [CONTRIBUTING.md][🤝contributing].
|
|
961
|
-
|
|
962
|
-
### Code Coverage
|
|
963
|
-
|
|
964
|
-
<details markdown="1">
|
|
965
|
-
<summary>Coverage service badges</summary>
|
|
966
|
-
|
|
967
|
-
[![Coverage Graph][🏀codecov-g]][🏀codecov]
|
|
968
|
-
|
|
969
|
-
[![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls]
|
|
970
|
-
|
|
971
|
-
[![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov]
|
|
972
|
-
|
|
973
|
-
</details>
|
|
974
|
-
|
|
975
|
-
### 🪇 Code of Conduct
|
|
976
|
-
|
|
977
|
-
Everyone interacting with this project's codebases, issue trackers,
|
|
978
|
-
chat rooms and mailing lists agrees to follow the [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct].
|
|
979
|
-
|
|
980
|
-
## 🌈 Contributors
|
|
981
|
-
|
|
982
|
-
[![Contributors][🖐contributors-img]][🖐contributors]
|
|
983
|
-
|
|
984
|
-
Made with [contributors-img][🖐contrib-rocks].
|
|
985
|
-
|
|
986
|
-
Also see GitLab Contributors: [https://gitlab.com/kettle-dev/kettle-family/-/graphs/main][🚎contributors-gl]
|
|
987
|
-
|
|
988
936
|
## 📌 Versioning
|
|
989
937
|
|
|
990
938
|
This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
|
|
@@ -1018,55 +966,9 @@ See [LICENSE.md][📄license] for details.
|
|
|
1018
966
|
|
|
1019
967
|
If none of the available licenses suit your use case, please [contact us](mailto:floss@galtzo.com) to discuss a custom commercial license.
|
|
1020
968
|
|
|
1021
|
-
### © Copyright
|
|
1022
|
-
|
|
1023
|
-
See [LICENSE.md][📄license] for the official copyright notice.
|
|
1024
|
-
|
|
1025
|
-
<details markdown="1">
|
|
1026
|
-
<summary>Copyright holders</summary>
|
|
1027
|
-
|
|
1028
|
-
- Copyright (c) 2026 Peter H. Boling
|
|
1029
|
-
|
|
1030
|
-
</details>
|
|
1031
|
-
|
|
1032
|
-
## 🤑 A request for help
|
|
1033
|
-
|
|
1034
|
-
Maintainers have teeth and need to pay their dentists.
|
|
1035
|
-
After getting laid off in an RIF in March, and encountering difficulty finding a new one,
|
|
1036
|
-
I began spending most of my time building open source tools.
|
|
1037
|
-
I'm hoping to be able to pay for my kids' health insurance this month,
|
|
1038
|
-
so if you value the work I am doing, I need your support.
|
|
1039
|
-
Please consider sponsoring me or the project.
|
|
1040
|
-
|
|
1041
|
-
To join the community or get help, use the RubyForum or Discord.
|
|
1042
|
-
|
|
1043
|
-
[![Ruby Users Forum][✉️ruby-forum-img]][✉️ruby-forum]
|
|
1044
|
-
[![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite]
|
|
1045
|
-
|
|
1046
|
-
To say "thanks!" ☝️ Join the community or 👇️ send money.
|
|
1047
|
-
|
|
1048
|
-
[![Sponsor kettle-dev/kettle-family on Open Source Collective][🖇osc-all-bottom-img]][🖇osc] 💌 [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal]
|
|
1049
|
-
|
|
1050
|
-
### Please give the project a star ⭐ ♥.
|
|
1051
|
-
|
|
1052
|
-
Many parts of this project are actively managed by a [kettle-jem](https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/kettle-jem) smart template utilizing [StructuredMerge.org](https://structuredmerge.org) merge contracts.
|
|
1053
|
-
|
|
1054
|
-
Thanks for RTFM. ☺️
|
|
1055
|
-
|
|
1056
969
|
[⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay&color=a51611&style=flat
|
|
1057
970
|
[⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/pboling.svg?style=for-the-badge&logo=liberapay&color=a51611
|
|
1058
971
|
[⛳liberapay]: https://liberapay.com/pboling/donate
|
|
1059
|
-
[🖇osc-all-img]: https://img.shields.io/opencollective/all/kettle-dev
|
|
1060
|
-
[🖇osc-sponsors-img]: https://img.shields.io/opencollective/sponsors/kettle-dev
|
|
1061
|
-
[🖇osc-backers-img]: https://img.shields.io/opencollective/backers/kettle-dev
|
|
1062
|
-
[🖇osc-backers]: https://opencollective.com/kettle-dev#backer
|
|
1063
|
-
[🖇osc-backers-i]: https://opencollective.com/kettle-dev/backers/badge.svg?style=flat
|
|
1064
|
-
[🖇osc-sponsors]: https://opencollective.com/kettle-dev#sponsor
|
|
1065
|
-
[🖇osc-sponsors-i]: https://opencollective.com/kettle-dev/sponsors/badge.svg?style=flat
|
|
1066
|
-
[🖇osc-all-bottom-img]: https://img.shields.io/opencollective/all/kettle-dev?style=for-the-badge
|
|
1067
|
-
[🖇osc-sponsors-bottom-img]: https://img.shields.io/opencollective/sponsors/kettle-dev?style=for-the-badge
|
|
1068
|
-
[🖇osc-backers-bottom-img]: https://img.shields.io/opencollective/backers/kettle-dev?style=for-the-badge
|
|
1069
|
-
[🖇osc]: https://opencollective.com/kettle-dev
|
|
1070
972
|
[🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
|
|
1071
973
|
[🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-pboling-blue?style=for-the-badge&logo=github
|
|
1072
974
|
[🖇sponsor]: https://github.com/sponsors/pboling
|
|
@@ -1087,7 +989,6 @@ Thanks for RTFM. ☺️
|
|
|
1087
989
|
[✉️ruby-forum-top-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=flat&logo=discourse&label=Ruby%20Users%20Forum
|
|
1088
990
|
[✉️ruby-forum-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=for-the-badge&logo=discourse&label=Ruby%20Users%20Forum
|
|
1089
991
|
[✉️ruby-forum]: https://www.rubyforum.org/tag/kettle-dev
|
|
1090
|
-
|
|
1091
992
|
[✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
|
|
1092
993
|
[⛳️gem-namespace]: https://github.com/kettle-dev/kettle-family
|
|
1093
994
|
[⛳️namespace-img]: https://img.shields.io/badge/namespace-Kettle::Family-3C2D2D.svg?style=square&logo=ruby&logoColor=white
|
|
@@ -1177,7 +1078,6 @@ Thanks for RTFM. ☺️
|
|
|
1177
1078
|
[🖐contrib-rocks]: https://contrib.rocks
|
|
1178
1079
|
[🖐contributors]: https://github.com/kettle-dev/kettle-family/graphs/contributors
|
|
1179
1080
|
[🖐contributors-img]: https://contrib.rocks/image?repo=kettle-dev/kettle-family
|
|
1180
|
-
[🚎contributors-gl]: https://gitlab.com/kettle-dev/kettle-family/-/graphs/main
|
|
1181
1081
|
[🪇conduct]: https://github.com/kettle-dev/kettle-family/blob/main/CODE_OF_CONDUCT.md
|
|
1182
1082
|
[🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg
|
|
1183
1083
|
[📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
|
|
@@ -1191,7 +1091,7 @@ Thanks for RTFM. ☺️
|
|
|
1191
1091
|
[📌gitmoji]: https://gitmoji.dev
|
|
1192
1092
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1193
1093
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1194
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-8.
|
|
1094
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-8.263-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1195
1095
|
[🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
|
|
1196
1096
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1197
1097
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -1200,7 +1100,6 @@ Thanks for RTFM. ☺️
|
|
|
1200
1100
|
[📄license-img]: https://img.shields.io/badge/License-AGPL--3.0--only-259D6C.svg
|
|
1201
1101
|
[📄license-compat]: https://www.apache.org/legal/resolved.html#category-x
|
|
1202
1102
|
[📄license-compat-img]: https://img.shields.io/badge/Apache_Incompatible:_Category_X-%E2%9C%97-C0392B.svg?style=flat&logo=Apache
|
|
1203
|
-
|
|
1204
1103
|
[📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
|
|
1205
1104
|
[📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
|
|
1206
1105
|
[🚎yard-current]: http://rubydoc.info/gems/kettle-family
|
|
@@ -1212,14 +1111,3 @@ Thanks for RTFM. ☺️
|
|
|
1212
1111
|
[💎appraisal2]: https://github.com/appraisal-rb/appraisal2
|
|
1213
1112
|
[💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
|
|
1214
1113
|
[💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/
|
|
1215
|
-
|
|
1216
|
-
<!-- kettle-jem:metadata:start -->
|
|
1217
|
-
| Field | Value |
|
|
1218
|
-
|---|---|
|
|
1219
|
-
| Package | kettle-family |
|
|
1220
|
-
| Description | 👩👩👧👧 Kettle::Family provides scripts and conventions for coordinating related Ruby gems as one family. |
|
|
1221
|
-
| Homepage | https://github.com/kettle-dev/kettle-family |
|
|
1222
|
-
| Source | https://github.com/kettle-dev/kettle-family |
|
|
1223
|
-
| License | `AGPL-3.0-only` |
|
|
1224
|
-
| 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://www.buymeacoffee.com/pboling |
|
|
1225
|
-
<!-- kettle-jem:metadata:end -->
|
data/lib/kettle/family/cli.rb
CHANGED
|
@@ -1475,19 +1475,29 @@ module Kettle
|
|
|
1475
1475
|
def shared_bump_member_names(config:, members:)
|
|
1476
1476
|
return [] unless config.shared_changelog?
|
|
1477
1477
|
|
|
1478
|
-
members
|
|
1478
|
+
members
|
|
1479
|
+
.reject { |member| config.member_local_changelog?(member) }
|
|
1480
|
+
.reject { |member| config.independent_version_series_member?(member) }
|
|
1481
|
+
.map(&:name)
|
|
1479
1482
|
end
|
|
1480
1483
|
|
|
1481
1484
|
def shared_bump_target_version(config:, members:, options:, release_state_results: nil)
|
|
1482
1485
|
return unless config.shared_changelog?
|
|
1483
1486
|
return unless Kettle::Dev::VersionBump::BUMP_TYPES.include?(options[:target_version].to_s)
|
|
1484
1487
|
|
|
1488
|
+
# The lockstep raise cohort excludes members carrying their own
|
|
1489
|
+
# changelog AND independently versioned members: an independent
|
|
1490
|
+
# series must never be captured by (or drag) the shared version
|
|
1491
|
+
# line, in either direction.
|
|
1485
1492
|
shared_members = members.reject { |member| config.member_local_changelog?(member) }
|
|
1493
|
+
.reject { |member| config.independent_version_series_member?(member) }
|
|
1486
1494
|
return if shared_members.empty?
|
|
1487
1495
|
|
|
1488
1496
|
versions = shared_members.map { |member| Gem::Version.new(member.version.to_s) }.uniq
|
|
1489
1497
|
current = versions.max
|
|
1490
|
-
|
|
1498
|
+
# Without release-state results (explicit member selection) a
|
|
1499
|
+
# misaligned cohort still aligns to its maximum, as before.
|
|
1500
|
+
return current.to_s if versions.length > 1 && release_state_results.nil?
|
|
1491
1501
|
|
|
1492
1502
|
released_versions = Array(release_state_results).filter_map do |result|
|
|
1493
1503
|
next unless shared_members.any? { |member| member.name == result.member_name }
|
|
@@ -1502,8 +1512,15 @@ module Kettle
|
|
|
1502
1512
|
rescue ArgumentError
|
|
1503
1513
|
nil
|
|
1504
1514
|
end
|
|
1515
|
+
# Mid-prep (the cohort line already moved past the last release):
|
|
1516
|
+
# laggards align up to the in-flight version and nothing increments.
|
|
1505
1517
|
return current.to_s if released_versions.any? && current > released_versions.max
|
|
1506
1518
|
|
|
1519
|
+
# Otherwise the cohort owes a release: every shared member is raised
|
|
1520
|
+
# to the family's NEXT shared version — including a misaligned
|
|
1521
|
+
# cohort, which previously aligned to the current maximum and then
|
|
1522
|
+
# reported ok, leaving the unreleased-entries bump gate uncleared
|
|
1523
|
+
# with no documented command able to clear it (#17).
|
|
1507
1524
|
Kettle::Dev::VersionBump.resolve_target_version(options[:target_version].to_s, current.to_s)
|
|
1508
1525
|
end
|
|
1509
1526
|
|
|
@@ -232,19 +232,19 @@ module Kettle
|
|
|
232
232
|
@mise_adapter = mise_adapter || Kettle::Family.mise_adapter
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
-
def call(member:, phase:, command:, env: {}, interactive: false, raw: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true)
|
|
235
|
+
def call(member:, phase:, command:, env: {}, interactive: false, raw: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true, chdir: member.root, **options)
|
|
236
236
|
argv = command_argv(member: member, command: command, env: env, raw: raw)
|
|
237
237
|
process_env = process_env(member: member, env: env)
|
|
238
238
|
spawn_options = process_options
|
|
239
239
|
return skipped_result(member: member, phase: phase, argv: argv) unless execute
|
|
240
240
|
|
|
241
241
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
242
|
-
stdout, stderr, status = with_transcript(log_path, argv: argv, chdir:
|
|
242
|
+
stdout, stderr, status = with_transcript(log_path, argv: argv, chdir: chdir) do |log_io|
|
|
243
243
|
if interactive
|
|
244
244
|
run_interactive(
|
|
245
245
|
env: process_env,
|
|
246
246
|
argv: argv,
|
|
247
|
-
chdir:
|
|
247
|
+
chdir: chdir,
|
|
248
248
|
member_name: member.name,
|
|
249
249
|
process_options: spawn_options,
|
|
250
250
|
stdout_line_handler: stdout_line_handler,
|
|
@@ -252,9 +252,9 @@ module Kettle
|
|
|
252
252
|
passthrough_output: passthrough_output
|
|
253
253
|
)
|
|
254
254
|
elsif stdout_line_handler
|
|
255
|
-
run_streaming(env: process_env, argv: argv, chdir:
|
|
255
|
+
run_streaming(env: process_env, argv: argv, chdir: chdir, process_options: spawn_options, stdout_line_handler: stdout_line_handler, log_io: log_io)
|
|
256
256
|
else
|
|
257
|
-
result = Open3.capture3(process_env, *argv, chdir:
|
|
257
|
+
result = Open3.capture3(process_env, *argv, chdir: chdir, **spawn_options)
|
|
258
258
|
transcript_write(log_io, result[0])
|
|
259
259
|
transcript_write(log_io, result[1])
|
|
260
260
|
result
|
|
@@ -267,7 +267,7 @@ module Kettle
|
|
|
267
267
|
member_name: member.name,
|
|
268
268
|
phase: phase,
|
|
269
269
|
command: argv,
|
|
270
|
-
workdir:
|
|
270
|
+
workdir: chdir,
|
|
271
271
|
status: status.exitstatus,
|
|
272
272
|
success: status.success?,
|
|
273
273
|
stdout: stdout,
|
data/lib/kettle/family/config.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
|
+
require "pathname"
|
|
4
5
|
|
|
5
6
|
module Kettle
|
|
6
7
|
module Family
|
|
@@ -208,6 +209,21 @@ module Kettle
|
|
|
208
209
|
fetch_path("template", "jobs")
|
|
209
210
|
end
|
|
210
211
|
|
|
212
|
+
# Root paths that a nested monorepo member's Kettle Jem run may update.
|
|
213
|
+
# These are explicit repository-relative output paths, never globs.
|
|
214
|
+
def template_root_outputs
|
|
215
|
+
paths = Array(fetch_path("template", "root_outputs"))
|
|
216
|
+
paths.map do |path|
|
|
217
|
+
value = path.to_s
|
|
218
|
+
expanded = File.expand_path(value, root)
|
|
219
|
+
unless !value.empty? && !value.end_with?("/") && !Pathname.new(value).absolute? && Paths.within?(expanded, root) && expanded != root
|
|
220
|
+
raise Error, "template.root_outputs entries must be repository-relative paths below the family root: #{value.inspect}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
Pathname.new(expanded).relative_path_from(Pathname.new(root)).to_s
|
|
224
|
+
end.uniq
|
|
225
|
+
end
|
|
226
|
+
|
|
211
227
|
# Additional local dependency selectors used by the generated
|
|
212
228
|
# `local-template` task. Values may use Mise's `{{config_root}}` token.
|
|
213
229
|
def template_local_env
|
|
@@ -308,6 +324,17 @@ module Kettle
|
|
|
308
324
|
end.compact
|
|
309
325
|
end
|
|
310
326
|
|
|
327
|
+
# Bootstrap-managed gems whose committed member locks must not lag the
|
|
328
|
+
# newest published version satisfying the member's declared requirement
|
|
329
|
+
# (release.drift_check.gems; default: nomono, the bootstrap gem whose
|
|
330
|
+
# lock/registry drift killed the SM-ruby wave on 2026-10-30). An empty
|
|
331
|
+
# list disables the drift preflight phase entirely.
|
|
332
|
+
def release_drift_check_gems
|
|
333
|
+
configured = fetch_path("release", "drift_check", "gems")
|
|
334
|
+
gems = configured.nil? ? [NomonoBootstrap::GEM_NAME] : Array(configured)
|
|
335
|
+
gems.map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
336
|
+
end
|
|
337
|
+
|
|
311
338
|
def release_required_remotes
|
|
312
339
|
fetch_path("release", "required_remotes")
|
|
313
340
|
end
|
|
@@ -405,6 +432,24 @@ module Kettle
|
|
|
405
432
|
fetch_path("release", "aggregate_validation", "enabled") == true
|
|
406
433
|
end
|
|
407
434
|
|
|
435
|
+
# Members that keep their own monotonic version series even in a
|
|
436
|
+
# shared-version family (release.version_series.independent_members;
|
|
437
|
+
# default empty). An independent-series member is skipped by lockstep
|
|
438
|
+
# shared-version raises — its bumps stay on its own series — and is
|
|
439
|
+
# exempt from the aggregate GitHub Release same-version invariant,
|
|
440
|
+
# while its built artifacts still attach to the aggregate release.
|
|
441
|
+
def release_version_series_independent_members
|
|
442
|
+
Array(fetch_path("release", "version_series", "independent_members"))
|
|
443
|
+
.map(&:to_s)
|
|
444
|
+
.map(&:strip)
|
|
445
|
+
.reject(&:empty?)
|
|
446
|
+
.uniq
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def independent_version_series_member?(member)
|
|
450
|
+
release_version_series_independent_members.include?(member.name.to_s)
|
|
451
|
+
end
|
|
452
|
+
|
|
408
453
|
def release_aggregate_validation_excluded_members
|
|
409
454
|
Array(fetch_path("release", "aggregate_validation", "exclude_members"))
|
|
410
455
|
.map(&:to_s)
|
|
@@ -830,8 +830,9 @@ module Kettle
|
|
|
830
830
|
end
|
|
831
831
|
|
|
832
832
|
# Version-locked siblings move in lockstep. Members not declared
|
|
833
|
-
# independently releasable (release.aggregate_validation.exclude_members
|
|
834
|
-
# share the family
|
|
833
|
+
# independently releasable (release.aggregate_validation.exclude_members
|
|
834
|
+
# or release.version_series.independent_members) share the family
|
|
835
|
+
# version line, so:
|
|
835
836
|
# 1. when any of them needs a version bump, all of them do — including
|
|
836
837
|
# members without unreleased changelog entries of their own and
|
|
837
838
|
# brand-new members that have never been released (empty
|
|
@@ -839,12 +840,13 @@ module Kettle
|
|
|
839
840
|
# 2. a member whose checkout version sits behind the cohort's version
|
|
840
841
|
# line (e.g. a cohort that already bumped while it stayed put) needs
|
|
841
842
|
# a catch-up bump on its own.
|
|
842
|
-
# Independently
|
|
843
|
-
# bump only when their own unreleased entries require it
|
|
844
|
-
#
|
|
845
|
-
#
|
|
843
|
+
# Independently versioned members keep per-member bump semantics: they
|
|
844
|
+
# bump only when their own unreleased entries require it, and their
|
|
845
|
+
# (possibly higher) own-series version never drags the cohort line.
|
|
846
|
+
# Members whose checkout version already moved past their last release
|
|
847
|
+
# are mid-prep and are never double-bumped.
|
|
846
848
|
def normalize_lockstep_version_bump(results)
|
|
847
|
-
excluded =
|
|
849
|
+
excluded = independent_bump_member_names
|
|
848
850
|
return results if excluded.empty?
|
|
849
851
|
|
|
850
852
|
aggregate = results.select do |result|
|
|
@@ -894,6 +896,17 @@ module Kettle
|
|
|
894
896
|
nil
|
|
895
897
|
end
|
|
896
898
|
|
|
899
|
+
# Members exempt from lockstep bump semantics: aggregate-validation
|
|
900
|
+
# exclusions plus independently versioned members. Both keep per-member
|
|
901
|
+
# bump behavior; neither may be force-marked pending by the cohort, and
|
|
902
|
+
# an independent-series version never participates in the cohort line.
|
|
903
|
+
def independent_bump_member_names
|
|
904
|
+
Array(config&.release_aggregate_validation_excluded_members)
|
|
905
|
+
.concat(Array(config&.release_version_series_independent_members))
|
|
906
|
+
.map(&:to_s)
|
|
907
|
+
.uniq
|
|
908
|
+
end
|
|
909
|
+
|
|
897
910
|
def normalize_shared_version_bump(results)
|
|
898
911
|
shared_results = results.select { |result| result.ok? && shared_changelog_member_result?(result) }
|
|
899
912
|
return results unless shared_results.any? { |result| result.state["bump_release_pending"] == true }
|
|
@@ -919,7 +932,11 @@ module Kettle
|
|
|
919
932
|
|
|
920
933
|
def shared_changelog_member_result?(result)
|
|
921
934
|
member = members.find { |candidate| candidate.name == result.member_name }
|
|
922
|
-
member && !member_local_changelog?(member)
|
|
935
|
+
member && !member_local_changelog?(member) && !independent_version_series_member?(member)
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def independent_version_series_member?(member)
|
|
939
|
+
config&.independent_version_series_member?(member) || false
|
|
923
940
|
end
|
|
924
941
|
|
|
925
942
|
def git_root
|
|
@@ -5,7 +5,7 @@ module Kettle
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = "1.3.
|
|
8
|
+
VERSION = "1.3.38"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
|
@@ -108,6 +108,16 @@ module Kettle
|
|
|
108
108
|
REGISTRY_WAIT_ATTEMPTS = 15
|
|
109
109
|
REGISTRY_WAIT_INTERVAL_SECONDS = 15
|
|
110
110
|
TEMPLATE_GEMSPEC_RETRY_DELAY_SECONDS = 0.25
|
|
111
|
+
# Placeholder substituted for secret-valued env keys in generated resume
|
|
112
|
+
# commands (see aggregate_github_release_resume_command).
|
|
113
|
+
RESUME_COMMAND_REDACTED_VALUE = "<redacted-reauthorize>"
|
|
114
|
+
# Env key names whose values must never appear in printed commands.
|
|
115
|
+
# Matches by name shape, not a fixed list, so future secrets plumbing
|
|
116
|
+
# (new passphrase/OTP/token transports) is scrubbed by default. The
|
|
117
|
+
# excluded suffixes are routing metadata an operator needs to
|
|
118
|
+
# re-authorize (source/provider/broker/field NAME), never a secret.
|
|
119
|
+
RESUME_COMMAND_SECRET_ENV_KEY_RE = /PASSPHRASE|PASSWORD|SECRET|TOKEN|OTP|CREDENTIAL/i
|
|
120
|
+
RESUME_COMMAND_SECRET_ENV_KEY_EXCLUDE_RE = /(?:_SOURCE|_PROVIDER|_BROKER|_FIELD)\z/i
|
|
111
121
|
|
|
112
122
|
def initialize(command:, config:, members:, family_members: nil, execute: false, accept: true, commit: true, allow_dirty: false, autostash: true, template_cleanup: true, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, fast_recovery: nil, fast_recovery_members: nil, skip_ci: false, skip_changelog: false, skip_appraisals: false, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, validate_ci_bundles: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, gha_sha_pins_ttl_days: nil, env_overrides: {}, debug: false, verbose: false, gem_signing_password: nil, secrets_provider: nil, jobs: nil, progress_io: nil, reset_target: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil, template_branch_selection: {}, release_state_results: nil, event_tape: nil, mise_adapter: nil, **options)
|
|
113
123
|
@command = command
|
|
@@ -1119,7 +1129,15 @@ module Kettle
|
|
|
1119
1129
|
end
|
|
1120
1130
|
|
|
1121
1131
|
def monorepo_template_worktrees?(wave)
|
|
1122
|
-
monorepo_template_worktrees_enabled? && wave.length > 1
|
|
1132
|
+
(monorepo_template_worktrees_enabled? && wave.length > 1) || monorepo_nested_template_worktree?(wave)
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
def monorepo_nested_template_worktree?(wave)
|
|
1136
|
+
return false unless execute && monorepo_template? && wave.length == 1
|
|
1137
|
+
|
|
1138
|
+
member = wave.first
|
|
1139
|
+
nested = Paths.within?(member.root, config.root) && !Paths.same?(member.root, config.root)
|
|
1140
|
+
nested && !config.template_root_outputs.empty? && kettle_jem_template_command?(config.template_command || DEFAULT_COMMANDS.fetch("template"))
|
|
1123
1141
|
end
|
|
1124
1142
|
|
|
1125
1143
|
def monorepo_template_worktrees_enabled?
|
|
@@ -1197,7 +1215,8 @@ module Kettle
|
|
|
1197
1215
|
progress: progress,
|
|
1198
1216
|
progress_started: true,
|
|
1199
1217
|
commit_changes: false,
|
|
1200
|
-
wave_jobs: wave_jobs
|
|
1218
|
+
wave_jobs: wave_jobs,
|
|
1219
|
+
template_worktree_root: monorepo_template_allowed_paths(entry.fetch(:original_member)).empty? ? nil : entry.fetch(:worktree_root)
|
|
1201
1220
|
)
|
|
1202
1221
|
mutex.synchronize do
|
|
1203
1222
|
ordered_results[index] = member_results
|
|
@@ -1224,17 +1243,18 @@ module Kettle
|
|
|
1224
1243
|
paths = monorepo_template_worktree_changed_paths(worktree_root)
|
|
1225
1244
|
return template_worktree_failure_result(original_member, paths, phase: "template_worktree_materialize") if paths.is_a?(String)
|
|
1226
1245
|
|
|
1227
|
-
|
|
1246
|
+
allowed_paths = monorepo_template_allowed_paths(original_member)
|
|
1247
|
+
invalid_paths = paths.reject { |path| monorepo_template_path_allowed?(path, relative_root, allowed_paths) }
|
|
1228
1248
|
unless invalid_paths.empty?
|
|
1229
1249
|
return template_worktree_failure_result(
|
|
1230
1250
|
original_member,
|
|
1231
|
-
"isolated template worker modified shared path(s): #{invalid_paths.sort.join(", ")}; only #{relative_root}
|
|
1251
|
+
"isolated template worker modified shared path(s): #{invalid_paths.sort.join(", ")}; only #{([relative_root] + allowed_paths).join(", ")} may be materialized",
|
|
1232
1252
|
phase: "template_worktree_materialize"
|
|
1233
1253
|
)
|
|
1234
1254
|
end
|
|
1235
1255
|
|
|
1236
|
-
initial_state = entry.fetch(:primary_member_state)
|
|
1237
|
-
current_state =
|
|
1256
|
+
initial_state = entry[:primary_allowed_state] || entry.fetch(:primary_member_state)
|
|
1257
|
+
current_state = monorepo_template_allowed_state(config.root, relative_root, allowed_paths)
|
|
1238
1258
|
changed_primary_paths = changed_monorepo_template_state_paths(initial_state, current_state)
|
|
1239
1259
|
unless changed_primary_paths.empty?
|
|
1240
1260
|
return template_worktree_failure_result(
|
|
@@ -1261,19 +1281,40 @@ module Kettle
|
|
|
1261
1281
|
changed_paths = monorepo_template_worktree_changed_paths(config.root)
|
|
1262
1282
|
raise Error, changed_paths if changed_paths.is_a?(String)
|
|
1263
1283
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
end
|
|
1284
|
+
allowed_paths = monorepo_template_allowed_paths(original_member)
|
|
1285
|
+
member_changed_paths = changed_paths.select { |path| monorepo_template_path_allowed?(path, relative_root, allowed_paths) }
|
|
1267
1286
|
sync_monorepo_template_paths(config.root, entry.fetch(:worktree_root), member_changed_paths)
|
|
1268
|
-
primary_state =
|
|
1269
|
-
worktree_state =
|
|
1287
|
+
primary_state = monorepo_template_allowed_state(config.root, relative_root, allowed_paths)
|
|
1288
|
+
worktree_state = monorepo_template_allowed_state(entry.fetch(:worktree_root), relative_root, allowed_paths)
|
|
1270
1289
|
unless primary_state == worktree_state
|
|
1271
1290
|
changed = changed_monorepo_template_state_paths(primary_state, worktree_state)
|
|
1272
|
-
raise Error, "isolated template worker seed differs from primary
|
|
1291
|
+
raise Error, "isolated template worker seed differs from primary allowed paths: #{changed.join(", ")}"
|
|
1273
1292
|
end
|
|
1274
1293
|
|
|
1275
1294
|
entry[:primary_changed_paths] = member_changed_paths
|
|
1276
|
-
entry[:
|
|
1295
|
+
entry[:primary_allowed_state] = primary_state
|
|
1296
|
+
entry[:primary_member_state] = primary_state if allowed_paths.empty?
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
def monorepo_template_allowed_paths(member)
|
|
1300
|
+
return [] unless Paths.within?(member.root, config.root) && !Paths.same?(member.root, config.root)
|
|
1301
|
+
|
|
1302
|
+
config.template_root_outputs
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1305
|
+
def monorepo_template_path_allowed?(path, relative_root, allowed_paths)
|
|
1306
|
+
path == relative_root || path.start_with?("#{relative_root}/") || allowed_paths.include?(path)
|
|
1307
|
+
end
|
|
1308
|
+
|
|
1309
|
+
def monorepo_template_allowed_state(root, relative_root, allowed_paths)
|
|
1310
|
+
member_state = monorepo_template_member_state(root, relative_root)
|
|
1311
|
+
shared_state = allowed_paths.each_with_object({}) do |allowed, state|
|
|
1312
|
+
paths = git_worktree_paths(root, ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", allowed])
|
|
1313
|
+
raise Error, paths if paths.is_a?(String)
|
|
1314
|
+
|
|
1315
|
+
paths.each { |path| state[path] = monorepo_template_path_signature(root, path) }
|
|
1316
|
+
end
|
|
1317
|
+
member_state.merge(shared_state)
|
|
1277
1318
|
end
|
|
1278
1319
|
|
|
1279
1320
|
def monorepo_template_relative_root(member)
|
|
@@ -1546,7 +1587,7 @@ module Kettle
|
|
|
1546
1587
|
)
|
|
1547
1588
|
end
|
|
1548
1589
|
|
|
1549
|
-
def template_results_for_member(member, progress: nil, progress_key: nil, progress_label: nil, progress_started: false, commit_changes: true, wave_jobs: 1)
|
|
1590
|
+
def template_results_for_member(member, progress: nil, progress_key: nil, progress_label: nil, progress_started: false, commit_changes: true, wave_jobs: 1, template_worktree_root: nil)
|
|
1550
1591
|
progress_key ||= member.name
|
|
1551
1592
|
progress_label ||= member.name
|
|
1552
1593
|
start_template_member_progress(progress, member, progress_key: progress_key, progress_label: progress_label) unless progress_started
|
|
@@ -1573,6 +1614,7 @@ module Kettle
|
|
|
1573
1614
|
phase: command,
|
|
1574
1615
|
command: workflow_command(member),
|
|
1575
1616
|
env: template_command_env(wave_jobs: wave_jobs, member: member),
|
|
1617
|
+
chdir: template_worktree_root || member.root,
|
|
1576
1618
|
stdout_line_handler: template_event_line_handler(member, progress: progress, progress_key: progress_key, progress_label: progress_label)
|
|
1577
1619
|
)
|
|
1578
1620
|
emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label)
|
|
@@ -2024,12 +2066,13 @@ module Kettle
|
|
|
2024
2066
|
def release_preflight_phases
|
|
2025
2067
|
maintenance_phase = {label: "pre-release maintenance", method: :release_preflight_maintenance_results} unless config.release_preflight_commands.empty?
|
|
2026
2068
|
bump_phase = {label: "release version bump", method: :release_preflight_version_bump_results} unless release_state_results.empty?
|
|
2069
|
+
drift_phase = {label: "bootstrap dependency drift", method: :release_preflight_bootstrap_drift_results} unless config.release_drift_check_gems.empty?
|
|
2027
2070
|
dirty_phase = {label: "branch checkout readiness", method: :release_preflight_branch_checkout_dirty_results}
|
|
2028
2071
|
signing_phase = {label: "gem signing password", method: :release_preflight_signing_password_results}
|
|
2029
2072
|
secrets_phase = release_secrets_authorization_required? ? {label: "secrets provider authorization", method: :release_preflight_secrets_authorization_results} : nil
|
|
2030
2073
|
gha_sha_pins_phase = release_gha_sha_pins_review_required? ? {label: "GitHub Actions SHA pins", method: :release_preflight_gha_sha_pins_results} : nil
|
|
2031
2074
|
|
|
2032
|
-
[maintenance_phase, bump_phase, secrets_phase, gha_sha_pins_phase, dirty_phase, signing_phase].compact
|
|
2075
|
+
[maintenance_phase, bump_phase, drift_phase, secrets_phase, gha_sha_pins_phase, dirty_phase, signing_phase].compact
|
|
2033
2076
|
end
|
|
2034
2077
|
|
|
2035
2078
|
# Family-declared pre-release maintenance hooks (release.preflight_commands
|
|
@@ -2075,6 +2118,108 @@ module Kettle
|
|
|
2075
2118
|
results
|
|
2076
2119
|
end
|
|
2077
2120
|
|
|
2121
|
+
# Bootstrap-managed gems (config release.drift_check.gems, default:
|
|
2122
|
+
# nomono) are activated by a member's own Gemfile bootstrap, while CI
|
|
2123
|
+
# resolves fresh bundles from the registry. When a newer published
|
|
2124
|
+
# version satisfies the member's declared floor but the committed lock
|
|
2125
|
+
# pins an older one, CI activates the newer version while the member
|
|
2126
|
+
# bootstrap activates the locked older one, and the wave dies mid-job
|
|
2127
|
+
# with a Gem::LoadError deep in a cold-start (observed 2026-10-30:
|
|
2128
|
+
# nomono locked 1.1.16 vs published 1.1.17 satisfying a ">= 1.1.16"
|
|
2129
|
+
# floor, run 40998206316). Nothing compared locked vs
|
|
2130
|
+
# published-satisfying versions pre-release, so this phase blocks early
|
|
2131
|
+
# with the remediation instead of letting CI discover the drift.
|
|
2132
|
+
def release_preflight_bootstrap_drift_results
|
|
2133
|
+
config.release_drift_check_gems.flat_map do |gem|
|
|
2134
|
+
members.filter_map { |member| bootstrap_dependency_drift_result(member, gem) }
|
|
2135
|
+
end
|
|
2136
|
+
end
|
|
2137
|
+
|
|
2138
|
+
def bootstrap_dependency_drift_result(member, gem)
|
|
2139
|
+
locked = locked_bootstrap_gem_version(member, gem)
|
|
2140
|
+
return nil if locked.nil?
|
|
2141
|
+
|
|
2142
|
+
requirement = declared_bootstrap_gem_requirement(member, gem)
|
|
2143
|
+
newest = newest_published_satisfying_version(gem, requirement)
|
|
2144
|
+
if newest.nil?
|
|
2145
|
+
# A registry outage must never become a false release block: treat
|
|
2146
|
+
# "could not consult the registry" as a skip with a diagnostic.
|
|
2147
|
+
warn "[kettle-family] bootstrap dependency drift check skipped for #{gem}: registry could not be consulted"
|
|
2148
|
+
return nil
|
|
2149
|
+
end
|
|
2150
|
+
return nil if locked >= newest
|
|
2151
|
+
|
|
2152
|
+
release_preflight_error_result(
|
|
2153
|
+
"bootstrap_dependency_drift",
|
|
2154
|
+
"Release blocked: bootstrap dependency drift: #{member.name} locks #{gem} #{locked} but #{newest} satisfies its floor; " \
|
|
2155
|
+
"run the family template sweep or `bundle update #{gem}` in the member before releasing.",
|
|
2156
|
+
member: member
|
|
2157
|
+
)
|
|
2158
|
+
end
|
|
2159
|
+
|
|
2160
|
+
# The version a member's committed lock pins for the gem, from the
|
|
2161
|
+
# registry-sourced GEM section only: a PATH or GIT pin is local-only
|
|
2162
|
+
# state that has no published counterpart to drift against.
|
|
2163
|
+
def locked_bootstrap_gem_version(member, gem)
|
|
2164
|
+
lockfile_path = File.join(member.root, "Gemfile.lock")
|
|
2165
|
+
return nil unless File.file?(lockfile_path)
|
|
2166
|
+
|
|
2167
|
+
section = nil
|
|
2168
|
+
File.foreach(lockfile_path) do |line|
|
|
2169
|
+
section = line.strip unless line.match?(/\A\s/)
|
|
2170
|
+
next unless section == "GEM"
|
|
2171
|
+
|
|
2172
|
+
match = line.match(/\A {4}#{Regexp.escape(gem)} \(([^)]+)\)\s*\z/)
|
|
2173
|
+
next unless match
|
|
2174
|
+
|
|
2175
|
+
begin
|
|
2176
|
+
return Gem::Version.new(match[1])
|
|
2177
|
+
rescue ArgumentError
|
|
2178
|
+
return nil
|
|
2179
|
+
end
|
|
2180
|
+
end
|
|
2181
|
+
nil
|
|
2182
|
+
end
|
|
2183
|
+
|
|
2184
|
+
# The requirement the member's Gemfile declared for the gem, read from
|
|
2185
|
+
# the lock's DEPENDENCIES section (Bundler records the DSL requirement
|
|
2186
|
+
# there verbatim). A bare declaration means "any version".
|
|
2187
|
+
def declared_bootstrap_gem_requirement(member, gem)
|
|
2188
|
+
lockfile_path = File.join(member.root, "Gemfile.lock")
|
|
2189
|
+
return Gem::Requirement.default unless File.file?(lockfile_path)
|
|
2190
|
+
|
|
2191
|
+
section = nil
|
|
2192
|
+
File.foreach(lockfile_path) do |line|
|
|
2193
|
+
section = line.strip unless line.match?(/\A\s/)
|
|
2194
|
+
next unless section == "DEPENDENCIES"
|
|
2195
|
+
|
|
2196
|
+
match = line.match(/\A {2}#{Regexp.escape(gem)}(?: \((.+)\))?\s*\z/)
|
|
2197
|
+
next unless match
|
|
2198
|
+
|
|
2199
|
+
constraints = (match[1] || "").split(",").map(&:strip).reject(&:empty?)
|
|
2200
|
+
return Gem::Requirement.new(constraints) unless constraints.empty?
|
|
2201
|
+
|
|
2202
|
+
return Gem::Requirement.default
|
|
2203
|
+
end
|
|
2204
|
+
Gem::Requirement.default
|
|
2205
|
+
end
|
|
2206
|
+
|
|
2207
|
+
# Newest published (non-prerelease) version satisfying the requirement,
|
|
2208
|
+
# via the ETag-cached registry lookup shared with the rest of the
|
|
2209
|
+
# toolchain; nil when the registry could not be consulted.
|
|
2210
|
+
def newest_published_satisfying_version(gem, requirement)
|
|
2211
|
+
numbers = Kettle::Dev::RubyGemsVersions.published_version_numbers(gem)
|
|
2212
|
+
return nil if numbers.nil?
|
|
2213
|
+
|
|
2214
|
+
numbers.filter_map do |number|
|
|
2215
|
+
Gem::Version.new(number)
|
|
2216
|
+
rescue ArgumentError
|
|
2217
|
+
nil
|
|
2218
|
+
end.select do |version|
|
|
2219
|
+
!version.prerelease? && requirement.satisfied_by?(version)
|
|
2220
|
+
end.max
|
|
2221
|
+
end
|
|
2222
|
+
|
|
2078
2223
|
def release_gha_sha_pins_review_required?
|
|
2079
2224
|
execute && kettle_release_command?(raw_release_command) && !release_gha_sha_pins_offline_requested?
|
|
2080
2225
|
end
|
|
@@ -2119,11 +2264,14 @@ module Kettle
|
|
|
2119
2264
|
@secrets_provider.is_a?(Secrets::OnePassword)
|
|
2120
2265
|
end
|
|
2121
2266
|
|
|
2122
|
-
def release_preflight_error_result(phase, message)
|
|
2267
|
+
def release_preflight_error_result(phase, message, member: nil)
|
|
2123
2268
|
# A preflight failure blocks the selected release invocation. Attribute
|
|
2124
2269
|
# it to that invocation's first member so generated resume commands do
|
|
2125
|
-
# not target the synthetic family name or an unselected sibling.
|
|
2126
|
-
member
|
|
2270
|
+
# not target the synthetic family name or an unselected sibling. Phases
|
|
2271
|
+
# that identify a specific offending member (e.g. bootstrap dependency
|
|
2272
|
+
# drift) override that attribution so the resume hint targets the
|
|
2273
|
+
# member that needs the fix.
|
|
2274
|
+
member ||= members.first || family_member
|
|
2127
2275
|
CommandResult.new(
|
|
2128
2276
|
member_name: member.name,
|
|
2129
2277
|
phase: phase,
|
|
@@ -3889,6 +4037,16 @@ module Kettle
|
|
|
3889
4037
|
aggregate_validation_members.include?(member)
|
|
3890
4038
|
end
|
|
3891
4039
|
|
|
4040
|
+
# An independently versioned member (release.version_series.
|
|
4041
|
+
# independent_members) keeps its own monotonic series: it is exempt
|
|
4042
|
+
# from lockstep raises and from the aggregate same-version invariant,
|
|
4043
|
+
# but remains part of the aggregate release for artifact attachment.
|
|
4044
|
+
def independent_version_series_member?(member)
|
|
4045
|
+
return false unless member
|
|
4046
|
+
|
|
4047
|
+
config.independent_version_series_member?(member)
|
|
4048
|
+
end
|
|
4049
|
+
|
|
3892
4050
|
def aggregate_validation_member_name?(member)
|
|
3893
4051
|
member && member.name == (@aggregate_validation_member_name || aggregate_validation_members.first&.name)
|
|
3894
4052
|
end
|
|
@@ -4109,12 +4267,19 @@ module Kettle
|
|
|
4109
4267
|
|
|
4110
4268
|
# simplecov:disable Covered by monorepo release integration, not sibling-repository suite.
|
|
4111
4269
|
def aggregate_monorepo_github_release(members)
|
|
4112
|
-
version
|
|
4270
|
+
# The same-version invariant describes the shared-version cohort.
|
|
4271
|
+
# Independently versioned members (release.version_series.
|
|
4272
|
+
# independent_members) can never satisfy it by design, so they are
|
|
4273
|
+
# exempt from the uniformity check while their artifacts still
|
|
4274
|
+
# attach — at their own series version, not the cohort's.
|
|
4275
|
+
shared_members = members.reject { |member| independent_version_series_member?(member) }
|
|
4276
|
+
version = shared_members.map { |member| member.version.to_s }.uniq
|
|
4113
4277
|
return aggregate_release_result("all selected members must have the same version") unless version.length == 1
|
|
4114
4278
|
|
|
4115
4279
|
assets = members.flat_map do |member|
|
|
4116
|
-
|
|
4117
|
-
|
|
4280
|
+
asset_version = independent_version_series_member?(member) ? member.version.to_s : version.first
|
|
4281
|
+
gem_path = Paths.glob(member.root, "pkg", "*.gem").select { |path| File.basename(path).end_with?("-#{asset_version}.gem") }
|
|
4282
|
+
checksum_paths = %w[sha256 sha512].map { |algorithm| File.join(member.root, "checksums", "#{member.name}-#{asset_version}.gem.#{algorithm}") }
|
|
4118
4283
|
gem_path + checksum_paths.select { |path| File.file?(path) }
|
|
4119
4284
|
end
|
|
4120
4285
|
command = ["bundle", "exec", "kettle-gh-release", "--allow-unpublished", "--events", "--release-version", version.first]
|
|
@@ -4159,11 +4324,26 @@ module Kettle
|
|
|
4159
4324
|
|
|
4160
4325
|
def aggregate_github_release_resume_command(env, command)
|
|
4161
4326
|
assignments = env.sort_by { |key, _value| key }.filter_map do |key, value|
|
|
4162
|
-
|
|
4327
|
+
next if value.nil?
|
|
4328
|
+
|
|
4329
|
+
# Resume commands land in terminal output, run summaries, and
|
|
4330
|
+
# transcripts. Env built from the broker-cached secrets used to
|
|
4331
|
+
# embed KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE in plaintext here
|
|
4332
|
+
# (2026-10-30 incident). Redact secret-valued keys instead: the
|
|
4333
|
+
# printed command keeps its shape, and the operator re-authorizes
|
|
4334
|
+
# the secret through the release secrets provider (1Password, or
|
|
4335
|
+
# the KETTLE_RELEASE_SECRETS_BROKER cache) — or pastes the real
|
|
4336
|
+
# passphrase in place of the placeholder — before resuming.
|
|
4337
|
+
rendered_value = resume_command_secret_env_key?(key) ? Shellwords.escape(RESUME_COMMAND_REDACTED_VALUE) : Shellwords.escape(value.to_s)
|
|
4338
|
+
"#{key}=#{rendered_value}"
|
|
4163
4339
|
end
|
|
4164
4340
|
(["env", *assignments].join(" ") + " " + Shellwords.join(command)).strip
|
|
4165
4341
|
end
|
|
4166
4342
|
|
|
4343
|
+
def resume_command_secret_env_key?(key)
|
|
4344
|
+
key.match?(RESUME_COMMAND_SECRET_ENV_KEY_RE) && !key.match?(RESUME_COMMAND_SECRET_ENV_KEY_EXCLUDE_RE)
|
|
4345
|
+
end
|
|
4346
|
+
|
|
4167
4347
|
def aggregate_github_release_event_line_handler(member)
|
|
4168
4348
|
lambda do |line|
|
|
4169
4349
|
event = parse_template_event(line)
|
|
@@ -5896,14 +6076,17 @@ module Kettle
|
|
|
5896
6076
|
return unless memo.all?(&:ok?)
|
|
5897
6077
|
|
|
5898
6078
|
synchronize_template_commit do
|
|
6079
|
+
allowed_paths = monorepo_template_allowed_paths(member)
|
|
6080
|
+
paths = allowed_paths.empty? ? ["."] : [monorepo_template_relative_root(member)] + allowed_paths
|
|
6081
|
+
pathspec = paths.map { |path| Shellwords.escape(path) }.join(" ")
|
|
5899
6082
|
memo << runner.call(
|
|
5900
6083
|
member: member,
|
|
5901
6084
|
phase: "commit_template",
|
|
5902
6085
|
command: [
|
|
5903
6086
|
"sh",
|
|
5904
6087
|
"-lc",
|
|
5905
|
-
"if [ -n \"$(git status --porcelain --
|
|
5906
|
-
"git add -A --
|
|
6088
|
+
"if [ -n \"$(git status --porcelain -- #{pathspec})\" ]; then " \
|
|
6089
|
+
"git add -A -- #{pathspec} && git commit -m #{Shellwords.escape(template_commit_message(member))} -- #{pathspec}; " \
|
|
5907
6090
|
"fi"
|
|
5908
6091
|
],
|
|
5909
6092
|
env: workflow_env
|
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: 1.3.
|
|
4
|
+
version: 1.3.38
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -134,7 +134,7 @@ dependencies:
|
|
|
134
134
|
version: '1.0'
|
|
135
135
|
- - ">="
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: 1.0.
|
|
137
|
+
version: 1.0.16
|
|
138
138
|
type: :runtime
|
|
139
139
|
prerelease: false
|
|
140
140
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -144,7 +144,7 @@ dependencies:
|
|
|
144
144
|
version: '1.0'
|
|
145
145
|
- - ">="
|
|
146
146
|
- !ruby/object:Gem::Version
|
|
147
|
-
version: 1.0.
|
|
147
|
+
version: 1.0.16
|
|
148
148
|
- !ruby/object:Gem::Dependency
|
|
149
149
|
name: kettle-deps-floor
|
|
150
150
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -234,7 +234,7 @@ dependencies:
|
|
|
234
234
|
version: '0.1'
|
|
235
235
|
- - ">="
|
|
236
236
|
- !ruby/object:Gem::Version
|
|
237
|
-
version: 0.1.
|
|
237
|
+
version: 0.1.23
|
|
238
238
|
type: :runtime
|
|
239
239
|
prerelease: false
|
|
240
240
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -244,7 +244,7 @@ dependencies:
|
|
|
244
244
|
version: '0.1'
|
|
245
245
|
- - ">="
|
|
246
246
|
- !ruby/object:Gem::Version
|
|
247
|
-
version: 0.1.
|
|
247
|
+
version: 0.1.23
|
|
248
248
|
- !ruby/object:Gem::Dependency
|
|
249
249
|
name: kettle-test
|
|
250
250
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -254,7 +254,7 @@ dependencies:
|
|
|
254
254
|
version: '2.0'
|
|
255
255
|
- - ">="
|
|
256
256
|
- !ruby/object:Gem::Version
|
|
257
|
-
version: 2.0.
|
|
257
|
+
version: 2.0.29
|
|
258
258
|
type: :runtime
|
|
259
259
|
prerelease: false
|
|
260
260
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -264,7 +264,7 @@ dependencies:
|
|
|
264
264
|
version: '2.0'
|
|
265
265
|
- - ">="
|
|
266
266
|
- !ruby/object:Gem::Version
|
|
267
|
-
version: 2.0.
|
|
267
|
+
version: 2.0.29
|
|
268
268
|
- !ruby/object:Gem::Dependency
|
|
269
269
|
name: psych-merge
|
|
270
270
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -627,11 +627,11 @@ homepage: https://github.com/kettle-dev/kettle-family
|
|
|
627
627
|
licenses:
|
|
628
628
|
- AGPL-3.0-only
|
|
629
629
|
metadata:
|
|
630
|
-
homepage_uri: https://
|
|
631
|
-
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.
|
|
632
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.
|
|
630
|
+
homepage_uri: https://structuredmerge.org
|
|
631
|
+
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.38
|
|
632
|
+
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.38/CHANGELOG.md
|
|
633
633
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
|
|
634
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.
|
|
634
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.38
|
|
635
635
|
funding_uri: https://github.com/sponsors/pboling
|
|
636
636
|
wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
|
|
637
637
|
news_uri: https://www.railsbling.com/tags/kettle-family
|
|
@@ -662,7 +662,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
662
662
|
- !ruby/object:Gem::Version
|
|
663
663
|
version: '0'
|
|
664
664
|
requirements: []
|
|
665
|
-
rubygems_version: 4.0.
|
|
665
|
+
rubygems_version: 4.0.24
|
|
666
666
|
specification_version: 4
|
|
667
667
|
summary: "\U0001F469\U0001F469\U0001F467\U0001F467 CLI helpers for managing related
|
|
668
668
|
Ruby gems as a family."
|
metadata.gz.sig
CHANGED
|
Binary file
|