kettle-dev 3.0.34 → 3.1.1

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: cf4388ae02268bbe6676ece5cee25b5548277a6e60432554c460f189ca02d2f0
4
- data.tar.gz: 80841fd013dbd2868b22e36b185c18c0a7f90ed3698f5cb3a2d666c46a80b41b
3
+ metadata.gz: eaf2fc5f85eac56d87d38a8029c740631c50410cfb76c35d5e488df48374f170
4
+ data.tar.gz: d229bb60bdd25990a7e9666dcd7f54f61c42b58ecb695c63f70db3e1a17f2802
5
5
  SHA512:
6
- metadata.gz: 713c1d308268ce8cde18124db06ad5efed08d53fc945af0c723843de08d840b6db23f946941de187b2adcf61e482e206e307fda8bbd52b562c3aaed1fd61ee91
7
- data.tar.gz: 8e94e9a97e3ae52b955fd098db642ab23639fb6e0d67b2abed14fe8684248430c50618b8f8d6f3d0a57bb1934954e1f41fe7b17711fac151bdca9928b98a0795
6
+ metadata.gz: c439b9406442d64e949a50fc2669eb1eb4a94a43e36cde5c71797e86d16e4278cff16bf582e212b54b1cf50ffbdaa982a334ffd25f4f8c12b6d887b6c8565aad
7
+ data.tar.gz: '0920ce88eb1b862b8071d1958ef9c638dc55166b68c6abab21b6b93d96fea1e8656187e00086c5a43ee2fb2dd408bb8c737cf5a524a86cab1fbd19e09d6fc90b'
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,30 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.1.1] - 2026-09-14
34
+
35
+ - TAG: [v3.1.1][3.1.1t]
36
+ - COVERAGE: 91.54% -- 6138/6705 lines in 48 files
37
+ - BRANCH COVERAGE: 76.08% -- 2344/3081 branches in 48 files
38
+ - 51.02% documented
39
+
40
+ ### Fixed
41
+
42
+ - Close release-created branch-stack validation PRs after successful publication instead of leaving obsolete conflicted PRs open.
43
+
44
+ ## [3.1.0] - 2026-09-10
45
+
46
+ - TAG: [v3.1.0][3.1.0t]
47
+ - COVERAGE: 91.86% -- 6141/6685 lines in 48 files
48
+ - BRANCH COVERAGE: 76.10% -- 2337/3071 branches in 48 files
49
+ - 51.02% documented
50
+
51
+ ### Fixed
52
+
53
+ - [kc] release-graph-contracts: Honor the serialized release graph for lockfile normalization and child commands, retaining only declared CI-resident monorepo paths.
54
+
55
+ - Validate serialized family release graph contracts in release lockfile normalization and child commands instead of reconstructing local-path policy from ambient environment.
56
+
33
57
  ## [3.0.34] - 2026-09-09
34
58
 
35
59
  - TAG: [v3.0.34][3.0.34t]
@@ -3847,7 +3871,11 @@ Please file a bug if you notice a violation of semantic versioning.
3847
3871
  - Selecting will run the selected workflow via `act`
3848
3872
  - This may move to its own gem in the future.
3849
3873
 
