fbe 0.39.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcdd3a7dc6452b707cd14157c1009b9b7a1f551f9ea66c5cc31c08849e87a2d3
4
- data.tar.gz: 2d1b97f86162a5e853df3a19e8ba6c4f96f398c51c19aa5a2bab0ee036cba1b4
3
+ metadata.gz: 1e140ff5523bde9171719f3481941dfdf36b381696f313c98b7bb570b172ec49
4
+ data.tar.gz: a0fa3c06b03e0d0de48ab5e00e9d7985e933b68d174f0625b6a5b9388d2d980a
5
5
  SHA512:
6
- metadata.gz: a8e6f7be8faebeaa3aa2ad93f809988ef7fba01265faa67da6bf0c292cf088f1a655305820b1a72570dcd2282a065a1f6570daa349c0ea26458beed96a3d0845
7
- data.tar.gz: 4c2747bdf3bee54e7ecdf667db8d55f93323fc4448f1398753200b963404d5723e094f34064461787999456fa1316a551f470890dc7c6569310f28ccd8338c68
6
+ metadata.gz: a1fdf1f6e32990d2f34dd33337b4b6f73f0e423092c09834bce6d216746d5c387eb61befa3786c907a84c37e75dd00a0058ff8816b4b7ddc9de09ed166564e55
7
+ data.tar.gz: 5b7a3331b84db4c7b2b36b700e132987be7dd235b86b97ee7f3d4032a43b30d0bdf0d7a907700726bf7ca8fafcd13fb35e03c895f35b0e32964e57064c2d1a57
@@ -16,4 +16,4 @@ jobs:
16
16
  runs-on: ubuntu-24.04
17
17
  steps:
18
18
  - uses: actions/checkout@v5
19
- - uses: fsfe/reuse-action@v5
19
+ - uses: fsfe/reuse-action@v6
@@ -16,6 +16,6 @@ jobs:
16
16
  runs-on: ubuntu-24.04
17
17
  steps:
18
18
  - uses: actions/checkout@v5
19
- - uses: crate-ci/typos@v1.36.3
19
+ - uses: crate-ci/typos@v1.37.2
20
20
  with:
21
21
  config: .github/.typos.toml
data/lib/fbe/conclude.rb CHANGED
@@ -8,6 +8,7 @@ require_relative '../fbe'
8
8
  require_relative 'fb'
9
9
  require_relative 'if_absent'
10
10
  require_relative 'octo'
11
+ require_relative 'over'
11
12
 
12
13
  # Creates an instance of {Fbe::Conclude} and evals it with the block provided.
13
14
  #
@@ -195,29 +196,16 @@ class Fbe::Conclude
195
196
  # end
196
197
  # end
197
198
  def roll(&)
198
- if @lifetime_aware && @options.lifetime && Time.now - @epoch > @options.lifetime * 0.9
199
- @loog.debug("We ran out of lifetime (#{@epoch.ago} already), can't even start")
200
- return
201
- end
202
- if @timeout_aware && @options.timeout && Time.now - @kickoff > @options.timeout * 0.9
203
- @loog.debug("We've spent more than #{@kickoff.ago}, won't even start")
204
- return
205
- end
199
+ return 0 if Fbe.over?(
200
+ global: @global, options: @options, loog: @loog, epoch: @epoch, kickoff: @kickoff,
201
+ quota_aware: @quota_aware, lifetime_aware: @lifetime_aware, timeout_aware: @timeout_aware
202
+ )
206
203
  passed = 0
207
- oct = Fbe.octo(loog: @loog, options: @options, global: @global)
208
204
  @fb.query(@query).each do |a|
