kettle-family 1.2.21 → 1.2.22

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: 8352dd3e0095e5bdec1437a5e0098a50e375f64362ffa592aa01aa056d48172b
4
+ data.tar.gz: 947519ebac1a4a0ad9c468efd5defaaddcec2feabdaff037e3470e3c19a0cc2c
5
5
  SHA512:
6
- metadata.gz: 0b2ff66410811aa28af2f578fd354cb9f9d7174a648f3863c7ad68e3e3098b129cb4a2bc9678a24a0fe8d478bba7a81bf61bae35013c48fae124ed0e113c56a2
7
- data.tar.gz: e25a8ee6c06843662c0e2da1d9fa68d15b1aa6af30c5f5bbcdafe1690c0912e0d04e7f1b8bc1e814d4e5fef79850f1f4c430a69191d647679b393fa001547632
6
+ metadata.gz: bd6b1cf2bbef2d4cabdcb2c8fadaf2a71c4437b1fbef55c5d2ae716997dc69276ac1dd3cc95a2d8b457a5599754169f7d004bbe38ba52c2afdbae3b41e04d5b9
7
+ data.tar.gz: 2e6ea899f8012f72619f58fb1cefb1495511b8a3252d5e7c8be57c1f204ed92b55439e1d0c10278b57f4793bab8043d572ba38eed9b7b0b1f218f6468ab54997
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,50 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.22] - 2026-08-01
34
+
35
+ - TAG: [v1.2.22][1.2.22t]
36
+ - COVERAGE: 93.87% -- 4626/4928 lines in 31 files
37
+ - BRANCH COVERAGE: 76.18% -- 1807/2372 branches in 31 files
38
+ - 29.31% documented
39
+
40
+ ### Added
41
+
42
+ - Template runs can now use --autostash to preserve dirty worktrees while synchronizing and templating.
43
+
44
+ ### Changed
45
+
46
+ - Template runs now pull configured upstream branches before planning; branch-target runs synchronize each checked-out branch.
47
+
48
+ - Template autostash is now enabled by default; use --no-autostash to fail on dirty worktrees.
49
+
50
+ - Clarify that replayed template changelog entries are destination release work and affect release state.
51
+
52
+ - Release state now reports filter-aware transfer changelog lag as missing versus applicable entries.
53
+
54
+ - kettle-jem-template-20260801-001 - Generated README gem dashboard links now
55
+ use ClickGems instead of BestGems.
56
+
57
+ ### Fixed
58
+
59
+ - Evaluate release-state GitHub release and commit counts against each configured target branch.
60
+
61
+ - kettle-jem-template-20260801-002 - Generated RSpec helpers now normalize
62
+ managed configuration block bindings structurally, preventing mixed block
63
+ parameter names from producing invalid configuration after a merge.
64
+ - kettle-jem-template-20260801-003 - Generated project metadata and
65
+ documentation now normalize configured underscore hostnames to valid
66
+ hyphenated hostnames.
67
+ - kettle-jem-template-20260801-004 - Generated organization README logos now
68
+ use GitHub's stable organization avatar endpoint instead of assuming a
69
+ matching Galtzo-hosted asset exists.
70
+
71
+ - Template autostash now recovers lockfile-only restore conflicts by keeping the normalized lockfile.
72
+
73
+ - Allow family configuration to define ordered single-member release waves.
74
+
75
+ - Remove retired pboling/appraisal declarations before templating legacy members.
76
+
33
77
  ## [1.2.21] - 2026-08-01
34
78
 
35
79
  - TAG: [v1.2.21][1.2.21t]
@@ -1608,7 +1652,9 @@ Please file a bug if you notice a violation of semantic versioning.
1608
1652
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
1609
1653
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
1610
1654
 