3850
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.34...HEAD
3874
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.1...HEAD
3875
+ [3.1.1]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.0...v3.1.1
3876
+ [3.1.1t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.1
3877
+ [3.1.0]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.34...v3.1.0
3878
+ [3.1.0t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.0
3851
3879
  [3.0.34]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.33...v3.0.34
3852
3880
  [3.0.34t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.0.34
3853
3881
  [3.0.33]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.32...v3.0.33
data/README.md CHANGED
@@ -1066,7 +1066,7 @@ Thanks for RTFM. ☺️
1066
1066
  [📌gitmoji]: https://gitmoji.dev
1067
1067
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1068
1068
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1069
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.635-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1069
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.705-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1070
1070
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
1071
1071
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1072
1072
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -9,6 +9,7 @@ require "bundler"
9
9
  require "ripper"
10
10
 
11
11
  require_relative "bundler_env_guard"
12
+ require_relative "release_graph_contract"
12
13
 
13
14
  module Kettle
14
15
  module Dev
@@ -22,8 +23,6 @@ module Kettle
22
23
  "GALTZO_FLOSS_DEV" => "false",
23
24
  "UR_BRAIN_DEV" => "false"
24
25
  }.freeze
25
- ALLOWED_LOCAL_PATH_ROOTS_ENV = "KETTLE_RELEASE_ALLOWED_LOCAL_PATH_ROOTS"
26
- ALLOWED_LOCAL_PATH_ENVS_ENV = "KETTLE_RELEASE_ALLOWED_LOCAL_PATH_ENVS"
27
26
  RELEASE_LOCKFILES_TARGET = "release-lockfiles"
28
27
  SUPPORTED_TARGETS = ["Gemfile.lock", "Appraisal.root.gemfile.lock", RELEASE_LOCKFILES_TARGET].freeze
29
28
  UNBUNDLED_ENV_KEYS = (BundlerEnvGuard::RESET_ENV_KEYS + %w[
@@ -113,28 +112,30 @@ module Kettle
113
112
  @root = root
114
113
  @command_runner = command_runner
115
114
  @release_platforms = {}
116
- @allowed_local_path_roots = configured_allowed_local_path_roots
117
- @allowed_local_path_env_names = configured_allowed_local_path_env_names
115
+ @release_graph_contract = ReleaseGraphContract.from_environment(root: root)
118
116
  end
119
117
 
120
118
  def reset(target, skip_changelog_dependency: false)
121
119
  BundlerEnvGuard.warn_unexpected_env!
122
120
  paths = lockfile_paths_for(target)
123
- # A monorepo lockfile may deliberately use paths rooted in the
124
- # repository's members directory. Keep that valid development graph;
125
- # only rebuild when an unapproved path or other diagnostic remains.
126
- force_full_update = release_lockfiles_target?(target) &&
127
- (allowed_local_path_roots.empty? || paths.any? { |path| normalization_needed?(path) })
121
+ release_lockfiles = release_lockfiles_target?(target)
122
+ # The selected graph governs both tracked release locks and disposable
123
+ # release-task locks. Registry contracts reject every PATH source;
124
+ # CI-resident monorepo contracts retain only declared contained paths.
125
+ force_full_update = release_lockfiles && (
126
+ release_graph_contract.registry_only? || paths.any? { |path| normalization_needed?(path, strict: true) }
127
+ )
128
128
  platforms = release_platforms_for(paths) if force_full_update
129
129
  uninstall_unreleased_local_gems(paths) if force_full_update
130
130
  paths.each do |path|
131
- if force_full_update || normalization_needed?(path)
131
+ if force_full_update || normalization_needed?(path, strict: release_lockfiles)
132
132
  reset_lockfile!(
133
133
  path,
134
134
  full_update: force_full_update,
135
135
  platforms: platforms&.fetch(path),
136
136
  skip_changelog_dependency: skip_changelog_dependency,
137
- update_bundler: force_full_update
137
+ update_bundler: force_full_update,
138
+ strict: release_lockfiles
138
139
  )
139
140
  end
140
141
  end
@@ -145,17 +146,18 @@ module Kettle
145
146
  full_update: true,
146
147
  platforms: platforms.fetch(path),
147
148
  skip_changelog_dependency: skip_changelog_dependency,
148
- update_bundler: true
149
+ update_bundler: true,
150
+ strict: release_lockfiles
149
151
  )
150
152
  end
151
153
  end
152
- diagnostics = paths.flat_map { |path| diagnostics(path) }
154
+ diagnostics = paths.flat_map { |path| diagnostics(path, strict: release_lockfiles) }
153
155
  raise Error, validation_message(target, diagnostics) unless diagnostics.empty?
154
156
 
155
157
  paths
156
158
  end
157
159
 
158
- def reset_lockfile!(path, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false)
160
+ def reset_lockfile!(path, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false, strict: false)
159
161
  gemfile = gemfile_for_lockfile(path)
160
162
  unless gemfile && File.file?(gemfile)
161
163
  warn("Cannot reset #{display_path(path)} because its Gemfile was not found.")
@@ -171,9 +173,10 @@ module Kettle
171
173
  platforms: platforms,
172
174
  skip_changelog_dependency: skip_changelog_dependency,
173
175
  update_bundler: update_bundler,
174
- isolated_gem_home: gem_home
176
+ isolated_gem_home: gem_home,
177
+ strict: strict
175
178
  )
176
- if full_update || has_local_path_remote?(path)
179
+ if full_update || has_local_path_remote?(path, strict: strict)
177
180
  rebuild_lockfile(path) { command_runner.call(command) }
178
181
  else
179
182
  command_runner.call(command)
@@ -181,11 +184,11 @@ module Kettle
181
184
  end
182
185
  end
183
186
 
184
- def reset_command(path:, gemfile:, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false, isolated_gem_home: nil, isolated_gem_path: nil)
185
- update_gems = (full_update || has_local_path_remote?(path)) ? [] : reset_update_gems(path)
187
+ def reset_command(path:, gemfile:, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false, isolated_gem_home: nil, isolated_gem_path: nil, strict: false)
188
+ update_gems = (full_update || has_local_path_remote?(path, strict: strict)) ? [] : reset_update_gems(path)
186
189
  platforms ||= reset_platforms(path)
187
190
  removed_platforms = full_update ? [] : lockfile_platforms(path) - platforms
188
- env = normalization_env.merge(
191
+ env = (strict ? release_normalization_env : normalization_env).merge(
189
192
  "BUNDLE_GEMFILE" => gemfile,
190
193
  "BUNDLE_LOCKFILE" => path
191
194
  )
@@ -263,14 +266,14 @@ module Kettle
263
266
  ).uniq.sort
264
267
  end
265
268
 
266
- def normalization_needed?(path)
267
- has_local_path_remote?(path) ||
269
+ def normalization_needed?(path, strict: false)
270
+ has_local_path_remote?(path, strict: strict) ||
268
271
  !empty_registry_checksums(path).empty? ||
269
272
  !unreleased_workspace_registry_specs(path).empty? ||
270
273
  bundled_with_checksum_mismatch?(path)
271
274
  end
272
275
 
273
- def diagnostics(path)
276
+ def diagnostics(path, strict: false)
274
277
  diagnostics = []
275
278
  disallowed_local_path_remote_lines(path).each do |line_number|
276
279
  diagnostics << "#{display_path(path)} has local path remote at line #{line_number}"
@@ -332,9 +335,17 @@ module Kettle
332
335
  end
333
336
 
334
337
  def normalization_env
335
- DEFAULT_DISABLED_ENV
336
- .reject { |name, _| allowed_local_path_env_names.include?(name) }
337
- .merge(dynamic_local_path_env)
338
+ disabled_local_path_env
339
+ end
340
+
341
+ # The release graph, not ambient development ENV, decides which local
342
+ # selectors remain active while Bundler normalizes a release lockfile.
343
+ def release_normalization_env
344
+ disabled_local_path_env.merge(release_graph_contract.normalization_environment)
345
+ end
346
+
347
+ def disabled_local_path_env
348
+ DEFAULT_DISABLED_ENV.merge(dynamic_local_path_env)
338
349
  end
339
350
 
340
351
  # Appraisal generation normally uses the release-normalized environment.
@@ -354,7 +365,7 @@ module Kettle
354
365
  ENV.each_with_object({}) do |(key, value), env|
355
366
  next unless key.end_with?("_DEV", "_LOCAL")
356
367
  next unless local_path_env_value?(value)
357
- next if allowed_local_path_env_names.include?(key)
368
+ next if release_graph_contract.selector_env.key?(key)
358
369
 
359
370
  env[key] = "false"
360
371
  end
@@ -376,15 +387,16 @@ module Kettle
376
387
  local_path_env_value?(ENV[name])
377
388
  end
378
389
 
379
- def has_local_path_remote?(path)
380
- !disallowed_local_path_remote_lines(path).empty?
390
+ def has_local_path_remote?(path, strict: false)
391
+ paths = disallowed_local_path_remote_lines(path)
392
+ !paths.empty?
381
393
  end
382
394
 
383
395
  def local_path_remote_lines(path)
384
396
  self.class.local_path_remote_lines_from_source(File.read(path))
385
397
  end
386
398
 
387
- attr_reader :allowed_local_path_roots, :allowed_local_path_env_names
399
+ attr_reader :release_graph_contract
388
400
 
389
401
  def disallowed_local_path_remote_lines(path)
390
402
  local_path_remote_lines(path).reject do |line_number|
@@ -392,25 +404,12 @@ module Kettle
392
404
  end
393
405
  end
394
406
 
395
- def configured_allowed_local_path_roots
396
- ENV.fetch(ALLOWED_LOCAL_PATH_ROOTS_ENV, "").split(File::PATH_SEPARATOR).filter_map do |value|
397
- next if value.strip.empty?
398
-
399
- canonical_path(value)
400
- end.uniq
401
- end
402
-
403
- def configured_allowed_local_path_env_names
404
- ENV.fetch(ALLOWED_LOCAL_PATH_ENVS_ENV, "").split(",").map(&:strip).reject(&:empty?).uniq
405
- end
406
-
407
407
  def local_path_remote_at(path, line_number)
408
408
  File.readlines(path)[line_number - 1].to_s.split("remote:", 2).last.to_s.strip
409
409
  end
410
410
 
411
411
  def allowed_local_path?(path)
412
- remote = canonical_path(path)
413
- allowed_local_path_roots.any? { |root| remote == root || remote.start_with?("#{root}/") }
412
+ release_graph_contract.allowed_path?(path)
414
413
  end
415
414
 
416
415
  def canonical_path(path)
@@ -214,6 +214,7 @@ module Kettle
214
214
  @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
215
215
  @appraisal_task = normalize_appraisal_task(appraisal_task || ENV["KETTLE_RELEASE_APPRAISAL_TASK"])
216
216
  @release_candidate = nil
217
+ @release_ci_pull_request = nil
217
218
  @event_stream = options[:event_stream]
218
219
  @event_recorder = Kettle::Ndjson.event_recorder(@event_stream, phase_timings: [])
219
220
  @secrets_provider = options[:secrets_provider] || Kettle::Dev::ReleaseSecrets::Factory.build(provider_name: secrets_provider_name)
@@ -555,6 +556,11 @@ module Kettle
555
556
  # 19. push tags to remotes (final step)
556
557
  push_tags! if run_step?(19) && !local_ci?
557
558
 
559
+ # Branch-stack PRs exist only to trigger pull-request-only CI. They are
560
+ # deliberately not merged into trunk, so close the release-created PR
561
+ # after the publication has completed successfully.
562
+ close_generated_branch_stack_pull_request!
563
+
558
564
  # Final success message
559
565
  begin
560
566
  version ||= detect_version
@@ -888,11 +894,11 @@ module Kettle
888
894
  end
889
895
 
890
896
  def release_lockfile_normalization_needed?(path)
891
- lockfile_reset.normalization_needed?(path)
897
+ lockfile_reset.normalization_needed?(path, strict: true)
892
898
  end
893
899
 
894
900
  def normalize_release_lockfile!(path)
895
- lockfile_reset.reset_lockfile!(path)
901
+ lockfile_reset.reset_lockfile!(path, strict: true)
896
902
  end
897
903
 
898
904
  def release_gemfile_for_lockfile(path)
@@ -900,29 +906,32 @@ module Kettle
900
906
  end
901
907
 
902
908
  def release_lockfile_normalization_env
903
- lockfile_reset.normalization_env
909
+ lockfile_reset.release_normalization_env
904
910
  end
905
911
 
906
912
  # The release process has three distinct lockfile roles:
907
913
  #
908
- # - canonical: tracked project lockfiles, always registry-backed;
914
+ # - canonical: tracked project lockfiles, using the declared release
915
+ # graph (normally registry-only, or contained CI-resident monorepo
916
+ # paths when kettle-family supplied that contract);
909
917
  # - task: a disposable Gemfile.lock copy for build and publication; and
910
918
  # - tool: a tool-owned Gemfile, such as kettle-changelog's release bundle.
911
919
  #
912
- # A family may use local sibling gems to launch this process, but no
913
- # release child may inherit that graph. Git hooks are release children
914
- # too: they commonly load project tooling and otherwise can silently
915
- # rewrite the canonical lockfile with local PATH sources.
920
+ # A family may use local sibling gems to launch this process. Release
921
+ # children receive only the serialized release graph, never the ambient
922
+ # launcher graph. Git hooks are release children too: they commonly load
923
+ # project tooling and otherwise can silently rewrite a lockfile with an
924
+ # undeclared local PATH source.
916
925
  def release_git_hook_environment
917
926
  release_child_environment
918
927
  end
919
928
 
920
929
  def release_lockfile_diagnostics(path)
921
- lockfile_reset.diagnostics(path)
930
+ lockfile_reset.diagnostics(path, strict: true)
922
931
  end
923
932
 
924
933
  def release_lockfile_has_local_path_remote?(path)
925
- lockfile_reset.has_local_path_remote?(path)
934
+ lockfile_reset.has_local_path_remote?(path, strict: true)
926
935
  end
927
936
 
928
937
  def release_lockfile_local_path_remote_lines(path)
@@ -1123,9 +1132,9 @@ module Kettle
1123
1132
  end
1124
1133
 
1125
1134
  # Every release child runs outside the release tool's Bundler context and
1126
- # with local sibling switches disabled. The optional lockfile identifies
1127
- # a disposable task lockfile; omitting it deliberately selects the
1128
- # canonical tracked lockfile for setup, checks, docs, and checksums.
1135
+ # with only the serialized release graph enabled. The optional lockfile
1136
+ # identifies a disposable task lockfile; omitting it deliberately selects
1137
+ # the canonical tracked lockfile for setup, checks, docs, and checksums.
1129
1138
  def release_child_command(command, environment: {}, lockfile: nil)
1130
1139
  command_body = command
1131
1140
  command = +"env"
@@ -1457,11 +1466,24 @@ module Kettle
1457
1466
 
1458
1467
  pull_request = github_pull_request_for_branch(owner: owner, repo: repo, branch: branch, base: trunk)
1459
1468
  if pull_request
1460
- puts "GitHub pull request ##{pull_request.fetch("number")} already open for #{branch} -> #{trunk}: #{pull_request.fetch("url")}"
1469
+ body = pull_request["body"]
1470
+ @release_ci_pull_request = {
1471
+ owner: owner,
1472
+ repo: repo,
1473
+ number: pull_request["number"],
1474
+ cleanup: body.is_a?(String) && body.index("Automated release validation PR for") == 0
1475
+ }
1476
+ puts "GitHub pull request ##{pull_request["number"]} already open for #{branch} -> #{trunk}: #{pull_request["url"]}"
1461
1477
  return
1462
1478
  end
1463
1479
 
1464
- create_github_pull_request!(owner: owner, repo: repo, branch: branch, base: trunk)
1480
+ number = create_github_pull_request!(owner: owner, repo: repo, branch: branch, base: trunk)
1481
+ @release_ci_pull_request = {
1482
+ owner: owner,
1483
+ repo: repo,
1484
+ number: number,
1485
+ cleanup: true
1486
+ }
1465
1487
  end
1466
1488
 
1467
1489
  def github_pull_request_for_branch(owner:, repo:, branch:, base:)
@@ -1477,7 +1499,7 @@ module Kettle
1477
1499
  "--state",
1478
1500
  "open",
1479
1501
  "--json",
1480
- "number,url",
1502
+ "number,url,body",
1481
1503
  "--limit",
1482
1504
  "1"
1483
1505
  )
@@ -1500,9 +1522,36 @@ module Kettle
1500
1522
  "--title",
1501
1523
  "Release #{branch}",
1502
1524
  "--body",
1503
- "Automated release validation PR for `#{branch}`.\n\nThis PR lets GitHub Actions run before kettle-release merges the branch into `#{base}`."
1525
+ release_validation_pull_request_body(branch)
1504
1526
  )
