kettle-dev 3.1.9 → 3.1.11

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: 452c1019273fac5fa450b42b6bf0d24fee016a6641971eeb3c5bddcf08efaa64
4
- data.tar.gz: 4919f5ac1cd4aeb579da6ae378109923cf7fe86c6568b77ab42101e2f8bd4087
3
+ metadata.gz: c6532c0b875c1c1581f43bf3035bc3f9b4ff7ba8d04f7a90f6bf78be7fb167ac
4
+ data.tar.gz: 9894c1dd7477202261502e23f1e6d37b6f0f174350ac456b834dc8df19b143ba
5
5
  SHA512:
6
- metadata.gz: 58be74c463a06ea48b7571ec6fffa36bcaca0da0eb5e1a7cab2b4d3765f7fe2a4be2932f1cf09bfc496c97a9fa32badcf36182eabcdf58e61f5eb8c7242c9d41
7
- data.tar.gz: 6e679af06a75e16ff6321f348cae64f8fa8c6884408f4fca7f20c7d5d8b5498bd4cf6bac270c0dc04a84747f3e67dffe7562c89e15d91afc38d11dd7f59e82f6
6
+ metadata.gz: 5bf4bd022743bdac03fc5806ff9598781abf17814c14ec9cddd5a917d33af628055dea5adbdbf11ee3d4284aa9f5dc2926c8456dab0710fc59ea7093aa23d457
7
+ data.tar.gz: a9aa5312d72d80c8868becdb10cbf8e11f57ae4df5e37edc78f781656873b4dea9bc1fd48aca064a4ec63e76ee2156dfe05626e8efa90743a7ffda4b24a397bd
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,34 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.1.11] - 2026-10-11
34
+
35
+ - TAG: [v3.1.11][3.1.11t]
36
+ - COVERAGE: 91.71% -- 6352/6926 lines in 49 files
37
+ - BRANCH COVERAGE: 75.79% -- 2420/3193 branches in 49 files
38
+ - 52.20% documented
39
+
40
+ ### Fixed
41
+
42
+ - CI monitor aborts on the first failed GitHub Actions job instead of waiting for run-level completion: a large matrix keeps the run in_progress for minutes after a red job, so releases burned dead time a re-run must spend anyway; the abort names the job and links it
43
+
44
+ ## [3.1.10] - 2026-10-11
45
+
46
+ - TAG: [v3.1.10][3.1.10t]
47
+ - COVERAGE: 91.78% -- 6341/6909 lines in 49 files
48
+ - BRANCH COVERAGE: 75.83% -- 2412/3181 branches in 49 files
49
+ - 52.10% documented
50
+
51
+ ### Fixed
52
+
53
+ - appraisal:generate now deletes a stale Appraisal.root.gemfile.lock before its bundle install. The root appraisal lockfile is a gitignored local artifact that drifts after dependency bumps; a stale pin made appraisal:generate fail with an opaque "locked to X ... can no longer be found" (observed templating kettle-jem after ast-crispr-ruby-prism's floor moved to an unpublished version). appraisal:generate's contract is to work without resolving appraisal locks, so the stale lock is removed and Bundler resolves fresh.
54
+
55
+ ### Security
56
+
57
+ - InteractiveReleaseCommand now scrubs every configured secret from child output before printing, capturing, or logging it: a PTY echoes bytes written to its input back onto the output stream while the child's terminal echo is on (e.g. before RubyGems' noecho wraps the password read), so the gem signing passphrase previously appeared in release logs; the scrubber tracks written secrets in memory, removes them from every chunk including secrets split across chunk boundaries, and clears the prompt buffer after answering so a stale prompt cannot trigger a duplicate write. Prompt detection runs on a scrubbed-but-untruncated view of the stream while emitted output keeps the anti-leak holdback, fixing a deadlock where `gem push` printed the RubyGems OTP prompt as its final output and blocked forever on stdin with the prompt tail withheld (the fix landed mid-release and was omitted from this section's original content)
58
+
59
+ - kettle-release aborts at startup under a prerelease Bundler, and LockfileReset now treats a lockfile BUNDLED WITH a prerelease Bundler as drift: Bundler auto-installs the locked version (including betas) on every bundle install, so one beta-pinned lockfile re-infects every machine that bundles against it; the reset re-locks with the currently running stable Bundler
60
+
33
61
  ## [3.1.9] - 2026-10-09
34
62
 
35
63
  - TAG: [v3.1.9][3.1.9t]
@@ -4021,7 +4049,11 @@ Please file a bug if you notice a violation of semantic versioning.
4021
4049
  - Selecting will run the selected workflow via `act`
4022
4050
  - This may move to its own gem in the future.
4023
4051
 
4024
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.9...HEAD
4052
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.11...HEAD
4053
+ [3.1.11]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.10...v3.1.11
4054
+ [3.1.11t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.11
4055
+ [3.1.10]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.9...v3.1.10
4056
+ [3.1.10t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.10
4025
4057
  [3.1.9]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.8...v3.1.9
4026
4058
  [3.1.9t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.9
4027
4059
  [3.1.8]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.7...v3.1.8
data/README.md CHANGED
@@ -421,6 +421,7 @@ What it does:
421
421
  - Select workflow with `K_RELEASE_LOCAL_CI_WORKFLOW` (with or without .yml/.yaml). Defaults to `locked_deps.yml` if present; otherwise the first workflow discovered.
422
422
  - On failure, the release prep commit is soft-rolled-back (`git reset --soft HEAD^`) and the process aborts.
423
423
  - Ensures trunk sync and rebases feature branches as needed, pushes, monitors GitHub Actions with a progress bar, and merges feature to trunk on success.
424
+ - CI monitoring aborts as soon as any single workflow job fails (not only when the whole run concludes), naming the failed job and linking to it — a large matrix can keep a run in_progress for many minutes after its first red job.
424
425
  - For release branches listed in `.kettle-family.yml` branch-stack targets, it intentionally does not pull/rebase from trunk or merge the leaf branch into trunk. Instead, it starts the selected GitHub Actions workflows directly on the pushed release branch; each selected workflow must declare `workflow_dispatch`. This validates the exact branch-stack commit without requiring an often-conflicting PR to trunk.
425
426
  - Exports `SOURCE_DATE_EPOCH`, builds (optionally signed), creates gem checksums, and runs `bundle exec rake release` (prompts for signing key + RubyGems MFA OTP as needed).
426
427
  - Options:
@@ -1067,7 +1068,7 @@ Thanks for RTFM. ☺️
1067
1068
  [📌gitmoji]: https://gitmoji.dev
1068
1069
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1069
1070
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1070
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.848-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1071
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.926-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1071
1072
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
1072
1073
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1073
1074
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -188,6 +188,38 @@ module Kettle
188
188
  run && run["status"] == "completed" && run["conclusion"] && run["conclusion"] != "success"
189
189
  end
190
190
 
191
+ # Jobs of a workflow run. Used to detect a failed job BEFORE the
192
+ # workflow-level status completes: a large matrix keeps the run
193
+ # in_progress for many minutes after its first job has already
194
+ # concluded failure, while an operator watching the Actions UI sees
195
+ # the red job immediately.
196
+ # @param owner [String]
197
+ # @param repo [String]
198
+ # @param run_id [Integer, String]
199
+ # @param token [String, nil]
200
+ # @return [Array<Hash>] job summaries: name, status, conclusion, html_url
201
+ def run_jobs(owner:, repo:, run_id:, token: default_token)
202
+ return [] unless owner && repo && run_id
203
+
204
+ data = github_get_json(
205
+ "https://api.github.com/repos/#{owner}/#{repo}/actions/runs/#{run_id}/jobs?per_page=100",
206
+ token: token
207
+ )
208
+ return [] unless data
209
+
210
+ Array(data["jobs"]).map do |job|
211
+ {
212
+ "name" => job["name"],
213
+ "status" => job["status"],
214
+ "conclusion" => job["conclusion"],
215
+ "html_url" => job["html_url"]
216
+ }
217
+ end
218
+ rescue => e
219
+ Kettle::Dev.debug_error(e, __method__)
220
+ []
221
+ end
222
+
191
223
  # Default GitHub token sourced from environment.
192
224
  # @return [String, nil]
193
225
  def default_token
@@ -404,9 +404,31 @@ module Kettle
404
404
  total: total
405
405
  )
406
406
  end
407
- elsif Kettle::Dev::CIHelpers.failed?(run) && !passed[wf] && !failed[wf]
407
+ elsif !failed[wf]
408
+ # Detect a failure at the JOB level while the run is still
409
+ # in_progress: a large matrix keeps the run-level status
410
+ # pending for many minutes after its first job has already
411
+ # concluded failure, but an operator watching the Actions UI
412
+ # sees the red job immediately. Abort as soon as any job is
413
+ # definitively red so the release stops burning minutes that
414
+ # a re-run will have to spend anyway.
415
+ run_failed = Kettle::Dev::CIHelpers.failed?(run)
416
+ failed_job = nil
417
+ if !passed[wf] && !run_failed && run["status"] != "completed"
418
+ # Run still in flight: poll its jobs so a single red job in
419
+ # a large matrix aborts immediately instead of waiting for
420
+ # the whole workflow to conclude.
421
+ jobs = Kettle::Dev::CIHelpers.run_jobs(owner: owner, repo: repo, run_id: run["id"])
422
+ failed_job = jobs.find do |job|
423
+ job["status"] == "completed" && job["conclusion"] && job["conclusion"] != "success" && job["conclusion"] != "skipped"
424
+ end
425
+ end
426
+ next unless run_failed || failed_job
427
+
408
428
  puts
409
429
  wf_url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"
430
+ failed_job_note = failed_job ? " (job: #{failed_job["name"]})" : ""
431
+ wf_url = failed_job["html_url"] if failed_job && failed_job["html_url"]
410
432
  emit_ci_monitor_event(
411
433
  event_recorder,
412
434
  action: "github_workflow",
@@ -416,12 +438,13 @@ module Kettle
416
438
  url: wf_url,
417
439
  run_status: run["status"],
418
440
  conclusion: run["conclusion"],
441
+ failed_job: failed_job && failed_job["name"],
419
442
  completed: passed.size,
420
443
  total: total,
421
- reason: "Workflow failed"
444
+ reason: "Workflow failed#{failed_job_note}"
422
445
  )
423
446
  unless continue_ci_failures?
424
- abort("Workflow failed: #{wf} -> #{wf_url} Fix the workflow, then restart this tool from CI validation with: #{restart_hint}")
447
+ abort("Workflow failed#{failed_job_note}: #{wf} -> #{wf_url} Fix the workflow, then restart this tool from CI validation with: #{restart_hint}")
425
448
  end
426
449
 
427
450
  failed[wf] = true
@@ -14,6 +14,14 @@ module Kettle
14
14
  @secret_event_handler = secret_event_handler
15
15
  @forward_stdin = forward_stdin
16
16
  @gem_signing_passphrase = nil
17
+ # Secrets are written to the PTY master to answer child prompts. A PTY
18
+ # slave still echoes what is typed while the child has not yet disabled
19
+ # terminal echo (RubyGems' ask_for_password only wraps gets in noecho),
20
+ # so the secret bytes can bounce back into the PTY output stream and —
21
+ # without filtering — land in release logs. Track every secret written
22
+ # and scrub it from all child output before it is printed or captured.
23
+ @written_secrets = []
24
+ @secret_scrub_carryover = +""
17
25
  end
18
26
 
19
27
  def call(env, cmd)
@@ -44,14 +52,35 @@ module Kettle
44
52
  input.write(@input.readpartial(1024))
45
53
  else
46
54
  chunk = output.readpartial(1024)
55
+ # Two views of the same bytes, with independent state.
56
+ #
57
+ # Emitted view: holds back a trailing window so a secret
58
+ # split across chunks can never reach the log; the window
59
+ # is released by flush_secret_scrub_carryover at stream end.
60
+ #
61
+ # Prompt view: scrubbed but never held back. A child that
62
+ # prints its prompt and then blocks on stdin (Gem's OTP
63
+ # `Code: ` ask) produces no further output; if the prompt
64
+ # tail sat in the emitted view's holdback, detection would
65
+ # never fire and the child would deadlock. The prompt view
66
+ # therefore scrubs the raw chunk directly (plus the emitted
67
+ # view's carryover, read-only, so bytes spanning the
68
+ # boundary are still masked) instead of consuming it.
69
+ chunk = scrub_secrets_from_chunk(chunk)
70
+ prompt_text = scrub_secrets_for_prompt_detection(chunk)
47
71
  stdout_str << chunk
48
72
  @output.print(chunk)
49
- prompt_buffer = handle_prompt(input, chunk, prompt_buffer: prompt_buffer)
73
+ prompt_buffer = handle_prompt(input, prompt_text, prompt_buffer: prompt_buffer)
50
74
  end
51
75
  end
52
76
  end
53
77
  rescue Errno::EIO, EOFError
54
78
  # PTY platforms signal a closed child with EIO or EOFError.
79
+ chunk = flush_secret_scrub_carryover
80
+ unless chunk.empty?
81
+ stdout_str << chunk
82
+ @output.print(chunk)
83
+ end
55
84
  rescue Kettle::Dev::Error
56
85
  Process.kill("TERM", pid)
57
86
  raise
@@ -78,6 +107,7 @@ module Kettle
78
107
  else
79
108
  begin
80
109
  chunk = reader.readpartial(1024)
110
+ chunk = scrub_secrets_from_chunk(chunk)
81
111
  if reader.equal?(output)
82
112
  stdout_str << chunk
83
113
  @output.print(chunk)
@@ -93,6 +123,11 @@ module Kettle
93
123
  end
94
124
  end
95
125
  status = wait_thread.value
126
+ chunk = flush_secret_scrub_carryover
127
+ unless chunk.empty?
128
+ stdout_str << chunk
129
+ @output.print(chunk)
130
+ end
96
131
  end
97
132
  [stdout_str, stderr_str, status]
98
133
  end
@@ -110,7 +145,15 @@ module Kettle
110
145
  return prompt_buffer
111
146
  end
112
147
 
113
- write_secret(input, label: "gem signing passphrase", source: "gem_signing_passphrase") { gem_signing_passphrase } if signing_password_prompt?(prompt_buffer)
148
+ if signing_password_prompt?(prompt_buffer)
149
+ write_secret(input, label: "gem signing passphrase", source: "gem_signing_passphrase") { gem_signing_passphrase }
150
+ # Clear the buffer after answering: otherwise the stale prompt text
151
+ # re-matches on every subsequent chunk and writes the secret again.
152
+ # The duplicate write happens after the child has restored terminal
153
+ # echo (RubyGems' noecho wraps only the gets call), so its bytes echo
154
+ # back through the PTY and land in the captured output and logs.
155
+ return +""
156
+ end
114
157
  prompt_buffer
115
158
  end
116
159
 
@@ -129,6 +172,11 @@ module Kettle
129
172
  @output.puts("#{label} loaded from configured secrets provider.")
130
173
  input.write("#{secret}\n")
131
174
  input.flush
175
+ # Register AFTER writing: a chunk currently in flight may contain a
176
+ # legitimately echoed prompt line, but anything the child echoes from
177
+ # this point on that contains the secret bytes is PTY echo of the write
178
+ # and must never reach @output, captured output, or release logs.
179
+ register_secret!(secret)
132
180
  emit_secret_event(source: source, action: "prompt_response", status: "ok", label: label)
133
181
  rescue Kettle::Dev::Error => error
134
182
  emit_secret_event(source: source, action: "prompt_response", status: "failed", label: label, reason: error.message)
@@ -139,6 +187,57 @@ module Kettle
139
187
  secret_event_handler&.call(payload)
140
188
  end
141
189
 
190
+ # Never persist secret values. Register the exact byte string so the
191
+ # scrubber can recognize it in child output without logging it.
192
+ def register_secret!(secret)
193
+ secret = secret.to_s
194
+ return if secret.empty?
195
+
196
+ @written_secrets << secret unless @written_secrets.include?(secret)
197
+ @written_secrets.shift if @written_secrets.length > 8
198
+ end
199
+
200
+ # Secrets typed into a PTY can be echoed back when the child's terminal
201
+ # echo is momentarily on (e.g. before Gem's noecho gets runs). Remove
202
+ # registered secret byte sequences from every chunk before printing or
203
+ # capturing. A chunk boundary can split a secret, so carry a trailing
204
+ # window of the longest secret forward to the next chunk.
205
+ def scrub_secrets_from_chunk(chunk)
206
+ return chunk if @written_secrets.empty? || chunk.empty?
207
+
208
+ window = @written_secrets.map(&:bytesize).max || 0
209
+ buffer = @secret_scrub_carryover + chunk
210
+ buffer = buffer.gsub(Regexp.union(@written_secrets), "[SECRET REMOVED]")
211
+ if buffer.bytesize >= window && window > 0
212
+ @secret_scrub_carryover = buffer.byteslice(-window, window).to_s
213
+ buffer.byteslice(0, buffer.bytesize - window).to_s
214
+ else
215
+ @secret_scrub_carryover = buffer
216
+ +""
217
+ end
218
+ end
219
+
220
+ # At stream end nothing else can reveal a secret split across chunks, so
221
+ # release the held trailing window (already scrubbed once).
222
+ def flush_secret_scrub_carryover
223
+ chunk = @secret_scrub_carryover
224
+ @secret_scrub_carryover = +""
225
+ chunk
226
+ end
227
+
228
+ # Prompt-detection view: scrubbed, but never holds back a trailing
229
+ # window. Reads (never consumes) the emitted view's carryover so bytes
230
+ # spanning a chunk boundary are still masked here. Without this view, a
231
+ # prompt whose tail falls inside the holdback — a child that blocks on
232
+ # stdin and produces no further output — would never be answered, and
233
+ # the release would deadlock.
234
+ def scrub_secrets_for_prompt_detection(chunk)
235
+ return chunk if @written_secrets.empty?
236
+
237
+ buffer = @secret_scrub_carryover + chunk
238
+ buffer.gsub(Regexp.union(@written_secrets), "[SECRET REMOVED]")
239
+ end
240
+
142
241
  def otp_prompt?(chunk)
143
242
  chunk.match?(/(?:multi-factor authentication|OTP code|one-time password|\bCode:\s*)/i)
144
243
  end
@@ -311,6 +311,7 @@ module Kettle
311
311
  has_local_path_remote?(path, strict: strict) ||
312
312
  !empty_registry_checksums(path).empty? ||
313
313
  !unreleased_workspace_registry_specs(path).empty? ||
314
+ bundled_with_prerelease?(path) ||
314
315
  bundled_with_checksum_mismatch?(path)
315
316
  end
316
317
 
@@ -325,6 +326,9 @@ module Kettle
325
326
  unreleased_workspace_registry_specs(path).each do |name, version, line_number|
326
327
  diagnostics << "#{display_path(path)} locks local workspace gem #{name} #{version} as a registry gem, but that version is not resolvable from the configured gem source at line #{line_number}"
327
328
  end
329
+ if bundled_with_prerelease?(path)
330
+ diagnostics.push("#{display_path(path)} BUNDLED WITH #{bundled_with_version(path)} is a prerelease Bundler; re-lock with a stable Bundler")
331
+ end
328
332
  if bundled_with_checksum_mismatch?(path)
329
333
  diagnostics.push("#{display_path(path)} BUNDLED WITH #{bundled_with_version(path)} has no matching Bundler checksum")
330
334
  end
@@ -542,6 +546,19 @@ module Kettle
542
546
  ""
543
547
  end
544
548
 
549
+ # A lockfile pinned to a prerelease Bundler re-infects every machine that
550
+ # runs bundle install against it: Bundler auto-installs the locked version
551
+ # (including betas) before doing anything else. Treat it as drift and let
552
+ # the reset re-lock with the currently running stable Bundler.
553
+ def bundled_with_prerelease?(path)
554
+ version = bundled_with_version(path)
555
+ return false if version.empty?
556
+
557
+ Gem::Version.new(version).prerelease?
558
+ rescue ArgumentError
559
+ false
560
+ end
561
+
545
562
  def path_source_gems(path)
546
563
  lockfile_parser(path).specs.each_with_object(Set.new) do |spec, gems|
547
564
  source = spec.source
@@ -54,6 +54,19 @@ begin
54
54
  end
55
55
 
56
56
  run_generate_steps = lambda do
57
+ # The root appraisal lockfile is a local artifact (gitignored) that drifts
58
+ # after dependency bumps. A stale pin makes `bundle install` fail with an
59
+ # opaque "locked to X ... can no longer be found" — observed when a family
60
+ # member's gemspec floor was raised to an unpublished sibling version.
61
+ # appraisal:generate's contract is to work without resolving appraisal
62
+ # locks, so delete the stale root lock and let Bundler resolve fresh from
63
+ # Appraisal.root.gemfile.
64
+ root_appraisal_lock = File.join("Appraisal.root.gemfile.lock")
65
+ if File.file?(root_appraisal_lock)
66
+ File.delete(root_appraisal_lock)
67
+ puts("[kettle-dev][appraisal:generate] deleted stale #{root_appraisal_lock}")
68
+ end
69
+
57
70
  # 1) BUNDLE_GEMFILE=Appraisal.root.gemfile bundle install
58
71
  run_command.call(
59
72
  "appraisal:generate failed: BUNDLE_GEMFILE=Appraisal.root.gemfile bundle install",
@@ -1859,6 +1859,13 @@ module Kettle
1859
1859
  if ver < min
1860
1860
  abort("kettle-release requires Bundler >= 2.7.0 for reproducible builds by default. Current: #{Bundler::VERSION}. Please upgrade bundler.")
1861
1861
  end
1862
+ if ver.prerelease?
1863
+ abort(
1864
+ "kettle-release refuses to run under a prerelease Bundler (#{Bundler::VERSION}). " \
1865
+ "Prerelease Bundler has repeatedly introduced release-breaking behavior changes. " \
1866
+ "Run `gem uninstall bundler -v #{Bundler::VERSION}` and install the latest stable Bundler."
1867
+ )
1868
+ end
1862
1869
  end
1863
1870
 
1864
1871
  # Keep Bundler maintenance separate from the release metadata commit.
@@ -1882,8 +1889,18 @@ module Kettle
1882
1889
  appraisal_gemfile = File.join(@root, "Appraisal.root.gemfile")
1883
1890
  if File.file?(appraisal_gemfile)
1884
1891
  appraisal_lockfile = File.join(@root, "Appraisal.root.gemfile.lock")
1885
- appraisal_command = "BUNDLE_GEMFILE=#{Shellwords.escape(appraisal_gemfile)} " \
1886
- "BUNDLE_LOCKFILE=#{Shellwords.escape(appraisal_lockfile)} bundle update --bundler=#{bundler_version}"
1892
+ appraisal_env = "BUNDLE_GEMFILE=#{Shellwords.escape(appraisal_gemfile)} " \
1893
+ "BUNDLE_LOCKFILE=#{Shellwords.escape(appraisal_lockfile)}"
1894
+ # The appraisal root lockfile is a gitignored local artifact; the
1895
+ # release lockfile reset restores repo state by removing untracked
1896
+ # files, so it is frequently absent here. `bundle update` refuses to
1897
+ # run against a missing lockfile ("This Bundle hasn't been installed
1898
+ # yet"), so materialize the bundle first — bundle install writes the
1899
+ # lockfile and is a no-op when everything is already installed.
1900
+ unless File.file?(appraisal_lockfile)
1901
+ run_cmd!(release_project_command("#{appraisal_env} bundle install"))
1902
+ end
1903
+ appraisal_command = "#{appraisal_env} bundle update --bundler=#{bundler_version}"
1887
1904
  run_cmd!(release_project_command(appraisal_command))
1888
1905
  run_cmd!(release_project_command("bundle exec rake appraisal:reset"))
1889
1906
  end
@@ -12,6 +12,47 @@ module Kettle
12
12
  ENV_TRUE_RE = /\A(1|true|y|yes)\z/i unless const_defined?(:ENV_TRUE_RE, false)
13
13
 
14
14
  module RubyGemsVersions
15
+ # ---------------------------------------------------------------------------
16
+ # Registry answer caches: which one to build on
17
+ # ---------------------------------------------------------------------------
18
+ #
19
+ # Registry answers are cached at three layers. Each has a distinct scope
20
+ # and purpose; pick by the question your tool asks, not by proximity.
21
+ #
22
+ # 1. THE PERSISTENT CACHE — this module (disk, ~30-day TTL, keyed
23
+ # [name, source]). The only cross-process layer. Use it, via
24
+ # #published_version_numbers / #fetch, whenever your tool needs
25
+ # "what versions exist on this registry" and can tolerate the TTL
26
+ # window. Source-keyed because gem.coop can lag rubygems.org — a
27
+ # version present on one can be absent from the other, so the two
28
+ # registries must never share entries.
29
+ # Built on by: kettle-jem deps-floor, kettle-family bup pre-clean.
30
+ #
31
+ # 2. THE SNAPSHOT MEMO — kettle-family PublishedVersionCheck::REGISTRY_MEMO
32
+ # (process-local, keyed [name, remote, version]). NOT a performance
33
+ # layer. Exists for snapshot consistency: one command run must see one
34
+ # registry snapshot, so a gem landing mid-run cannot be reported
35
+ # unpublished for early members and published for later ones. It also
36
+ # owns the just-released bypass: the disk cache can hold a pre-publish
37
+ # answer, and only this layer knows which exact pinned version the
38
+ # caller cares about, so only here can kettle-release's on-disk
39
+ # release marker override the disk cache for that gem+version.
40
+ # Built on by: kettle-family bup/release lockfile validation.
41
+ #
42
+ # 3. THE RUN-LEVEL MEMO — kettle-family ReleasePreClean@published
43
+ # (per-command-instance, keyed name). Fail-open sentinel: registry
44
+ # answers memoized as nil mean "unreachable — remove nothing", which
45
+ # is this command's safety rule. Also collapses repeated per-member
46
+ # lookups of the same name into one answer within the run.
47
+ # Built on by: kettle-family release-mode bup pre-clean.
48
+ #
49
+ # Rule of thumb: persistence goes at layer 1, correctness-scoping
50
+ # (snapshot/bypass) at layer 2, command-local safety policy at layer 3.
51
+ # A future tool that needs "is this version published" should call
52
+ # layer 1, and add a higher layer ONLY if it needs snapshot
53
+ # consistency or its own failure policy.
54
+ #
55
+ # -------------------------------------------------------------------------
15
56
  CACHE_BUST_TTL_SECONDS = 30 * 24 * 60 * 60
16
57
  VERSION_CACHE_TTL_SECONDS = 30 * 24 * 60 * 60
17
58
  HTTP_OPEN_TIMEOUT_SECONDS = 5
@@ -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.1.9"
8
+ VERSION = "3.1.11"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
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.1.9
4
+ version: 3.1.11
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.16
63
+ version: 0.3.17
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.16
73
+ version: 0.3.17
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.18
83
+ version: 0.1.19
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.18
93
+ version: 0.1.19
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.15
103
+ version: 0.1.17
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.15
113
+ version: 0.1.17
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.23
123
+ version: 2.0.24
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.23
133
+ version: 2.0.24
134
134
  - !ruby/object:Gem::Dependency
135
135
  name: ruby-progressbar
136
136
  requirement: !ruby/object:Gem::Requirement
@@ -422,10 +422,10 @@ licenses:
422
422
  - AGPL-3.0-only
423
423
  metadata:
424
424
  homepage_uri: https://kettle-dev.galtzo.com
425
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.1.9
426
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.9/CHANGELOG.md
425
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.1.11
426
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.11/CHANGELOG.md
427
427
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
428
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.1.9
428
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.1.11
429
429
  funding_uri: https://github.com/sponsors/pboling
430
430
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
431
431
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file