kettle-family 1.3.4 → 1.3.5

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: 99e6506531c29efe6259ee3f592eb50d654eeef9f7074a1179f4f61960d7efdd
4
- data.tar.gz: d3424e4481d1ced32dbb89228de517e3a0adf30a099883879c16fbb9f6feec8e
3
+ metadata.gz: 3e18a444e22f9d52b19ef8396ce75bc80b85bb579fb353889fecb8547ddc8bec
4
+ data.tar.gz: b91bb8aa9018297be574e1eda7ddc26c6b8f314692cd4c4881c95fa4fe52062a
5
5
  SHA512:
6
- metadata.gz: dfe9f450d1c65671d3599bfe3625952dc7106cfbfe896960bc236d0df04c033dd73116a94b49b5dba82502022ff503339e7a1457ff6c507888df0736489b5dd7
7
- data.tar.gz: 4fc7889e44bd08e673899b0f3ea040ecec5129e4b90ef2ef166f11c51de0e4a05c2066036a8a600bf9dc9e35fd495f95ac60f603799709ec08e0e74522f22ef8
6
+ metadata.gz: 50d83077bd30c1c673772be79765f02d263cf499b382355f8b647889255abb9608155e8a0434d6c98d549ddad5188186f50e56b9608537e47b1cb5f2c1cfec5a
7
+ data.tar.gz: e81978953b9a9d94d2c518d770cd72e6b6efbcd5a3fd2c010da97f72cb9107d71624bd126cdf1799c1e17276f3546adaca3246089e151b6bccc7edc04db4cef2
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,43 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.3.5] - 2026-10-05
34
+
35
+ - TAG: [v1.3.5][1.3.5t]
36
+ - COVERAGE: 93.16% -- 6636/7123 lines in 37 files
37
+ - BRANCH COVERAGE: 77.64% -- 2701/3479 branches in 37 files
38
+ - 33.64% documented
39
+
40
+ ### Added
41
+
42
+ - `Kettle::Family::ReleaseStateCheck` accepts a `branches:` scope, and `Kettle::Family::Selection.branches_matching` reports which of a member branch stack currently satisfies a release-state token. Both are the seams for per-branch release and bump decisions.
43
+
44
+ ### Changed
45
+
46
+ - A blocked release now reports phase `release_blocked` instead of `release_skip`, so a hard failure is never reported as a skipped member in the progress summary or report.
47
+
48
+ - [kc] kettle-jem/prepare: updated 12 project files:
49
+ - dependencies (12)
50
+
51
+ - [kc] kettle-jem/template: updated 6 project files:
52
+ - code and tests (1)
53
+ - dependencies (1)
54
+ - documentation (1)
55
+ - other (2)
56
+ - workflows (1)
57
+
58
+ ### Fixed
59
+
60
+ - Already-released detection for branch-stack members is now scoped to the branch being released instead of the whole stack, so a clean published branch is no longer reported as having unreleased changes and blocked with a release_skip failure.
61
+
62
+ - A published version whose release tag is followed only by post-release maintenance commits (checksums, dependency-floor lockfile reconciliation) is now skipped as already released; only changes to the gemspec, version file, or changelog after the tag make the published version stale.
63
+
64
+ - Release-state status-token selection (`--only unreleased|prepared|pending|bump`) no longer collapses a branch-stack member to whichever branch happened to be evaluated last; a member is selected when any of its branches matches, and multiple tokens are still ANDed within a single branch.
65
+
66
+ - `kettle-family bump` now bumps each branch of a member branch stack independently: branches with no pending version bump are reported as skipped instead of being bumped, so an already-released branch is no longer given version bumps it never needed. An explicit `--only MEMBER` remains an unconditional request and is not filtered.
67
+
68
+ - Release-state worktrees orphaned by a killed run are reaped on the next run. Directories are named `worktree-PID-RANDOM`, so only those whose owning PID is no longer running are removed; live worktrees from concurrent runs are left alone.
69
+
33
70
  ## [1.3.4] - 2026-10-02
34
71
 
35
72
  - TAG: [v1.3.4][1.3.4t]
@@ -2847,7 +2884,9 @@ Please file a bug if you notice a violation of semantic versioning.
2847
2884
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2848
2885
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2849
2886
 