1505
1527
  puts "Created GitHub pull request for #{branch} -> #{base}: #{output.strip}"
1528
+ output[/\/pull\/(\d+)/, 1]
1529
+ end
1530
+
1531
+ def release_validation_pull_request_body(branch)
1532
+ "Automated release validation PR for `#{branch}`.\n\nThis PR exists only to run pull-request-only GitHub Actions before publishing the branch-stack release. Branch-stack releases are not merged into trunk."
1533
+ end
1534
+
1535
+ def close_generated_branch_stack_pull_request!
1536
+ pull_request = @release_ci_pull_request
1537
+ return unless pull_request && pull_request[:cleanup]
1538
+
1539
+ branch = current_branch
1540
+ trunk = detect_trunk_branch
1541
+ return unless branch_stack_release_branch?(branch, trunk)
1542
+
1543
+ args = [
1544
+ "pr", "close", pull_request[:number].to_s,
1545
+ "--repo", "#{pull_request[:owner]}/#{pull_request[:repo]}",
1546
+ "--comment", "Release completed successfully; closing the CI-only validation PR."
1547
+ ]
1548
+ stdout, stderr, status = Open3.capture3(self.class.send(:command_env), "gh", *args)
1549
+ if status.success?
1550
+ puts "Closed release validation PR ##{pull_request[:number]} for #{branch}."
1551
+ else
1552
+ detail = stderr.strip.empty? ? stdout.strip : stderr.strip
1553
+ warn "Could not close release validation PR ##{pull_request[:number]}: #{detail}"
1554
+ end
1506
1555
  end
