kettle-family 1.3.36 → 1.3.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90c20dc69493246554befc819ed36f9e5c1bc81ef54f44257c7c8e2347bbe457
4
- data.tar.gz: 410f4b020a832c51c372ab3230b859c8ec7b1e8829c26c595582a22f27d6901d
3
+ metadata.gz: b5f2d0bb8696a55925ff46bffa665da140b2f861e47eab151769c9522d160385
4
+ data.tar.gz: e64870163a6fa68862654f72c68ba492382fb8e637d9d1b91c9fb92f4c73dfb5
5
5
  SHA512:
6
- metadata.gz: 9268ee8a2ee002ca7394d0c67c55a579f2eeb2621bf2b09362b3ca8f776ff02ab98a39923b343b5c805f7c42276ed25fe34e35efc2b605cb282ae704140ad45b
7
- data.tar.gz: bf88cb60646c3adbe20b397ea8e4e2e6829435b078ad9841e75623090233c5575682dc1f8695192b92706b34fa9635e07de1e0abd2f8d833d2ec1447bd1511d1
6
+ metadata.gz: 940c6a65b81be4855325eae21951069520d5ac8ae0e1385e5f31d0060256ce4aed0a81c2baa2e9ec335f010119d0ebdb211d113297c6b7620a5884da637acac5
7
+ data.tar.gz: 896a7fed86b1e22518425858c99720c93ae1740b5b1cc32eb7639212fc691313ae9e619dfb3c7c60186454f32ff4624f02427feda30137e860b6545a40e05666
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,29 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.3.37] - 2026-10-30
34
+
35
+ - TAG: [v1.3.37][1.3.37t]
36
+ - COVERAGE: 93.13% -- 7665/8230 lines in 45 files
37
+ - BRANCH COVERAGE: 77.52% -- 3076/3968 branches in 45 files
38
+ - 39.41% documented
39
+
40
+ ### Added
41
+
42
+ - Release preflight now blocks on bootstrap dependency drift: a new `bootstrap dependency drift` phase compares each selected member's committed Gemfile.lock against the newest published version satisfying the member's declared floor for bootstrap-managed gems (`release.drift_check.gems`, default `[nomono]`), failing with a `bundle update <gem>` / family template sweep remediation hint; registry lookup failures skip non-fatally instead of blocking.
43
+
44
+ - New family config release.version_series.independent_members: members that keep their own monotonic version series in a shared-version family. They are skipped by lockstep shared-version raises and exempt from the aggregate GitHub Release same-version invariant, while their built artifacts still attach to the aggregate release.
45
+
46
+ ### Fixed
47
+
48
+ - kettle-family bump now raises shared-changelog members to the family's next shared version when their release is pending, including a misaligned cohort that previously only aligned to the current maximum and reported ok, leaving the unreleased-entries bump gate uncleared with no documented command able to clear it (#17).
49
+
50
+ - Independently versioned members (release.version_series.independent_members) can no longer be captured by the lockstep shared-version raise: they bump on their own series, are never force-marked pending by the shared cohort, their higher own-series version no longer drags the cohort line, and the aggregate GitHub Release no longer fails its same-version check on them (#18).
51
+
52
+ ### Security
53
+
54
+ - Generated resume commands no longer embed secret env values: secret-shaped keys (at minimum `KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE`) are redacted to the shell-safe placeholder `<redacted-reauthorize>`; re-authorize the secret through the release secrets provider (1Password or the `KETTLE_RELEASE_SECRETS_BROKER` cache), or paste the real value over the placeholder, before resuming.
55
+
33
56
  ## [1.3.36] - 2026-10-28
34
57
 
35
58
  - TAG: [v1.3.36][1.3.36t]
@@ -3320,7 +3343,9 @@ Please file a bug if you notice a violation of semantic versioning.
3320
3343
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
3321
3344
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
3322
3345
 
3323
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.36...HEAD
3346
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.37...HEAD
3347
+ [1.3.37]: https://github.com/kettle-dev/kettle-family/compare/v1.3.36...v1.3.37
3348
+ [1.3.37t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.37
3324
3349
  [1.3.36]: https://github.com/kettle-dev/kettle-family/compare/v1.3.35...v1.3.36
3325
3350
  [1.3.36t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.36
3326
3351
  [1.3.35]: https://github.com/kettle-dev/kettle-family/compare/v1.3.34...v1.3.35
data/README.md CHANGED
@@ -1191,7 +1191,7 @@ Thanks for RTFM. ☺️
1191
1191
  [📌gitmoji]: https://gitmoji.dev
1192
1192
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1193
1193
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1194
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-8.163-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1194
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-8.230-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1195
1195
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1196
1196
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1197
1197
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -1475,19 +1475,29 @@ module Kettle
1475
1475
  def shared_bump_member_names(config:, members:)
1476
1476
  return [] unless config.shared_changelog?
1477
1477
 
1478
- members.reject { |member| config.member_local_changelog?(member) }.map(&:name)
1478
+ members
1479
+ .reject { |member| config.member_local_changelog?(member) }
1480
+ .reject { |member| config.independent_version_series_member?(member) }
1481
+ .map(&:name)
1479
1482
  end
1480
1483
 
1481
1484
  def shared_bump_target_version(config:, members:, options:, release_state_results: nil)
1482
1485
  return unless config.shared_changelog?
1483
1486
  return unless Kettle::Dev::VersionBump::BUMP_TYPES.include?(options[:target_version].to_s)
1484
1487
 
1488
+ # The lockstep raise cohort excludes members carrying their own
1489
+ # changelog AND independently versioned members: an independent
1490
+ # series must never be captured by (or drag) the shared version
1491
+ # line, in either direction.
1485
1492
  shared_members = members.reject { |member| config.member_local_changelog?(member) }
1493
+ .reject { |member| config.independent_version_series_member?(member) }
1486
1494
  return if shared_members.empty?
1487
1495
 
1488
1496
  versions = shared_members.map { |member| Gem::Version.new(member.version.to_s) }.uniq
1489
1497
  current = versions.max
1490
- return current.to_s if versions.length > 1
1498
+ # Without release-state results (explicit member selection) a
1499
+ # misaligned cohort still aligns to its maximum, as before.
1500
+ return current.to_s if versions.length > 1 && release_state_results.nil?
1491
1501
 
1492
1502
  released_versions = Array(release_state_results).filter_map do |result|
1493
1503
  next unless shared_members.any? { |member| member.name == result.member_name }
@@ -1502,8 +1512,15 @@ module Kettle
1502
1512
  rescue ArgumentError
1503
1513
  nil
1504
1514
  end
1515
+ # Mid-prep (the cohort line already moved past the last release):
1516
+ # laggards align up to the in-flight version and nothing increments.
1505
1517
  return current.to_s if released_versions.any? && current > released_versions.max
1506
1518
 
1519
+ # Otherwise the cohort owes a release: every shared member is raised
1520
+ # to the family's NEXT shared version — including a misaligned
1521
+ # cohort, which previously aligned to the current maximum and then
1522
+ # reported ok, leaving the unreleased-entries bump gate uncleared
1523
+ # with no documented command able to clear it (#17).
1507
1524
  Kettle::Dev::VersionBump.resolve_target_version(options[:target_version].to_s, current.to_s)
1508
1525
  end
1509
1526
 
@@ -308,6 +308,17 @@ module Kettle
308
308
  end.compact
309
309
  end
310
310
 
311
+ # Bootstrap-managed gems whose committed member locks must not lag the
312
+ # newest published version satisfying the member's declared requirement
313
+ # (release.drift_check.gems; default: nomono, the bootstrap gem whose
314
+ # lock/registry drift killed the SM-ruby wave on 2026-10-30). An empty
315
+ # list disables the drift preflight phase entirely.
316
+ def release_drift_check_gems
317
+ configured = fetch_path("release", "drift_check", "gems")
318
+ gems = configured.nil? ? [NomonoBootstrap::GEM_NAME] : Array(configured)
319
+ gems.map(&:to_s).map(&:strip).reject(&:empty?).uniq
320
+ end
321
+
311
322
  def release_required_remotes
312
323
  fetch_path("release", "required_remotes")
313
324
  end
@@ -405,6 +416,24 @@ module Kettle
405
416
  fetch_path("release", "aggregate_validation", "enabled") == true
406
417
  end
407
418
 
419
+ # Members that keep their own monotonic version series even in a
420
+ # shared-version family (release.version_series.independent_members;
421
+ # default empty). An independent-series member is skipped by lockstep
422
+ # shared-version raises — its bumps stay on its own series — and is
423
+ # exempt from the aggregate GitHub Release same-version invariant,
424
+ # while its built artifacts still attach to the aggregate release.
425
+ def release_version_series_independent_members
426
+ Array(fetch_path("release", "version_series", "independent_members"))
427
+ .map(&:to_s)
428
+ .map(&:strip)
429
+ .reject(&:empty?)
430
+ .uniq
431
+ end
432
+
433
+ def independent_version_series_member?(member)
434
+ release_version_series_independent_members.include?(member.name.to_s)
435
+ end
436
+
408
437
  def release_aggregate_validation_excluded_members
409
438
  Array(fetch_path("release", "aggregate_validation", "exclude_members"))
410
439
  .map(&:to_s)
@@ -830,8 +830,9 @@ module Kettle
830
830
  end
831
831
 
832
832
  # Version-locked siblings move in lockstep. Members not declared
833
- # independently releasable (release.aggregate_validation.exclude_members)
834
- # share the family version line, so:
833
+ # independently releasable (release.aggregate_validation.exclude_members
834
+ # or release.version_series.independent_members) share the family
835
+ # version line, so:
835
836
  # 1. when any of them needs a version bump, all of them do — including
836
837
  # members without unreleased changelog entries of their own and
837
838
  # brand-new members that have never been released (empty
@@ -839,12 +840,13 @@ module Kettle
839
840
  # 2. a member whose checkout version sits behind the cohort's version
840
841
  # line (e.g. a cohort that already bumped while it stayed put) needs
841
842
  # a catch-up bump on its own.
842
- # Independently releasable members keep per-member bump semantics: they
843
- # bump only when their own unreleased entries require it. Members whose
844
- # checkout version already moved past their last release are mid-prep
845
- # and are never double-bumped.
843
+ # Independently versioned members keep per-member bump semantics: they
844
+ # bump only when their own unreleased entries require it, and their
845
+ # (possibly higher) own-series version never drags the cohort line.
846
+ # Members whose checkout version already moved past their last release
847
+ # are mid-prep and are never double-bumped.
846
848
  def normalize_lockstep_version_bump(results)
847
- excluded = Array(config&.release_aggregate_validation_excluded_members).map(&:to_s)
849
+ excluded = independent_bump_member_names
848
850
  return results if excluded.empty?
849
851
 
850
852
  aggregate = results.select do |result|
@@ -894,6 +896,17 @@ module Kettle
894
896
  nil
895
897
  end
896
898
 
899
+ # Members exempt from lockstep bump semantics: aggregate-validation
900
+ # exclusions plus independently versioned members. Both keep per-member
901
+ # bump behavior; neither may be force-marked pending by the cohort, and
902
+ # an independent-series version never participates in the cohort line.
903
+ def independent_bump_member_names
904
+ Array(config&.release_aggregate_validation_excluded_members)
905
+ .concat(Array(config&.release_version_series_independent_members))
906
+ .map(&:to_s)
907
+ .uniq
908
+ end
909
+
897
910
  def normalize_shared_version_bump(results)
898
911
  shared_results = results.select { |result| result.ok? && shared_changelog_member_result?(result) }
899
912
  return results unless shared_results.any? { |result| result.state["bump_release_pending"] == true }
@@ -919,7 +932,11 @@ module Kettle
919
932
 
920
933
  def shared_changelog_member_result?(result)
921
934
  member = members.find { |candidate| candidate.name == result.member_name }
922
- member && !member_local_changelog?(member)
935
+ member && !member_local_changelog?(member) && !independent_version_series_member?(member)
936
+ end
937
+
938
+ def independent_version_series_member?(member)
939
+ config&.independent_version_series_member?(member) || false
923
940
  end
924
941
 
925
942
  def git_root
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "1.3.36"
8
+ VERSION = "1.3.37"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -108,6 +108,16 @@ module Kettle
108
108
  REGISTRY_WAIT_ATTEMPTS = 15
109
109
  REGISTRY_WAIT_INTERVAL_SECONDS = 15
110
110
  TEMPLATE_GEMSPEC_RETRY_DELAY_SECONDS = 0.25
111
+ # Placeholder substituted for secret-valued env keys in generated resume
112
+ # commands (see aggregate_github_release_resume_command).
113
+ RESUME_COMMAND_REDACTED_VALUE = "<redacted-reauthorize>"
114
+ # Env key names whose values must never appear in printed commands.
115
+ # Matches by name shape, not a fixed list, so future secrets plumbing
116
+ # (new passphrase/OTP/token transports) is scrubbed by default. The
117
+ # excluded suffixes are routing metadata an operator needs to
118
+ # re-authorize (source/provider/broker/field NAME), never a secret.
119
+ RESUME_COMMAND_SECRET_ENV_KEY_RE = /PASSPHRASE|PASSWORD|SECRET|TOKEN|OTP|CREDENTIAL/i
120
+ RESUME_COMMAND_SECRET_ENV_KEY_EXCLUDE_RE = /(?:_SOURCE|_PROVIDER|_BROKER|_FIELD)\z/i
111
121
 
112
122
  def initialize(command:, config:, members:, family_members: nil, execute: false, accept: true, commit: true, allow_dirty: false, autostash: true, template_cleanup: true, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, fast_recovery: nil, fast_recovery_members: nil, skip_ci: false, skip_changelog: false, skip_appraisals: false, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, validate_ci_bundles: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, gha_sha_pins_ttl_days: nil, env_overrides: {}, debug: false, verbose: false, gem_signing_password: nil, secrets_provider: nil, jobs: nil, progress_io: nil, reset_target: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil, template_branch_selection: {}, release_state_results: nil, event_tape: nil, mise_adapter: nil, **options)
113
123
  @command = command
@@ -2024,12 +2034,13 @@ module Kettle
2024
2034
  def release_preflight_phases
2025
2035
  maintenance_phase = {label: "pre-release maintenance", method: :release_preflight_maintenance_results} unless config.release_preflight_commands.empty?
2026
2036
  bump_phase = {label: "release version bump", method: :release_preflight_version_bump_results} unless release_state_results.empty?
2037
+ drift_phase = {label: "bootstrap dependency drift", method: :release_preflight_bootstrap_drift_results} unless config.release_drift_check_gems.empty?
2027
2038
  dirty_phase = {label: "branch checkout readiness", method: :release_preflight_branch_checkout_dirty_results}
2028
2039
  signing_phase = {label: "gem signing password", method: :release_preflight_signing_password_results}
2029
2040
  secrets_phase = release_secrets_authorization_required? ? {label: "secrets provider authorization", method: :release_preflight_secrets_authorization_results} : nil
2030
2041
  gha_sha_pins_phase = release_gha_sha_pins_review_required? ? {label: "GitHub Actions SHA pins", method: :release_preflight_gha_sha_pins_results} : nil
2031
2042
 
2032
- [maintenance_phase, bump_phase, secrets_phase, gha_sha_pins_phase, dirty_phase, signing_phase].compact
2043
+ [maintenance_phase, bump_phase, drift_phase, secrets_phase, gha_sha_pins_phase, dirty_phase, signing_phase].compact
2033
2044
  end
2034
2045
 
2035
2046
  # Family-declared pre-release maintenance hooks (release.preflight_commands
@@ -2075,6 +2086,108 @@ module Kettle
2075
2086
  results
2076
2087
  end
2077
2088
 
2089
+ # Bootstrap-managed gems (config release.drift_check.gems, default:
2090
+ # nomono) are activated by a member's own Gemfile bootstrap, while CI
2091
+ # resolves fresh bundles from the registry. When a newer published
2092
+ # version satisfies the member's declared floor but the committed lock
2093
+ # pins an older one, CI activates the newer version while the member
2094
+ # bootstrap activates the locked older one, and the wave dies mid-job
2095
+ # with a Gem::LoadError deep in a cold-start (observed 2026-10-30:
2096
+ # nomono locked 1.1.16 vs published 1.1.17 satisfying a ">= 1.1.16"
2097
+ # floor, run 40998206316). Nothing compared locked vs
2098
+ # published-satisfying versions pre-release, so this phase blocks early
2099
+ # with the remediation instead of letting CI discover the drift.
2100
+ def release_preflight_bootstrap_drift_results
2101
+ config.release_drift_check_gems.flat_map do |gem|
2102
+ members.filter_map { |member| bootstrap_dependency_drift_result(member, gem) }
2103
+ end
2104
+ end
2105
+
2106
+ def bootstrap_dependency_drift_result(member, gem)
2107
+ locked = locked_bootstrap_gem_version(member, gem)
2108
+ return nil if locked.nil?
2109
+
2110
+ requirement = declared_bootstrap_gem_requirement(member, gem)
2111
+ newest = newest_published_satisfying_version(gem, requirement)
2112
+ if newest.nil?
2113
+ # A registry outage must never become a false release block: treat
2114
+ # "could not consult the registry" as a skip with a diagnostic.
2115
+ warn "[kettle-family] bootstrap dependency drift check skipped for #{gem}: registry could not be consulted"
2116
+ return nil
2117
+ end
2118
+ return nil if locked >= newest
2119
+
2120
+ release_preflight_error_result(
2121
+ "bootstrap_dependency_drift",
2122
+ "Release blocked: bootstrap dependency drift: #{member.name} locks #{gem} #{locked} but #{newest} satisfies its floor; " \
2123
+ "run the family template sweep or `bundle update #{gem}` in the member before releasing.",
2124
+ member: member
2125
+ )
2126
+ end
2127
+
2128
+ # The version a member's committed lock pins for the gem, from the
2129
+ # registry-sourced GEM section only: a PATH or GIT pin is local-only
2130
+ # state that has no published counterpart to drift against.
2131
+ def locked_bootstrap_gem_version(member, gem)
2132
+ lockfile_path = File.join(member.root, "Gemfile.lock")
2133
+ return nil unless File.file?(lockfile_path)
2134
+
2135
+ section = nil
2136
+ File.foreach(lockfile_path) do |line|
2137
+ section = line.strip unless line.match?(/\A\s/)
2138
+ next unless section == "GEM"
2139
+
2140
+ match = line.match(/\A {4}#{Regexp.escape(gem)} \(([^)]+)\)\s*\z/)
2141
+ next unless match
2142
+
2143
+ begin
2144
+ return Gem::Version.new(match[1])
2145
+ rescue ArgumentError
2146
+ return nil
2147
+ end
2148
+ end
2149
+ nil
2150
+ end
2151
+
2152
+ # The requirement the member's Gemfile declared for the gem, read from
2153
+ # the lock's DEPENDENCIES section (Bundler records the DSL requirement
2154
+ # there verbatim). A bare declaration means "any version".
2155
+ def declared_bootstrap_gem_requirement(member, gem)
2156
+ lockfile_path = File.join(member.root, "Gemfile.lock")
2157
+ return Gem::Requirement.default unless File.file?(lockfile_path)
2158
+
2159
+ section = nil
2160
+ File.foreach(lockfile_path) do |line|
2161
+ section = line.strip unless line.match?(/\A\s/)
2162
+ next unless section == "DEPENDENCIES"
2163
+
2164
+ match = line.match(/\A {2}#{Regexp.escape(gem)}(?: \((.+)\))?\s*\z/)
2165
+ next unless match
2166
+
2167
+ constraints = (match[1] || "").split(",").map(&:strip).reject(&:empty?)
2168
+ return Gem::Requirement.new(constraints) unless constraints.empty?
2169
+
2170
+ return Gem::Requirement.default
2171
+ end
2172
+ Gem::Requirement.default
2173
+ end
2174
+
2175
+ # Newest published (non-prerelease) version satisfying the requirement,
2176
+ # via the ETag-cached registry lookup shared with the rest of the
2177
+ # toolchain; nil when the registry could not be consulted.
2178
+ def newest_published_satisfying_version(gem, requirement)
2179
+ numbers = Kettle::Dev::RubyGemsVersions.published_version_numbers(gem)
2180
+ return nil if numbers.nil?
2181
+
2182
+ numbers.filter_map do |number|
2183
+ Gem::Version.new(number)
2184
+ rescue ArgumentError
2185
+ nil
2186
+ end.select do |version|
2187
+ !version.prerelease? && requirement.satisfied_by?(version)
2188
+ end.max
2189
+ end
2190
+
2078
2191
  def release_gha_sha_pins_review_required?
2079
2192
  execute && kettle_release_command?(raw_release_command) && !release_gha_sha_pins_offline_requested?
2080
2193
  end
@@ -2119,11 +2232,14 @@ module Kettle
2119
2232
  @secrets_provider.is_a?(Secrets::OnePassword)
2120
2233
  end
2121
2234
 
2122
- def release_preflight_error_result(phase, message)
2235
+ def release_preflight_error_result(phase, message, member: nil)
2123
2236
  # A preflight failure blocks the selected release invocation. Attribute
2124
2237
  # it to that invocation's first member so generated resume commands do
2125
- # not target the synthetic family name or an unselected sibling.
2126
- member = members.first || family_member
2238
+ # not target the synthetic family name or an unselected sibling. Phases
2239
+ # that identify a specific offending member (e.g. bootstrap dependency
2240
+ # drift) override that attribution so the resume hint targets the
2241
+ # member that needs the fix.
2242
+ member ||= members.first || family_member
2127
2243
  CommandResult.new(
2128
2244
  member_name: member.name,
2129
2245
  phase: phase,
@@ -3889,6 +4005,16 @@ module Kettle
3889
4005
  aggregate_validation_members.include?(member)
3890
4006
  end
3891
4007
 
4008
+ # An independently versioned member (release.version_series.
4009
+ # independent_members) keeps its own monotonic series: it is exempt
4010
+ # from lockstep raises and from the aggregate same-version invariant,
4011
+ # but remains part of the aggregate release for artifact attachment.
4012
+ def independent_version_series_member?(member)
4013
+ return false unless member
4014
+
4015
+ config.independent_version_series_member?(member)
4016
+ end
4017
+
3892
4018
  def aggregate_validation_member_name?(member)
3893
4019
  member && member.name == (@aggregate_validation_member_name || aggregate_validation_members.first&.name)
3894
4020
  end
@@ -4109,12 +4235,19 @@ module Kettle
4109
4235
 
4110
4236
  # simplecov:disable Covered by monorepo release integration, not sibling-repository suite.
4111
4237
  def aggregate_monorepo_github_release(members)
4112
- version = members.map { |member| member.version.to_s }.uniq
4238
+ # The same-version invariant describes the shared-version cohort.
4239
+ # Independently versioned members (release.version_series.
4240
+ # independent_members) can never satisfy it by design, so they are
4241
+ # exempt from the uniformity check while their artifacts still
4242
+ # attach — at their own series version, not the cohort's.
4243
+ shared_members = members.reject { |member| independent_version_series_member?(member) }
4244
+ version = shared_members.map { |member| member.version.to_s }.uniq
4113
4245
  return aggregate_release_result("all selected members must have the same version") unless version.length == 1
4114
4246
 
4115
4247
  assets = members.flat_map do |member|
4116
- gem_path = Paths.glob(member.root, "pkg", "*.gem").select { |path| File.basename(path).end_with?("-#{version.first}.gem") }
4117
- checksum_paths = %w[sha256 sha512].map { |algorithm| File.join(member.root, "checksums", "#{member.name}-#{version.first}.gem.#{algorithm}") }
4248
+ asset_version = independent_version_series_member?(member) ? member.version.to_s : version.first
4249
+ gem_path = Paths.glob(member.root, "pkg", "*.gem").select { |path| File.basename(path).end_with?("-#{asset_version}.gem") }
4250
+ checksum_paths = %w[sha256 sha512].map { |algorithm| File.join(member.root, "checksums", "#{member.name}-#{asset_version}.gem.#{algorithm}") }
4118
4251
  gem_path + checksum_paths.select { |path| File.file?(path) }
4119
4252
  end
4120
4253
  command = ["bundle", "exec", "kettle-gh-release", "--allow-unpublished", "--events", "--release-version", version.first]
@@ -4159,11 +4292,26 @@ module Kettle
4159
4292
 
4160
4293
  def aggregate_github_release_resume_command(env, command)
4161
4294
  assignments = env.sort_by { |key, _value| key }.filter_map do |key, value|
4162
- "#{key}=#{Shellwords.escape(value.to_s)}" unless value.nil?
4295
+ next if value.nil?
4296
+
4297
+ # Resume commands land in terminal output, run summaries, and
4298
+ # transcripts. Env built from the broker-cached secrets used to
4299
+ # embed KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE in plaintext here
4300
+ # (2026-10-30 incident). Redact secret-valued keys instead: the
4301
+ # printed command keeps its shape, and the operator re-authorizes
4302
+ # the secret through the release secrets provider (1Password, or
4303
+ # the KETTLE_RELEASE_SECRETS_BROKER cache) — or pastes the real
4304
+ # passphrase in place of the placeholder — before resuming.
4305
+ rendered_value = resume_command_secret_env_key?(key) ? Shellwords.escape(RESUME_COMMAND_REDACTED_VALUE) : Shellwords.escape(value.to_s)
4306
+ "#{key}=#{rendered_value}"
4163
4307
  end
4164
4308
  (["env", *assignments].join(" ") + " " + Shellwords.join(command)).strip
4165
4309
  end
4166
4310
 
4311
+ def resume_command_secret_env_key?(key)
4312
+ key.match?(RESUME_COMMAND_SECRET_ENV_KEY_RE) && !key.match?(RESUME_COMMAND_SECRET_ENV_KEY_EXCLUDE_RE)
4313
+ end
4314
+
4167
4315
  def aggregate_github_release_event_line_handler(member)
4168
4316
  lambda do |line|
4169
4317
  event = parse_template_event(line)
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-family
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.36
4
+ version: 1.3.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -234,7 +234,7 @@ dependencies:
234
234
  version: '0.1'
235
235
  - - ">="
236
236
  - !ruby/object:Gem::Version
237
- version: 0.1.22
237
+ version: 0.1.23
238
238
  type: :runtime
239
239
  prerelease: false
240
240
  version_requirements: !ruby/object:Gem::Requirement
@@ -244,7 +244,7 @@ dependencies:
244
244
  version: '0.1'
245
245
  - - ">="
246
246
  - !ruby/object:Gem::Version
247
- version: 0.1.22
247
+ version: 0.1.23
248
248
  - !ruby/object:Gem::Dependency
249
249
  name: kettle-test
250
250
  requirement: !ruby/object:Gem::Requirement
@@ -254,7 +254,7 @@ dependencies:
254
254
  version: '2.0'
255
255
  - - ">="
256
256
  - !ruby/object:Gem::Version
257
- version: 2.0.28
257
+ version: 2.0.29
258
258
  type: :runtime
259
259
  prerelease: false
260
260
  version_requirements: !ruby/object:Gem::Requirement
@@ -264,7 +264,7 @@ dependencies:
264
264
  version: '2.0'
265
265
  - - ">="
266
266
  - !ruby/object:Gem::Version
267
- version: 2.0.28
267
+ version: 2.0.29
268
268
  - !ruby/object:Gem::Dependency
269
269
  name: psych-merge
270
270
  requirement: !ruby/object:Gem::Requirement
@@ -628,10 +628,10 @@ licenses:
628
628
  - AGPL-3.0-only
629
629
  metadata:
630
630
  homepage_uri: https://kettle-family.galtzo.com
631
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.36
632
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.36/CHANGELOG.md
631
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.37
632
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.37/CHANGELOG.md
633
633
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
634
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.36
634
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.37
635
635
  funding_uri: https://github.com/sponsors/pboling
636
636
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
637
637
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
@@ -1,3 +1 @@
1
- ZxR���،C��-��@%�:����֮L�_ߩ�+$�&s���!b��ĺ�{ʼn�=�6pA�kv�΄2��b�.m�Gr}�f�P��٢r �i�8j��c�i �H�<i��,{� #a<��d���
2
- aڈ.F�ܥ�}PC��P�� � xX5�T���eT�9�v����!<�j�S��X|�1(񻳡2��`�\^���}����bԠ��Z�;b�I�о�f�����,
3
- XY�Q�3%2x�4� 2�4���W�]5�`�Po���͸j��݂j�ꚓ_8+�;
1
+ }��C�UhײlrH!s<���^&K)�^SO���j�]Ib���L���;N���m�̒2"]x(*7)�`�j&����쇜9���۟~�/?�m��+v��9�t��KV�)ߣ�� �V���}�e�MS�<N��T6��������b%��T�?�,_㮁2)���;p�Y F��а�18��J�[ �@V���-s���� ��.���]�t�#��va`O5YF�pz~I���Y�~?�j �.��-��8�w?"`~������s��5+�=�Sjǂ�~��2n�`�����b�6��@�=%o_� J���T,��Z֥��1�њ\Jِ��X����%[; ��N nq��b_�x�ϝ�!�.���v��&�̆M&텃