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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +33 -1
- data/README.md +2 -1
- data/lib/kettle/dev/ci_helpers.rb +32 -0
- data/lib/kettle/dev/ci_monitor.rb +26 -3
- data/lib/kettle/dev/interactive_release_command.rb +101 -2
- data/lib/kettle/dev/lockfile_reset.rb +17 -0
- data/lib/kettle/dev/rakelib/appraisal.rake +13 -0
- data/lib/kettle/dev/release_cli.rb +19 -2
- data/lib/kettle/dev/ruby_gems_versions.rb +41 -0
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +12 -12
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6532c0b875c1c1581f43bf3035bc3f9b4ff7ba8d04f7a90f6bf78be7fb167ac
|
|
4
|
+
data.tar.gz: 9894c1dd7477202261502e23f1e6d37b6f0f174350ac456b834dc8df19b143ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1886
|
-
"BUNDLE_LOCKFILE=#{Shellwords.escape(appraisal_lockfile)}
|
|
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
|
data/lib/kettle/dev/version.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
426
|
-
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.
|
|
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.
|
|
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
|