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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90c20dc69493246554befc819ed36f9e5c1bc81ef54f44257c7c8e2347bbe457
4
- data.tar.gz: 410f4b020a832c51c372ab3230b859c8ec7b1e8829c26c595582a22f27d6901d
3
+ metadata.gz: 6f6f4ceee082895db3eb6f2b0c90735263adbcf3e8693640edd5d7b929a01244
4
+ data.tar.gz: 9a1c2c5c05bea72ade7f755329bec2e361aac45b171b92bdb76fa6812209a632
5
5
  SHA512:
6
- metadata.gz: 9268ee8a2ee002ca7394d0c67c55a579f2eeb2621bf2b09362b3ca8f776ff02ab98a39923b343b5c805f7c42276ed25fe34e35efc2b605cb282ae704140ad45b
7
- data.tar.gz: bf88cb60646c3adbe20b397ea8e4e2e6829435b078ad9841e75623090233c5575682dc1f8695192b92706b34fa9635e07de1e0abd2f8d833d2ec1447bd1511d1
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.36...HEAD
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
- [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
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.163-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
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 -->
@@ -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.reject { |member| config.member_local_changelog?(member) }.map(&:name)
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
- return current.to_s if versions.length > 1
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: member.root) do |log_io|
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: member.root,
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: member.root, process_options: spawn_options, stdout_line_handler: stdout_line_handler, log_io: log_io)
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: member.root, **spawn_options)
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: member.root,
270
+ workdir: chdir,
271
271
  status: status.exitstatus,
272
272
  success: status.success?,
273
273
  stdout: stdout,
@@ -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 version line, so:
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 releasable members keep per-member bump semantics: they
843
- # bump only when their own unreleased entries require it. Members whose
844
- # checkout version already moved past their last release are mid-prep
845
- # and are never double-bumped.
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 = Array(config&.release_aggregate_validation_excluded_members).map(&:to_s)
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.36"
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
- invalid_paths = paths.reject { |path| path == relative_root || path.start_with?("#{relative_root}/") }
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}/ may be materialized",
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 = monorepo_template_member_state(config.root, relative_root)
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
- member_changed_paths = changed_paths.select do |path|
1265
- path == relative_root || path.start_with?("#{relative_root}/")
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 = monorepo_template_member_state(config.root, relative_root)
1269
- worktree_state = monorepo_template_member_state(entry.fetch(:worktree_root), relative_root)
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 member: #{changed.join(", ")}"
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[:primary_member_state] = primary_state
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 = members.first || family_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 = members.map { |member| member.version.to_s }.uniq
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
- gem_path = Paths.glob(member.root, "pkg", "*.gem").select { |path| File.basename(path).end_with?("-#{version.first}.gem") }
4117
- checksum_paths = %w[sha256 sha512].map { |algorithm| File.join(member.root, "checksums", "#{member.name}-#{version.first}.gem.#{algorithm}") }
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
- "#{key}=#{Shellwords.escape(value.to_s)}" unless value.nil?
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 -- .)\" ]; then " \
5906
- "git add -A -- . && git commit -m #{Shellwords.escape(template_commit_message(member))}; " \
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.36
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.15
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.15
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.22
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.22
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.28
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.28
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://kettle-family.galtzo.com
631
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.36
632
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.36/CHANGELOG.md
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.36
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.20
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