kettle-dev 3.1.11 → 3.1.13
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 +50 -1
- data/README.md +21 -5
- data/lib/kettle/dev/ci_helpers.rb +153 -2
- data/lib/kettle/dev/ci_monitor.rb +45 -5
- data/lib/kettle/dev/lockfile_reset.rb +41 -14
- data/lib/kettle/dev/release_cli.rb +26 -0
- data/lib/kettle/dev/tasks/ci_task.rb +4 -0
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +8 -8
- 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: fee58bbb5088875b1370305934e2816bc7b1f7ad5103776d7877f7bfb8383bd2
|
|
4
|
+
data.tar.gz: 75dd9745eda2bd54c6dcf1a9a5ae9bf8b36fdfcb2edd2e8a3c26ab3bde366aa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e21b6253a77ecf67f42b54a464405baa078615cc99585f1146f432135f347f5fc0e7c4044137841f3da5f589486fc3971cd8b5dbe6285bf1f194de9b7bbd8359
|
|
7
|
+
data.tar.gz: 0dd4e28b43e6d4ba4ae3481d0464fbeb8b1c7aa39ae1628f0fc9c2ce80fe9d850a738d62414ceda91f6daf43887ee4b400c6621df7b5b1c473bfaa934ade7941
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,51 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [3.1.13] - 2026-10-14
|
|
34
|
+
|
|
35
|
+
- TAG: [v3.1.13][3.1.13t]
|
|
36
|
+
- COVERAGE: 91.53% -- 6427/7022 lines in 49 files
|
|
37
|
+
- BRANCH COVERAGE: 75.69% -- 2456/3245 branches in 49 files
|
|
38
|
+
- 53.23% documented
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- GitHub API request budget is now enforced machine-wide through an flock'd XDG ledger (XDG_STATE_HOME/kettle-dev/ci-rate-budget.json, override with K_RELEASE_CI_RATE_STATE_FILE) instead of per-process memory: parallel family releases, wave jobs, ci:act, and kettle-release GitHub Releases calls all share one budget (default 90 req/min) matching GitHub's per-token quota; unavailable ledger degrades to in-process spacing
|
|
43
|
+
|
|
44
|
+
- [kc] kettle-jem/template: updated 80 project files:
|
|
45
|
+
- code and tests (5)
|
|
46
|
+
- configuration (2)
|
|
47
|
+
- dependencies (15)
|
|
48
|
+
- documentation (1)
|
|
49
|
+
- other (3)
|
|
50
|
+
- workflows (54)
|
|
51
|
+
- [kc] kettle-jem/prepare: updated 1 project file:
|
|
52
|
+
- dependencies (1)
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- release:guard_clean no longer aborts a --start-step 15 resume: the publish-boundary lockfile normalization added in 3.1.13 lived only inside the step 14 block, so resuming at step 15 skipped it and the parent's own boot (family local-path toggle + gemspec self-version bump) re-dirtied the canonical Gemfile.lock with sibling PATH remotes; normalization now runs once per run from BOTH the step 14 and step 15 entry points
|
|
57
|
+
- LockfileReset#rebuild_lockfile no longer crashes with Errno::ENOENT when a generated appraisal lockfile (e.g. Appraisal.root.gemfile.lock) vanishes between the entry guard and the backup copy (TOCTOU race with mid-process CI cleanup); the rebuild now yields without a backup and skips restore when the target was already gone
|
|
58
|
+
|
|
59
|
+
- Test suite no longer mutates the live repo checkout: appraisal rake task specs now run in a sandboxed CWD (the task's File.delete of the relative Appraisal.root.gemfile.lock path was really deleting the repo's lockfile mid-suite, racing parallel workers), and release_cli flow examples stub reset_release_lockfiles! instead of running real bundler resets against the live checkout — root cause of the ENOENT flake seen in ruby-3.4 CI
|
|
60
|
+
|
|
61
|
+
- CI rate limiter no longer sleeps while holding the machine-wide ledger flock: claims now reserve a future slot under the lock and sleep outside it (ticket limiter), so a wedged or slow claimer can no longer deadlock every other process sharing the ledger — the observed CI hang signature (spec-run worker held the flock for minutes while peers starved past the watchdog). The ledger path override is now read at call time, and the test suite isolates itself onto a per-run tmp ledger instead of the production file
|
|
62
|
+
|
|
63
|
+
## [3.1.12] - 2026-10-12
|
|
64
|
+
|
|
65
|
+
- TAG: [v3.1.12][3.1.12t]
|
|
66
|
+
- COVERAGE: 91.38% -- 6378/6980 lines in 49 files
|
|
67
|
+
- BRANCH COVERAGE: 75.57% -- 2437/3225 branches in 49 files
|
|
68
|
+
- 52.49% documented
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- 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
|
|
73
|
+
|
|
74
|
+
- 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
|
|
75
|
+
|
|
76
|
+
- 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
|
|
77
|
+
|
|
33
78
|
## [3.1.11] - 2026-10-11
|
|
34
79
|
|
|
35
80
|
- TAG: [v3.1.11][3.1.11t]
|
|
@@ -4049,7 +4094,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
4049
4094
|
- Selecting will run the selected workflow via `act`
|
|
4050
4095
|
- This may move to its own gem in the future.
|
|
4051
4096
|
|
|
4052
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.
|
|
4097
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.13...HEAD
|
|
4098
|
+
[3.1.13]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.12...v3.1.13
|
|
4099
|
+
[3.1.13t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.13
|
|
4100
|
+
[3.1.12]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.11...v3.1.12
|
|
4101
|
+
[3.1.12t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.12
|
|
4053
4102
|
[3.1.11]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.10...v3.1.11
|
|
4054
4103
|
[3.1.11t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.11
|
|
4055
4104
|
[3.1.10]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.9...v3.1.10
|
data/README.md
CHANGED
|
@@ -93,7 +93,7 @@ out to `kettle-release` for each member that needs publishing.
|
|
|
93
93
|
| Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] |
|
|
94
94
|
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
95
95
|
| Works with JRuby | [![JRuby 9.2 Compat][💎jruby-9.2i]][🚎jruby-9.2-wf] [![JRuby 9.3 Compat][💎jruby-9.3i]][🚎jruby-9.3-wf] <br/> [![JRuby 9.4 Compat][💎jruby-9.4i]][🚎jruby-9.4-wf] [![JRuby 10.0 Compat][💎jruby-10.0i]][🚎jruby-10.0-wf] [![JRuby current Compat][💎jruby-c-i]][🚎10-j-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf]|
|
|
96
|
-
| Works with Truffle Ruby | [![Truffle Ruby 22.3 Compat][💎truby-22.3i]][🚎truby-22.3-wf] [![Truffle Ruby 23.
|
|
96
|
+
| Works with Truffle Ruby | [![Truffle Ruby 22.3 Compat][💎truby-22.3i]][🚎truby-22.3-wf] [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎truby-23.1-wf] <br/> [![Truffle Ruby 24.2 Compat][💎truby-24.2i]][🚎truby-24.2-wf] [![Truffle Ruby 25.0 Compat][💎truby-25.0i]][🚎truby-25.0-wf] [![Truffle Ruby 33.0 Compat][💎truby-33.0i]][🚎truby-33.0-wf] [![Truffle Ruby current Compat][💎truby-c-i]][🚎9-t-wf]|
|
|
97
97
|
| Works with MRI Ruby 4 | [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf]|
|
|
98
98
|
| Works with MRI Ruby 3 | [![Ruby 3.0 Compat][💎ruby-3.0i]][🚎ruby-3.0-wf] [![Ruby 3.1 Compat][💎ruby-3.1i]][🚎ruby-3.1-wf] [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎ruby-3.2-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎ruby-3.3-wf] [![Ruby 3.4 Compat][💎ruby-3.4i]][🚎ruby-3.4-wf]|
|
|
99
99
|
| Works with MRI Ruby 2 | [![Ruby 2.4 Compat][💎ruby-2.4i]][🚎ruby-2.4-wf] [![Ruby 2.5 Compat][💎ruby-2.5i]][🚎ruby-2.5-wf] [![Ruby 2.6 Compat][💎ruby-2.6i]][🚎ruby-2.6-wf] [![Ruby 2.7 Compat][💎ruby-2.7i]][🚎ruby-2.7-wf]|
|
|
@@ -132,6 +132,23 @@ The _amazing_ test matrix is powered by the kettle-dev stack.
|
|
|
132
132
|
|
|
133
133
|
</details>
|
|
134
134
|
|
|
135
|
+
### Runtime and Platform Support
|
|
136
|
+
|
|
137
|
+
Support tiers describe what this project's CI actually exercises, not what may
|
|
138
|
+
work in an untested environment:
|
|
139
|
+
|
|
140
|
+
- **Parallel support**: CI runs the test suite with multiple workers available.
|
|
141
|
+
- **Unsupported**: the engine/platform combination is not covered by CI and is
|
|
142
|
+
not a project compatibility claim.
|
|
143
|
+
|
|
144
|
+
| Ruby engine/version | Linux | macOS | Windows | Notes |
|
|
145
|
+
|---------------------|-------|-------|---------|-------|
|
|
146
|
+
| MRI 3.1-3.4 | Parallel support | Unsupported | Unsupported | Versioned Ruby workflows run on Linux. |
|
|
147
|
+
| MRI current | Parallel support | Parallel support | Unsupported | Current Ruby workflow covers the declared OS platforms. |
|
|
148
|
+
| MRI head | Parallel support | Unsupported | Unsupported | CI is allowed to fail while Ruby is in development. |
|
|
149
|
+
| JRuby (all CI lanes) | Parallel support | Unsupported | Unsupported | Engine workflows run on Linux; macOS and Windows are not exercised by CI. |
|
|
150
|
+
| TruffleRuby (all CI lanes) | Parallel support | Unsupported | Unsupported | Never shipped a Windows build (zero Windows assets across 66 releases scanned 2026-10-10); setup-ruby hard-fails ([source](https://github.com/ruby/setup-ruby/blob/master/README.md)) |
|
|
151
|
+
|
|
135
152
|
### Federated DVCS
|
|
136
153
|
|
|
137
154
|
<details markdown="1">
|
|
@@ -289,6 +306,8 @@ GitHub API and CI helpers
|
|
|
289
306
|
|
|
290
307
|
- `GITHUB_TOKEN` or `GH_TOKEN`: Token used by `ci:act` and release workflow checks to query GitHub Actions status at higher rate limits
|
|
291
308
|
- `GITLAB_TOKEN` or `GL_TOKEN`: Token used by `ci:act` and CI monitor to query GitLab pipeline status
|
|
309
|
+
- `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
|
|
310
|
+
- `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
311
|
|
|
293
312
|
Releasing and signing
|
|
294
313
|
|
|
@@ -994,7 +1013,6 @@ Thanks for RTFM. ☺️
|
|
|
994
1013
|
[🚎jruby-9.4-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/jruby-9.4.yml
|
|
995
1014
|
[🚎jruby-10.0-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/jruby-10.0.yml
|
|
996
1015
|
[🚎truby-22.3-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-22.3.yml
|
|
997
|
-
[🚎truby-23.0-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-23.0.yml
|
|
998
1016
|
[🚎truby-23.1-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-23.1.yml
|
|
999
1017
|
[🚎truby-24.2-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-24.2.yml
|
|
1000
1018
|
[🚎truby-25.0-wf]: https://github.com/kettle-dev/kettle-dev/actions/workflows/truffleruby-25.0.yml
|
|
@@ -1029,13 +1047,11 @@ Thanks for RTFM. ☺️
|
|
|
1029
1047
|
[💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
|
|
1030
1048
|
[💎ruby-headi]: https://img.shields.io/badge/Ruby-HEAD-CC342D?style=for-the-badge&logo=ruby&logoColor=blue
|
|
1031
1049
|
[💎truby-22.3i]: https://img.shields.io/badge/Truffle_Ruby-22.3-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
1032
|
-
[💎truby-23.0i]: https://img.shields.io/badge/Truffle_Ruby-23.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
1033
1050
|
[💎truby-23.1i]: https://img.shields.io/badge/Truffle_Ruby-23.1-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
1034
1051
|
[💎truby-24.2i]: https://img.shields.io/badge/Truffle_Ruby-24.2-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
1035
1052
|
[💎truby-25.0i]: https://img.shields.io/badge/Truffle_Ruby-25.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
1036
1053
|
[💎truby-33.0i]: https://img.shields.io/badge/Truffle_Ruby-33.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
|
|
1037
1054
|
[💎truby-c-i]: https://img.shields.io/badge/Truffle_Ruby-current-34BCB1?style=for-the-badge&logo=ruby&logoColor=green
|
|
1038
|
-
[💎truby-headi]: https://img.shields.io/badge/Truffle_Ruby-HEAD-34BCB1?style=for-the-badge&logo=ruby&logoColor=blue
|
|
1039
1055
|
[💎jruby-9.2i]: https://img.shields.io/badge/JRuby-9.2-FBE742?style=for-the-badge&logo=ruby&logoColor=red
|
|
1040
1056
|
[💎jruby-9.3i]: https://img.shields.io/badge/JRuby-9.3-FBE742?style=for-the-badge&logo=ruby&logoColor=red
|
|
1041
1057
|
[💎jruby-9.4i]: https://img.shields.io/badge/JRuby-9.4-FBE742?style=for-the-badge&logo=ruby&logoColor=red
|
|
@@ -1068,7 +1084,7 @@ Thanks for RTFM. ☺️
|
|
|
1068
1084
|
[📌gitmoji]: https://gitmoji.dev
|
|
1069
1085
|
[📌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
1086
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1071
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
1087
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-7.022-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1072
1088
|
[🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
|
|
1073
1089
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1074
1090
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -5,9 +5,23 @@ require "open3"
|
|
|
5
5
|
require "net/http"
|
|
6
6
|
require "json"
|
|
7
7
|
require "uri"
|
|
8
|
+
require "fileutils"
|
|
8
9
|
|
|
9
10
|
module Kettle
|
|
10
11
|
module Dev
|
|
12
|
+
# GitHub API rejected the request because the token is invalid/revoked.
|
|
13
|
+
class CIAuthError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# GitHub API quota exhausted. Recoverable: retry after the reset time.
|
|
16
|
+
class CIRateLimitError < StandardError
|
|
17
|
+
attr_reader :reset_at
|
|
18
|
+
|
|
19
|
+
def initialize(message, reset_at: nil)
|
|
20
|
+
@reset_at = reset_at
|
|
21
|
+
super(message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
11
25
|
# CI-related helper functions used by Rake tasks and release tooling.
|
|
12
26
|
#
|
|
13
27
|
# This module only exposes module-functions (no instance state) and is
|
|
@@ -130,6 +144,13 @@ module Kettle
|
|
|
130
144
|
"id" => run["id"],
|
|
131
145
|
"head_sha" => run["head_sha"]
|
|
132
146
|
}
|
|
147
|
+
rescue Kettle::Dev::CIAuthError, Kettle::Dev::CIRateLimitError
|
|
148
|
+
# Auth failures are fatal; rate limits are recoverable but must reach
|
|
149
|
+
# the monitor (which parks until the reset) — swallowing either turns
|
|
150
|
+
# the poll into an infinite 0/N tick.
|
|
151
|
+
# fail the same way, so swallowing this turns the monitor into an
|
|
152
|
+
# infinite 0/N tick. Re-raise for the caller to abort with guidance.
|
|
153
|
+
raise
|
|
133
154
|
rescue => e
|
|
134
155
|
Kettle::Dev.debug_error(e, __method__)
|
|
135
156
|
nil
|
|
@@ -163,15 +184,140 @@ module Kettle
|
|
|
163
184
|
end
|
|
164
185
|
end
|
|
165
186
|
|
|
187
|
+
# Rate-limit budget shared by every CIHelpers caller. The budget is
|
|
188
|
+
# enforced MACHINE-WIDE: parallel family releases (kettle-family spawns
|
|
189
|
+
# one kettle-release process per member job, and every family can run
|
|
190
|
+
# concurrently) all share one GitHub per-token quota, so the limiter
|
|
191
|
+
# coordinates through an flock'd ledger file under XDG state rather than
|
|
192
|
+
# per-process memory. kettle-dev owns the file because it makes the
|
|
193
|
+
# requests; kettle-family never calls the API itself.
|
|
194
|
+
CI_RATE_BUDGET_PER_MINUTE = (ENV["K_RELEASE_CI_REQUESTS_PER_MINUTE"] || 90).to_f
|
|
195
|
+
CI_RATE_MIN_INTERVAL = CI_RATE_BUDGET_PER_MINUTE.positive? ? 60.0 / CI_RATE_BUDGET_PER_MINUTE : 0.0
|
|
196
|
+
@ci_rate_lock = Mutex.new
|
|
197
|
+
|
|
198
|
+
# Ledger location: XDG_STATE_HOME/kettle-dev/ci-rate-budget.json
|
|
199
|
+
# (XDG default is ~/.local/state). Overridable wholesale with
|
|
200
|
+
# K_RELEASE_CI_RATE_STATE_FILE. Read at CALL time (not frozen at load)
|
|
201
|
+
# so test suites can point the limiter at a per-run tmp ledger via
|
|
202
|
+
# stubbed_env: the shared ledger is effectively a local production
|
|
203
|
+
# system and specs must never consume or corrupt its state.
|
|
204
|
+
def ci_rate_state_path
|
|
205
|
+
override = ENV["K_RELEASE_CI_RATE_STATE_FILE"]
|
|
206
|
+
return override if override && !override.empty?
|
|
207
|
+
|
|
208
|
+
base = ENV["XDG_STATE_HOME"]
|
|
209
|
+
base = (base && !base.empty?) ? base : File.join(Dir.home, ".local", "state")
|
|
210
|
+
File.join(base, "kettle-dev", "ci-rate-budget.json")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Cross-process spacing, ticket-style: each caller claims the next
|
|
214
|
+
# request slot under an exclusive flock on the ledger file, RELEASES the
|
|
215
|
+
# lock, and only then sleeps out its wait. The critical section is a few
|
|
216
|
+
# microseconds regardless of budget, so hammering claimers queue briefly
|
|
217
|
+
# on the flock instead of each holding it for a whole interval — the old
|
|
218
|
+
# in-lock sleep serialized the whole machine behind one interval per
|
|
219
|
+
# claim, and a claimer wedged mid-sleep deadlocked every other process
|
|
220
|
+
# (observed in CI: a spec-run worker held the flock for minutes while
|
|
221
|
+
# its peers silently starved past the watchdog threshold).
|
|
222
|
+
# The flock is released automatically if a process dies. A missing or
|
|
223
|
+
# corrupt ledger starts fresh.
|
|
224
|
+
def github_rate_limit_wait!
|
|
225
|
+
return if CI_RATE_MIN_INTERVAL.zero?
|
|
226
|
+
|
|
227
|
+
wait = claim_ci_rate_slot
|
|
228
|
+
sleep(wait) if wait.positive?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Returns how long the caller must wait before its request (0.0 when the
|
|
232
|
+
# budget is unspent). Never sleeps itself. Lock hold time is bounded by
|
|
233
|
+
# the file IO only.
|
|
234
|
+
def claim_ci_rate_slot
|
|
235
|
+
@ci_rate_lock.synchronize do
|
|
236
|
+
path = ci_rate_state_path
|
|
237
|
+
dir = File.dirname(path)
|
|
238
|
+
begin
|
|
239
|
+
FileUtils.mkdir_p(dir)
|
|
240
|
+
rescue SystemCallError
|
|
241
|
+
# No writable state dir: degrade to in-process throttling only.
|
|
242
|
+
return ci_rate_claim_in_process
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
begin
|
|
246
|
+
File.open(path, File::RDWR | File::CREAT, 0o644) do |f|
|
|
247
|
+
f.flock(File::LOCK_EX)
|
|
248
|
+
payload = f.size.zero? ? {} : JSON.parse(f.read)
|
|
249
|
+
last = payload["next_slot"] || payload["last_request"] || 0.0
|
|
250
|
+
now = Time.now.to_f
|
|
251
|
+
# Absurdly future (clock skew) or ancient entries start fresh.
|
|
252
|
+
last = 0.0 if !last.is_a?(Numeric) || (last - now).abs > 3600
|
|
253
|
+
slot = [last + CI_RATE_MIN_INTERVAL, now].max
|
|
254
|
+
f.rewind
|
|
255
|
+
f.truncate(0)
|
|
256
|
+
f.write(JSON.generate({"next_slot" => slot}))
|
|
257
|
+
f.flush
|
|
258
|
+
wait = slot - now
|
|
259
|
+
return wait.positive? ? wait : 0.0
|
|
260
|
+
end
|
|
261
|
+
rescue SystemCallError, JSON::ParserError
|
|
262
|
+
# Ledger unusable (read-only FS, broken JSON, no flock support):
|
|
263
|
+
# degrade to in-process throttling rather than failing releases.
|
|
264
|
+
return ci_rate_claim_in_process
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Fallback when no shared ledger is available. In-process ticket claims,
|
|
270
|
+
# sized to the same budget; the caller sleeps outside this method.
|
|
271
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable -- callers hold @ci_rate_lock, so this state has no concurrent writers; the degradation path is intentionally module-scoped.
|
|
272
|
+
def ci_rate_claim_in_process
|
|
273
|
+
@ci_rate_inproc_last = 0.0 unless defined?(@ci_rate_inproc_last)
|
|
274
|
+
now = Time.now.to_f
|
|
275
|
+
slot = [@ci_rate_inproc_last + CI_RATE_MIN_INTERVAL, now].max
|
|
276
|
+
@ci_rate_inproc_last = slot
|
|
277
|
+
wait = slot - now
|
|
278
|
+
wait.positive? ? wait : 0.0
|
|
279
|
+
end
|
|
280
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
281
|
+
|
|
166
282
|
def github_get_json(url, token:)
|
|
283
|
+
github_rate_limit_wait!
|
|
167
284
|
uri = URI(url)
|
|
168
285
|
req = Net::HTTP::Get.new(uri)
|
|
169
286
|
req["User-Agent"] = "kettle-dev/ci-helpers"
|
|
170
287
|
req["Authorization"] = "token #{token}" if token && !token.empty?
|
|
171
288
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
|
172
|
-
|
|
289
|
+
# Dispatch on is_a? rather than `case`/===: verifying doubles
|
|
290
|
+
# (instance_double(Net::HTTPOK)) used in specs do not reliably match
|
|
291
|
+
# `Net::HTTPSuccess === res`, which silently degraded every stubbed
|
|
292
|
+
# response to "no data".
|
|
293
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
294
|
+
JSON.parse(res.body)
|
|
295
|
+
elsif res.is_a?(Net::HTTPUnauthorized)
|
|
296
|
+
# 401 is not retryable: an invalid or revoked token fails EVERY
|
|
297
|
+
# request, so a monitor treating it as "no data yet" would tick
|
|
298
|
+
# forever at 0/N. Abort with an actionable message.
|
|
299
|
+
raise(
|
|
300
|
+
Kettle::Dev::CIAuthError,
|
|
301
|
+
"GitHub API rejected the request (401: invalid or revoked token) for #{url}. " \
|
|
302
|
+
"Check GITHUB_TOKEN / GH_TOKEN."
|
|
303
|
+
)
|
|
304
|
+
elsif res.is_a?(Net::HTTPForbidden)
|
|
305
|
+
if res["X-RateLimit-Remaining"].to_i.zero?
|
|
306
|
+
# Rate limited: recoverable. Honor the reset time, then let the
|
|
307
|
+
# caller retry the poll cycle.
|
|
308
|
+
reset_at = Time.at(res["X-RateLimit-Reset"].to_i).utc
|
|
309
|
+
raise(
|
|
310
|
+
Kettle::Dev::CIRateLimitError,
|
|
311
|
+
"GitHub API rate limit exceeded; resets at #{reset_at.strftime("%H:%M:%SZ")}"
|
|
312
|
+
)
|
|
313
|
+
end
|
|
173
314
|
|
|
174
|
-
|
|
315
|
+
# Non-quota 403 (e.g. resource permissions): treat as no data, like
|
|
316
|
+
# other transient responses.
|
|
317
|
+
nil
|
|
318
|
+
else
|
|
319
|
+
nil
|
|
320
|
+
end
|
|
175
321
|
end
|
|
176
322
|
|
|
177
323
|
# Whether a run has completed successfully.
|
|
@@ -215,6 +361,11 @@ module Kettle
|
|
|
215
361
|
"html_url" => job["html_url"]
|
|
216
362
|
}
|
|
217
363
|
end
|
|
364
|
+
rescue Kettle::Dev::CIAuthError, Kettle::Dev::CIRateLimitError
|
|
365
|
+
# See latest_run: auth/rate-limit failures must abort or park the
|
|
366
|
+
# monitor, not degrade
|
|
367
|
+
# to "no jobs yet".
|
|
368
|
+
raise
|
|
218
369
|
rescue => e
|
|
219
370
|
Kettle::Dev.debug_error(e, __method__)
|
|
220
371
|
[]
|
|
@@ -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.")
|
|
@@ -285,7 +295,7 @@ module Kettle
|
|
|
285
295
|
|
|
286
296
|
def lockfile_platforms(path)
|
|
287
297
|
in_platforms = false
|
|
288
|
-
|
|
298
|
+
safe_lockfile_lines(path).filter_map do |line|
|
|
289
299
|
stripped = line.strip
|
|
290
300
|
if stripped.match?(/\A[A-Z][A-Z ]*\z/)
|
|
291
301
|
in_platforms = stripped == "PLATFORMS"
|
|
@@ -438,7 +448,7 @@ module Kettle
|
|
|
438
448
|
end
|
|
439
449
|
|
|
440
450
|
def local_path_remote_lines(path)
|
|
441
|
-
self.class.local_path_remote_lines_from_source(
|
|
451
|
+
self.class.local_path_remote_lines_from_source(safe_lockfile_lines(path).join)
|
|
442
452
|
end
|
|
443
453
|
|
|
444
454
|
attr_reader :release_graph_contract
|
|
@@ -450,7 +460,7 @@ module Kettle
|
|
|
450
460
|
end
|
|
451
461
|
|
|
452
462
|
def local_path_remote_at(path, line_number)
|
|
453
|
-
|
|
463
|
+
safe_lockfile_lines(path)[line_number - 1].to_s.split("remote:", 2).last.to_s.strip
|
|
454
464
|
end
|
|
455
465
|
|
|
456
466
|
def allowed_local_path?(path)
|
|
@@ -467,7 +477,7 @@ module Kettle
|
|
|
467
477
|
def empty_registry_checksums(path)
|
|
468
478
|
non_registry_gems = non_registry_source_gems(path) | path_dependency_gems(path)
|
|
469
479
|
in_checksums = false
|
|
470
|
-
entries =
|
|
480
|
+
entries = safe_lockfile_lines(path).filter_map.with_index(1) do |line, index|
|
|
471
481
|
stripped = line.strip
|
|
472
482
|
if stripped == "CHECKSUMS"
|
|
473
483
|
in_checksums = true
|
|
@@ -507,7 +517,7 @@ module Kettle
|
|
|
507
517
|
|
|
508
518
|
def has_any_sha_checksum?(path)
|
|
509
519
|
in_checksums = false
|
|
510
|
-
|
|
520
|
+
safe_lockfile_lines(path).any? do |line|
|
|
511
521
|
stripped = line.strip
|
|
512
522
|
if stripped == "CHECKSUMS"
|
|
513
523
|
in_checksums = true
|
|
@@ -528,14 +538,14 @@ module Kettle
|
|
|
528
538
|
version = bundled_with_version(path)
|
|
529
539
|
return false if version.empty?
|
|
530
540
|
|
|
531
|
-
checksums = self.class.checksum_entries_from_source(
|
|
541
|
+
checksums = self.class.checksum_entries_from_source(safe_lockfile_lines(path).join)
|
|
532
542
|
return false unless checksums&.keys&.any? { |name, _checksum_version| name == "bundler" }
|
|
533
543
|
|
|
534
544
|
!checksums.key?(["bundler", version])
|
|
535
545
|
end
|
|
536
546
|
|
|
537
547
|
def bundled_with_version(path)
|
|
538
|
-
lines =
|
|
548
|
+
lines = safe_lockfile_lines(path)
|
|
539
549
|
marker = lines.index { |line| line.strip == "BUNDLED WITH" }
|
|
540
550
|
return "" unless marker
|
|
541
551
|
|
|
@@ -616,7 +626,8 @@ module Kettle
|
|
|
616
626
|
|
|
617
627
|
def gemfile_bootstrap_gems(gemfile)
|
|
618
628
|
gemfile_source_paths(gemfile).each_with_index.each_with_object({}) do |(path, index), gems|
|
|
619
|
-
|
|
629
|
+
source = File.file?(path) ? File.read(path) : ""
|
|
630
|
+
calls = require_calls_from_ripper(Ripper.sexp(source))
|
|
620
631
|
calls.filter_map { |method_name, argument| bootstrap_gem_name(method_name, argument) }.each do |name|
|
|
621
632
|
gems[name] = gems.fetch(name, false) || index.zero?
|
|
622
633
|
end
|
|
@@ -701,7 +712,7 @@ module Kettle
|
|
|
701
712
|
|
|
702
713
|
def lockfile_source_content(path)
|
|
703
714
|
in_checksums = false
|
|
704
|
-
|
|
715
|
+
safe_lockfile_lines(path).filter_map do |line|
|
|
705
716
|
stripped = line.strip
|
|
706
717
|
if stripped == "CHECKSUMS"
|
|
707
718
|
in_checksums = true
|
|
@@ -731,13 +742,18 @@ module Kettle
|
|
|
731
742
|
def rebuild_lockfile(path)
|
|
732
743
|
backup = backup_path_for(path)
|
|
733
744
|
FileUtils.mkdir_p(File.dirname(backup))
|
|
734
|
-
|
|
745
|
+
# TOCTOU: the target can vanish between the scan/entry guard and this
|
|
746
|
+
# copy (CI cleanup of generated appraisal lockfiles mid-process).
|
|
747
|
+
# Nothing to preserve in that case — yield so the rebuild writes a
|
|
748
|
+
# fresh lockfile, and skip the backup restore entirely.
|
|
749
|
+
existed = File.file?(path)
|
|
750
|
+
FileUtils.cp(path, backup) if existed
|
|
735
751
|
FileUtils.rm_f(path)
|
|
736
752
|
yield
|
|
737
|
-
FileUtils.mv(backup, path, force: true)
|
|
753
|
+
FileUtils.mv(backup, path, force: true) if existed && !File.file?(path)
|
|
738
754
|
FileUtils.rm_f(backup)
|
|
739
755
|
rescue
|
|
740
|
-
FileUtils.mv(backup, path, force: true) if File.file?(backup)
|
|
756
|
+
FileUtils.mv(backup, path, force: true) if existed && File.file?(backup)
|
|
741
757
|
raise
|
|
742
758
|
end
|
|
743
759
|
|
|
@@ -779,11 +795,22 @@ module Kettle
|
|
|
779
795
|
registry_specs_with_lines(path).map { |name, version, _line_number| [name, version] }
|
|
780
796
|
end
|
|
781
797
|
|
|
798
|
+
# Single read path for lockfile contents: a generated appraisal lock
|
|
799
|
+
# can vanish between the lockfile_paths scan and any read (CI workers
|
|
800
|
+
# create and clean up locks in the repo root mid-process). Every reader
|
|
801
|
+
# in this class goes through here and treats absence as an empty file
|
|
802
|
+
# so a deleted ghost lock degrades to "nothing to do" instead of
|
|
803
|
+
# crashing the release pipeline with Errno::ENOENT.
|
|
804
|
+
def safe_lockfile_lines(path)
|
|
805
|
+
return [].freeze unless File.file?(path)
|
|
806
|
+
File.readlines(path)
|
|
807
|
+
end
|
|
808
|
+
|
|
782
809
|
def registry_specs_with_lines(path)
|
|
783
810
|
specs = []
|
|
784
811
|
in_gem = false
|
|
785
812
|
in_specs = false
|
|
786
|
-
|
|
813
|
+
safe_lockfile_lines(path).each.with_index(1) do |line, line_number|
|
|
787
814
|
stripped = line.chomp
|
|
788
815
|
if stripped == "GEM"
|
|
789
816
|
in_gem = true
|
|
@@ -852,7 +879,7 @@ module Kettle
|
|
|
852
879
|
def registry_source_urls(path)
|
|
853
880
|
urls = []
|
|
854
881
|
in_gem = false
|
|
855
|
-
|
|
882
|
+
safe_lockfile_lines(path).each do |line|
|
|
856
883
|
stripped = line.strip
|
|
857
884
|
if stripped == "GEM"
|
|
858
885
|
in_gem = true
|
|
@@ -492,6 +492,7 @@ module Kettle
|
|
|
492
492
|
# 14. build
|
|
493
493
|
with_release_resume_step(14) do
|
|
494
494
|
if run_step?(14)
|
|
495
|
+
normalize_release_lockfiles_before_publish_tasks!
|
|
495
496
|
ensure_release_secrets_ready_for_signing! if signing_enabled? && release_secrets_configured?
|
|
496
497
|
if signing_enabled? && release_secrets_configured?
|
|
497
498
|
puts "Running build with gem signing passphrase from configured secrets provider (#{release_secrets_provider_label})..."
|
|
@@ -505,6 +506,7 @@ module Kettle
|
|
|
505
506
|
# 15. release and tag
|
|
506
507
|
with_release_resume_step(15) do
|
|
507
508
|
if run_step?(15)
|
|
509
|
+
normalize_release_lockfiles_before_publish_tasks!
|
|
508
510
|
version ||= detect_version
|
|
509
511
|
gem_name = detect_gem_name
|
|
510
512
|
@release_candidate = build_release_candidate(gem_name, version)
|
|
@@ -763,6 +765,27 @@ module Kettle
|
|
|
763
765
|
reset_release_lockfiles!(stage: "before release task bundle installs")
|
|
764
766
|
end
|
|
765
767
|
|
|
768
|
+
# When resuming at step 14/15 the step 0-5 lockfile preflight is skipped,
|
|
769
|
+
# but this process's own boot (`bundle exec kettle-release` with the
|
|
770
|
+
# family launcher's local-path toggle active) can silently rewrite the
|
|
771
|
+
# canonical Gemfile.lock with sibling PATH remotes — the gemspec
|
|
772
|
+
# self-version bump makes every boot re-resolve. `rake release` aborts on
|
|
773
|
+
# the dirty worktree at release:guard_clean. Normalize at the publish
|
|
774
|
+
# boundary too: once per run, invoked from BOTH the step 14 and step 15
|
|
775
|
+
# entry points so a `--start-step 15` resume (which skips step 14) is
|
|
776
|
+
# covered as well. Projects without any tracked release lockfile (bare
|
|
777
|
+
# scratch specs) have nothing to normalize; LockfileReset would abort.
|
|
778
|
+
def normalize_release_lockfiles_before_publish_tasks!
|
|
779
|
+
return if @release_lockfiles_normalized_before_publish
|
|
780
|
+
return if lockfile_reset.lockfile_paths.empty?
|
|
781
|
+
|
|
782
|
+
reset_release_lockfiles!(stage: "before publish tasks")
|
|
783
|
+
validate_release_lockfiles!(stage: "before publish tasks")
|
|
784
|
+
prepare_release_lockfiles_for_release_tasks!
|
|
785
|
+
materialize_release_lockfiles_for_release_tasks!
|
|
786
|
+
@release_lockfiles_normalized_before_publish = true
|
|
787
|
+
end
|
|
788
|
+
|
|
766
789
|
# Resetting can select a platform-specific artifact that is not installed
|
|
767
790
|
# in the invoking development bundle. Materialize every canonical release
|
|
768
791
|
# bundle before a release gate runs `bundle exec`. Use a disposable copy:
|
|
@@ -3371,6 +3394,7 @@ module Kettle
|
|
|
3371
3394
|
prerelease: false
|
|
3372
3395
|
})
|
|
3373
3396
|
|
|
3397
|
+
Kettle::Dev::CIHelpers.github_rate_limit_wait!
|
|
3374
3398
|
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
3375
3399
|
http.request(req)
|
|
3376
3400
|
end
|
|
@@ -3441,6 +3465,7 @@ module Kettle
|
|
|
3441
3465
|
request["Accept"] = "application/vnd.github+json"
|
|
3442
3466
|
request["Authorization"] = "token #{token}"
|
|
3443
3467
|
request["User-Agent"] = "kettle-dev-release-cli"
|
|
3468
|
+
Kettle::Dev::CIHelpers.github_rate_limit_wait!
|
|
3444
3469
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
|
|
3445
3470
|
return [JSON.parse(response.body), nil] if response.is_a?(Net::HTTPSuccess)
|
|
3446
3471
|
|
|
@@ -3540,6 +3565,7 @@ module Kettle
|
|
|
3540
3565
|
request["Authorization"] = "token #{token}"
|
|
3541
3566
|
request["User-Agent"] = "kettle-dev-release-cli"
|
|
3542
3567
|
request.body = JSON.dump(name: title, body: body)
|
|
3568
|
+
Kettle::Dev::CIHelpers.github_rate_limit_wait!
|
|
3543
3569
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
|
|
3544
3570
|
response.is_a?(Net::HTTPSuccess) ? [true, "updated"] : [false, "HTTP #{response.code}: #{response.body}"]
|
|
3545
3571
|
rescue => e
|
|
@@ -68,6 +68,8 @@ module Kettle
|
|
|
68
68
|
token = Kettle::Dev::CIHelpers.default_token
|
|
69
69
|
req["Authorization"] = "token #{token}" if token && !token.empty?
|
|
70
70
|
begin
|
|
71
|
+
# Share the machine-wide GitHub API budget with release monitors.
|
|
72
|
+
Kettle::Dev::CIHelpers.github_rate_limit_wait!
|
|
71
73
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
|
72
74
|
if res.is_a?(Net::HTTPSuccess)
|
|
73
75
|
data = JSON.parse(res.body)
|
|
@@ -299,6 +301,8 @@ module Kettle
|
|
|
299
301
|
req = Net::HTTP::Get.new(uri)
|
|
300
302
|
req["User-Agent"] = "ci:act rake task"
|
|
301
303
|
req["Authorization"] = "token #{tk}" if tk && !tk.empty?
|
|
304
|
+
# Share the machine-wide GitHub API budget with release monitors.
|
|
305
|
+
Kettle::Dev::CIHelpers.github_rate_limit_wait!
|
|
302
306
|
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
|
303
307
|
if res.is_a?(Net::HTTPSuccess)
|
|
304
308
|
data = JSON.parse(res.body)
|
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.13"
|
|
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.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -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.25
|
|
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.25
|
|
134
134
|
- !ruby/object:Gem::Dependency
|
|
135
135
|
name: ruby-progressbar
|
|
136
136
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -276,7 +276,7 @@ dependencies:
|
|
|
276
276
|
version: '3.2'
|
|
277
277
|
- - ">="
|
|
278
278
|
- !ruby/object:Gem::Version
|
|
279
|
-
version: 3.2.
|
|
279
|
+
version: 3.2.17
|
|
280
280
|
type: :development
|
|
281
281
|
prerelease: false
|
|
282
282
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -286,7 +286,7 @@ dependencies:
|
|
|
286
286
|
version: '3.2'
|
|
287
287
|
- - ">="
|
|
288
288
|
- !ruby/object:Gem::Version
|
|
289
|
-
version: 3.2.
|
|
289
|
+
version: 3.2.17
|
|
290
290
|
- !ruby/object:Gem::Dependency
|
|
291
291
|
name: stone_checksums
|
|
292
292
|
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.13
|
|
426
|
+
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.13/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.13
|
|
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
|