209
- if @quota_aware && oct.off_quota?
210
- @loog.info('We ran out of GitHub quota, must stop here')
211
- break
212
- end
213
- if @lifetime_aware && @options.lifetime && Time.now - @epoch > @options.lifetime * 0.9
214
- @loog.debug("We ran out of lifetime (#{@epoch.ago} already), must stop here")
215
- break
216
- end
217
- if @timeout_aware && @options.timeout && Time.now - @kickoff > @options.timeout * 0.9
218
- @loog.debug("We've spent more than #{@kickoff.ago}, must stop here")
219
- break
220
- end
205
+ break if Fbe.over?(
206
+ global: @global, options: @options, loog: @loog, epoch: @epoch, kickoff: @kickoff,
207
+ quota_aware: @quota_aware, lifetime_aware: @lifetime_aware, timeout_aware: @timeout_aware
208
+ )
221
209
  @fb.txn do |fbt|
222
210
  n = yield fbt, a
223
211
  @loog.info("#{n.what}: #{n.details}") unless n.nil?
@@ -226,6 +214,9 @@ class Fbe::Conclude
226
214
  end
227
215
  @loog.debug("Found and processed #{passed} facts by: #{@query}")
228
216
  passed
217
+ rescue Fbe::OffQuota => e
218
+ @loog.info(e.message)
219
+ passed
229
220
  end
230
221
 
231
222
  # Populates a new fact based on a previous fact and a processing block.
@@ -232,6 +232,132 @@ class Fbe::Graph
232
232
  }
233
233
  end
234
234
 
235
+ # Get pulls id and number with review from since
236
+ #
237
+ # @param [String] owner The repository owner (username or organization)
238
+ # @param [String] name The repository name
239
+ # @param [Time] since The datetime from
240
+ # @param [String, nil] cursor Github cursor for next page
241
+ # @return [Hash] A hash with pulls
242
+ # @example
243
+ # graph = Fbe::Graph.new(token: 'github_token')
244
+ # cursor = nil
245
+ # pulls = []
246
+ # loop do
247
+ # json = graph.pull_requests_with_reviews(
248
+ # 'zerocracy', 'judges-action', Time.parse('2025-08-01T18:00:00Z'), cursor:
249
+ # )
250
+ # json['pulls_with_reviews'].each do |p|
251
+ # pulls.push(p['number'])
252
+ # end
253
+ # break unless json['has_next_page']
254
+ # cursor = json['next_cursor']
255
+ # end
256
+ def pull_requests_with_reviews(owner, name, since, cursor: nil)
257
+ result = query(
258
+ <<~GRAPHQL
259
+ {
260
+ repository(owner: "#{owner}", name: "#{name}") {
261
+ pullRequests(first: 100, after: "#{cursor}") {
262
+ nodes {
263
+ id
264
+ number
265
+ timelineItems(first: 1, itemTypes: [PULL_REQUEST_REVIEW], since: "#{since.utc.iso8601}") {
266
+ nodes {
267
+ ... on PullRequestReview { id }
268
+ }
269
+ }
270
+ }
271
+ pageInfo {
272
+ hasNextPage
273
+ endCursor
274
+ }
275
+ }
276
+ }
277
+ }
278
+ GRAPHQL
279
+ ).to_h
280
+ {
281
+ 'pulls_with_reviews' => result
282
+ .dig('repository', 'pullRequests', 'nodes')
283
+ .reject { _1.dig('timelineItems', 'nodes').empty? }
284
+ .map do |pull|
285
+ {
286
+ 'id' => pull['id'],
287
+ 'number' => pull['number']
288
+ }
289
+ end,
290
+ 'has_next_page' => result.dig('repository', 'pullRequests', 'pageInfo', 'hasNextPage'),
291
+ 'next_cursor' => result.dig('repository', 'pullRequests', 'pageInfo', 'endCursor')
292
+ }
293
+ end
294
+
295
+ # Get reviews by pull numbers
296
+ #
297
+ # @param [String] owner The repository owner (username or organization)
298
+ # @param [String] name The repository name
299
+ # @param [Array<Array<Integer, (String, nil)>>] pulls Array of pull number and Github cursor
300
+ # @return [Hash] A hash with reviews
301
+ # @example
302
+ # graph = Fbe::Graph.new(token: 'github_token')
303
+ # queue = [[1108, nil], [1105, nil]]
304
+ # until queue.empty?
305
+ # pulls = graph.pull_request_reviews('zerocracy', 'judges-action', pulls: queue.shift(10))
306
+ # pulls.each do |pull|
307
+ # puts pull['id'], pull['number']
308
+ # pull['reviews'].each do |r|
309
+ # puts r['id'], r['submitted_at']
310
+ # end
311
+ # end
312
+ # pulls.select { _1['reviews_has_next_page'] }.each do |p|
313
+ # queue.push([p['number'], p['reviews_next_cursor']])
314
+ # end
315
+ # end
316
+ def pull_request_reviews(owner, name, pulls: [])
317
+ requests =
318
+ pulls.map do |number, cursor|
319
+ <<~GRAPHQL
320
+ pr_#{number}: pullRequest(number: #{number}) {
321
+ id
322
+ number
323
+ reviews(first: 100, after: "#{cursor}") {
324
+ nodes {
325
+ id
326
+ submittedAt
327
+ }
328
+ pageInfo {
329
+ hasNextPage
330
+ endCursor
331
+ }
332
+ }
333
+ }
334
+ GRAPHQL
335
+ end
336
+ result = query(
337
+ <<~GRAPHQL
338
+ {
339
+ repository(owner: "#{owner}", name: "#{name}") {
340
+ #{requests.join("\n")}
341
+ }
342
+ }
343
+ GRAPHQL
344
+ ).to_h
345
+ result['repository'].map do |_k, v|
346
+ {
347
+ 'id' => v['id'],
348
+ 'number' => v['number'],
349
+ 'reviews' => v.dig('reviews', 'nodes').map do |r|
350
+ {
351
+ 'id' => r['id'],
352
+ 'submitted_at' => Time.parse(r['submittedAt'])
353
+ }
354
+ end,
355
+ 'reviews_has_next_page' => v.dig('reviews', 'pageInfo', 'hasNextPage'),
356
+ 'reviews_next_cursor' => v.dig('reviews', 'pageInfo', 'endCursor')
357
+ }
358
+ end
359
+ end
360
+
235
361
  private
