kettle-dev 3.1.11 → 3.1.12
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 +18 -1
- data/README.md +3 -1
- data/lib/kettle/dev/ci_helpers.rb +81 -2
- data/lib/kettle/dev/ci_monitor.rb +45 -5
- data/lib/kettle/dev/lockfile_reset.rb +15 -0
- data/lib/kettle/dev/release_cli.rb +15 -0
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +4 -4
- 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: a53813cd27173724428ba3d36dfc9c49c2cd2602235cc6e67ee4c334ac459f19
|
|
4
|
+
data.tar.gz: 259280a12837d7baa9521d1846977d400ed626779f05d45448dd751711d1b11e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57d215d343dfbfe520e7d17802afe6ee84cf54ea9a5558924c42c301a43ea768989045e63714fbd4c3b225a57f4b08fe61d3e2aad9f51248d47c8b9d4457d0b8
|
|
7
|
+
data.tar.gz: 656b8a321db50a699c0a883c17156c46553cacfc47830de36d2926dfc8bafffab4e83948771692f640325a7474b8a7abec613227e23f321b27ad750aa2d62a6f
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,21 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [3.1.12] - 2026-10-12
|
|
34
|
+
|
|
35
|
+
- TAG: [v3.1.12][3.1.12t]
|
|
36
|
+
- COVERAGE: 91.38% -- 6378/6980 lines in 49 files
|
|
37
|
+
- BRANCH COVERAGE: 75.57% -- 2437/3225 branches in 49 files
|
|
38
|
+
- 52.49% documented
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- CI monitor no longer stalls silently at 0/N when GitHub API requests fail: request budget throttling (K_RELEASE_CI_REQUESTS_PER_MINUTE, default 90/min) keeps polling inside the quota; exhausted-quota 403s raise CIRateLimitError so the monitor parks until the reset (emitting a github_rate_limited event) instead of treating the wall as 'no data yet'; 401s raise CIAuthError and abort with token guidance; job-level failure polling is gated to K_RELEASE_CI_JOBS_POLL_INTERVAL (default 60s) — the jobs polling added for early failure detection had multiplied request volume past the quota on large matrices
|
|
43
|
+
|
|
44
|
+
- kettle-release no longer aborts at release:guard_clean when resuming at step 14/15: the process boot can silently rewrite the canonical Gemfile.lock with sibling PATH remotes (gemspec self-version bump forces re-resolution under the family local-path toggle), so canonical lockfiles are now normalized and re-validated immediately before the build and rake release children
|
|
45
|
+
|
|
46
|
+
- kettle-release lockfile normalization no longer crashes with Errno::ENOENT when a generated appraisal lockfile (Appraisal.root.gemfile.lock) vanishes between the lockfile scan and the reset in CI
|
|
47
|
+
|
|
33
48
|
## [3.1.11] - 2026-10-11
|
|
34
49
|
|
|
35
50
|
- TAG: [v3.1.11][3.1.11t]
|
|
@@ -4049,7 +4064,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
4049
4064
|
- Selecting will run the selected workflow via `act`
|
|
4050
4065
|
- This may move to its own gem in the future.
|
|
4051
4066
|
|
|
4052
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.
|
|
4067
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.12...HEAD
|
|
4068
|
+
[3.1.12]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.11...v3.1.12
|
|
4069
|
+
[3.1.12t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.12
|
|
4053
4070
|
[3.1.11]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.10...v3.1.11
|
|
4054
4071
|
[3.1.11t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.11
|
|
4055
4072
|
[3.1.10]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.9...v3.1.10
|
data/README.md
CHANGED
|
@@ -289,6 +289,8 @@ GitHub API and CI helpers
|
|
|
289
289
|
|
|
290
290
|
- `GITHUB_TOKEN` or `GH_TOKEN`: Token used by `ci:act` and release workflow checks to query GitHub Actions status at higher rate limits
|
|
291
291
|
- `GITLAB_TOKEN` or `GL_TOKEN`: Token used by `ci:act` and CI monitor to query GitLab pipeline status
|
|
292
|
+
- `K_RELEASE_CI_REQUESTS_PER_MINUTE`: GitHub API request budget for the release CI monitor (default 90/min). Requests above the budget are spaced out; an exhausted quota parks the monitor until the reset instead of failing
|
|
293
|
+
- `K_RELEASE_CI_JOBS_POLL_INTERVAL`: Minimum seconds between job-level polls of an in-flight workflow run during release CI monitoring (default 60). Job-level detection catches a failed job in a large matrix before the run concludes, without multiplying API request volume
|
|
292
294
|
|
|
293
295
|
Releasing and signing
|
|
294
296
|
|
|
@@ -1068,7 +1070,7 @@ Thanks for RTFM. ☺️
|
|
|
1068
1070
|
[📌gitmoji]: https://gitmoji.dev
|
|
1069
1071
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1070
1072
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1071
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-6.
|
|
1073
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-6.980-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1072
1074
|
[🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
|
|
1073
1075
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1074
1076
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -8,6 +8,19 @@ require "uri"
|
|
|
8
8
|
|
|
9
9
|
module Kettle
|
|
10
10
|
module Dev
|
|
11
|
+
# GitHub API rejected the request because the token is invalid/revoked.
|
|
12
|
+
class CIAuthError < StandardError; end
|
|
13
|
+
|
|
14
|
+
# GitHub API quota exhausted. Recoverable: retry after the reset time.
|
|
15
|
+
class CIRateLimitError < StandardError
|
|
16
|
+
attr_reader :reset_at
|
|
17
|
+
|
|
18
|
+
def initialize(message, reset_at: nil)
|
|
19
|
+
@reset_at = reset_at
|
|
20
|
+
super(message)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
11
24
|
# CI-related helper functions used by Rake tasks and release tooling.
|
|
12
25
|
#
|
|
13
26
|
# This module only exposes module-functions (no instance state) and is
|
|
@@ -130,6 +143,13 @@ module Kettle
|
|
|
130
143
|
"id" => run["id"],
|
|
131
144
|
"head_sha" => run["head_sha"]
|
|
132
145
|
}
|
|
146
|
+
rescue Kettle::Dev::CIAuthError, Kettle::Dev::CIRateLimitError
|
|
147
|
+
# Auth failures are fatal; rate limits are recoverable but must reach
|
|
148
|
+
# the monitor (which parks until the reset) — swallowing either turns
|
|
149
|
+
# the poll into an infinite 0/N tick.
|
|
150
|
+
# fail the same way, so swallowing this turns the monitor into an
|
|
151
|
+
# infinite 0/N tick. Re-raise for the caller to abort with guidance.
|
|
152
|
+
raise
|
|
133
153
|
rescue => e
|
|
134
154
|
Kettle::Dev.debug_error(e, __method__)
|
|
135
155
|
nil
|
|
@@ -163,15 +183,69 @@ module Kettle
|
|
|
163
183
|
end
|
|
164
184
|
end
|
|
165
185
|
|
|
186
|
+
# Process-wide rate-limit state shared by every CIHelpers caller in this
|
|
187
|
+
# process (the release runner polls many member/monitor children in one
|
|
188
|
+
# process tree, and each child gets its own copy — this throttles within
|
|
189
|
+
# a process; the budget is sized so even a fully parallel family run
|
|
190
|
+
# stays inside GitHub's per-token allowance).
|
|
191
|
+
CI_RATE_BUDGET_PER_MINUTE = (ENV["K_RELEASE_CI_REQUESTS_PER_MINUTE"] || 90).to_f
|
|
192
|
+
CI_RATE_MIN_INTERVAL = CI_RATE_BUDGET_PER_MINUTE.positive? ? 60.0 / CI_RATE_BUDGET_PER_MINUTE : 0.0
|
|
193
|
+
@ci_rate_last_request = 0.0
|
|
194
|
+
@ci_rate_lock = Mutex.new
|
|
195
|
+
|
|
196
|
+
def github_rate_limit_wait!
|
|
197
|
+
return if CI_RATE_MIN_INTERVAL.zero?
|
|
198
|
+
|
|
199
|
+
@ci_rate_lock.synchronize do
|
|
200
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
201
|
+
delta = now - @ci_rate_last_request
|
|
202
|
+
if delta < CI_RATE_MIN_INTERVAL
|
|
203
|
+
sleep(CI_RATE_MIN_INTERVAL - delta)
|
|
204
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
205
|
+
end
|
|
206
|
+
@ci_rate_last_request = now
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
166
210
|
def github_get_json(url, token:)
|
|
211
|
+
github_rate_limit_wait!
|
|
167
212
|
uri = URI(url)
|
|
168
213
|
req = Net::HTTP::Get.new(uri)
|
|
169
214
|
req["User-Agent"] = "kettle-dev/ci-helpers"
|
|
170
215
|
req["Authorization"] = "token #{token}" if token && !token.empty?
|
|
171
216
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
|
172
|
-
|
|
217
|
+
# Dispatch on is_a? rather than `case`/===: verifying doubles
|
|
218
|
+
# (instance_double(Net::HTTPOK)) used in specs do not reliably match
|
|
219
|
+
# `Net::HTTPSuccess === res`, which silently degraded every stubbed
|
|
220
|
+
# response to "no data".
|
|
221
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
222
|
+
JSON.parse(res.body)
|
|
223
|
+
elsif res.is_a?(Net::HTTPUnauthorized)
|
|
224
|
+
# 401 is not retryable: an invalid or revoked token fails EVERY
|
|
225
|
+
# request, so a monitor treating it as "no data yet" would tick
|
|
226
|
+
# forever at 0/N. Abort with an actionable message.
|
|
227
|
+
raise(
|
|
228
|
+
Kettle::Dev::CIAuthError,
|
|
229
|
+
"GitHub API rejected the request (401: invalid or revoked token) for #{url}. " \
|
|
230
|
+
"Check GITHUB_TOKEN / GH_TOKEN."
|
|
231
|
+
)
|
|
232
|
+
elsif res.is_a?(Net::HTTPForbidden)
|
|
233
|
+
if res["X-RateLimit-Remaining"].to_i.zero?
|
|
234
|
+
# Rate limited: recoverable. Honor the reset time, then let the
|
|
235
|
+
# caller retry the poll cycle.
|
|
236
|
+
reset_at = Time.at(res["X-RateLimit-Reset"].to_i).utc
|
|
237
|
+
raise(
|
|
238
|
+
Kettle::Dev::CIRateLimitError,
|
|
239
|
+
"GitHub API rate limit exceeded; resets at #{reset_at.strftime("%H:%M:%SZ")}"
|
|
240
|
+
)
|
|
241
|
+
end
|
|
173
242
|
|
|
174
|
-
|
|
243
|
+
# Non-quota 403 (e.g. resource permissions): treat as no data, like
|
|
244
|
+
# other transient responses.
|
|
245
|
+
nil
|
|
246
|
+
else
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
175
249
|
end
|
|
176
250
|
|
|
177
251
|
# Whether a run has completed successfully.
|
|
@@ -215,6 +289,11 @@ module Kettle
|
|
|
215
289
|
"html_url" => job["html_url"]
|
|
216
290
|
}
|
|
217
291
|
end
|
|
292
|
+
rescue Kettle::Dev::CIAuthError, Kettle::Dev::CIRateLimitError
|
|
293
|
+
# See latest_run: auth/rate-limit failures must abort or park the
|
|
294
|
+
# monitor, not degrade
|
|
295
|
+
# to "no jobs yet".
|
|
296
|
+
raise
|
|
218
297
|
rescue => e
|
|
219
298
|
Kettle::Dev.debug_error(e, __method__)
|
|
220
299
|
[]
|
|
@@ -50,10 +50,17 @@ module Kettle
|
|
|
50
50
|
# @param restart_hint [String] guidance command shown on failure
|
|
51
51
|
# @return [Boolean] whether all configured CI providers passed
|
|
52
52
|
def monitor_all!(restart_hint: "bundle exec kettle-release --start-step 10", workflows: nil, keepalive: nil, event_recorder: nil, **options)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
# Rate limiting is recoverable: when the GitHub monitor parks for the
|
|
54
|
+
# quota reset it returns :rate_limited and we re-enter it. GitLab has
|
|
55
|
+
# no equivalent, so only the GitHub result drives the retry.
|
|
56
|
+
results = nil
|
|
57
|
+
loop do
|
|
58
|
+
results = [
|
|
59
|
+
monitor_github_internal!(restart_hint: restart_hint, workflows: workflows, keepalive: keepalive, event_recorder: event_recorder),
|
|
60
|
+
monitor_gitlab_internal!(restart_hint: restart_hint, keepalive: keepalive, event_recorder: event_recorder)
|
|
61
|
+
]
|
|
62
|
+
break unless results.first == :rate_limited
|
|
63
|
+
end
|
|
57
64
|
abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") if results.all?(&:nil?)
|
|
58
65
|
|
|
59
66
|
results.all? { |result| result != false }
|
|
@@ -343,6 +350,18 @@ module Kettle
|
|
|
343
350
|
start_deadline = monotonic_time + start_timeout
|
|
344
351
|
keepalive_timer = keepalive_timer(keepalive)
|
|
345
352
|
all_started_emitted = false
|
|
353
|
+
# Job-level failure detection polls one jobs endpoint per in-flight
|
|
354
|
+
# workflow per cycle; on big matrices that multiplies request volume.
|
|
355
|
+
# Gate it to once per K_RELEASE_CI_JOBS_POLL_INTERVAL seconds
|
|
356
|
+
# (default 60) so run-level polling carries the fast loop and jobs
|
|
357
|
+
# checks stay a bounded add-on.
|
|
358
|
+
jobs_poll_interval = begin
|
|
359
|
+
Float(ENV["K_RELEASE_CI_JOBS_POLL_INTERVAL"])
|
|
360
|
+
rescue
|
|
361
|
+
nil
|
|
362
|
+
end
|
|
363
|
+
jobs_poll_interval = (jobs_poll_interval && jobs_poll_interval >= 0) ? jobs_poll_interval : 60.0
|
|
364
|
+
last_jobs_poll = 0.0
|
|
346
365
|
loop do
|
|
347
366
|
keepalive_timer.call
|
|
348
367
|
|
|
@@ -414,7 +433,9 @@ module Kettle
|
|
|
414
433
|
# a re-run will have to spend anyway.
|
|
415
434
|
run_failed = Kettle::Dev::CIHelpers.failed?(run)
|
|
416
435
|
failed_job = nil
|
|
417
|
-
|
|
436
|
+
jobs_poll_due = monotonic_time - last_jobs_poll >= jobs_poll_interval
|
|
437
|
+
if !passed[wf] && !run_failed && run["status"] != "completed" && jobs_poll_due
|
|
438
|
+
last_jobs_poll = monotonic_time
|
|
418
439
|
# Run still in flight: poll its jobs so a single red job in
|
|
419
440
|
# a large matrix aborts immediately instead of waiting for
|
|
420
441
|
# the whole workflow to conclude.
|
|
@@ -504,6 +525,25 @@ module Kettle
|
|
|
504
525
|
puts "\nGitHub workflows completed with failures (#{passed.size}/#{total} passed); continuing because K_RELEASE_CI_CONTINUE=true."
|
|
505
526
|
end
|
|
506
527
|
failed.empty?
|
|
528
|
+
rescue Kettle::Dev::CIRateLimitError => error
|
|
529
|
+
# Rate limiting is recoverable: park until the quota resets, emit an
|
|
530
|
+
# event, then re-enter the same monitor (the caller wraps this method
|
|
531
|
+
# in a retry loop keyed on the :rate_limited return value).
|
|
532
|
+
pbar&.finish unless pbar&.finished?
|
|
533
|
+
reset_at = (error.respond_to?(:reset_at) && error.reset_at) ? error.reset_at : (Time.now.utc + 60)
|
|
534
|
+
wait = [(reset_at - Time.now.utc).ceil, 1].max
|
|
535
|
+
puts "\n#{error.message}; waiting #{wait}s before retrying."
|
|
536
|
+
emit_ci_monitor_event(
|
|
537
|
+
event_recorder,
|
|
538
|
+
action: "github_rate_limited",
|
|
539
|
+
status: "started",
|
|
540
|
+
provider: "github",
|
|
541
|
+
reset_at: reset_at.utc.strftime("%H:%M:%SZ"),
|
|
542
|
+
wait_seconds: wait,
|
|
543
|
+
head_sha: head_sha
|
|
544
|
+
)
|
|
545
|
+
sleep(wait)
|
|
546
|
+
:rate_limited
|
|
507
547
|
end
|
|
508
548
|
module_function :monitor_github_internal!
|
|
509
549
|
|
|
@@ -161,6 +161,12 @@ module Kettle
|
|
|
161
161
|
platforms = release_platforms_for(paths) if force_full_update
|
|
162
162
|
uninstall_unreleased_local_gems(paths) if force_full_update
|
|
163
163
|
paths.each do |path|
|
|
164
|
+
# Re-check existence: in CI the checkout can contain a stale
|
|
165
|
+
# appraisal lock that later steps (or a parallel worker) delete
|
|
166
|
+
# between the lockfile_paths scan and this loop; ENOENT here would
|
|
167
|
+
# abort the whole release pipeline for a file that no longer needs
|
|
168
|
+
# resetting.
|
|
169
|
+
next unless File.file?(path)
|
|
164
170
|
if force_full_update || normalization_needed?(path, strict: release_lockfiles)
|
|
165
171
|
reset_lockfile!(
|
|
166
172
|
path,
|
|
@@ -191,6 +197,10 @@ module Kettle
|
|
|
191
197
|
end
|
|
192
198
|
|
|
193
199
|
def reset_lockfile!(path, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false, strict: false)
|
|
200
|
+
# Mirrors the reset loop guard: a lockfile that vanished after the
|
|
201
|
+
# scan (CI cleanup of generated appraisal locks) has nothing to
|
|
202
|
+
# normalize; aborting the whole release for it would be wrong.
|
|
203
|
+
return unless File.file?(path)
|
|
194
204
|
gemfile = gemfile_for_lockfile(path)
|
|
195
205
|
unless gemfile && File.file?(gemfile)
|
|
196
206
|
warn("Cannot reset #{display_path(path)} because its Gemfile was not found.")
|
|
@@ -701,6 +711,11 @@ module Kettle
|
|
|
701
711
|
|
|
702
712
|
def lockfile_source_content(path)
|
|
703
713
|
in_checksums = false
|
|
714
|
+
# A lockfile can vanish between the lockfile_paths scan and this read
|
|
715
|
+
# (CI workers clean up generated appraisal locks mid-process). Treat a
|
|
716
|
+
# missing lockfile as an empty graph: bootstrap version lookups return
|
|
717
|
+
# nil and the reset skips what no longer exists.
|
|
718
|
+
return [].freeze unless File.file?(path)
|
|
704
719
|
File.readlines(path).filter_map do |line|
|
|
705
720
|
stripped = line.strip
|
|
706
721
|
if stripped == "CHECKSUMS"
|
|
@@ -492,6 +492,21 @@ module Kettle
|
|
|
492
492
|
# 14. build
|
|
493
493
|
with_release_resume_step(14) do
|
|
494
494
|
if run_step?(14)
|
|
495
|
+
# When resuming at step 14/15 the step 0-5 lockfile preflight is
|
|
496
|
+
# skipped, but this process's own boot (`bundle exec
|
|
497
|
+
# kettle-release` with the family launcher's local-path toggle
|
|
498
|
+
# active) can silently rewrite the canonical Gemfile.lock with
|
|
499
|
+
# sibling PATH remotes — the gemspec self-version bump makes every
|
|
500
|
+
# boot re-resolve. `rake release` aborts on the dirty worktree at
|
|
501
|
+
# release:guard_clean. Normalize at the publish boundary too.
|
|
502
|
+
# Projects without any tracked release lockfile (bare scratch
|
|
503
|
+
# specs) have nothing to normalize; LockfileReset would abort.
|
|
504
|
+
unless lockfile_reset.lockfile_paths.empty?
|
|
505
|
+
reset_release_lockfiles!(stage: "before publish tasks")
|
|
506
|
+
validate_release_lockfiles!(stage: "before publish tasks")
|
|
507
|
+
prepare_release_lockfiles_for_release_tasks!
|
|
508
|
+
materialize_release_lockfiles_for_release_tasks!
|
|
509
|
+
end
|
|
495
510
|
ensure_release_secrets_ready_for_signing! if signing_enabled? && release_secrets_configured?
|
|
496
511
|
if signing_enabled? && release_secrets_configured?
|
|
497
512
|
puts "Running build with gem signing passphrase from configured secrets provider (#{release_secrets_provider_label})..."
|
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.12"
|
|
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.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -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.12
|
|
426
|
+
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.12/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.12
|
|
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
|