ruby-link-checker 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/coverage.yml +24 -0
- data/.github/workflows/danger-comment.yml +17 -0
- data/.github/workflows/danger.yml +12 -0
- data/.github/workflows/lint.yml +20 -0
- data/.github/workflows/release.yml +14 -0
- data/.github/workflows/test.yml +25 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rubocop.yml +26 -0
- data/.rubocop_todo.yml +332 -0
- data/AGENTS.md +23 -0
- data/CHANGELOG.md +27 -0
- data/CONTRIBUTING.md +4 -0
- data/Dangerfile +2 -0
- data/Gemfile +5 -1
- data/README.md +151 -7
- data/RELEASING.md +17 -40
- data/Rakefile +8 -0
- data/benchmarks/check.rb +27 -20
- data/lib/ruby-link-checker/async/http/checker.rb +118 -0
- data/lib/ruby-link-checker/async/http/config.rb +39 -0
- data/lib/ruby-link-checker/async/http/result.rb +85 -0
- data/lib/ruby-link-checker/async/http.rb +3 -0
- data/lib/ruby-link-checker/cache.rb +101 -0
- data/lib/ruby-link-checker/callbacks.rb +2 -2
- data/lib/ruby-link-checker/checker.rb +76 -4
- data/lib/ruby-link-checker/config.rb +64 -0
- data/lib/ruby-link-checker/errors/connection_error.rb +23 -0
- data/lib/ruby-link-checker/errors/redirect_limit_error.rb +15 -0
- data/lib/ruby-link-checker/errors.rb +2 -0
- data/lib/ruby-link-checker/net/http/checker.rb +4 -1
- data/lib/ruby-link-checker/net/http/result.rb +4 -0
- data/lib/ruby-link-checker/result.rb +25 -0
- data/lib/ruby-link-checker/task.rb +2 -0
- data/lib/ruby-link-checker/tasks.rb +43 -7
- data/lib/ruby-link-checker/typhoeus/hydra/checker.rb +29 -6
- data/lib/ruby-link-checker/typhoeus/hydra/result.rb +4 -0
- data/lib/ruby-link-checker/version.rb +1 -1
- data/lib/ruby-link-checker.rb +2 -0
- data/ruby-link-checker.gemspec +3 -2
- data/spec/ruby-link-checker/async/http/checker_spec.rb +184 -0
- data/spec/ruby-link-checker/callbacks_spec.rb +33 -0
- data/spec/ruby-link-checker/checker_spec.rb +89 -2
- data/spec/ruby-link-checker/config_spec.rb +6 -0
- data/spec/ruby-link-checker/errors/redirect_loop_error_spec.rb +18 -0
- data/spec/ruby-link-checker/net/http/checker_spec.rb +32 -0
- data/spec/ruby-link-checker/result_error_spec.rb +34 -0
- data/spec/ruby-link-checker/result_spec.rb +41 -0
- data/spec/ruby-link-checker/task_spec.rb +20 -0
- data/spec/ruby-link-checker/tasks_spec.rb +91 -0
- data/spec/ruby-link-checker/typhoeus/hydra/checker_spec.rb +116 -3
- data/spec/spec_helper.rb +3 -1
- data/spec/support/link_checker.rb +267 -3
- metadata +42 -35
- data/Gemfile.lock +0 -215
- data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_asc.png +0 -0
- data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
- data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_both.png +0 -0
- data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_desc.png +0 -0
- data/coverage/assets/0.13.1/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
- data/coverage/assets/0.13.1/application.css +0 -1
- data/coverage/assets/0.13.1/application.js +0 -7
- data/coverage/assets/0.13.1/colorbox/border.png +0 -0
- data/coverage/assets/0.13.1/colorbox/controls.png +0 -0
- data/coverage/assets/0.13.1/colorbox/loading.gif +0 -0
- data/coverage/assets/0.13.1/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.13.1/favicon_green.png +0 -0
- data/coverage/assets/0.13.1/favicon_red.png +0 -0
- data/coverage/assets/0.13.1/favicon_yellow.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.13.1/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.13.1/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.13.1/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.13.1/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.13.1/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.13.1/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.13.1/loading.gif +0 -0
- data/coverage/assets/0.13.1/magnify.png +0 -0
- data/coverage/index.html +0 -15014
data/README.md
CHANGED
|
@@ -2,9 +2,8 @@ Fast Ruby Link Checker
|
|
|
2
2
|
======================
|
|
3
3
|
|
|
4
4
|
[](http://badge.fury.io/rb/ruby-link-checker)
|
|
5
|
-
[](https://github.com/dblock/ruby-link-checker/actions)
|
|
6
|
-
[](https://codeclimate.com/github/dblock/ruby-link-checker/test_coverage)
|
|
5
|
+
[](https://github.com/dblock/ruby-link-checker/actions/workflows/test.yml)
|
|
6
|
+
[](https://codecov.io/gh/dblock/ruby-link-checker)
|
|
8
7
|
|
|
9
8
|
A fast Ruby link checker with support for multiple HTTP libraries. Does not parse documents, just checks links. Fast. Anecdotal benchmarking on a M1 mac and T1 Internet yields ~50 URLs per second with `LinkChecker::Typhoeus::Hydra`.
|
|
10
9
|
|
|
@@ -16,16 +15,23 @@ A fast Ruby link checker with support for multiple HTTP libraries. Does not pars
|
|
|
16
15
|
- [Passing Options](#passing-options)
|
|
17
16
|
- [Checkers](#checkers)
|
|
18
17
|
- [LinkChecker::Typhoeus::Hydra](#linkcheckertyphoeushydra)
|
|
18
|
+
- [LinkChecker::Async::HTTP](#linkcheckerasynchttp)
|
|
19
19
|
- [LinkChecker::Net::HTTP](#linkcheckernethttp)
|
|
20
20
|
- [Options](#options)
|
|
21
21
|
- [Retries](#retries)
|
|
22
|
+
- [Redirects](#redirects)
|
|
22
23
|
- [Retry After](#retry-after)
|
|
24
|
+
- [Retry Policy](#retry-policy)
|
|
23
25
|
- [Results](#results)
|
|
24
26
|
- [Methods](#methods)
|
|
27
|
+
- [Deduplication](#deduplication)
|
|
28
|
+
- [Caching](#caching)
|
|
25
29
|
- [Logger](#logger)
|
|
26
30
|
- [User-Agent](#user-agent)
|
|
31
|
+
- [Headers](#headers)
|
|
27
32
|
- [Global Configuration](#global-configuration)
|
|
28
33
|
- [Callbacks and Events](#callbacks-and-events)
|
|
34
|
+
- [Benchmarks](#benchmarks)
|
|
29
35
|
- [Contributing](#contributing)
|
|
30
36
|
- [Copyright and License](#copyright-and-license)
|
|
31
37
|
|
|
@@ -127,6 +133,63 @@ LinkChecker::Typhoeus::Hydra.configure do |config|
|
|
|
127
133
|
end
|
|
128
134
|
```
|
|
129
135
|
|
|
136
|
+
#### [LinkChecker::Async::HTTP](lib/ruby-link-checker/async/http/checker.rb)
|
|
137
|
+
|
|
138
|
+
Fast, concurrent link checker that uses [async-http](https://github.com/socketry/async-http).
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
require 'async'
|
|
142
|
+
require 'async/http'
|
|
143
|
+
require 'ruby-link-checker'
|
|
144
|
+
|
|
145
|
+
# create a new instance of a checker
|
|
146
|
+
checker = LinkChecker::Async::HTTP::Checker.new
|
|
147
|
+
|
|
148
|
+
# log requests and response codes
|
|
149
|
+
checker.logger.level = Logger::INFO
|
|
150
|
+
|
|
151
|
+
links = [...] # array of URLs
|
|
152
|
+
links.each do |url|
|
|
153
|
+
checker.check url
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# examine failures and errors as they come
|
|
157
|
+
checker.on :error, :failure do |result|
|
|
158
|
+
puts "FAIL: #{result}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# runs all queued checks concurrently, will block until all requests have completed
|
|
162
|
+
checker.run
|
|
163
|
+
|
|
164
|
+
# examine results
|
|
165
|
+
checker.results.each_pair do |bucket, results|
|
|
166
|
+
puts "#{bucket}: #{results.size}"
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
You can pass `Async::HTTP` timeout options into a new instance of a checker, or configure timeouts globally.
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
LinkChecker::Async::HTTP.configure do |config|
|
|
174
|
+
config.read_timeout = 5
|
|
175
|
+
config.open_timeout = 10
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
You can also force a specific HTTP protocol version, `:http1` or `:http2`, per instance or globally. By default, `async-http` negotiates the protocol version with the server.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
LinkChecker::Async::HTTP.configure do |config|
|
|
183
|
+
config.protocol = :http2
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Async::HTTP reuses connection-pooling clients per scheme/host/port. Its default concurrency is unlimited; set `max_concurrency` to bound active requests without counting delayed retries against that limit.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
checker = LinkChecker::Async::HTTP::Checker.new(max_concurrency: 25)
|
|
191
|
+
```
|
|
192
|
+
|
|
130
193
|
#### [LinkChecker::Net::HTTP](lib/ruby-link-checker/net/http/checker.rb)
|
|
131
194
|
|
|
132
195
|
Slow, sequential checker.
|
|
@@ -165,18 +228,43 @@ end
|
|
|
165
228
|
|
|
166
229
|
#### Retries
|
|
167
230
|
|
|
168
|
-
By default link checkers do not retry.
|
|
231
|
+
By default link checkers do not retry. Set `retries` to enable retries for transient connection errors/timeouts and HTTP statuses 408, 425, 429, 500, 502, 503, and 504. Permanent HTTP failures and invalid URLs are not retried. HEAD-to-GET fallback still applies independently of the retry policy. The retry budget is shared across methods: with `retries: 2`, repeated transient failures produce `HEAD`, `HEAD`, `HEAD`, then `GET`.
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
checker = LinkChecker::Net::HTTP::Checker.new(retries: 1)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Redirects
|
|
238
|
+
|
|
239
|
+
Checkers follow up to 20 redirects per attempt and detect redirect loops. Relative redirects are resolved against the current response URL. Set `max_redirects` to change the limit, or use `0` to disable redirects.
|
|
169
240
|
|
|
170
241
|
```ruby
|
|
171
|
-
checker = LinkChecker::
|
|
242
|
+
checker = LinkChecker::Typhoeus::Hydra::Checker.new(max_redirects: 10)
|
|
172
243
|
```
|
|
173
244
|
|
|
174
245
|
#### Retry After
|
|
175
246
|
|
|
176
|
-
|
|
247
|
+
Set `retry_after` to a minimum delay in seconds (default `0`). Optional `retry_backoff` multiplies that delay on successive retries (default `1`), and `retry_jitter` adds a random fraction between `0` and the configured value (default `0`, maximum `1`). All delays are capped by `max_retry_delay` (default `60` seconds).
|
|
177
248
|
|
|
178
249
|
```ruby
|
|
179
|
-
checker = LinkChecker::
|
|
250
|
+
checker = LinkChecker::Typhoeus::Hydra::Checker.new(
|
|
251
|
+
retries: 2,
|
|
252
|
+
retry_after: 1,
|
|
253
|
+
retry_backoff: 2,
|
|
254
|
+
retry_jitter: 0.1,
|
|
255
|
+
respect_retry_after: true,
|
|
256
|
+
max_retry_delay: 60
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
With `respect_retry_after: true` (default `false`), HTTP `Retry-After` delta-seconds and dates provide a minimum delay, subject to the same cap. Invalid or past dates are ignored. Async delays yield without blocking other requests. Hydra queues delayed retries after the active batch drains, so actual delays can exceed the configured minimum without blocking completion callbacks. Delays apply to retries, not ordinary HEAD-to-GET fallback.
|
|
261
|
+
|
|
262
|
+
#### Retry Policy
|
|
263
|
+
|
|
264
|
+
Customize `retry_status_codes` to choose retryable HTTP statuses. Set it to `nil` to restore the previous behavior of retrying every failure and error, including malformed URLs and programming errors.
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
checker = LinkChecker::Net::HTTP::Checker.new(retries: 1, retry_status_codes: [429, 503])
|
|
180
268
|
```
|
|
181
269
|
|
|
182
270
|
#### Results
|
|
@@ -211,6 +299,34 @@ The following examples disables `GET` and only makes `HEAD` requests.
|
|
|
211
299
|
checker = LinkChecker::Net::HTTP::Checker.new(methods: %w[HEAD])
|
|
212
300
|
```
|
|
213
301
|
|
|
302
|
+
#### Deduplication
|
|
303
|
+
|
|
304
|
+
Set `deduplicate: true` to coalesce identical in-flight requests and reuse successful results for the lifetime of the checker. Failures and errors are rechecked after completion. Each submission still receives its own result events and metadata, including events emitted before it joined an in-flight check. Deduplication is disabled by default.
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
checker = LinkChecker::Typhoeus::Hydra::Checker.new(deduplicate: true)
|
|
308
|
+
checker.check('https://www.example.org', location: 'one.html')
|
|
309
|
+
checker.check('https://www.example.org', location: 'two.html')
|
|
310
|
+
checker.run # one check, two sets of result events
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Identity includes the complete URL (including query values and fragment), transport, methods, headers, user-agent, and request-related settings. Changing those settings prevents reuse. URLs containing user-info credentials bypass deduplication and caching.
|
|
314
|
+
|
|
315
|
+
#### Caching
|
|
316
|
+
|
|
317
|
+
Persistent success caching is disabled by default. Enable it with an explicit file path and positive TTL in seconds. Recent successful checks can then be reused across checker instances; failures and errors are never stored. Cache writes are atomic and locked, and unreadable or malformed files fall back to normal checking.
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
checker = LinkChecker::Typhoeus::Hydra::Checker.new(
|
|
321
|
+
deduplicate: true,
|
|
322
|
+
cache: { file: 'tmp/link-checker/cache.json', ttl: 3600 }
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Each cache hit emits `:result` and `:success` with the new submission's metadata and `cached? == true`. Persistent hits return `LinkChecker::CachedResult` with the HTTP method, status, original/final URIs, and metadata; native request/response objects and headers are not persisted. Redirect targets containing user-info credentials are not written. Add the cache directory to your ignore rules.
|
|
327
|
+
|
|
328
|
+
Caching alone does not coalesce in-flight checks. With deduplication enabled, successful results remain reusable within that checker even after the persistent TTL expires. Cached successes do not replay historical redirect/retry events.
|
|
329
|
+
|
|
214
330
|
#### Logger
|
|
215
331
|
|
|
216
332
|
Pass your own logger.
|
|
@@ -227,6 +343,20 @@ Pass your own user-agent. Default is `Ruby Link Checker/x.y.z`.
|
|
|
227
343
|
checker = LinkChecker::Net::HTTP::Checker.new(user_agent: 'Custom Agent/1.0')
|
|
228
344
|
```
|
|
229
345
|
|
|
346
|
+
#### Headers
|
|
347
|
+
|
|
348
|
+
Some sites reject or return errors (403, 404) to requests that look like bots. Pass additional headers to mimic a browser.
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
checker = LinkChecker::Net::HTTP::Checker.new(
|
|
352
|
+
headers: {
|
|
353
|
+
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
354
|
+
'Accept-Language' => 'en-US,en;q=0.5',
|
|
355
|
+
'DNT' => '1'
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
```
|
|
359
|
+
|
|
230
360
|
### Global Configuration
|
|
231
361
|
|
|
232
362
|
All options can also be configured globally.
|
|
@@ -234,6 +364,7 @@ All options can also be configured globally.
|
|
|
234
364
|
```ruby
|
|
235
365
|
LinkChecker.configure do |config|
|
|
236
366
|
config.user_agent = 'Custom Agent/1.0'
|
|
367
|
+
config.headers = { 'DNT' => '1' }
|
|
237
368
|
config.methods = ['HEAD', 'GET']
|
|
238
369
|
config.logger = ::Logger.new(STDOUT)
|
|
239
370
|
end
|
|
@@ -274,9 +405,22 @@ Events are called with results, which contain the following properties.
|
|
|
274
405
|
| :request_headers | Request headers. |
|
|
275
406
|
| :redirect_to | A redirect URL in case of redirects. |
|
|
276
407
|
| :error | A raised error in case of errors. |
|
|
408
|
+
| :error_type | `:timeout`, `:connection`, or `:other` for errors; otherwise `nil`. |
|
|
409
|
+
| :response_headers | Response headers, accessible using `[]`. |
|
|
410
|
+
| :cached? | Whether this result was reused from a successful check. |
|
|
277
411
|
|
|
278
412
|
See [result.rb](lib/ruby-link-checker/result.rb) for more details.
|
|
279
413
|
|
|
414
|
+
## Benchmarks
|
|
415
|
+
|
|
416
|
+
See [benchmarks/check.rb](benchmarks/check.rb) for a script comparing the performance of the three checkers (`LinkChecker::Net::HTTP`, `LinkChecker::Typhoeus::Hydra`, and `LinkChecker::Async::HTTP`) against a fixed set of URLs.
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
bundle exec ruby -Ilib benchmarks/check.rb
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Anecdotal results show `LinkChecker::Async::HTTP` using significantly less CPU time than `LinkChecker::Typhoeus::Hydra` (both concurrent checkers), while wall-clock time is comparable between the two and depends on timeouts and the responsiveness of the URLs being checked. `LinkChecker::Net::HTTP` checks links sequentially and is significantly slower in wall-clock time for large batches of URLs; it's commented out by default in the script.
|
|
423
|
+
|
|
280
424
|
## Contributing
|
|
281
425
|
|
|
282
426
|
You're encouraged to contribute to ruby-link-checker. See [CONTRIBUTING](CONTRIBUTING.md) for details.
|
data/RELEASING.md
CHANGED
|
@@ -2,59 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
There are no hard rules about when to release ruby-link-checker. Release bug fixes frequently, features not so frequently and breaking API changes rarely.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Releases use [ruby-gem-release](https://github.com/dblock/ruby-gem-release/tree/v0.3.1) and the manual [Release workflow](.github/workflows/release.yml). Merges do not publish. Every workflow dispatch performs a real release.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## One-Time Setup
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
bundle install
|
|
11
|
-
rake
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Check that the last build succeeded.
|
|
15
|
-
|
|
16
|
-
Add a date to this release in [CHANGELOG.md](CHANGELOG.md).
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
### 0.2.2 (2015/7/10)
|
|
20
|
-
```
|
|
9
|
+
Create a GitHub environment named `release` in this repository. Restrict deployment to `main` and require approval when available.
|
|
21
10
|
|
|
22
|
-
|
|
11
|
+
On [RubyGems.org](https://rubygems.org/gems/ruby-link-checker/trusted_publishers), configure a trusted publisher with repository owner `dblock`, repository name `ruby-link-checker`, workflow filename `release.yml`, and environment `release`. Set the separate workflow repository owner to `dblock` and name to `ruby-gem-release` because publication uses its reusable workflow. No stored RubyGems API key is needed.
|
|
23
12
|
|
|
24
|
-
|
|
13
|
+
The workflow requests `contents: write` and `id-token: write`. Branch rules must allow the release identity to push fast-forward release and next-version commits to `main` and create tags, without allowing force pushes or deletion. Adding the workflow does not configure these permissions or the trusted publisher.
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
git add README.md CHANGELOG.md lib/ruby-link-checker/version.rb
|
|
28
|
-
git commit -m "Preparing for release, 0.2.2."
|
|
29
|
-
```
|
|
15
|
+
## Prepare and Publish
|
|
30
16
|
|
|
31
|
-
|
|
17
|
+
Prepare the intended version in `lib/ruby-link-checker/version.rb` and its matching `### VERSION (Next)` section in `CHANGELOG.md` through a PR. Keep exactly one `* Your contribution here.` placeholder in that section. The workflow releases the current version, dates the changelog, and removes the placeholder; it does not select a new release version.
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
$ rake release
|
|
19
|
+
Run tests and RuboCop locally and review the release changes before merging:
|
|
35
20
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Pushed git commits and tags.
|
|
39
|
-
Pushed ruby-link-checker 0.2.2 to rubygems.org.
|
|
21
|
+
```sh
|
|
22
|
+
bundle exec rake
|
|
40
23
|
```
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Add the next release to [CHANGELOG.md](CHANGELOG.md).
|
|
25
|
+
After merging, explicit release approval, and successful Trusted Publishing setup, dispatch from `main`:
|
|
45
26
|
|
|
27
|
+
```sh
|
|
28
|
+
gh workflow run release.yml --ref main
|
|
46
29
|
```
|
|
47
|
-
### 0.2.3 (Next)
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
```
|
|
31
|
+
The workflow runs `bundle exec rake`, publishes the verified gem to RubyGems, pushes the release commit and tag atomically, and creates a GitHub Release with the gem attached. It then increments the patch version and pushes a new development commit with a fresh changelog section. Actions-token pushes do not trigger ordinary push workflows, so the release workflow runs tests and RuboCop before publication.
|
|
51
32
|
|
|
52
|
-
|
|
33
|
+
## Verify and Recover
|
|
53
34
|
|
|
54
|
-
|
|
35
|
+
Verify the published RubyGems version, matching tag and GitHub Release, attached gem, and next development version on `main`.
|
|
55
36
|
|
|
56
|
-
|
|
57
|
-
git add CHANGELOG.md lib/ruby-link-checker/version.rb
|
|
58
|
-
git commit -m "Preparing for next development iteration, 0.2.3."
|
|
59
|
-
git push origin main
|
|
60
|
-
```
|
|
37
|
+
Never automatically rerun a failed or timed-out publication: RubyGems may have accepted the upload. Check registry and GitHub state first. Do not republish an existing version or move its tag. Available `release.gem`, `release.json`, and `release.bundle` recovery artifacts are retained for 30 days; follow the [upstream recovery instructions](https://github.com/dblock/ruby-gem-release/blob/v0.3.1/RELEASING.md#verify-and-recover) to complete only the missing steps.
|
data/Rakefile
CHANGED
|
@@ -15,4 +15,12 @@ end
|
|
|
15
15
|
require 'rubocop/rake_task'
|
|
16
16
|
RuboCop::RakeTask.new(:rubocop)
|
|
17
17
|
|
|
18
|
+
require 'ruby-gem-release/workflow/ruby_gem'
|
|
19
|
+
require 'ruby-gem-release/changelog'
|
|
20
|
+
|
|
21
|
+
RubyGemRelease::Workflow::RubyGem.new(
|
|
22
|
+
version: RubyGemRelease::Version::RubyConstant.new(path: 'lib/ruby-link-checker/version.rb', constant: 'VERSION'),
|
|
23
|
+
changelog: RubyGemRelease::Changelog::Intridea.new(placeholder: '* Your contribution here.')
|
|
24
|
+
).install_tasks
|
|
25
|
+
|
|
18
26
|
task default: %i[rubocop spec]
|
data/benchmarks/check.rb
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
require 'benchmark'
|
|
2
2
|
require 'net/http'
|
|
3
3
|
require 'typhoeus'
|
|
4
|
+
require 'async'
|
|
5
|
+
require 'async/http'
|
|
4
6
|
require 'ruby-link-checker'
|
|
5
7
|
|
|
6
8
|
input = File.readlines(File.join(__dir__, 'data/opensearch.org/small.txt')).map(&:strip)
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# checker.check url
|
|
14
|
-
# end
|
|
15
|
-
# checker.results.each_pair do |bucket, results|
|
|
16
|
-
# puts "#{bucket}: #{results.size}"
|
|
17
|
-
# end
|
|
18
|
-
# end
|
|
10
|
+
CHECKERS = [
|
|
11
|
+
# LinkChecker::Net::HTTP::Checker,
|
|
12
|
+
LinkChecker::Typhoeus::Hydra::Checker,
|
|
13
|
+
LinkChecker::Async::HTTP::Checker
|
|
14
|
+
].freeze
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
checker.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
Benchmark.bm(40) do |benchmark|
|
|
17
|
+
CHECKERS.each do |checker_klass|
|
|
18
|
+
benchmark.report(checker_klass.name) do
|
|
19
|
+
checker = checker_klass.new
|
|
20
|
+
checker.logger.level = Logger::INFO
|
|
21
|
+
case checker
|
|
22
|
+
when LinkChecker::Typhoeus::Hydra::Checker
|
|
23
|
+
checker.timeout = 5 # 1/12th of the default 60s
|
|
24
|
+
checker.connecttimeout = 1 # 1/10th of the default 10s
|
|
25
|
+
else
|
|
26
|
+
checker.read_timeout = 5 # 1/12th of the default 60s
|
|
27
|
+
checker.open_timeout = 5 # 1/12th of the default 60s
|
|
28
|
+
end
|
|
29
|
+
input.each do |url|
|
|
30
|
+
checker.check url
|
|
31
|
+
end
|
|
32
|
+
checker.run if checker.respond_to?(:run)
|
|
33
|
+
checker.results.each_pair do |bucket, results|
|
|
34
|
+
puts "#{bucket}: #{results.size}"
|
|
35
|
+
end
|
|
29
36
|
end
|
|
30
37
|
end
|
|
31
38
|
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module LinkChecker
|
|
2
|
+
module Async
|
|
3
|
+
module HTTP
|
|
4
|
+
class Task < ::LinkChecker::Task
|
|
5
|
+
def run!
|
|
6
|
+
checker._queue(delay: delay) { _run! }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def _run!
|
|
12
|
+
client = checker._client_for(uri)
|
|
13
|
+
headers = ::Protocol::HTTP::Headers[
|
|
14
|
+
[['user-agent', checker.user_agent]] + checker.headers.map { |key, value| [key, value] }
|
|
15
|
+
]
|
|
16
|
+
request = ::Protocol::HTTP::Request[method, uri.request_uri, headers]
|
|
17
|
+
response = _call(client, request)
|
|
18
|
+
result! Result.new(uri, method, original_uri, request, response, options)
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
result! ResultError.new(uri, method, original_uri, e, options)
|
|
21
|
+
ensure
|
|
22
|
+
response&.close
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def _call(client, request)
|
|
26
|
+
if checker.read_timeout
|
|
27
|
+
::Async::Task.current.with_timeout(checker.read_timeout) { client.call(request) }
|
|
28
|
+
else
|
|
29
|
+
client.call(request)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Checker < LinkChecker::Checker
|
|
35
|
+
extend ::LinkChecker::Async::HTTP::Config
|
|
36
|
+
|
|
37
|
+
attr_accessor(*LinkChecker::Async::HTTP::Config::ATTRIBUTES)
|
|
38
|
+
|
|
39
|
+
def initialize(options = {})
|
|
40
|
+
LinkChecker::Async::HTTP::Config::ATTRIBUTES.each do |key|
|
|
41
|
+
send("#{key}=", options[key] || LinkChecker::Async::HTTP::Config.send(key))
|
|
42
|
+
end
|
|
43
|
+
@queue = []
|
|
44
|
+
@clients = {}
|
|
45
|
+
if max_concurrency && (!max_concurrency.is_a?(Integer) || max_concurrency < 1)
|
|
46
|
+
raise ArgumentError, "Invalid concurrency limit: #{max_concurrency}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Runs all queued checks concurrently and blocks until they complete.
|
|
53
|
+
def run
|
|
54
|
+
require 'async/semaphore'
|
|
55
|
+
|
|
56
|
+
Barrier do |barrier|
|
|
57
|
+
@barrier = barrier
|
|
58
|
+
@semaphore = ::Async::Semaphore.new(max_concurrency) if max_concurrency
|
|
59
|
+
_flush!
|
|
60
|
+
end
|
|
61
|
+
ensure
|
|
62
|
+
@barrier = nil
|
|
63
|
+
@semaphore = nil
|
|
64
|
+
_close_clients!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def _queue(delay: 0, &block)
|
|
68
|
+
if @barrier
|
|
69
|
+
@barrier.async do
|
|
70
|
+
sleep(delay) if delay.positive?
|
|
71
|
+
@semaphore ? @semaphore.acquire(&block) : block.call
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
@queue << [delay, block]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns a client for the given URI's host/port/scheme, reusing (and
|
|
79
|
+
# pooling connections on) one client per endpoint across all checks.
|
|
80
|
+
def _client_for(uri)
|
|
81
|
+
key = [uri.scheme, uri.host, uri.port]
|
|
82
|
+
@clients[key] ||= begin
|
|
83
|
+
endpoint = ::Async::HTTP::Endpoint.parse(uri.to_s, timeout: open_timeout, **_endpoint_options)
|
|
84
|
+
::Async::HTTP::Client.new(endpoint)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Maps the configured protocol (:http1 or :http2) to the
|
|
91
|
+
# corresponding async-http protocol implementation.
|
|
92
|
+
def _endpoint_options
|
|
93
|
+
case protocol
|
|
94
|
+
when :http1
|
|
95
|
+
{ protocol: ::Async::HTTP::Protocol::HTTP1 }
|
|
96
|
+
when :http2
|
|
97
|
+
{ protocol: ::Async::HTTP::Protocol::HTTP2 }
|
|
98
|
+
when nil
|
|
99
|
+
{}
|
|
100
|
+
else
|
|
101
|
+
raise ArgumentError, "Unsupported protocol: #{protocol.inspect}, expected :http1 or :http2"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def _flush!
|
|
106
|
+
queue = @queue
|
|
107
|
+
@queue = []
|
|
108
|
+
queue.each { |delay, block| _queue(delay: delay, &block) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def _close_clients!
|
|
112
|
+
@clients.each_value(&:close)
|
|
113
|
+
@clients = {}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkChecker
|
|
4
|
+
module Async
|
|
5
|
+
module HTTP
|
|
6
|
+
module Config
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
ATTRIBUTES = %i[
|
|
10
|
+
read_timeout
|
|
11
|
+
open_timeout
|
|
12
|
+
protocol
|
|
13
|
+
max_concurrency
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
attr_accessor(*Config::ATTRIBUTES)
|
|
17
|
+
|
|
18
|
+
def reset
|
|
19
|
+
self.read_timeout = nil
|
|
20
|
+
self.open_timeout = nil
|
|
21
|
+
self.protocol = nil
|
|
22
|
+
self.max_concurrency = nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def configure
|
|
28
|
+
block_given? ? yield(Config) : Config
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def config
|
|
32
|
+
Config
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
LinkChecker::Async::HTTP::Config.reset
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module LinkChecker
|
|
2
|
+
module Async
|
|
3
|
+
module HTTP
|
|
4
|
+
# A thin, case-insensitive wrapper around Protocol::HTTP::Headers,
|
|
5
|
+
# since Protocol::HTTP::Headers#[] expects a lower-cased key.
|
|
6
|
+
class Headers
|
|
7
|
+
def initialize(headers)
|
|
8
|
+
@headers = headers
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def [](key)
|
|
12
|
+
value = @headers[key.to_s.downcase]
|
|
13
|
+
value.is_a?(Array) ? value.join(', ') : value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Wraps an Async::HTTP response to expose a `code` and case-insensitive
|
|
18
|
+
# `headers`, similar to the other checkers' response objects.
|
|
19
|
+
class Response
|
|
20
|
+
attr_reader :status, :headers
|
|
21
|
+
|
|
22
|
+
def initialize(response)
|
|
23
|
+
@status = response.status
|
|
24
|
+
@headers = Headers.new(response.headers)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def code
|
|
28
|
+
@status.to_s
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Result < ::LinkChecker::Result
|
|
33
|
+
attr_accessor :request, :response
|
|
34
|
+
|
|
35
|
+
def initialize(uri, method, original_uri, request, response, options)
|
|
36
|
+
@request = request
|
|
37
|
+
@response = response && Response.new(response)
|
|
38
|
+
super(uri, method, original_uri, options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def error?
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def failure?
|
|
46
|
+
!success? && !redirect?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def code
|
|
50
|
+
@code ||= begin
|
|
51
|
+
response.status
|
|
52
|
+
rescue StandardError
|
|
53
|
+
-1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def request_headers
|
|
58
|
+
Headers.new(request.headers)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def response_headers
|
|
62
|
+
response&.headers || {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def redirect_to
|
|
66
|
+
return nil unless response
|
|
67
|
+
|
|
68
|
+
response.headers['location']
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def redirect?
|
|
72
|
+
return false unless response
|
|
73
|
+
|
|
74
|
+
[301, 302, 303, 307, 308].include?(code)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def success?
|
|
78
|
+
return false unless response
|
|
79
|
+
|
|
80
|
+
code >= 200 && code <= 299
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|