1507
1556
 
1508
1557
  def gh_output!(*args)
@@ -1745,7 +1794,7 @@ module Kettle
1745
1794
  def git_output(args)
1746
1795
  # Route all git interactions through the GitAdapter so tests can safely mock them
1747
1796
  out, ok = @git.capture(args)
1748
- [out.to_s.strip, !!ok]
1797
+ [out ? out.strip : "", !!ok]
1749
1798
  end
1750
1799
 
1751
1800
  def ensure_git_user!
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Kettle
6
+ module Dev
7
+ # Parsed contract supplied by kettle-family for a single release target.
8
+ # Direct kettle-release calls receive the safe registry-only default.
9
+ class ReleaseGraphContract
10
+ ENV_KEY = "KETTLE_RELEASE_GRAPH_CONTRACT_JSON"
11
+ NAMES = %w[registry_only wave_transition monorepo_ci_local branch_terminal].freeze
12
+
13
+ attr_reader :name, :ci_root, :local_path_roots, :selector_env
14
+
15
+ def self.from_environment(root:)
16
+ raw = (ENV[ENV_KEY] || "").strip
17
+ return new(name: "registry_only", root: root, ci_root: root) if raw.empty?
18
+
19
+ data = JSON.parse(raw)
20
+ new(
21
+ name: data["name"],
22
+ ci_root: data["ci_root"] || root,
23
+ local_path_roots: data["local_path_roots"] || [],
24
+ selector_env: data["selector_env"] || {},
25
+ root: root
26
+ )
27
+ rescue JSON::ParserError, KeyError, TypeError => error
28
+ raise Error, "invalid #{ENV_KEY}: #{error.message}"
29
+ end
30
+
31
+ def initialize(name:, root:, ci_root:, local_path_roots: [], selector_env: {})
32
+ @name = String(name)
33
+ @root = File.realpath(root)
34
+ @ci_root = canonical_path(ci_root)
35
+ @local_path_roots = Array(local_path_roots).map { |path| canonical_path(path) }.uniq.freeze
36
+ @selector_env = selector_env.to_h.map { |key, value| [String(key), String(value)] }.to_h.freeze
37
+ validate!
38
+ freeze
39
+ end
40
+
41
+ def local_paths?
42
+ !local_path_roots.empty?
43
+ end
44
+
45
+ def registry_only?
46
+ name == "registry_only" || name == "branch_terminal"
47
+ end
48
+
49
+ def allowed_path?(path)
50
+ candidate = canonical_path(path)
51
+ local_path_roots.any? { |root| path_within_root?(candidate, root) }
52
+ end
53
+
54
+ def normalization_environment
55
+ selector_env
56
+ end
57
+
58
+ private
59
+
60
+ def validate!
61
+ raise Error, "unknown release graph contract #{name.inspect}" unless NAMES.include?(name)
62
+
63
+ if registry_only? && (local_paths? || !selector_env.empty?)
64
+ raise Error, "#{name} release graph cannot declare local paths or selectors"
65
+ end
66
+ return unless %w[wave_transition monorepo_ci_local].include?(name)
67
+
68
+ raise Error, "#{name} release graph requires local path roots" unless local_paths?
69
+ raise Error, "#{name} release graph requires local path selectors" if selector_env.empty?
70
+
71
+ selector_env.each_value do |value|
72
+ next if allowed_path?(value)
73
+
74
+ raise Error, "#{name} selector #{value.inspect} is outside its declared local path roots"
75
+ end
76
+ return unless name == "monorepo_ci_local"
77
+
78
+ unless path_within_root?(@root, ci_root)
79
+ raise Error, "monorepo_ci_local release root #{@root.inspect} is outside CI root #{ci_root.inspect}"
80
+ end
81
+ local_path_roots.each do |path|
82
+ next if path_within_root?(path, ci_root)
83
+
84
+ raise Error, "monorepo_ci_local path #{path.inspect} is outside CI root #{ci_root.inspect}"
85
+ end
86
+ end
87
+
88
+ def canonical_path(path)
89
+ expanded = File.expand_path(path, @root)
90
+ File.realpath(expanded)
91
+ rescue Errno::ENOENT
92
+ expanded
93
+ end
94
+
95
+ def path_within_root?(path, root)
96
+ path == root || (path[0, root.length] == root && path[root.length] == "/")
97
+ end
98
+ end
99
+ end
100
+ end
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "3.0.34"
8
+ VERSION = "3.1.1"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data/lib/kettle/dev.rb CHANGED
@@ -49,6 +49,7 @@ module Kettle
49
49
  autoload :OpenCollectiveConfig, "kettle/dev/open_collective_config"