236
362
 
237
363
  # Creates or returns a cached GraphQL client instance.
@@ -403,6 +529,47 @@ class Fbe::Graph
403
529
  end
404
530
  end
405
531
 
532
+ def pull_requests_with_reviews(_owner, _name, _since, **)
533
+ {
534
+ 'pulls_with_reviews' => [
535
+ { 'id' => 'PR_kwDOL6J6Ss6iprCx', 'number' => 2 },
536
+ { 'id' => 'PR_kwDOL6J6Ss6rhJ7T', 'number' => 5 },
537
+ { 'id' => 'PR_kwDOL6J6Ss6r13fG', 'number' => 21 }
538
+ ],
539
+ 'has_next_page' => false,
540
+ 'next_cursor' => 'Y3Vyc29yOnYyOpHOdh_xUw=='
541
+ }
542
+ end
543
+
544
+ def pull_request_reviews(_owner, _name, **)
545
+ [
546
+ {
547
+ 'id' => 'PR_kwDOL6J6Ss6iprCx',
548
+ 'number' => 2,
549
+ 'reviews' => [
550
+ { 'id' => 'PRR_kwDOL6J6Ss647NCl', 'submitted_at' => Time.parse('2025-10-02 12:58:42 UTC') },
551
+ { 'id' => 'PRR_kwDOL6J6Ss647NC8', 'submitted_at' => Time.parse('2025-10-02 15:58:42 UTC') }
552
+ ],
553
+ 'reviews_has_next_page' => false,
554
+ 'reviews_next_cursor' => 'yc29yOnYyO1'
555
+ },
556
+ {
557
+ 'id' => 'PR_kwDOL6J6Ss6rhJ7T',
558
+ 'number' => 5,
559
+ 'reviews' => [{ 'id' => 'PRR_kwDOL6J6Ss64_mnn', 'submitted_at' => Time.parse('2025-10-03 15:58:42 UTC') }],
560
+ 'reviews_has_next_page' => false,
561
+ 'reviews_next_cursor' => 'yc29yOnYyO2'
562
+ },
563
+ {
564
+ 'id' => 'PR_kwDOL6J6Ss6r13fG',
565
+ 'number' => 21,
566
+ 'reviews' => [{ 'id' => 'PRR_kwDOL6J6Ss65AbIA', 'submitted_at' => Time.parse('2025-10-04 15:58:42 UTC') }],
567
+ 'reviews_has_next_page' => false,
568
+ 'reviews_next_cursor' => 'yc29yOnYyO3'
569
+ }
570
+ ]
571
+ end
572
+
406
573
  private