2850
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.4...HEAD
2887
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.5...HEAD
2888
+ [1.3.5]: https://github.com/kettle-dev/kettle-family/compare/v1.3.4...v1.3.5
2889
+ [1.3.5t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.5
2851
2890
  [1.3.4]: https://github.com/kettle-dev/kettle-family/compare/v1.3.3...v1.3.4
2852
2891
  [1.3.4t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.4
2853
2892
  [1.3.3]: https://github.com/kettle-dev/kettle-family/compare/v1.3.2...v1.3.3
data/README.md CHANGED
@@ -1192,7 +1192,7 @@ Thanks for RTFM. ☺️
1192
1192
  [📌gitmoji]: https://gitmoji.dev
1193
1193
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1194
1194
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1195
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-7.046-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1195
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-7.123-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1196
1196
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1197
1197
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1198
1198
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -28,6 +28,16 @@ module Kettle
28
28
  main(argv, stdout: out, stderr: err)
29
29
  end
30
30
 
31
+ # Defined as a singleton method above the `private` keyword: a `private`
32
+ # instance modifier does not apply to `def self.` definitions.
33
+ def self.default_only_filter(command:, only:)
34
+ return only unless only.to_s.empty?
35
+ return "bump" if %w[bump bump-version].include?(command)
36
+ return "pending" if command == "release"
37
+
38
+ only
39
+ end
40
+
31
41
  module SharedOptions
32
42
  def self.included(base)
33
43
  base.option :root, value: {type: String, usage: "PATH"}, desc: "Workspace or family root"
@@ -188,7 +198,12 @@ module Kettle
188
198
  end
189
199
 
190
200
  def run_family(command, overrides = {})
191
- Kettle::Family::CLI.new(stdout: stdout, stderr: stderr).run_command(command, family_options(overrides))
201
+ effective = family_options(overrides)
202
+ # Thread the *effective* --only (including the command's default
203
+ # release-state token) downstream so per-branch logic sees the same
204
+ # selection the member selector used.
205
+ effective[:only] = Kettle::Family::CLI.default_only_filter(command: command, only: effective[:only])
206
+ Kettle::Family::CLI.new(stdout: stdout, stderr: stderr).run_command(command, effective)
192
207
  end
193
208
 
194
209
  def truthy_option?(name)
@@ -822,11 +837,7 @@ module Kettle
822
837
  end
823
838
 
824
839
  def default_only_filter(command:, only:)
825
- return only unless only.to_s.empty?
826
- return "bump" if %w[bump bump-version].include?(command)
827
- return "pending" if command == "release"
828
-
829
- only
840
+ self.class.default_only_filter(command: command, only: only)
830
841
  end
831
842
 
832
843
  def direct_member_only_names(only)
@@ -1122,6 +1133,17 @@ module Kettle
1122
1133
  memo.last.branch = branch
1123
1134
  break unless memo.last.ok?
1124
1135
 
1136
+ if (skipped = branch_bump_skip_result(
1137
+ member: member,
1138
+ branch: branch,
1139
+ command: command,
1140
+ options: options,
1141
+ release_state_results: release_state_results
1142
+ ))
1143
+ memo << skipped
1144
+ next
1145
+ end
1146
+
1125
1147
  branch_members = rediscovered_selected_members(config: member_config, selected_names: [member.name], command: command)
1126
1148
  branch_members = [member] if branch_members.empty?
1127
1149
  branch_results = command_results_for_current_branch(
@@ -1140,6 +1162,41 @@ module Kettle
1140
1162
  end
1141
1163
  end
1142
1164
 
1165
+ BUMP_COMMANDS = %w[bump bump-version].freeze
1166
+
1167
+ # Bumping a whole branch stack must be per-branch: a branch already
1168
+ # released with nothing pending has nothing to bump. `--only bump` is a
1169
+ # status-driven selection, so it is filtered by each branch's own
1170
+ # release-state. An explicit `--only MEMBER` is a direct request and is
1171
+ # honored as given.
1172
+ def branch_bump_skip_result(member:, branch:, command:, options:, release_state_results:)
1173
+ return unless BUMP_COMMANDS.include?(command)
1174
+ return unless status_token_only?(options[:only])
1175
+
1176
+ matching = Selection.branches_matching(release_state_results, member_name: member.name, tokens: %w[bump])
1177
+ return if matching.empty? || matching.include?(branch)
1178
+
1179
+ CommandResult.new(
1180
+ member_name: member.name,
1181
+ phase: command,
1182
+ command: ["internal", "branch-bump-skip", branch],
1183
+ workdir: member.root,
1184
+ status: 0,
1185
+ success: true,
1186
+ stdout: "#{member.name}@#{branch} has no unreleased entries at its released version; skipping bump on this branch",
1187
+ stderr: "",
1188
+ elapsed_seconds: 0.0,
1189
+ skipped: true,
1190
+ reason: "no version bump pending on #{branch}",
1191
+ branch: branch
1192
+ )
1193
+ end
1194
+
1195
+ def status_token_only?(only)
1196
+ names = only.to_s.split(",").map(&:strip).reject(&:empty?)
1197
+ !names.empty? && names.all? { |name| Selection.status_token?(name) }
1198
+ end
1199
+
1143
1200
  def rediscovered_selected_members(config:, selected_names:, command:)
1144
1201
  discovered = Discovery.new(config: config).members
1145
1202
  ordered = (command == "install") ? install_order(discovered, config) : Orderer.new(members: discovered, mode: config.order_mode, hints: config.order_hints).ordered
@@ -25,10 +25,16 @@ module Kettle
25
25
  CACHE_MISS = Object.new.freeze
26
26
  WORKTREE_ADMIN_MUTEX = Mutex.new
27
27
 
28
- def initialize(members:, config: nil, jobs: nil)
28
+ # +branches+ scopes the check to specific release target branches. A
29
+ # branch-stack member produces one result per branch; a per-branch
30
+ # decision (is this branch already published? does it need a bump?) must
31
+ # never consult the whole stack, or every branch inherits the state of
32
+ # the branch that happens to have unreleased entries.
33
+ def initialize(members:, config: nil, jobs: nil, branches: nil)
29
34
  @members = members
30
35
  @config = config
31
36
  @jobs = jobs
37
+ @branches = branches&.map(&:to_s)&.reject(&:empty?)
32
38
  @github_latest_release_by_repo = {}
33
39
  @transfer_changelog_status_by_root = {}
34
40
  @transfer_changelog_lag_by_key = {}
@@ -38,6 +44,7 @@ module Kettle
38
44
  end
39
45
 
40
46
  def results(event_handler: nil)
47
+ return scoped_checkout_results(event_handler: event_handler) if checked_out_scoped_results?
41
48
  return branch_results(event_handler: event_handler) unless release_target_branches.empty?
42
49
  if shared_changelog?
43
50
  results = parallel_map(members) { |member| check_shared_changelog_member_or_local(member, event_handler: event_handler) }
@@ -53,7 +60,25 @@ module Kettle
53
60
 
54
61
  private
55
62
 
56
- attr_reader :members, :config, :jobs
63
+ attr_reader :members, :config, :jobs, :branches
64
+
65
+ # A single-branch scope whose checkout already sits on that branch needs
66
+ # no worktree: the primary checkout is the branch under test.
67
+ def checked_out_scoped_results?
68
+ return false unless branches&.one?
69
+
70
+ members.any? { |member| current_branch_for(member) == branches.first }
71
+ end
72
+
73
+ def scoped_checkout_results(event_handler: nil)
74
+ branch = branches.first
75
+ results = parallel_map(members) { |member| check_member(member, branch: branch, event_handler: event_handler) }
76
+ results.flatten
77
+ end
78
+
79
+ def current_branch_for(member)
80
+ git_output(member.root, "branch", "--show-current")
81
+ end
57
82
 
58
83
  def branch_results(event_handler: nil)
59
84
  root = git_root
@@ -769,6 +794,7 @@ module Kettle
769
794
 
770
795
  def release_target_branches
771
796
  return [] unless config
797
+ return @branches if @branches
772
798
 
773
799
  config.release_target_branches
774
800
  end
@@ -840,6 +866,7 @@ module Kettle
840
866
  with_worktree_registry_lock(root: root) do
841
867
  base = File.join(root, "tmp", "kettle-family-release-state")
842
868
  FileUtils.mkdir_p(base)
869
+ prune_stale_worktrees(root: root)
843
870
  entries = branches.map do |branch|
844
871
  {branch: branch, worktree_root: File.join(base, "worktree-#{Process.pid}-#{SecureRandom.hex(8)}")}
845
872
  end
@@ -869,6 +896,40 @@ module Kettle
869
896
  end
870
897
  end
871
898
 
899
+ # A killed run (SIGKILL, terminal close) skips the ensure-block cleanup and
900
+ # leaves registered worktrees behind. Registered-but-ownerless worktrees
901
+ # are pinned refs that accumulate forever, so reap them on the way in.
902
+ def prune_stale_worktrees(root:)
903
+ base = File.join(root, "tmp", "kettle-family-release-state")
904
+ WORKTREE_ADMIN_MUTEX.synchronize do
905
+ Open3.capture3("git", "worktree", "prune", chdir: root)
906
+ orphaned_worktree_paths(base).each do |path|
907
+ Open3.capture3("git", "worktree", "remove", "--force", path, chdir: root)
908
+ FileUtils.rm_rf(path)
909
+ end
910
+ end
911
+ end
912
+
913
+ # Worktree directories are named worktree-PID-RANDOM. A PID that is no
914
+ # longer running cannot still own a live worktree.
915
+ def orphaned_worktree_paths(base)
916
+ Dir[File.join(base, "worktree-*")].select do |path|
917
+ pid = File.basename(path)[/worktree-(\d+)-/, 1]
918
+ next false if pid.nil? || pid == Process.pid.to_s
919
+
920
+ !process_alive?(pid)
921
+ end
922
+ end
923
+
924
+ def process_alive?(pid)
925
+ Process.kill(0, pid)
926
+ true
927
+ rescue Errno::ESRCH
928
+ false
929
+ rescue Errno::EPERM
930
+ true
931
+ end
932
+
872
933
  def add_branch_worktree(root:, branch:, worktree_root:)
873
934
  WORKTREE_ADMIN_MUTEX.synchronize do
874
935
  _stdout, stderr, status = Open3.capture3("git", "worktree", "add", "--detach", worktree_root, branch, chdir: root)
@@ -517,6 +517,7 @@ module Kettle
517
517
 
518
518
  def release_terminal_result?(result)
519
519
  result.phase == "release_skip" ||
520
+ result.phase == "release_blocked" ||
520
521
  result.phase == "release_publish" ||
521
522
  result.phase == "release_build"
522
523
  end
@@ -21,6 +21,21 @@ module Kettle
21
21
  STATUS_TOKEN_KEYS.key?(value.to_s)
22
22
  end
23
23
 
24
+ # Defined as a singleton method above the `private` keyword: a `private`
25
+ # instance modifier does not apply to `def self.` definitions.
26
+ #
27
+ # Branches of one member that currently satisfy +tokens+. Used to decide
28
+ # per-branch bump work, so a bump never touches a branch that has nothing
29
+ # to release.
30
+ def self.branches_matching(release_state_results, member_name:, tokens:)
31
+ keys = Array(tokens).map(&:to_s).filter_map { |token| STATUS_TOKEN_KEYS[token] }
32
+ return [] if keys.empty?
33
+
34
+ Array(release_state_results).select do |result|
35
+ result.member_name == member_name && keys.all? { |key| result.state[key] == true }
36
+ end.map(&:branch).compact.uniq
37
+ end
38
+
24
39
  def self.validate_release_state_only_filter!(only)
25
40
  names = only.to_s.split(",").map(&:strip).reject(&:empty?)
26
41
  status_tokens = names.select { |name| status_token?(name) }
@@ -83,22 +98,29 @@ module Kettle
83
98
  selected.drop(index)
84
99
  end
85
100
 
101
+ # A branch-stack member yields one release-state result per branch, all
102
+ # sharing a member_name. Collapsing them by member_name alone would let
103
+ # whichever branch came last decide selection for every branch, so a
104
+ # member qualifies when *any* of its branches matches the token.
86
105
  def select_release_state_status(selected, status_tokens)
87
106
  results_by_member = release_state_results_by_member
88
- failed = results_by_member.values.select { |result| !result.ok? }
89
- raise Error, "release-state check failed for: #{failed.map(&:member_name).join(", ")}" unless failed.empty?
107
+ failed = results_by_member.values.flatten.select { |result| !result.ok? }
108
+ raise Error, "release-state check failed for: #{failed.map(&:member_name).uniq.join(", ")}" unless failed.empty?
90
109
 
91
110
  selected.select do |candidate|
92
- result = results_by_member[candidate.name]
93
- result && status_tokens.all? { |token| truthy_state?(result.state[STATUS_TOKEN_KEYS.fetch(token)]) }
111
+ Array(results_by_member[candidate.name]).any? do |result|
112
+ status_tokens.all? { |token| truthy_state?(result.state[STATUS_TOKEN_KEYS.fetch(token)]) }
113
+ end
94
114
  end
95
115
  end
96
116
 
117
+ # Branch-stack members report one result per branch, all sharing a member
118
+ # name, so results are grouped per member instead of collapsed to one.
97
119
  def release_state_results_by_member
98
120
  raise Error, "--only release-state tokens require release-state results" unless release_state_results
99
121
 
100
122
  release_state_results.each_with_object({}) do |result, memo|
101
- memo[result.member_name] = result
123
+ (memo[result.member_name] ||= []) << result
102
124
  end
103
125
  end
104
126
 
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "1.3.4"
8
+ VERSION = "1.3.5"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -3978,28 +3978,38 @@ module Kettle
3978
3978
  @release_progress&.update(member, status: "ci bundle #{relative} #{attempt}/#{REGISTRY_WAIT_ATTEMPTS}", mark: ">")
3979
3979
  end
3980
3980
 
3981
+ # A published version only means "already released" when nothing
3982
+ # releasable landed after its tag. kettle-release commits checksums
3983
+ # (and the family commits dependency-floor lockfiles) *after* tagging,
3984
+ # so comparing HEAD to the tag directly never matches; compare the
3985
+ # release-relevant paths instead.
3981
3986
  def already_released_result(member)
3982
3987
  tag = release_tag_name(member.version)
3983
- current_release_head = released_version_current_head?(member, tag)
3984
- if current_release_head
3985
- stdout = "#{member.name} #{member.version} is already published and current HEAD matches #{tag}; skipping release"
3988
+ branch = current_member_branch(member)
3989
+ scope = branch ? "#{member.name}@#{branch}" : member.name
3990
+ if no_releasable_changes_since_tag?(member, tag)
3991
+ stdout = "#{scope} #{member.version} is already published#{branch_suffix(branch)} and nothing releasable " \
3992
+ "landed after #{tag}; skipping release"
3986
3993
  reason = "already released"
3987
3994
  skipped = true
3988
- elsif unreleased_changes_pending?(member)
3989
- stdout = "#{member.name} #{member.version} is already published, but release-state reports unreleased changes. " \
3990
- "Bump the version with `kettle-family bump patch --execute --only #{member.name}` before releasing."
3995
+ elsif unreleased_changes_pending?(member, branch: branch)
3996
+ stdout = "#{scope} #{member.version} is already published#{branch_suffix(branch)}, but release-state reports " \
3997
+ "unreleased changes on this branch. Bump only this branch with " \
3998
+ "`kettle-family bump patch --execute --only #{member.name}` before releasing."
3991
3999
  reason = "published version has unreleased changes"
3992
4000
  skipped = false
3993
4001
  else
3994
- stdout = "#{member.name} #{member.version} is already published, current HEAD is newer than #{tag}, " \
3995
- "and release-state reports no unreleased changes; skipping release"
4002
+ stdout = "#{scope} #{member.version} is already published#{branch_suffix(branch)}, and release-state reports " \
4003
+ "no unreleased changes on this branch; skipping release"
3996
4004
  reason = "already released; no unreleased changes"
3997
4005
  skipped = true
3998
4006
  end
3999
4007
 
4000
4008
  CommandResult.new(
4001
4009
  member_name: member.name,
4002
- phase: "release_skip",
4010
+ # A blocked release is a distinct terminal phase from a skip: it is a
4011
+ # hard failure and must never be reported as "skipped".
4012
+ phase: skipped ? "release_skip" : "release_blocked",
4003
4013
  command: ["internal", "released-version-check", member.version],
4004
4014
  workdir: member.root,
4005
4015
  status: skipped ? 0 : 1,
@@ -4008,12 +4018,34 @@ module Kettle
4008
4018
  stderr: "",
4009
4019
  elapsed_seconds: 0.0,
4010
4020
  skipped: skipped,
4011
- reason: reason
4021
+ reason: reason,
4022
+ branch: branch
4012
4023
  )
4013
4024
  end
4014
4025
 
4015
- def unreleased_changes_pending?(member)
4016
- results = ReleaseStateCheck.new(members: [member], config: config).results
4026
+ def branch_suffix(branch)
4027
+ branch ? " on #{branch}" : ""
4028
+ end
4029
+
4030
+ def current_member_branch(member)
4031
+ return unless git_work_tree?(member.root)
4032
+
4033
+ stdout, _stderr, status = Open3.capture3("git", "branch", "--show-current", chdir: member.root)
4034
+ return unless status.success?
4035
+
4036
+ branch = stdout.strip
4037
+ branch.empty? ? nil : branch
4038
+ end
4039
+
4040
+ # Scope the check to the branch being released. Consulting the whole
4041
+ # branch stack here would report every already-published branch as
4042
+ # having unreleased changes whenever any other branch in the stack does.
4043
+ def unreleased_changes_pending?(member, branch: nil)
4044
+ results = ReleaseStateCheck.new(
4045
+ members: [member],
4046
+ config: config,
4047
+ branches: branch ? [branch] : nil
4048
+ ).results
4017
4049
  return true unless results.all?(&:ok?)
4018
4050
 
4019
4051
  results.any? { |result| result.state.fetch("unreleased_entries", true) }
@@ -4023,12 +4055,42 @@ module Kettle
4023
4055
  "v#{version}"
4024
4056
  end
4025
4057
 
4026
- def released_version_current_head?(member, tag)
4027
- return true unless git_work_tree?(member.root)
4058
+ # Post-release maintenance commits (checksums, lockfile reconciliation,
4059
+ # docs) land after the tag by design, so "HEAD != tag" cannot mean
4060
+ # "needs a release". Only a change to the gemspec, version file, or
4061
+ # changelog between the tag and HEAD makes the published version stale.
4062
+ def no_releasable_changes_since_tag?(member, tag)
4063
+ return false unless git_work_tree?(member.root)
4028
4064
 
4029
4065
  tag_sha = git_rev_parse(member.root, "refs/tags/#{tag}^{}")
4030
- head_sha = git_rev_parse(member.root, "HEAD")
4031
- !tag_sha.to_s.empty? && tag_sha == head_sha
4066
+ return false if tag_sha.to_s.empty?
4067
+ return true if tag_sha == git_rev_parse(member.root, "HEAD")
4068
+
4069
+ releasable_changes_since_tag(member, tag).empty?
4070
+ end
4071
+
4072
+ def releasable_changes_since_tag(member, tag)
4073
+ paths = releasable_paths(member)
4074
+ return [] if paths.empty?
4075
+
4076
+ stdout, _stderr, status = Open3.capture3(
4077
+ "git", "diff", "--name-only", "#{tag}^{}", "HEAD", "--", *paths, chdir: member.root
4078
+ )
4079
+ # An unreadable diff is not evidence of "nothing to release".
4080
+ return [tag] unless status.success?
4081
+
4082
+ stdout.lines.map(&:strip).reject(&:empty?)
4083
+ end
4084
+
4085
+ def releasable_paths(member)
4086
+ paths = [member.gemspec_path, member.version_file, config.changelog_full_path(member)]
4087
+ paths.compact.filter_map do |path|
4088
+ next unless File.file?(path)
4089
+
4090
+ Pathname.new(path).relative_path_from(Pathname.new(member.root)).to_s
4091
+ rescue ArgumentError
4092
+ path.to_s
4093
+ end.uniq
4032
4094
  end
4033
4095
 
4034
4096
  def git_work_tree?(root)
@@ -4634,7 +4696,9 @@ module Kettle
4634
4696
  def emit_release_progress_summary(results, progress:)
4635
4697
  return unless progress
4636
4698
 
4637
- release_results = results.select { |result| result.phase == release_phase || result.phase == "release_skip" }
4699
+ release_results = results.select do |result|
4700
+ result.phase == release_phase || result.phase == "release_skip" || result.phase == "release_blocked"
4701
+ end
4638
4702
  progress.stop
4639
4703
  progress.summary("release summary: #{release_results.count(&:ok?)}/#{release_results.length} members ok")
4640
4704
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-family
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.4
4
+ version: 1.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -94,7 +94,7 @@ dependencies:
94
94
  version: '3.1'
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 3.1.5
97
+ version: 3.1.7
98
98
  type: :runtime
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ dependencies:
104
104
  version: '3.1'
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 3.1.5
107
+ version: 3.1.7
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: kettle-gha-pins
110
110
  requirement: !ruby/object:Gem::Requirement
@@ -304,7 +304,7 @@ dependencies:
304
304
  version: '3.2'
305
305
  - - ">="
306
306
  - !ruby/object:Gem::Version
307
- version: 3.2.4
307
+ version: 3.2.5
308
308
  type: :development
309
309
  prerelease: false
310
310
  version_requirements: !ruby/object:Gem::Requirement
@@ -314,7 +314,7 @@ dependencies:
314
314
  version: '3.2'
315
315
  - - ">="
316
316
  - !ruby/object:Gem::Version
317
- version: 3.2.4
317
+ version: 3.2.5
318
318
  - !ruby/object:Gem::Dependency
319
319
  name: turbo_tests2
320
320
  requirement: !ruby/object:Gem::Requirement
@@ -324,7 +324,7 @@ dependencies:
324
324
  version: '3.2'
325
325
  - - ">="
326
326
  - !ruby/object:Gem::Version
327
- version: 3.2.11
327
+ version: 3.2.13
328
328
  type: :development
329
329
  prerelease: false
330
330
  version_requirements: !ruby/object:Gem::Requirement
@@ -334,21 +334,7 @@ dependencies:
334
334
  version: '3.2'
335
335
  - - ">="
336
336
  - !ruby/object:Gem::Version
337
- version: 3.2.11
338
- - !ruby/object:Gem::Dependency
339
- name: webrick
340
- requirement: !ruby/object:Gem::Requirement
341
- requirements:
342
- - - "~>"
343
- - !ruby/object:Gem::Version
344
- version: '1.9'
345
- type: :development
346
- prerelease: false
347
- version_requirements: !ruby/object:Gem::Requirement
348
- requirements:
349
- - - "~>"
350
- - !ruby/object:Gem::Version
351
- version: '1.9'
337
+ version: 3.2.13
352
338
  - !ruby/object:Gem::Dependency
353
339
  name: ruby-progressbar
354
340
  requirement: !ruby/object:Gem::Requirement
@@ -392,7 +378,7 @@ dependencies:
392
378
  version: '2.0'
393
379
  - - ">="
394
380
  - !ruby/object:Gem::Version
395
- version: 2.0.13
381
+ version: 2.0.15
396
382
  type: :development
397
383
  prerelease: false
398
384
  version_requirements: !ruby/object:Gem::Requirement
@@ -402,7 +388,21 @@ dependencies:
402
388
  version: '2.0'
403
389
  - - ">="
404
390
  - !ruby/object:Gem::Version
405
- version: 2.0.13
391
+ version: 2.0.15
392
+ - !ruby/object:Gem::Dependency
393
+ name: webrick
394
+ requirement: !ruby/object:Gem::Requirement
395
+ requirements:
396
+ - - "~>"
397
+ - !ruby/object:Gem::Version
398
+ version: '1.9'
399
+ type: :development
400
+ prerelease: false
401
+ version_requirements: !ruby/object:Gem::Requirement
402
+ requirements:
403
+ - - "~>"
404
+ - !ruby/object:Gem::Version
405
+ version: '1.9'
406
406
  description: "\U0001F469‍\U0001F469‍\U0001F467‍\U0001F467 Kettle::Family provides
407
407
  scripts and conventions for coordinating related Ruby gems as one family."
408
408
  email:
@@ -459,10 +459,10 @@ licenses:
459
459
  - AGPL-3.0-only
460
460
  metadata:
461
461
  homepage_uri: https://kettle-family.galtzo.com
462
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.4
463
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.4/CHANGELOG.md
462
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.5
463
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.5/CHANGELOG.md
464
464
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
465
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.4
465
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.5
466
466
  funding_uri: https://github.com/sponsors/pboling
467
467
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
468
468
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file