50
50
  autoload :ReleaseNotifier, "kettle/dev/release_notifier"
51
51
  autoload :ReleaseCLI, "kettle/dev/release_cli"
52
+ autoload :ReleaseGraphContract, "kettle/dev/release_graph_contract"
52
53
  autoload :ReleaseSecrets, "kettle/dev/release_secrets"
53
54
  autoload :ReleaseSecretsDoctor, "kettle/dev/release_secrets_doctor"
54
55
  autoload :ResetCLI, "kettle/dev/reset_cli"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.34
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -60,7 +60,7 @@ dependencies:
60
60
  version: '0.3'
61
61
  - - ">="
62
62
  - !ruby/object:Gem::Version
63
- version: 0.3.14
63
+ version: 0.3.15
64
64
  type: :runtime
65
65
  prerelease: false
66
66
  version_requirements: !ruby/object:Gem::Requirement
@@ -70,7 +70,7 @@ dependencies:
70
70
  version: '0.3'
71
71
  - - ">="
72
72
  - !ruby/object:Gem::Version
73
- version: 0.3.14
73
+ version: 0.3.15
74
74
  - !ruby/object:Gem::Dependency
75
75
  name: kettle-ndjson
76
76
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +80,7 @@ dependencies:
80
80
  version: '0.1'