407
574
 
408
575
  # Generates mock conversation thread data.
data/lib/fbe/iterate.rb CHANGED
@@ -10,6 +10,7 @@ require_relative '../fbe'
10
10
  require_relative 'fb'
11
11
  require_relative 'if_absent'
12
12
  require_relative 'octo'
13
+ require_relative 'over'
13
14
  require_relative 'overwrite'
14
15
  require_relative 'unmask_repos'
15
16
 
@@ -249,6 +250,7 @@ class Fbe::Iterate
249
250
  repos = Fbe.unmask_repos(
250
251
  loog: @loog, options: @options, global: @global, quota_aware: @quota_aware
251
252
  ).map { |n| oct.repo_id_by_name(n) }
253
+ started = Time.now
252
254
  restarted = []
253
255
  before =
254
256
  repos.to_h do |repo|
@@ -266,29 +268,19 @@ class Fbe::Iterate
266
268
  starts = before.dup
267
269
  values = {}
268
270
  loop do
269
- if @quota_aware && oct.off_quota?
270
- @loog.info("We are off GitHub quota, time to stop after #{started.ago}")
271
- break
272
- end
273
- if @lifetime_aware && @options.lifetime && Time.now - @epoch > @options.lifetime * 0.9
274
- @loog.debug("We ran out of lifetime (#{@epoch.ago} already), must stop here")
275
- break
276
- end
277
- if @timeout_aware && @options.timeout && Time.now - @kickoff > @options.timeout * 0.9
278
- @loog.debug("We've spent more than #{@kickoff.ago}, must stop here")
271
+ if Fbe.over?(
272
+ global: @global, options: @options, loog: @loog, epoch: @epoch, kickoff: @kickoff,
273
+ quota_aware: @quota_aware, lifetime_aware: @lifetime_aware, timeout_aware: @timeout_aware
274
+ )
275
+ @loog.info("Time to stop after #{started.ago}")
279
276
  break
280
277
  end
281
278
  repos.each do |repo|
282
- if @quota_aware && oct.off_quota?
283
- @loog.info("We are off GitHub quota, we must skip #{repo}")
284
- break
285
- end
286
- if @lifetime_aware && @options.lifetime && Time.now - @epoch > @options.lifetime * 0.9
287
- @loog.info("We are working for #{@epoch.ago} already, won't check repository ##{repo}")
288
- next
289
- end
290
- if @timeout_aware && @options.timeout && Time.now - @kickoff > @options.timeout * 0.9
291
- @loog.debug("We've spent more than #{@kickoff.ago}, won't check repository ##{repo}")
279
+ if Fbe.over?(
280
+ global: @global, options: @options, loog: @loog, epoch: @epoch, kickoff: @kickoff,
281
+ quota_aware: @quota_aware, lifetime_aware: @lifetime_aware, timeout_aware: @timeout_aware
282
+ )
283
+ @loog.info("Won't check repository ##{repo}")
292
284
  break
293
285
  end
294
286
  next if restarted.include?(repo)
@@ -334,16 +326,21 @@ class Fbe::Iterate
334
326
  break
335
327
  end
336
328
  end
