kettle-family 1.2.21 → 1.2.23

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: 351c882553083a30e5c52c10fb650195f5e00d5a77e452e19058da6e32c69c94
4
- data.tar.gz: c3cf82b1ec51d6f9d043e579e3de5d2b09fd16f0d187d71b3abdfa9626db4576
3
+ metadata.gz: c7444a3d6ecfad0866aae1d481043a9daebb360a185d628ffff7250dc46d8cf4
4
+ data.tar.gz: dbfa71d55c722830254b219d60b98e330918fc853e5d76f026c90e1b6929d971
5
5
  SHA512:
6
- metadata.gz: 0b2ff66410811aa28af2f578fd354cb9f9d7174a648f3863c7ad68e3e3098b129cb4a2bc9678a24a0fe8d478bba7a81bf61bae35013c48fae124ed0e113c56a2
7
- data.tar.gz: e25a8ee6c06843662c0e2da1d9fa68d15b1aa6af30c5f5bbcdafe1690c0912e0d04e7f1b8bc1e814d4e5fef79850f1f4c430a69191d647679b393fa001547632
6
+ metadata.gz: a10e4a4db9fe4bc4769c45757a93767286b19de7657b09b4e6cac32877510f5a46342c9a566dd35776d3ed26ac91e4b7c688deaf646107e8d162873331ed64de
7
+ data.tar.gz: dbb1cab4664586a13210038ac5e48a8fb8be2b7e5ec6022cc516f40f58da2c9587c7f3d884b6acc31c7afd89fc01b4b1e9d15b8296bff2986e037b9d5474f6c7
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,80 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.23] - 2026-08-02
34
+
35
+ - TAG: [v1.2.23][1.2.23t]
36
+ - COVERAGE: 93.91% -- 4658/4960 lines in 31 files
37
+ - BRANCH COVERAGE: 76.23% -- 1822/2390 branches in 31 files
38
+ - 29.31% documented
39
+
40
+ ### Changed
41
+
42
+ - Budget kettle-jem template workers across concurrent family members.
43
+
44
+ ### Fixed
45
+
46
+ - Remove trailing blank lines when retiring legacy appraisal source declarations during templating.
47
+
48
+ - Allow direct --only member selections to proceed when unrelated member Gemfiles are temporarily unparsable.
49
+
50
+ - kettle-jem-template-20260802-001 - Devcontainer JSON files now merge as JSONC,
51
+ preserving comments and trailing commas during template updates.
52
+
53
+ - Preserve the completed generated kettle-jem lockfile when restoring an autostash conflicts only on that file.
54
+
55
+ - Mise-managed template members now retain PATH when launching nested Bundler commands.
56
+
57
+ - Local kettle-jem template commands now bypass login-shell PATH resets.
58
+
59
+ - Nested Bundler commands now retain the active Ruby executable paths during autostashed templating.
60
+
61
+ - Autostashed mise configuration remains available while templating runs.
62
+
63
+ ## [1.2.22] - 2026-08-01
64
+
65
+ - TAG: [v1.2.22][1.2.22t]
66
+ - COVERAGE: 93.87% -- 4626/4928 lines in 31 files
67
+ - BRANCH COVERAGE: 76.18% -- 1807/2372 branches in 31 files
68
+ - 29.31% documented
69
+
70
+ ### Added
71
+
72
+ - Template runs can now use --autostash to preserve dirty worktrees while synchronizing and templating.
73
+
74
+ ### Changed
75
+
76
+ - Template runs now pull configured upstream branches before planning; branch-target runs synchronize each checked-out branch.
77
+
78
+ - Template autostash is now enabled by default; use --no-autostash to fail on dirty worktrees.
79
+
80
+ - Clarify that replayed template changelog entries are destination release work and affect release state.
81
+
82
+ - Release state now reports filter-aware transfer changelog lag as missing versus applicable entries.
83
+
84
+ - kettle-jem-template-20260801-001 - Generated README gem dashboard links now
85
+ use ClickGems instead of BestGems.
86
+
87
+ ### Fixed
88
+
89
+ - Evaluate release-state GitHub release and commit counts against each configured target branch.
90
+
91
+ - kettle-jem-template-20260801-002 - Generated RSpec helpers now normalize
92
+ managed configuration block bindings structurally, preventing mixed block
93
+ parameter names from producing invalid configuration after a merge.
94
+ - kettle-jem-template-20260801-003 - Generated project metadata and
95
+ documentation now normalize configured underscore hostnames to valid
96
+ hyphenated hostnames.
97
+ - kettle-jem-template-20260801-004 - Generated organization README logos now
98
+ use GitHub's stable organization avatar endpoint instead of assuming a
99
+ matching Galtzo-hosted asset exists.
100
+
101
+ - Template autostash now recovers lockfile-only restore conflicts by keeping the normalized lockfile.
102
+
103
+ - Allow family configuration to define ordered single-member release waves.
104
+
105
+ - Remove retired pboling/appraisal declarations before templating legacy members.
106
+
33
107
  ## [1.2.21] - 2026-08-01
34
108
 
35
109
  - TAG: [v1.2.21][1.2.21t]
@@ -396,10 +470,6 @@ Please file a bug if you notice a violation of semantic versioning.
396
470
 
397
471
  ### Added
398
472
 
399
- - kettle-jem-template-20260729-005 - Gemspec metadata now publishes this
400
- project's RubyForum tag as `mailing_list_uri`, and support docs link to the
401
- tagged RubyForum community alongside Discord.
402
-
403
473
  ### Fixed
404
474
 
405
475
  - Family release changelog phases now pass `--yes` to configured
@@ -407,25 +477,16 @@ Please file a bug if you notice a violation of semantic versioning.
407
477
  behavior and keeping publish runs non-interactive unless `--no-accept` is
408
478
  used.
409
479
 
410
- - kettle-jem-template-20260728-003 - Generated dep-heads workflows now run
411
- TruffleRuby jobs with current RubyGems and Bundler, avoiding setup failures
412
- before the test suite starts.
413
480
  - kettle-jem-template-20260728-004 - Generated dep-heads workflows now use the
414
481
  setup-ruby Bundler install path for direct appraisal Gemfiles, avoiding rv
415
482
  lockfile parser failures on Git and path dependencies.
416
483
  - kettle-jem-template-20260728-005 - VersionGem bootstrap now creates the
417
484
  missing canonical version spec when a project only has shim namespace version
418
485
  specs.
419
- - kettle-jem-template-20260729-001 - Generated JRuby 9.4 workflows now use the
420
- legacy manual bundle install path, avoiding setup-time Bundler full-index
421
- failures against `gem.coop`.
422
486
  - kettle-jem-template-20260729-002 - VersionGem bootstrap now preserves
423
487
  and templates dedicated `version_gem.rb` entrypoints even when the gemspec
424
488
  dependency is intentionally omitted, and generated anonymous-loader specs
425
489
  cover both `version.rb` and `version_gem.rb`.
426
- - kettle-jem-template-20260729-003 - Old-Ruby gems below the VersionGem runtime
427
- floor now get managed minimal `version.rb` files and anonymous-loader version
428
- specs without adding `version_gem`.
429
490
 
430
491
  ## [1.2.3] - 2026-07-28
431
492
 
@@ -647,11 +708,6 @@ Please file a bug if you notice a violation of semantic versioning.
647
708
 
648
709
  ### Changed
649
710
 
650
- - kettle-jem-template-20260725-001 - Generated JRuby and TruffleRuby workflow
651
- files now run when pull request head branches start with `feature/release`,
652
- so release CI monitoring does not report intentionally skipped engine
653
- workflows as failures.
654
-
655
711
  ## [1.1.5] - 2026-07-25
656
712
 
657
713
  - TAG: [v1.1.5][1.1.5t]
@@ -835,8 +891,6 @@ Please file a bug if you notice a violation of semantic versioning.
835
891
 