81
81
  - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: 0.1.15
83
+ version: 0.1.16
84
84
  type: :runtime
85
85
  prerelease: false
86
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  version: '0.1'
91
91
  - - ">="
92
92
  - !ruby/object:Gem::Version
93
- version: 0.1.15
93
+ version: 0.1.16
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: kettle-rb
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -100,7 +100,7 @@ dependencies:
100
100
  version: '0.1'
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: 0.1.12
103
+ version: 0.1.13
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
@@ -110,7 +110,7 @@ dependencies:
110
110
  version: '0.1'
111
111
  - - ">="
112
112
  - !ruby/object:Gem::Version
113
- version: 0.1.12
113
+ version: 0.1.13
114
114
  - !ruby/object:Gem::Dependency
115
115
  name: kettle-test
116
116
  requirement: !ruby/object:Gem::Requirement
@@ -120,7 +120,7 @@ dependencies:
120
120
  version: '2.0'
121
121
  - - ">="
122
122
  - !ruby/object:Gem::Version
123
- version: 2.0.21
123
+ version: 2.0.22
124
124
  type: :runtime
125
125
  prerelease: false
126
126
  version_requirements: !ruby/object:Gem::Requirement
@@ -130,7 +130,7 @@ dependencies:
130
130
  version: '2.0'
131
131
  - - ">="
132
132
  - !ruby/object:Gem::Version
133
- version: 2.0.21
133
+ version: 2.0.22
134
134
  - !ruby/object:Gem::Dependency
135
135
  name: ruby-progressbar
136
136
  requirement: !ruby/object:Gem::Requirement
@@ -382,6 +382,7 @@ files:
382
382
  - lib/kettle/dev/rakelib/yard.rake
383
383
  - lib/kettle/dev/readme_backers.rb
384
384
  - lib/kettle/dev/release_cli.rb
385
+ - lib/kettle/dev/release_graph_contract.rb
385
386
  - lib/kettle/dev/release_notifier.rb
386
387
  - lib/kettle/dev/release_secrets.rb
387
388
  - lib/kettle/dev/release_secrets_doctor.rb
@@ -400,10 +401,10 @@ licenses:
400
401
  - AGPL-3.0-only
401
402
  metadata:
402
403
  homepage_uri: https://kettle-dev.galtzo.com
403
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.0.34
404
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.0.34/CHANGELOG.md
404
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.1.1
405
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.1/CHANGELOG.md
405
406
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
406
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.0.34
407
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.1.1
407
408
  funding_uri: https://github.com/sponsors/pboling
408
409
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
409
410
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file