337
- repos.each do |repo|
338
- next if before[repo] == starts[repo]
339
- f =
340
- Fbe.if_absent(fb: @fb, always: true) do |n|
341
- n.what = 'iterate'
342
- n.where = 'github'
343
- n.repository = repo
344
- end
345
- Fbe.overwrite(f, @label, before[repo], fb: @fb)
346
- end
347
329
  @loog.debug("Finished scanning #{repos.size} repos in #{@kickoff.ago}: #{seen.map { |k, v| "#{k}:#{v}" }.joined}")
330
+ rescue Fbe::OffQuota => e
331
+ @loog.info(e.message)
332
+ ensure
333
+ if defined?(repos) && defined?(before) && defined?(starts)
334
+ repos.each do |repo|
335
+ next if before[repo] == starts[repo]
336
+ f =
337
+ Fbe.if_absent(fb: @fb, always: true) do |n|
338
+ n.what = 'iterate'
339
+ n.where = 'github'
340
+ n.repository = repo
341
+ end
342
+ Fbe.overwrite(f, @label, before[repo], fb: @fb)
343
+ end
344
+ end
348
345
  end
349
346
  end
data/lib/fbe/octo.rb CHANGED
@@ -24,6 +24,9 @@ require_relative 'middleware/rate_limit'
24
24
  require_relative 'middleware/sqlite_store'
25
25
  require_relative 'middleware/trace'
26
26
 
27
+ # When we are off quota.
28
+ class Fbe::OffQuota < StandardError; end
29
+
27
30
  # Makes a call to the GitHub API.
28
31
  #
29
32
  # It is supposed to be used instead of +Octokit::Client+, because it
@@ -221,7 +224,7 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
221
224
  o =
222
225
  intercepted(o) do |e, m, _args, _r|
223
226
  if e == :before && m != :off_quota? && m != :print_trace! && m != :rate_limit && o.off_quota?
224
- raise "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{m}()"
227
+ raise Fbe::OffQuota, "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{m}()"
225
228
  end
226
229
  end
227
230
  o
data/lib/fbe/over.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../fbe'
7
+ require_relative 'octo'
8
+
9
+ # Check GitHub API quota, lifetime, and timeout.
10
+ #
11
+ # @param [Hash] global Hash of global options
12
+ # @param [Judges::Options] options The options available globally
13
+ # @param [Loog] loog Logging facility
14
+ # @param [Time] epoch When the entire update started
15
+ # @param [Time] kickoff When the particular judge started
16
+ # @param [Boolean] quota_aware Enable or disable check of GitHub API quota
17
+ # @param [Boolean] lifetime_aware Enable or disable check of lifetime limitations
18
+ # @param [Boolean] timeout_aware Enable or disable check of timeout limitations
19
+ # @return [Boolean] check result
20
+ def Fbe.over?(
21
+ global: $global, options: $options, loog: $loog,
22
+ epoch: $epoch || Time.now, kickoff: $kickoff || Time.now,
23
+ quota_aware: true, lifetime_aware: true, timeout_aware: true
24
+ )
25
+ if quota_aware && Fbe.octo(loog:, options:, global:).off_quota?(threshold: 100)
26
+ loog.info('We are off GitHub quota, time to stop')
27
+ return true
28
+ end
29
+ if lifetime_aware && options.lifetime && Time.now - epoch > options.lifetime * 0.9
30
+ loog.info("We ran out of lifetime (#{epoch.ago} already), must stop here")
31
+ return true
32
+ end
33
+ if timeout_aware && options.timeout && Time.now - kickoff > options.timeout * 0.9
34
+ loog.info("We've spent more than #{kickoff.ago}, must stop here")
35
+ return true
36
+ end
37
+ false
38
+ end
data/lib/fbe.rb CHANGED
@@ -10,5 +10,5 @@
10
10
  # License:: MIT
11
11
  module Fbe
12
12
  # Current version of the gem (changed by +.rultor.yml+ on every release)
13
- VERSION = '0.39.0' unless const_defined?(:VERSION)
13
+ VERSION = '0.41.0' unless const_defined?(:VERSION)
14
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fbe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.39.0
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -384,6 +384,7 @@ files:
384
384
  - lib/fbe/middleware/sqlite_store.rb
385
385
  - lib/fbe/middleware/trace.rb
386
386
  - lib/fbe/octo.rb
387
+ - lib/fbe/over.rb
387
388
  - lib/fbe/overwrite.rb
388
389
  - lib/fbe/pmp.rb
389
390
  - lib/fbe/regularly.rb