836
892
  - `kettle-family release-state` now reports the checked-out branch and displays
837
893
  release distance as local/remote ahead and behind counts.
838
- - kettle-jem-template-20260720-001 - Generated READMEs can now render
839
- template-managed corporate sponsor logos from project or family config.
840
894
  - kettle-jem-template-20260720-002 - Generated development Gemfiles now use the
841
895
  released `tree_sitter_language_pack` gem 1.13.3 or newer by default.
842
896
  - kettle-jem-template-20260720-003 - Generated StructuredMerge Git diff driver
@@ -923,8 +977,6 @@ Please file a bug if you notice a violation of semantic versioning.
923
977
  the next `kettle-jem` release depends on unreleased `kettle-family` fixes.
924
978
  - Raised the runtime Ruby floor to 4.0.0.
925
979
 
926
- - kettle-jem-template-20260716-001 - Shim gemspec manifests now include
927
- `LICENSE.md` instead of nonexistent `LICENSE.txt`.
928
980
  - kettle-jem-template-20260716-002 - Generated gemspec manifests now ship fewer
929
981
  repository-only files by default to reduce downstream distro packaging churn.
930
982
 
@@ -1608,7 +1660,11 @@ Please file a bug if you notice a violation of semantic versioning.
1608
1660
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
1609
1661
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
1610
1662
 
1611
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.21...HEAD
1663
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.23...HEAD
1664
+ [1.2.23]: https://github.com/kettle-dev/kettle-family/compare/v1.2.22...v1.2.23
1665
+ [1.2.23t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.23
1666
+ [1.2.22]: https://github.com/kettle-dev/kettle-family/compare/v1.2.21...v1.2.22
1667
+ [1.2.22t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.22
1612
1668
  [1.2.21]: https://github.com/kettle-dev/kettle-family/compare/v1.2.20...v1.2.21
1613
1669
  [1.2.21t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.21
1614
1670
  [1.2.20]: https://github.com/kettle-dev/kettle-family/compare/v1.2.19...v1.2.20
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- <a href="https://github.com/kettle-dev"><img alt="kettle-dev Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/kettle-dev/avatar-128px.svg" width="14%" align="right"/></a>
1
+ <a href="https://github.com/kettle-dev"><img alt="kettle-dev Logo by GitHub" src="https://github.com/kettle-dev.png?size=192" width="14%" align="right"/></a>
2
2
 
3
3
  # 👩‍👩‍👧‍👧 Kettle::Family
4
4
 
5
- [![Version][👽versioni]][👽version] [![Ruby Users Forum][✉️ruby-forum-top-img]][✉️ruby-forum] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: AGPL-3.0-only][📄license-img]][📄license] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf]
5
+ [![Version][👽versioni]][👽version] [![Ruby Users Forum][✉️ruby-forum-top-img]][✉️ruby-forum] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: AGPL-3.0-only][📄license-img]][📄license] [![Total downloads][👽dl-ranki]][👽dl-rank] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf]
6
6
 