1611
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.21...HEAD
1655
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.22...HEAD
1656
+ [1.2.22]: https://github.com/kettle-dev/kettle-family/compare/v1.2.21...v1.2.22
1657
+ [1.2.22t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.22
1612
1658
  [1.2.21]: https://github.com/kettle-dev/kettle-family/compare/v1.2.20...v1.2.21
1613
1659
  [1.2.21t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.21
1614
1660
  [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.928-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,95 @@
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
+ Kettle::Dev::VersionBump.file_edit(path, source, line_start, line_end, "")
73
+ end
74
+
75
+ def legacy_appraisal_call?(node)
76
+ return false unless node.is_a?(Prism::CallNode) && node.name == :gem
77
+
78
+ arguments = node.arguments&.arguments || []
79
+ name_node = arguments.first
80
+ return false unless name_node.is_a?(Prism::StringNode) && name_node.unescaped == LEGACY_GEM_NAME
81
+
82
+ keyword_values(arguments).values_at("github", "branch") == [LEGACY_GITHUB, LEGACY_BRANCH]
83
+ end
84
+
85
+ def keyword_values(arguments)
86
+ arguments.grep(Prism::KeywordHashNode).flat_map(&:elements).each_with_object({}) do |association, values|
87
+ next unless association.is_a?(Prism::AssocNode)
88
+ next unless association.key.is_a?(Prism::SymbolNode) && association.value.is_a?(Prism::StringNode)
89
+
90
+ values[association.key.unescaped] = association.value.unescaped
91
+ end
92
+ end
93
+ end
94
+ end
95
+ 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: [],
@@ -697,6 +702,7 @@ module Kettle
697
702
  accept: options[:accept],
698
703
  commit: options[:commit],
699
704
  allow_dirty: options[:allow_dirty],
705
+ autostash: options[:autostash],
700
706
  publish: options[:publish],
701
707
  push: options[:push],
702
708
  tag: options[:tag],
@@ -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
@@ -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.22"
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,151 @@ 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_lockfile_only_conflict?(stash.fetch(:member))
241
+
242
+ runner.call(
243
+ member: stash.fetch(:member),
244
+ phase: "template_autostash_lockfile_recovery",
245
+ command: [
246
+ "sh", "-lc",
247
+ "git checkout --ours -- Gemfile.lock && git add -- Gemfile.lock && git stash drop #{Shellwords.escape(stash.fetch(:ref))}"
248
+ ]
249
+ )
250
+ end
251
+ end
252
+
253
+ def template_lockfile_only_conflict?(member)
254
+ stdout, _stderr, status = Open3.capture3("git", "diff", "--name-only", "--diff-filter=U", chdir: member.root)
255
+ status.success? && stdout.lines.map(&:strip).reject(&:empty?) == ["Gemfile.lock"]
256
+ end
257
+
258
+ def template_branch_sync_results(branch_members, runner:)
259
+ template_sync_members_for(branch_members).each_with_object([]) do |member, memo|
260
+ next unless git_upstream_for(member)
261
+
262
+ memo << runner.call(member: member, phase: "template_sync", command: ["git", "pull", "--rebase"])
263
+ break memo unless memo.last.ok?
264
+ end
265
+ end
266
+
267
+ def template_sync_members
268
+ template_sync_members_for(members)
269
+ end
270
+
271
+ def template_sync_members_for(candidates)
272
+ candidates.each_with_object([]) do |member, unique_members|
273
+ unique_members << member unless unique_members.any? { |candidate| git_root_for(candidate) == git_root_for(member) }
274
+ end
275
+ end
276
+
277
+ def git_upstream_for(member)
278
+ stdout, _stderr, status = Open3.capture3("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}", chdir: member.root)
279
+ status.success? ? stdout.strip : nil
280
+ end
281
+
282
+ def template_autostash_command(member)
283
+ label = "kettle-family-template-#{member.name}-#{Process.pid}-#{Time.now.to_i}"
284
+ "git stash push --include-untracked --message #{Shellwords.escape(label)} && git stash list -1 --format=%gd"
285
+ end
286
+
287
+ def template_dirty_worktree_result(member, dirty_paths)
288
+ template_sync_failure_result(
289
+ member,
290
+ [
291
+ "dirty worktree blocks template sync; remove --no-autostash, or commit or stash changes before retrying",
292
+ *dirty_paths
293
+ ].join("\n"),
294
+ reason: "dirty worktree blocks template sync"
295
+ )
296
+ end
297
+
298
+ def template_sync_failure_result(member, message, reason: "template sync failed")
299
+ CommandResult.new(
300
+ member_name: member.name,
301
+ phase: "template_sync_preflight",
302
+ command: ["internal", "template-sync"],
303
+ workdir: member.root,
304
+ status: 1,
305
+ success: false,
306
+ stdout: "",
307
+ stderr: message,
308
+ elapsed_seconds: 0.0,
309
+ skipped: false,
310
+ reason: reason
311
+ )
312
+ end
166
313
 
167
314
  def current_branch_results(workflow_members)
168
315
  return check_results(workflow_members) if command == "check"
@@ -231,8 +378,11 @@ module Kettle
231
378
  debugger_results = template_debugger_bootstrap_results(workflow_members)
232
379
  return debugger_results unless debugger_results.all?(&:ok?)
233
380
 
381
+ appraisal_results = template_appraisal_bootstrap_results(workflow_members)
382
+ return debugger_results + appraisal_results unless appraisal_results.all?(&:ok?)
383
+
234
384
  bootstrap_results = template_bootstrap_dependency_results(workflow_members)
235
- return debugger_results + bootstrap_results unless bootstrap_results.all?(&:ok?)
385
+ return debugger_results + appraisal_results + bootstrap_results unless bootstrap_results.all?(&:ok?)
236
386
 
237
387
  template_progress = start_template_progress(workflow_members)
238
388
  results = template_dependency_waves(workflow_members).each_with_object([]) do |wave, memo|
@@ -241,7 +391,7 @@ module Kettle
241
391
  break memo unless wave_results.all?(&:ok?)
242
392
  end
243
393
  emit_template_progress_summary(results, progress: template_progress)
244
- debugger_results + bootstrap_results + results
394
+ debugger_results + appraisal_results + bootstrap_results + results
245
395
  end
246
396
 
247
397
  def template_debugger_bootstrap_results(workflow_members)
@@ -253,6 +403,15 @@ module Kettle
253
403
  [template_bootstrap_failure_result(workflow_members.first, error.message)]
254
404
  end
255
405
 
406
+ def template_appraisal_bootstrap_results(workflow_members)
407
+ bootstrap = AppraisalBootstrap.new(mode: :execute)
408
+ workflow_members.filter_map do |member|
409
+ bootstrap.bootstrap_member(member) if bootstrap.member_needs_bootstrap?(member)
410
+ end
411
+ rescue => error
412
+ [template_bootstrap_failure_result(workflow_members.first, error.message)]
413
+ end
414
+
256
415
  def template_results_for_wave(wave, progress:)
257
416
  queue = Queue.new
258
417
  wave.each_with_index { |member, index| queue << [index, member] }
@@ -433,6 +592,10 @@ module Kettle
433
592
 
434
593
  branch_members = rediscovered_selected_members(selected_names)
435
594
  branch_members = members if branch_members.empty?
595
+ if command == "template" && execute
596
+ memo.concat(template_branch_sync_results(branch_members, runner: runner))
597
+ break memo unless memo.last&.ok?
598
+ end
436
599
  branch_results = current_branch_results(branch_members)
437
600
  tag_branch_results(branch_results, branch)
438
601
  memo.concat(branch_results)
@@ -914,7 +1077,7 @@ module Kettle
914
1077
  end
915
1078
 
916
1079
  def release_waves(release_members)
917
- ReleaseWaves.new(members: release_members).waves
1080
+ ReleaseWaves.new(members: release_members, configured_waves: config.release_waves).waves
918
1081
  end
919
1082
 
920
1083
  def append_dependency_floor_results(released_members:, dependent_members:, runner:, memo:)
data/lib/kettle/family.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "family/unreleased_gem_cleanup"
18
18
  require_relative "family/local_install"
19
19
  require_relative "family/nomono_bootstrap"
20
20
  require_relative "family/debugger_bootstrap"
21
+ require_relative "family/appraisal_bootstrap"
21
22
  require_relative "family/release_waves"
22
23
  require_relative "family/workflow"
23
24
  require_relative "family/discovery"
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.22
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.22
349
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.22/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.22
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
@@ -1,2 +1,4 @@
1
- �N#oﳈ�2��W��a�u2�m�!�C<I����[�KRK���~(c�:dU����6ݣI��K���A�b�P����G���AZ����q-v��f|�b�-Q�X����Q^8)�Yg�2��e�N�*1��B���QA�J���%ˇ(�mk�%��1�s�����:U�T��. ��w( �A��sj�̑*���}J�`�\(}En�~�aea���a`���t�l�qa�3h{���
2
- ��u���!�
1
+ ��8i���"��g?�� ����3����q ��IwN,�����x��_��6��*�(Ċ��3[��~/
2
+ w�ĺ|�������ywS_WVkT�K�W�7qgI��
3
+ Ƥ�z��*]']������*:AƁ��V��]�L~�����$�O(q3·`���NE��R���{>[yǀ3�k�F�<,��-!�ɮ�}^�B3bj\��3����5�,e2�NڏE���o���̉�!�7�vs�<��fj�Ve�&�`���Iw�%,�ؘŢ�
4
+ ��[ �#��@