7
7
  `if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know on Discord][✉️discord-invite] or [RubyForum][✉️ruby-forum], as I may have missed the notification.
8
8
 
@@ -69,16 +69,16 @@ The _amazing_ test matrix is powered by the kettle-dev stack.
69
69
  <details markdown="1">
70
70
  <summary>How kettle-dev manages complexity in tests</summary>
71
71
 
72
- | Gem | Source | Role | Daily download rank |
72
+ | Gem | Source | Role | Total downloads |
73
73
  |-----|--------|------|---------------------|
74
- | [appraisal2](https://bestgems.org/gems/appraisal2) | [GitHub](https://github.com/appraisal-rb/appraisal2) | multi-dependency Appraisal matrix generation | [![Daily download rank for appraisal2](https://img.shields.io/gem/rd/appraisal2.svg?style=flat-square)](https://bestgems.org/gems/appraisal2) |
75
- | [appraisal2-rubocop](https://bestgems.org/gems/appraisal2-rubocop) | [GitHub](https://github.com/appraisal-rb/appraisal2-rubocop) | RuboCop Appraisal generator integration | [![Daily download rank for appraisal2-rubocop](https://img.shields.io/gem/rd/appraisal2-rubocop.svg?style=flat-square)](https://bestgems.org/gems/appraisal2-rubocop) |
76
- | [kettle-dev](https://bestgems.org/gems/kettle-dev) | [GitHub](https://github.com/kettle-dev/kettle-dev) | development, release, and CI workflow tooling | [![Daily download rank for kettle-dev](https://img.shields.io/gem/rd/kettle-dev.svg?style=flat-square)](https://bestgems.org/gems/kettle-dev) |
77
- | [kettle-jem](https://bestgems.org/gems/kettle-jem) | [GitHub](https://github.com/kettle-dev/kettle-jem) | Appraisals & CI workflow templates | [![Daily download rank for kettle-jem](https://img.shields.io/gem/rd/kettle-jem.svg?style=flat-square)](https://bestgems.org/gems/kettle-jem) |
78
- | [kettle-soup-cover](https://bestgems.org/gems/kettle-soup-cover) | [GitHub](https://github.com/kettle-dev/kettle-soup-cover) | SimpleCov coverage policy and reporting | [![Daily download rank for kettle-soup-cover](https://img.shields.io/gem/rd/kettle-soup-cover.svg?style=flat-square)](https://bestgems.org/gems/kettle-soup-cover) |
79
- | [kettle-test](https://bestgems.org/gems/kettle-test) | [GitHub](https://github.com/kettle-dev/kettle-test) | standard test runner and coverage harness | [![Daily download rank for kettle-test](https://img.shields.io/gem/rd/kettle-test.svg?style=flat-square)](https://bestgems.org/gems/kettle-test) |
80
- | [rubocop-lts](https://bestgems.org/gems/rubocop-lts) | [GitHub](https://github.com/rubocop-lts/rubocop-lts) | Ruby-version-aware linting | [![Daily download rank for rubocop-lts](https://img.shields.io/gem/rd/rubocop-lts.svg?style=flat-square)](https://bestgems.org/gems/rubocop-lts) |
81
- | [turbo_tests2](https://bestgems.org/gems/turbo_tests2) | [GitHub](https://github.com/galtzo-floss/turbo_tests2) | parallel test execution | [![Daily download rank for turbo_tests2](https://img.shields.io/gem/rd/turbo_tests2.svg?style=flat-square)](https://bestgems.org/gems/turbo_tests2) |
74
+ | [appraisal2](https://clickgems.clickhouse.com/dashboard/appraisal2) | [GitHub](https://github.com/appraisal-rb/appraisal2) | multi-dependency Appraisal matrix generation | [![Total downloads for appraisal2](https://img.shields.io/gem/dt/appraisal2.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/appraisal2) |
75
+ | [appraisal2-rubocop](https://clickgems.clickhouse.com/dashboard/appraisal2-rubocop) | [GitHub](https://github.com/appraisal-rb/appraisal2-rubocop) | RuboCop Appraisal generator integration | [![Total downloads for appraisal2-rubocop](https://img.shields.io/gem/dt/appraisal2-rubocop.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/appraisal2-rubocop) |
76
+ | [kettle-dev](https://clickgems.clickhouse.com/dashboard/kettle-dev) | [GitHub](https://github.com/kettle-dev/kettle-dev) | development, release, and CI workflow tooling | [![Total downloads for kettle-dev](https://img.shields.io/gem/dt/kettle-dev.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-dev) |
77
+ | [kettle-jem](https://clickgems.clickhouse.com/dashboard/kettle-jem) | [GitHub](https://github.com/kettle-dev/kettle-jem) | Appraisals & CI workflow templates | [![Total downloads for kettle-jem](https://img.shields.io/gem/dt/kettle-jem.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-jem) |
78
+ | [kettle-soup-cover](https://clickgems.clickhouse.com/dashboard/kettle-soup-cover) | [GitHub](https://github.com/kettle-dev/kettle-soup-cover) | SimpleCov coverage policy and reporting | [![Total downloads for kettle-soup-cover](https://img.shields.io/gem/dt/kettle-soup-cover.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-soup-cover) |
79
+ | [kettle-test](https://clickgems.clickhouse.com/dashboard/kettle-test) | [GitHub](https://github.com/kettle-dev/kettle-test) | standard test runner and coverage harness | [![Total downloads for kettle-test](https://img.shields.io/gem/dt/kettle-test.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-test) |
80
+ | [rubocop-lts](https://clickgems.clickhouse.com/dashboard/rubocop-lts) | [GitHub](https://github.com/rubocop-lts/rubocop-lts) | Ruby-version-aware linting | [![Total downloads for rubocop-lts](https://img.shields.io/gem/dt/rubocop-lts.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/rubocop-lts) |
81
+ | [turbo_tests2](https://clickgems.clickhouse.com/dashboard/turbo_tests2) | [GitHub](https://github.com/galtzo-floss/turbo_tests2) | parallel test execution | [![Total downloads for turbo_tests2](https://img.shields.io/gem/dt/turbo_tests2.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/turbo_tests2) |
82
82
 
83
83
  </details>
84
84
 
@@ -342,6 +342,15 @@ when it does not match `V.rel`. The `T(n)` header shows the total number of
342
342
  kettle-jem transfer changelog entries. Each row value in that column shows how
343
343
  many entries are after that member's stored replay cursor.
344
344
 
345
+ Transfer entries are destination release notes. `kettle-jem` may replay one
346
+ only when the template change affects a destination gem's users or developers,
347
+ such as behavior, configuration, compatibility, packaging, documentation, or a
348
+ developer workflow. They are deliberately placed in `Unreleased` and count as
349
+ pending release work. The stable `kettle-jem-template-YYYYMMDD-NNN` prefix is a
350
+ replay identifier only; it does not make an entry exempt from `unreleased`,
351
+ `pending`, or `bump`. `T(n)` reports replay progress separately and never
352
+ changes those release-state decisions.
353
+
345
354
  Use release-state tokens with `--only` to select gems by state instead of by
346
355
  name. Multiple tokens are combined with logical AND, so this selects only gems
347
356
  that are both unreleased and pending. Status tokens cannot be mixed with member
@@ -716,7 +725,7 @@ Thanks for RTFM. ☺️
716
725
  [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
717
726
  [⛳️gem-namespace]: https://github.com/kettle-dev/kettle-family
718
727
  [⛳️namespace-img]: https://img.shields.io/badge/namespace-Kettle::Family-3C2D2D.svg?style=square&logo=ruby&logoColor=white
719
- [⛳️gem-name]: https://bestgems.org/gems/kettle-family
728
+ [⛳️gem-name]: https://clickgems.clickhouse.com/dashboard/kettle-family
720
729
  [⛳️name-img]: https://img.shields.io/badge/name-kettle--family-3C2D2D.svg?style=square&logo=rubygems&logoColor=red
721
730
  [⛳️tag-img]: https://img.shields.io/github/tag/kettle-dev/kettle-family.svg
722
731
  [⛳️tag]: https://github.com/kettle-dev/kettle-family/releases
@@ -765,9 +774,9 @@ Thanks for RTFM. ☺️
765
774
  [📜gh-wiki]: https://github.com/kettle-dev/kettle-family/wiki
766
775
  [📜gl-wiki-img]: https://img.shields.io/badge/wiki-gitlab-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white
767
776
  [📜gh-wiki-img]: https://img.shields.io/badge/wiki-github-943CD2.svg?style=for-the-badge&logo=github&logoColor=white
768
- [👽dl-rank]: https://bestgems.org/gems/kettle-family
769
- [👽dl-ranki]: https://img.shields.io/gem/rd/kettle-family.svg
770
- [👽version]: https://bestgems.org/gems/kettle-family
777
+ [👽dl-rank]: https://clickgems.clickhouse.com/dashboard/kettle-family
778
+ [👽dl-ranki]: https://img.shields.io/gem/dt/kettle-family.svg
779
+ [👽version]: https://clickgems.clickhouse.com/dashboard/kettle-family
771
780
  [👽versioni]: https://img.shields.io/gem/v/kettle-family.svg
772
781
  [🏀qlty-mnt]: https://qlty.sh/gh/kettle-dev/projects/kettle-family
773
782
  [🏀qlty-mnti]: https://qlty.sh/gh/kettle-dev/projects/kettle-family/maintainability.svg
@@ -819,7 +828,7 @@ Thanks for RTFM. ☺️
819
828
  [📌gitmoji]: https://gitmoji.dev
820
829
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
821
830
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
822
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.741-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
831
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.960-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
823
832
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
824
833
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
825
834
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kettle/dev"
4
+
5
+ module Kettle
6
+ module Family
7
+ # Removes the retired pre-fork appraisal source before Bundler evaluates a
8
+ # legacy member. kettle-jem owns the current appraisal wiring.
9
+ class AppraisalBootstrap
10
+ LEGACY_GEM_NAME = "appraisal"
11
+ LEGACY_GITHUB = "pboling/appraisal"
12
+ LEGACY_BRANCH = "galtzo"
13
+
14
+ def initialize(mode: :dry_run)
15
+ @mode = mode
16
+ end
17
+
18
+ attr_reader :mode
19
+
20
+ def member_needs_bootstrap?(member)
21
+ edits_for(member).any?
22
+ end
23
+
24
+ def bootstrap_member(member)
25
+ edits = edits_for(member)
26
+ write_edits(edits)
27
+ CommandResult.new(
28
+ member.name,
29
+ "template_bootstrap_appraisal",
30
+ ["internal", "appraisal-bootstrap", LEGACY_GITHUB],
31
+ member.root,
32
+ 0,
33
+ true,
34
+ "removed retired #{LEGACY_GITHUB} appraisal dependency from #{edits.map { |edit| File.basename(edit.fetch(:path)) }.uniq.join(", ")}\n",
35
+ "",
36
+ 0.0,
37
+ mode != :execute,
38
+ (mode == :execute) ? nil : "dry run"
39
+ )
40
+ end
41
+
42
+ private
43
+
44
+ def write_edits(edits)
45
+ return if mode != :execute || edits.empty?
46
+
47
+ Kettle::Dev::VersionBump.write_edits(edits)
48
+ end
49
+
50
+ def edits_for(member)
51
+ [File.join(member.root, "Gemfile"), File.join(member.root, "Appraisal.root.gemfile")].filter_map do |path|
52
+ file_edits(path)
53
+ end.flatten
54
+ end
55
+
56
+ def file_edits(path)
57
+ return [] unless File.file?(path)
58
+
59
+ source = File.read(path)
60
+ parse_result = Kettle::Dev::VersionBump.parse_source(source, path)
61
+ Kettle::Dev::VersionBump.each_node(parse_result.value).filter_map do |node|
62
+ obsolete_appraisal_edit(path, source, node)
63
+ end
64
+ end
65
+
66
+ def obsolete_appraisal_edit(path, source, node)
67
+ return unless legacy_appraisal_call?(node)
68
+
69
+ line_start = source.rindex("\n", node.location.start_offset - 1).to_i + 1
70
+ line_end = source.index("\n", node.location.end_offset) || source.length
71
+ line_end += 1 if line_end < source.length
72
+ if final_standalone_call?(source, node, line_start, line_end)
73
+ # Remove the preceding separator and normalize EOF when the retired
74
+ # declaration is the final standalone line in the file.
75
+ line_start = source[0...line_start].rstrip.length
76
+ return Kettle::Dev::VersionBump.file_edit(path, source, line_start, source.length, "\n")
77
+ end
78
+
79
+ Kettle::Dev::VersionBump.file_edit(path, source, line_start, line_end, "")
80
+ end
81
+
82
+ def final_standalone_call?(source, node, line_start, line_end)
83
+ source[line_start...node.location.start_offset].to_s.strip.empty? &&
84
+ source[node.location.end_offset...line_end].to_s.strip.empty? &&
85
+ source[line_end..].to_s.strip.empty?
86
+ end
87
+
88
+ def legacy_appraisal_call?(node)
89
+ return false unless node.is_a?(Prism::CallNode) && node.name == :gem
90
+
91
+ arguments = node.arguments&.arguments || []
92
+ name_node = arguments.first
93
+ return false unless name_node.is_a?(Prism::StringNode) && name_node.unescaped == LEGACY_GEM_NAME
94
+
95
+ keyword_values(arguments).values_at("github", "branch") == [LEGACY_GITHUB, LEGACY_BRANCH]
96
+ end
97
+
98
+ def keyword_values(arguments)
99
+ arguments.grep(Prism::KeywordHashNode).flat_map(&:elements).each_with_object({}) do |association, values|
100
+ next unless association.is_a?(Prism::AssocNode)
101
+ next unless association.key.is_a?(Prism::SymbolNode) && association.value.is_a?(Prism::StringNode)
102
+
103
+ values[association.key.unescaped] = association.value.unescaped
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -59,6 +59,10 @@ module Kettle
59
59
  options[:commit] = false
60
60
  end
61
61
  base.option :allow_dirty, long: "--allow-dirty", desc: "Reserved for compatibility; member repos manage their own commit safety"
62
+ base.option :autostash, long: "--autostash", desc: "Use the default template autostash behavior"
63
+ base.option :no_autostash, long: "--no-autostash", desc: "Fail template runs when a member worktree is dirty" do
64
+ options[:autostash] = false
65
+ end
62
66
  end
63
67
  end
64
68
 
@@ -158,6 +162,7 @@ module Kettle
158
162
  push: false,
159
163
  commit: !options.key?(:commit) || options[:commit],
160
164
  allow_dirty: truthy_option?(:allow_dirty),
165
+ autostash: !options.key?(:autostash) || options[:autostash],
161
166
  target_version: nil,
162
167
  reset_target: nil,
163
168
  bup_args: [],
@@ -620,7 +625,11 @@ module Kettle
620
625
  def build_report(command, options)
621
626
  config = Config.load(root: options[:root], path: options[:config])
622
627
  start_at = parse_start_at(options[:start_at])
623
- discovery = Discovery.new(config: config)
628
+ effective_only = default_only_filter(command: command, only: options[:only])
629
+ discovery = Discovery.new(
630
+ config: config,
631
+ release_dependency_member_names: direct_member_only_names(effective_only)
632
+ )
624
633
  members = discovery.members
625
634
  ordered = if command == "install"
626
635
  install_order(members, config)
@@ -629,7 +638,6 @@ module Kettle
629
638
  else
630
639
  Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
631
640
  end
632
- effective_only = default_only_filter(command: command, only: options[:only])
633
641
  release_state_results = release_state_results_for_selection(config: config, members: ordered, only: effective_only)
634
642
  selected = Selection.new(members: ordered, release_state_results: release_state_results).apply(only: effective_only, exclude: options[:exclude], start_at: start_at.member)
635
643
  result_members = selected
@@ -678,6 +686,13 @@ module Kettle
678
686
  only
679
687
  end
680
688
 
689
+ def direct_member_only_names(only)
690
+ names = only.to_s.split(",").map(&:strip).reject(&:empty?)
691
+ return nil if names.empty? || names.any? { |name| Selection.status_token?(name) }
692
+
693
+ names
694
+ end
695
+
681
696
  def command_results_for_current_branch(command:, config:, members:, options:, start_at: StartAt.new(nil, nil))
682
697
  return mise_trust_results(config: config, members: members) if command == "mise-trust"
683
698
  return bump_version_results(members: members, options: options, phase: command) if %w[bump bump-version].include?(command)
@@ -697,6 +712,7 @@ module Kettle
697
712
  accept: options[:accept],
698
713
  commit: options[:commit],
699
714
  allow_dirty: options[:allow_dirty],
715
+ autostash: options[:autostash],
700
716
  publish: options[:publish],
701
717
  push: options[:push],
702
718
  tag: options[:tag],
@@ -586,6 +586,15 @@ module Kettle
586
586
 
587
587
  def process_env(member:, env:)
588
588
  base_env = unbundled_process_env
589
+ # Bundler's unbundled environment intentionally omits activation
590
+ # variables, but it may also omit PATH. When `unsetenv_others` is used
591
+ # for mise-managed members, dropping PATH makes nested commands such
592
+ # as `bundle` unresolvable inside kettle-jem.
593
+ base_env["PATH"] = if base_env.key?("PATH")
594
+ executable_path(base_env["PATH"])
595
+ else
596
+ ENV["PATH"]
597
+ end
589
598
  return base_env.merge(env) unless mise_configured?(member)
590
599
 
591
600
  base_env.merge(sensitive_env(env))
@@ -603,6 +612,11 @@ module Kettle
603
612
  SENSITIVE_ENV_KEYS.include?(key.to_s)
604
613
  end
605
614
 
615
+ def executable_path(path)
616
+ paths = [Gem.bindir, File.dirname(RbConfig.ruby), path, ENV["PATH"]]
617
+ paths.compact.map(&:to_s).flat_map { |value| value.split(File::PATH_SEPARATOR) }.reject(&:empty?).uniq.join(File::PATH_SEPARATOR)
618
+ end
619
+
606
620
  def unbundled_process_env
607
621
  return Bundler.original_env if defined?(Bundler) && Bundler.respond_to?(:original_env)
608
622
  return Bundler.unbundled_env if defined?(Bundler) && Bundler.respond_to?(:unbundled_env)
@@ -265,6 +265,12 @@ module Kettle
265
265
  fetch_path("release", "jobs")
266
266
  end
267
267
 
268
+ def release_waves
269
+ Array(fetch_path("release", "waves")).map do |wave|
270
+ Array(wave).map(&:to_s).map(&:strip).reject(&:empty?)
271
+ end.reject(&:empty?)
272
+ end
273
+
268
274
  def release_auto_dependency_floors?
269
275
  fetch_path("release", "auto_dependency_floors") != false
270
276
  end
@@ -5,8 +5,9 @@ require "open3"
5
5
  module Kettle
6
6
  module Family
7
7
  class Discovery
8
- def initialize(config:)
8
+ def initialize(config:, release_dependency_member_names: nil)
9
9
  @config = config
10
+ @release_dependency_member_names = release_dependency_member_names&.map(&:to_s)
10
11
  @warnings = []
11
12
  end
12
13
 
@@ -18,7 +19,7 @@ module Kettle
18
19
 
19
20
  private
20
21
 
21
- attr_reader :config
22
+ attr_reader :config, :release_dependency_member_names
22
23
 
23
24
  def build_members
24
25
  @warnings = []
@@ -51,11 +52,15 @@ module Kettle
51
52
  end
52
53
 
53
54
  def configured_root_members
54
- config.configured_member_roots.map { |root| member_from_gemspec(primary_gemspec(root)) }
55
+ config.configured_member_roots
56
+ .select { |root| selected_configured_root?(root) }
57
+ .map { |root| member_from_gemspec(primary_gemspec(root)) }
55
58
  end
56
59
 
57
60
  def explicit_members
58
- config.explicit_members.map do |entry|
61
+ config.explicit_members.filter_map do |entry|
62
+ next unless selected_configured_root?(entry.fetch("root"))
63
+
59
64
  gemspec_path = if entry["gemspec"]
60
65
  File.expand_path(entry["gemspec"], entry.fetch("root"))
61
66
  else
@@ -65,6 +70,12 @@ module Kettle
65
70
  end
66
71
  end
67
72
 
73
+ def selected_configured_root?(root)
74
+ return true if release_dependency_member_names.nil?
75
+
76
+ release_dependency_member_names.include?(File.basename(File.expand_path(root)))
77
+ end
78
+
68
79
  def primary_gemspec(root)
69
80
  gemspecs = Dir.glob(File.join(root, "*.gemspec"))
70
81
  raise Error, "no gemspec found in #{root}" if gemspecs.empty?
@@ -104,18 +115,23 @@ module Kettle
104
115
  version_file: version_file(root, spec.name),
105
116
  version: spec.version.to_s,
106
117
  dependencies: dependencies,
107
- release_dependencies: release_dependencies(root: root, spec: spec, dependencies: dependencies),
118
+ release_dependencies: release_dependencies(
119
+ root: root,
120
+ spec: spec,
121
+ dependencies: dependencies,
122
+ include_gemfile_dependencies: release_dependency_member_names.nil? || release_dependency_member_names.include?(spec.name)
123
+ ),
108
124
  required_ruby_version: required_ruby_version(spec),
109
125
  licenses: licenses(spec),
110
126
  authors: authors(spec)
111
127
  )
112
128
  end
113
129
 
114
- def release_dependencies(root:, spec:, dependencies:)
130
+ def release_dependencies(root:, spec:, dependencies:, include_gemfile_dependencies:)
115
131
  (
116
132
  dependencies +
117
133
  spec.development_dependencies.map(&:name) +
118
- gemfile_dependencies(root)
134
+ (include_gemfile_dependencies ? gemfile_dependencies(root) : [])
119
135
  ).uniq.sort
120
136
  end
121
137
 
@@ -23,6 +23,7 @@ module Kettle
23
23
  @members = members
24
24
  @config = config
25
25
  @github_latest_release_by_repo = {}
26
+ @transfer_changelog_status_by_root = {}
26
27
  @transfer_changelog_lag_by_key = {}
27
28
  @transfer_changelog_total_count = TRANSFER_CHANGELOG_TOTAL_UNSET
28
29
  end
@@ -72,8 +73,8 @@ module Kettle
72
73
  state = success ? JSON.parse(stdout) : {}
73
74
  state = branch_filtered_state(member, state, branch) if success && branch
74
75
  state = state_with_computed_booleans(state) if success
75
- state = enrich_git_state(member.root, state) if success
76
- state = enrich_github_release(member.root, state) if success
76
+ state = enrich_git_state(member.root, state, branch: branch) if success
77
+ state = enrich_github_release(member.root, state, branch: branch) if success
77
78
  state = enrich_transfer_changelog_lag(member.root, state) if success
78
79
  result(member: member, command: command, stdout: stdout, stderr: stderr, status: status.exitstatus, elapsed: elapsed, success: success, state: state, branch: branch)
79
80
  rescue JSON::ParserError => error
@@ -98,8 +99,8 @@ module Kettle
98
99
  success = status.success?
99
100
  state = success ? JSON.parse(stdout) : {}
100
101
  state = branch_filtered_state(member, state, branch) if success && branch
101
- state = enrich_git_state(member.root, state) if success
102
- state = enrich_github_release(member.root, state) if success
102
+ state = enrich_git_state(member.root, state, branch: branch) if success
103
+ state = enrich_github_release(member.root, state, branch: branch) if success
103
104
  state = enrich_transfer_changelog_lag(member.root, state) if success
104
105
  result(
105
106
  member: member,
@@ -219,29 +220,29 @@ module Kettle
219
220
  status.success?
220
221
  end
221
222
 
222
- def enrich_git_state(root, state)
223
+ def enrich_git_state(root, state, branch: nil)
223
224
  return state unless git_work_tree?(root)
224
225
 
225
226
  enriched = state.dup
226
- current_branch = git_output(root, "branch", "--show-current")
227
+ current_branch = branch || git_output(root, "branch", "--show-current")
227
228
  enriched["current_branch"] = current_branch unless current_branch.to_s.empty?
228
229
 
229
230
  version = state["latest_released"].to_s.empty? ? state["latest_changelog_version"] : state["latest_released"]
230
231
  return enriched if version.to_s.empty?
231
232
 
232
- local_default = default_branch(root)
233
- if local_default
234
- enriched["default_branch"] = local_default
235
- if (counts = release_counts_for_ref(root, version, local_default))
233
+ local_ref = branch || default_branch(root)
234
+ if local_ref
235
+ enriched["default_branch"] = local_ref
236
+ if (counts = release_counts_for_ref(root, version, local_ref))
236
237
  enriched["behind"] = counts.fetch(:behind)
237
238
  enriched["ahead"] = counts.fetch(:ahead)
238
239
  end
239
240
  end
240
241
 
241
- remote_default = remote_default_branch(root, local_default)
242
- if remote_default
243
- enriched["remote_default_branch"] = remote_default
244
- if (counts = release_counts_for_ref(root, version, remote_default))
242
+ remote_ref = branch ? "origin/#{branch}" : remote_default_branch(root, local_ref)
243
+ if remote_ref && git_ref_exists?(root, "refs/remotes/#{remote_ref}")
244
+ enriched["remote_default_branch"] = remote_ref
245
+ if (counts = release_counts_for_ref(root, version, remote_ref))
245
246
  enriched["remote_behind"] = counts.fetch(:behind)
246
247
  enriched["remote_ahead"] = counts.fetch(:ahead)
247
248
  end
@@ -319,17 +320,43 @@ module Kettle
319
320
  state["unreleased_entries"] == true && state["version"].to_s == state["latest_released"].to_s
320
321
  end
321
322
 
322
- def enrich_github_release(root, state)
323
- tag = github_latest_release(root)
323
+ def enrich_github_release(root, state, branch: nil)
324
+ tag = branch ? github_release_for_version(root, state["latest_released"]) : github_latest_release(root)
324
325
  return state unless tag
325
326
 
326
327
  state.merge("github_latest_release" => tag)
327
328
  end
328
329
 
330
+ def github_release_for_version(root, version)
331
+ return nil if version.to_s.empty?
332
+
333
+ repo = github_repo_slug(root)
334
+ return nil unless repo
335
+
336
+ tag = "v#{version.to_s.delete_prefix("v")}"
337
+ stdout, _stderr, status = Open3.capture3("gh", "release", "view", tag, "--repo", repo, "--json", "tagName", "--jq", ".tagName")
338
+ status.success? ? normalize_github_release_tag(stdout) : nil
339
+ rescue SystemCallError
340
+ nil
341
+ end
342
+
329
343
  def enrich_transfer_changelog_lag(root, state)
330
344
  kettle_jem_state = kettle_jem_config_state(root)
331
345
  return state unless kettle_jem_state
332
346
 
347
+ status = transfer_changelog_status(root)
348
+ if status
349
+ total = status.key?("total_count") ? status.fetch("total_count") : transfer_changelog_total_count
350
+ return state.merge(
351
+ "transfer_changelog_lag" => status.fetch("missing_count", 0).to_i,
352
+ "transfer_changelog_applicable" => status.fetch("applicable_count", 0).to_i,
353
+ "transfer_changelog_excluded_present" => status.fetch("excluded_present_count", 0).to_i,
354
+ "transfer_changelog_total" => total.to_i
355
+ )
356
+ end
357
+
358
+ # Compatibility with already released kettle-jem versions while a
359
+ # workspace is upgraded. New versions use the context-aware status API.
333
360
  replay = kettle_jem_state["changelog_replay"]
334
361
  replay = replay.is_a?(Hash) ? replay : {}
335
362
  last_entry_key = replay["last_entry_key"] || replay[:last_entry_key]
@@ -368,6 +395,15 @@ module Kettle
368
395
  nil
369
396
  end
370
397
 
398
+ def transfer_changelog_status(root)
399
+ cache_key = File.expand_path(root.to_s)
400
+ @transfer_changelog_status_by_root.fetch(cache_key) do
401
+ @transfer_changelog_status_by_root[cache_key] = in_process_transfer_changelog_status(cache_key) || external_transfer_changelog_status(cache_key)
402
+ end
403
+ rescue
404
+ nil
405
+ end
406
+
371
407
  def transfer_changelog_total_count
372
408
  return @transfer_changelog_total_count unless @transfer_changelog_total_count.equal?(TRANSFER_CHANGELOG_TOTAL_UNSET)
373
409
 
@@ -383,11 +419,20 @@ module Kettle
383
419
  nil
384
420
  end
385
421
 
422
+ def in_process_transfer_changelog_status(root)
423
+ return nil unless load_kettle_jem_transfer_api
424
+ return nil unless ::Kettle::Jem.respond_to?(:transfer_changelog_status)
425
+
426
+ normalize_transfer_changelog_lag(::Kettle::Jem.transfer_changelog_status(root))
427
+ rescue
428
+ nil
429
+ end
430
+
386
431
  def load_kettle_jem_transfer_api
387
- return true if defined?(::Kettle::Jem) && ::Kettle::Jem.respond_to?(:transfer_changelog_lag)
432
+ return true if defined?(::Kettle::Jem) && (::Kettle::Jem.respond_to?(:transfer_changelog_lag) || ::Kettle::Jem.respond_to?(:transfer_changelog_status))
388
433
 
389
434
  require "kettle/jem"
390
- defined?(::Kettle::Jem) && ::Kettle::Jem.respond_to?(:transfer_changelog_lag)
435
+ defined?(::Kettle::Jem) && (::Kettle::Jem.respond_to?(:transfer_changelog_lag) || ::Kettle::Jem.respond_to?(:transfer_changelog_status))
391
436
  rescue LoadError
392
437
  false
393
438
  end
@@ -409,6 +454,21 @@ module Kettle
409
454
  nil
410
455
  end
411
456
 
457
+ def external_transfer_changelog_status(root)
458
+ code = <<~RUBY
459
+ puts JSON.generate(Kettle::Jem.transfer_changelog_status(ARGV.fetch(0)))
460
+ RUBY
461
+ command = [RbConfig.ruby, "-rjson", "-rkettle/jem", "-e", code, root]
462
+ stdout, _stderr, status = if defined?(::Bundler)
463
+ ::Bundler.with_unbundled_env { Open3.capture3(*command) }
464
+ else
465
+ Open3.capture3(*command)
466
+ end
467
+ status.success? ? normalize_transfer_changelog_lag(JSON.parse(stdout)) : nil
468
+ rescue JSON::ParserError, SystemCallError
469
+ nil
470
+ end
471
+
412
472
  def normalize_transfer_changelog_lag(lag)
413
473
  return nil unless lag.is_a?(Hash)
414
474
 
@@ -3,12 +3,43 @@
3
3
  module Kettle
4
4
  module Family
5
5
  class ReleaseWaves
6
- def initialize(members:)
6
+ def initialize(members:, configured_waves: [])
7
7
  @members = members
8
+ @configured_waves = configured_waves
8
9
  end
9
10
 
10
11
  def waves
11
- by_name = members.to_h { |member| [member.name, member] }
12
+ configured = configured_waves_for_selected_members
13
+ configured_names = configured.flatten.map(&:name)
14
+ remaining_members = members.reject { |member| configured_names.include?(member.name) }
15
+
16
+ configured + inferred_waves(remaining_members)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :members, :configured_waves
22
+
23
+ def configured_waves_for_selected_members
24
+ duplicates = configured_waves.flatten.group_by(&:itself).select { |_name, names| names.length > 1 }.keys
25
+ raise Error, "release waves contain duplicate member(s): #{duplicates.join(", ")}" unless duplicates.empty?
26
+
27
+ selected_names = members.map(&:name)
28
+ selected_members = members.to_h { |member| [member.name, member] }
29
+ # Configured waves commonly name the complete family while --only selects
30
+ # a subset. Ignore entries outside the current selection.
31
+ return [] if configured_waves.empty?
32
+
33
+ configured_waves.filter_map do |wave|
34
+ names = wave & selected_names
35
+ names.map { |name| selected_members.fetch(name) } unless names.empty?
36
+ end
37
+ end
38
+
39
+ def inferred_waves(candidate_members)
40
+ return [] if candidate_members.empty?
41
+
42
+ by_name = candidate_members.to_h { |member| [member.name, member] }
12
43
  pending = by_name.keys
13
44
  completed = []
14
45
  [].tap do |planned_waves|
@@ -30,10 +61,6 @@ module Kettle
30
61
  end
31
62
  end
32
63
 
33
- private
34
-
35
- attr_reader :members
36
-
37
64
  def selected_hard_dependencies_for(member, by_name)
38
65
  Array(member.dependencies).map(&:to_s).select { |dependency| by_name.key?(dependency) }
39
66
  end
@@ -509,12 +509,12 @@ module Kettle
509
509
  def append_release_state_results(lines)
510
510
  lines << "release state:"
511
511
  lines << " boolean columns:"
512
- lines << " unrel: unreleased changelog entries are present"
512
+ lines << " unrel: unreleased changelog entries are present (including replayed transfer entries)"
513
513
  lines << " prep: V.ch.md matches V.rb and is ready to publish"
514
514
  lines << " pend: unrel or prep"
515
515
  lines << " bump: unrel is yes and V.rb matches V.rel"
516
516
  lines << " count columns:"
517
- lines << " T(n): kettle-jem transfer changelog lag; n is the total transfer changelog entry count with no replay cursor"
517
+ lines << " T(n): filter-aware kettle-jem transfer changelog lag; n is the total source entry count and row values are missing / applicable (x excluded-present)"
518
518
  rows = release_state_header
519
519
  results.each do |result|
520
520
  rows << release_state_row(result)
@@ -539,7 +539,7 @@ module Kettle
539
539
  state.fetch("latest_changelog_version", nil).to_s.empty? ? "none" : state.fetch("latest_changelog_version").to_s,
540
540
  state.fetch("latest_released", nil).to_s.empty? ? "unknown" : state.fetch("latest_released").to_s,
541
541
  release_state_github_release(state),
542
- state.fetch("transfer_changelog_lag", 0).to_i.to_s,
542
+ release_state_transfer_changelog_value(state),
543
543
  release_state_ahead_behind(state),
544
544
  yes_no(state.fetch("unreleased_entries", nil)),
545
545
  yes_no(state.fetch("prepared_release_pending", nil)),
@@ -566,6 +566,16 @@ module Kettle
566
566
  "T(#{total || "?"})"
567
567
  end
568
568
 
569
+ def release_state_transfer_changelog_value(state)
570
+ missing = state.fetch("transfer_changelog_lag", 0).to_i
571
+ applicable = state.fetch("transfer_changelog_applicable", nil)
572
+ return missing.to_s if applicable.nil?
573
+
574
+ excluded_present = state.fetch("transfer_changelog_excluded_present", 0).to_i
575
+ value = "#{missing} / #{applicable.to_i}"
576
+ excluded_present.positive? ? "#{value} (x#{excluded_present})" : value
577
+ end
578
+
569
579
  def release_state_github_release(state)
570
580
  github_release = state.fetch("github_latest_release", nil).to_s
571
581
  return "unknown" if github_release.empty?
@@ -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.2.21"
8
+ VERSION = "1.2.23"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -102,7 +102,7 @@ module Kettle
102
102
  REGISTRY_WAIT_ATTEMPTS = 15
103
103
  REGISTRY_WAIT_INTERVAL_SECONDS = 15
104
104
 
105
- def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, 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, **options)
105
+ def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, autostash: true, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, 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, **options)
106
106
  @command = command
107
107
  @config = config
108
108
  @members = members
@@ -110,6 +110,7 @@ module Kettle
110
110
  @accept = accept
111
111
  @commit = commit
112
112
  @allow_dirty = allow_dirty
113
+ @autostash = autostash
113
114
  @publish = publish
114
115
  @push = push
115
116
  @tag = tag
@@ -150,6 +151,8 @@ module Kettle
150
151
  return current_branch_results(members)
151
152
  end
152
153
 
154
+ return template_with_worktree_sync_results if command == "template" && execute
155
+
153
156
  preflight = branch_checkout_dirty_preflight_results
154
157
  return preflight if preflight.any? { |result| !result.ok? }
155
158
 
@@ -162,7 +165,159 @@ module Kettle
162
165
 
163
166
  private
164
167
 
165
- attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :ci_workflows, :skip_bundle_audit, :skip_remotes, :required_remotes, :auto_dependency_floors, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :verbose, :jobs, :progress_io, :reset_target, :bup_args, :bex_args, :start_member, :start_branch
168
+ attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :autostash, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :ci_workflows, :skip_bundle_audit, :skip_remotes, :required_remotes, :auto_dependency_floors, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :verbose, :jobs, :progress_io, :reset_target, :bup_args, :bex_args, :start_member, :start_branch
169
+
170
+ def template_with_worktree_sync_results
171
+ runner = command_runner
172
+ checkout_preflight = branch_checkout_dirty_preflight_results
173
+ if checkout_preflight.any? { |result| !result.ok? } && !autostash
174
+ return checkout_preflight
175
+ end
176
+ checkout_preflight = [] unless checkout_preflight.all?(&:ok?)
177
+
178
+ sync_results, stashes = template_worktree_sync_results(runner: runner)
179
+ unless sync_results.all?(&:ok?)
180
+ return checkout_preflight + sync_results + restore_template_autostashes(stashes, runner: runner)
181
+ end
182
+
183
+ workflow_results = branch_checkout_dirty_preflight_results
184
+ if workflow_results.all?(&:ok?)
185
+ workflow_results += if !config.release_target_branches.empty?
186
+ branch_target_results
187
+ elsif member_local_branch_targets?
188
+ member_local_branch_target_results
189
+ else
190
+ current_branch_results(members)
191
+ end
192
+ end
193
+
194
+ checkout_preflight + sync_results + workflow_results + restore_template_autostashes(stashes, runner: runner)
195
+ end
196
+
197
+ def template_worktree_sync_results(runner:)
198
+ stashes = []
199
+ results = []
200
+ template_sync_members.each do |member|
201
+ dirty_paths = GitStatus.dirty_paths(member.root)
202
+ if dirty_paths.any?
203
+ unless autostash
204
+ results << template_dirty_worktree_result(member, dirty_paths)
205
+ break
206
+ end
207
+
208
+ stash_result = runner.call(
209
+ member: member,
210
+ phase: "template_autostash",
211
+ command: ["sh", "-lc", template_autostash_command(member)]
212
+ )
213
+ results << stash_result
214
+ break unless stash_result.ok?
215
+
216
+ stash_ref = stash_result.stdout.to_s.lines.last.to_s.strip
217
+ if stash_ref.empty?
218
+ results << template_sync_failure_result(member, "could not determine the temporary template autostash reference")
219
+ break
220
+ end
221
+ stashes << {member: member, ref: stash_ref}
222
+ end
223
+
224
+ upstream = git_upstream_for(member)
225
+ next unless upstream
226
+
227
+ results << runner.call(member: member, phase: "template_sync", command: ["git", "pull", "--rebase"])
228
+ break unless results.last.ok?
229
+ end
230
+ [results, stashes]
231
+ end
232
+
233
+ def restore_template_autostashes(stashes, runner:)
234
+ stashes.reverse_each.map do |stash|
235
+ restore = runner.call(
236
+ member: stash.fetch(:member),
237
+ phase: "template_autostash_restore",
238
+ command: ["git", "stash", "pop", stash.fetch(:ref)]
239
+ )
240
+ next restore if restore.ok? || !template_generated_lockfile_only_conflict?(stash.fetch(:member))
241
+
242
+ runner.call(
243
+ member: stash.fetch(:member),
244
+ phase: "template_autostash_generated_lockfile_recovery",
245
+ command: [
246
+ "sh", "-lc",
247
+ "paths=$(git diff --name-only --diff-filter=U); " \
248
+ "git checkout --ours -- $paths && git add -- $paths && " \
249
+ "git stash drop #{Shellwords.escape(stash.fetch(:ref))}"
250
+ ]
251
+ )
252
+ end
253
+ end
254
+
255
+ def template_generated_lockfile_only_conflict?(member)
256
+ stdout, _stderr, status = Open3.capture3("git", "diff", "--name-only", "--diff-filter=U", chdir: member.root)
257
+ conflicts = stdout.lines.map(&:strip).reject(&:empty?).sort
258
+ status.success? && !conflicts.empty? && conflicts.all? { |path| ["Gemfile.lock", ".structuredmerge/kettle-jem.lock"].include?(path) }
259
+ end
260
+
261
+ def template_branch_sync_results(branch_members, runner:)
262
+ template_sync_members_for(branch_members).each_with_object([]) do |member, memo|
263
+ next unless git_upstream_for(member)
264
+
265
+ memo << runner.call(member: member, phase: "template_sync", command: ["git", "pull", "--rebase"])
266
+ break memo unless memo.last.ok?
267
+ end
268
+ end
269
+
270
+ def template_sync_members
271
+ template_sync_members_for(members)
272
+ end
273
+
274
+ def template_sync_members_for(candidates)
275
+ candidates.each_with_object([]) do |member, unique_members|
276
+ unique_members << member unless unique_members.any? { |candidate| git_root_for(candidate) == git_root_for(member) }
277
+ end
278
+ end
279
+
280
+ def git_upstream_for(member)
281
+ stdout, _stderr, status = Open3.capture3("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}", chdir: member.root)
282
+ status.success? ? stdout.strip : nil
283
+ end
284
+
285
+ def template_autostash_command(member)
286
+ label = "kettle-family-template-#{member.name}-#{Process.pid}-#{Time.now.to_i}"
287
+ "git stash push --include-untracked --message #{Shellwords.escape(label)} && " \
288
+ "ref=$(git stash list -1 --format=%gd) && " \
289
+ "git checkout \"$ref\" -- .tool-versions 2>/dev/null || true; " \
290
+ "for path in mise.toml .mise.toml; do " \
291
+ "if git cat-file -e \"$ref^3:$path\" 2>/dev/null; then git show \"$ref^3:$path\" > \"$path\"; fi; " \
292
+ "done; printf '%s\\n' \"$ref\""
293
+ end
294
+
295
+ def template_dirty_worktree_result(member, dirty_paths)
296
+ template_sync_failure_result(
297
+ member,
298
+ [
299
+ "dirty worktree blocks template sync; remove --no-autostash, or commit or stash changes before retrying",
300
+ *dirty_paths
301
+ ].join("\n"),
302
+ reason: "dirty worktree blocks template sync"
303
+ )
304
+ end
305
+
306
+ def template_sync_failure_result(member, message, reason: "template sync failed")
307
+ CommandResult.new(
308
+ member_name: member.name,
309
+ phase: "template_sync_preflight",
310
+ command: ["internal", "template-sync"],
311
+ workdir: member.root,
312
+ status: 1,
313
+ success: false,
314
+ stdout: "",
315
+ stderr: message,
316
+ elapsed_seconds: 0.0,
317
+ skipped: false,
318
+ reason: reason
319
+ )
320
+ end
166
321
 
167
322
  def current_branch_results(workflow_members)
168
323
  return check_results(workflow_members) if command == "check"
@@ -231,8 +386,11 @@ module Kettle
231
386
  debugger_results = template_debugger_bootstrap_results(workflow_members)
232
387
  return debugger_results unless debugger_results.all?(&:ok?)
233
388
 
389
+ appraisal_results = template_appraisal_bootstrap_results(workflow_members)
390
+ return debugger_results + appraisal_results unless appraisal_results.all?(&:ok?)
391
+
234
392
  bootstrap_results = template_bootstrap_dependency_results(workflow_members)
235
- return debugger_results + bootstrap_results unless bootstrap_results.all?(&:ok?)
393
+ return debugger_results + appraisal_results + bootstrap_results unless bootstrap_results.all?(&:ok?)
236
394
 
237
395
  template_progress = start_template_progress(workflow_members)
238
396
  results = template_dependency_waves(workflow_members).each_with_object([]) do |wave, memo|
@@ -241,7 +399,7 @@ module Kettle
241
399
  break memo unless wave_results.all?(&:ok?)
242
400
  end
243
401
  emit_template_progress_summary(results, progress: template_progress)
244
- debugger_results + bootstrap_results + results
402
+ debugger_results + appraisal_results + bootstrap_results + results
245
403
  end
246
404
 
247
405
  def template_debugger_bootstrap_results(workflow_members)
@@ -253,6 +411,15 @@ module Kettle
253
411
  [template_bootstrap_failure_result(workflow_members.first, error.message)]
254
412
  end
255
413
 
414
+ def template_appraisal_bootstrap_results(workflow_members)
415
+ bootstrap = AppraisalBootstrap.new(mode: :execute)
416
+ workflow_members.filter_map do |member|
417
+ bootstrap.bootstrap_member(member) if bootstrap.member_needs_bootstrap?(member)
418
+ end
419
+ rescue => error
420
+ [template_bootstrap_failure_result(workflow_members.first, error.message)]
421
+ end
422
+
256
423
  def template_results_for_wave(wave, progress:)
257
424
  queue = Queue.new
258
425
  wave.each_with_index { |member, index| queue << [index, member] }
@@ -433,6 +600,10 @@ module Kettle
433
600
 
434
601
  branch_members = rediscovered_selected_members(selected_names)
435
602
  branch_members = members if branch_members.empty?
603
+ if command == "template" && execute
604
+ memo.concat(template_branch_sync_results(branch_members, runner: runner))
605
+ break memo unless memo.last&.ok?
606
+ end
436
607
  branch_results = current_branch_results(branch_members)
437
608
  tag_branch_results(branch_results, branch)
438
609
  memo.concat(branch_results)
@@ -914,7 +1085,7 @@ module Kettle
914
1085
  end
915
1086
 
916
1087
  def release_waves(release_members)
917
- ReleaseWaves.new(members: release_members).waves
1088
+ ReleaseWaves.new(members: release_members, configured_waves: config.release_waves).waves
918
1089
  end
919
1090
 
920
1091
  def append_dependency_floor_results(released_members:, dependent_members:, runner:, memo:)
@@ -1868,6 +2039,7 @@ module Kettle
1868
2039
  if command == "template"
1869
2040
  env["KETTLE_JEM_TEMPLATE_PROFILE"] = config.template_profile if config.template_profile
1870
2041
  env["KJ_REPOSITORY_TOPOLOGY"] = config.template_repository_topology if config.template_repository_topology
2042
+ env["KETTLE_JEM_THREAD_WORKERS"] ||= template_thread_worker_budget.to_s
1871
2043
  if monorepo_template?
1872
2044
  template_git_lock_path = template_git_commit_lock_path
1873
2045
  env["KETTLE_JEM_GIT_LOCK"] = template_git_lock_path
@@ -1951,12 +2123,17 @@ module Kettle
1951
2123
  prefix = prefix[0...-2] if prefix.last(2) == %w[bundle exec]
1952
2124
  prefix + [RbConfig.ruby, executable] + argv[(index + 1)..]
1953
2125
  else
1954
- replacement = "#{Shellwords.escape(RbConfig.ruby)} #{Shellwords.escape(executable)}"
1955
- text = command_text.to_s
1956
- return text.sub(/\bbundle\s+exec\s+kettle-jem\b/, replacement) if text.match?(/\bbundle\s+exec\s+kettle-jem\b/)
2126
+ argv = Shellwords.split(command_text.to_s)
2127
+ index = argv.index("kettle-jem")
2128
+ return command_text unless index
2129
+ return command_text if argv.any? { |token| %w[&& || ; |].include?(token) }
1957
2130
 
1958
- text.sub(/\bkettle-jem\b/, replacement)
2131
+ prefix = argv[0...index]
2132
+ prefix = prefix[0...-2] if prefix.last(2) == %w[bundle exec]
2133
+ prefix + [RbConfig.ruby, executable] + argv[(index + 1)..]
1959
2134
  end
2135
+ rescue ArgumentError
2136
+ command_text
1960
2137
  end
1961
2138
 
1962
2139
  def local_kettle_jem_executable
@@ -2795,6 +2972,11 @@ module Kettle
2795
2972
  env
2796
2973
  end
2797
2974
 
2975
+ def template_thread_worker_budget
2976
+ member_jobs = template_jobs(members)
2977
+ [1, (Etc.nprocessors / member_jobs) - 1].max
2978
+ end
2979
+
2798
2980
  def normalize_release_lockfiles(member:, runner:, memo:)
2799
2981
  result = runner.call(
2800
2982
  member: member,
data/lib/kettle/family.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "family/version"
4
3
  require_relative "family/member"
5
4
  require_relative "family/command_result"
6
5
  require_relative "family/release_state_result"
@@ -18,6 +17,7 @@ require_relative "family/unreleased_gem_cleanup"
18
17
  require_relative "family/local_install"
19
18
  require_relative "family/nomono_bootstrap"
20
19
  require_relative "family/debugger_bootstrap"
20
+ require_relative "family/appraisal_bootstrap"
21
21
  require_relative "family/release_waves"
22
22
  require_relative "family/workflow"
23
23
  require_relative "family/discovery"
@@ -25,6 +25,7 @@ require_relative "family/orderer"
25
25
  require_relative "family/selection"
26
26
  require_relative "family/report"
27
27
  require_relative "family/cli"
28
+ require_relative "family/version"
28
29
 
29
30
  module Kettle
30
31
  module Family
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.2.21
4
+ version: 1.2.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -74,7 +74,7 @@ dependencies:
74
74
  version: '2.3'
75
75
  - - ">="
76
76
  - !ruby/object:Gem::Version
77
- version: 2.5.15
77
+ version: 2.5.16
78
78
  type: :runtime
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
@@ -84,7 +84,7 @@ dependencies:
84
84
  version: '2.3'
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
- version: 2.5.15
87
+ version: 2.5.16
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: kettle-test
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -94,7 +94,7 @@ dependencies:
94
94
  version: '2.0'
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 2.0.14
97
+ version: 2.0.18
98
98
  type: :runtime
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ dependencies:
104
104
  version: '2.0'
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 2.0.14
107
+ version: 2.0.18
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: tsort
110
110
  requirement: !ruby/object:Gem::Requirement
@@ -309,6 +309,7 @@ files:
309
309
  - README.md
310
310
  - exe/kettle-family
311
311
  - lib/kettle/family.rb
312
+ - lib/kettle/family/appraisal_bootstrap.rb
312
313
  - lib/kettle/family/branch_lane_audit.rb
313
314
  - lib/kettle/family/branch_target_config.rb
314
315
  - lib/kettle/family/changelog_check.rb
@@ -344,10 +345,10 @@ licenses:
344
345
  - AGPL-3.0-only
345
346
  metadata:
346
347
  homepage_uri: https://kettle-family.galtzo.com
347
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.21
348
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.21/CHANGELOG.md
348
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.23
349
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.23/CHANGELOG.md
349
350
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
350
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.21
351
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.23
351
352
  funding_uri: https://github.com/sponsors/pboling
352
353
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
353
354
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file