fbe 0.14.1 → 0.15.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 +4 -4
- data/.github/workflows/copyrights.yml +1 -1
- data/Gemfile.lock +4 -4
- data/lib/fbe/bylaws.rb +26 -6
- data/lib/fbe/copy.rb +13 -5
- data/lib/fbe/delete.rb +15 -6
- data/lib/fbe/enter.rb +18 -6
- data/lib/fbe/github_graph.rb +48 -1
- data/lib/fbe/if_absent.rb +27 -12
- data/lib/fbe/issue.rb +19 -10
- data/lib/fbe/iterate.rb +124 -36
- data/lib/fbe/just_one.rb +20 -12
- data/lib/fbe/middleware/formatter.rb +33 -5
- data/lib/fbe/middleware.rb +15 -2
- data/lib/fbe/octo.rb +124 -1
- data/lib/fbe/overwrite.rb +14 -6
- data/lib/fbe/regularly.rb +19 -6
- data/lib/fbe/repeatedly.rb +20 -5
- data/lib/fbe/sec.rb +13 -6
- data/lib/fbe/unmask_repos.rb +36 -15
- data/lib/fbe/who.rb +17 -10
- data/lib/fbe.rb +1 -1
- data/test/fbe/test_octo.rb +19 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db8d48febaa0fde74386158a1487da848c28058c4fadf85838b110105d70ada8
|
4
|
+
data.tar.gz: 46eec95f52085235848a464c2770887e5336f7b818644e61955fa3fbb026e84c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90435fd9b0555636891b524d69835835096b69e3673ed21fe9a49ee2a5b8792040bbdad2fa303f05f7a86c0cc06e35c595b7968ce32e8a7c32e6a94d5f839c9e
|
7
|
+
data.tar.gz: ca87fd6888148a43e582ab14cc110189aa741bad4e57c44aca4755db004de9bbef539e3b245dfe7330461b74eb39545f005a632eb0c8ae5ed7801c84f9b2b980
|
data/Gemfile.lock
CHANGED
@@ -110,7 +110,7 @@ GEM
|
|
110
110
|
i18n (1.14.7)
|
111
111
|
concurrent-ruby (~> 1.0)
|
112
112
|
iri (0.10.0)
|
113
|
-
json (2.12.
|
113
|
+
json (2.12.2)
|
114
114
|
judges (0.42.1)
|
115
115
|
backtrace (~> 0)
|
116
116
|
baza.rb (~> 0)
|
@@ -172,11 +172,11 @@ GEM
|
|
172
172
|
tago (> 0)
|
173
173
|
racc (1.8.1)
|
174
174
|
rainbow (3.1.1)
|
175
|
-
rake (13.
|
175
|
+
rake (13.3.0)
|
176
176
|
regexp_parser (2.10.0)
|
177
177
|
retries (0.0.5)
|
178
178
|
rexml (3.4.1)
|
179
|
-
rubocop (1.75.
|
179
|
+
rubocop (1.75.8)
|
180
180
|
json (~> 2.3)
|
181
181
|
language_server-protocol (~> 3.17.0.2)
|
182
182
|
lint_roller (~> 1.1.0)
|
@@ -190,7 +190,7 @@ GEM
|
|
190
190
|
rubocop-ast (1.44.1)
|
191
191
|
parser (>= 3.3.7.2)
|
192
192
|
prism (~> 1.4)
|
193
|
-
rubocop-minitest (0.38.
|
193
|
+
rubocop-minitest (0.38.1)
|
194
194
|
lint_roller (~> 1.1)
|
195
195
|
rubocop (>= 1.75.0, < 2.0)
|
196
196
|
rubocop-ast (>= 1.38.0, < 2.0)
|
data/lib/fbe/bylaws.rb
CHANGED
@@ -6,15 +6,35 @@
|
|
6
6
|
require 'liquid'
|
7
7
|
require_relative '../fbe'
|
8
8
|
|
9
|
-
# Generates policies/bylaws.
|
9
|
+
# Generates policies/bylaws from Liquid templates.
|
10
10
|
#
|
11
11
|
# Using the templates stored in the +assets/bylaws+ directory, this function
|
12
|
-
# creates a hash
|
12
|
+
# creates a hash where keys are bylaw names (derived from filenames) and values
|
13
|
+
# are the rendered formulas. Templates can use three parameters to control
|
14
|
+
# the strictness and generosity of the bylaws.
|
13
15
|
#
|
14
|
-
# @param [Integer] anger
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
16
|
+
# @param [Integer] anger Strictness level for punishments (0-4, default: 2)
|
17
|
+
# - 0: Very lenient, minimal punishments
|
18
|
+
# - 2: Balanced approach (default)
|
19
|
+
# - 4: Very strict, maximum punishments
|
20
|
+
# @param [Integer] love Generosity level for rewards (0-4, default: 2)
|
21
|
+
# - 0: Minimal rewards
|
22
|
+
# - 2: Balanced rewards (default)
|
23
|
+
# - 4: Maximum rewards
|
24
|
+
# @param [Integer] paranoia Requirements threshold for rewards (1-4, default: 2)
|
25
|
+
# - 1: Easy to earn rewards
|
26
|
+
# - 2: Balanced requirements (default)
|
27
|
+
# - 4: Very difficult to earn rewards
|
28
|
+
# @return [Hash<String, String>] Hash mapping bylaw names to their formulas
|
29
|
+
# @raise [RuntimeError] If parameters are out of valid ranges
|
30
|
+
# @example Generate balanced bylaws
|
31
|
+
# bylaws = Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
|
32
|
+
# bylaws['bug-report-was-rewarded']
|
33
|
+
# # => "award { 2 * love * paranoia }"
|
34
|
+
# @example Generate strict bylaws with minimal rewards
|
35
|
+
# bylaws = Fbe.bylaws(anger: 4, love: 1, paranoia: 3)
|
36
|
+
# bylaws['dud-was-punished']
|
37
|
+
# # => "award { -16 * anger }"
|
18
38
|
def Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
|
19
39
|
raise "The 'anger' must be in the [0..4] interval: #{anger.inspect}" unless !anger.negative? && anger < 5
|
20
40
|
raise "The 'love' must be in the [0..4] interval: #{love.inspect}" unless !love.negative? && love < 5
|
data/lib/fbe/copy.rb
CHANGED
@@ -9,12 +9,20 @@ require_relative 'fb'
|
|
9
9
|
# Makes a copy of a fact, moving all properties to a new fact.
|
10
10
|
#
|
11
11
|
# All properties from the +source+ will be copied to the +target+, except those
|
12
|
-
# listed in the +except
|
12
|
+
# listed in the +except+ array. Only copies properties that don't already exist
|
13
|
+
# in the target. Multi-valued properties are copied with all their values.
|
13
14
|
#
|
14
|
-
# @param [Factbase::Fact] source The source
|
15
|
-
# @param [Factbase::Fact] target The target
|
16
|
-
# @param [Array<String>] except List of
|
17
|
-
# @return [Integer]
|
15
|
+
# @param [Factbase::Fact] source The source fact to copy from
|
16
|
+
# @param [Factbase::Fact] target The target fact to copy to
|
17
|
+
# @param [Array<String>] except List of property names to NOT copy (defaults to empty)
|
18
|
+
# @return [Integer] The number of property values that were copied
|
19
|
+
# @raise [RuntimeError] If source, target, or except is nil
|
20
|
+
# @note Existing properties in target are preserved (not overwritten)
|
21
|
+
# @example Copy all properties except timestamps
|
22
|
+
# source = fb.query('(eq type "user")').first
|
23
|
+
# target = fb.insert
|
24
|
+
# count = Fbe.copy(source, target, except: ['_time', '_id'])
|
25
|
+
# puts "Copied #{count} property values"
|
18
26
|
def Fbe.copy(source, target, except: [])
|
19
27
|
raise 'The source is nil' if source.nil?
|
20
28
|
raise 'The target is nil' if target.nil?
|
data/lib/fbe/delete.rb
CHANGED
@@ -6,13 +6,22 @@
|
|
6
6
|
require_relative '../fbe'
|
7
7
|
require_relative 'fb'
|
8
8
|
|
9
|
-
# Delete a
|
9
|
+
# Delete properties from a fact by creating a new fact without them.
|
10
10
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# @
|
11
|
+
# This method doesn't modify the original fact. Instead, it deletes the existing
|
12
|
+
# fact from the factbase and creates a new one with all properties except those
|
13
|
+
# specified for deletion.
|
14
|
+
#
|
15
|
+
# @param [Factbase::Fact] fact The fact to delete properties from (must have an ID)
|
16
|
+
# @param [Array<String>] props List of property names to delete
|
17
|
+
# @param [Factbase] fb The factbase to use (defaults to Fbe.fb)
|
18
|
+
# @param [String] id The property name used as unique identifier (defaults to '_id')
|
19
|
+
# @return [Factbase::Fact] New fact without the deleted properties
|
20
|
+
# @raise [RuntimeError] If fact is nil, has no ID, or ID property doesn't exist
|
21
|
+
# @example Delete multiple properties from a fact
|
22
|
+
# fact = fb.query('(eq type "user")').first
|
23
|
+
# new_fact = Fbe.delete(fact, 'age', 'city')
|
24
|
+
# # new_fact will have all properties except 'age' and 'city'
|
16
25
|
def Fbe.delete(fact, *props, fb: Fbe.fb, id: '_id')
|
17
26
|
raise 'The fact is nil' if fact.nil?
|
18
27
|
i = fact[id]
|
data/lib/fbe/enter.rb
CHANGED
@@ -6,13 +6,25 @@
|
|
6
6
|
require 'baza-rb'
|
7
7
|
require_relative '../fbe'
|
8
8
|
|
9
|
-
# Enter a new valve.
|
9
|
+
# Enter a new valve in the Zerocracy system.
|
10
10
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# @param [
|
15
|
-
# @
|
11
|
+
# A valve is a checkpoint or gate in the processing pipeline. This method
|
12
|
+
# records the entry into a valve with a reason, unless in testing mode.
|
13
|
+
#
|
14
|
+
# @param [String] badge Unique badge identifier for the valve
|
15
|
+
# @param [String] why The reason for entering this valve
|
16
|
+
# @param [Judges::Options] options The options from judges tool (uses $options if not provided)
|
17
|
+
# @param [Loog] loog The logging facility (uses $loog if not provided)
|
18
|
+
# @yield Block to execute within the valve context
|
19
|
+
# @return [Object] The result of the yielded block
|
20
|
+
# @raise [RuntimeError] If badge, why, or required globals are nil
|
21
|
+
# @note Requires $options and $loog global variables to be set
|
22
|
+
# @note In testing mode (options.testing != nil), bypasses valve recording
|
23
|
+
# @example Enter a valve for processing
|
24
|
+
# Fbe.enter('payment-check', 'Validating payment data') do
|
25
|
+
# # Process payment validation
|
26
|
+
# validate_payment(data)
|
27
|
+
# end
|
16
28
|
def Fbe.enter(badge, why, options: $options, loog: $loog, &)
|
17
29
|
raise 'The badge is nil' if badge.nil?
|
18
30
|
raise 'The why is nil' if why.nil?
|
data/lib/fbe/github_graph.rb
CHANGED
@@ -271,12 +271,34 @@ class Fbe::Graph
|
|
271
271
|
end
|
272
272
|
end
|
273
273
|
|
274
|
-
# Fake GitHub GraphQL client
|
274
|
+
# Fake GitHub GraphQL client for testing.
|
275
|
+
#
|
276
|
+
# This class mocks the GraphQL client interface and returns predictable
|
277
|
+
# test data without making actual API calls. It's used when the application
|
278
|
+
# is in testing mode.
|
279
|
+
#
|
280
|
+
# @example Using the fake client in tests
|
281
|
+
# fake = Fbe::Graph::Fake.new
|
282
|
+
# result = fake.total_commits('owner', 'repo', 'main')
|
283
|
+
# # => 1484 (always returns the same value)
|
275
284
|
class Fake
|
285
|
+
# Executes a GraphQL query (mock implementation).
|
286
|
+
#
|
287
|
+
# @param [String] _query The GraphQL query (ignored)
|
288
|
+
# @return [Hash] Empty hash
|
276
289
|
def query(_query)
|
277
290
|
{}
|
278
291
|
end
|
279
292
|
|
293
|
+
# Returns mock resolved conversation threads.
|
294
|
+
#
|
295
|
+
# @param [String] owner Repository owner
|
296
|
+
# @param [String] name Repository name
|
297
|
+
# @param [Integer] _number Pull request number (ignored)
|
298
|
+
# @return [Array<Hash>] Array of conversation threads
|
299
|
+
# @example
|
300
|
+
# fake.resolved_conversations('zerocracy', 'baza', 42)
|
301
|
+
# # => [conversation data for zerocracy_baza]
|
280
302
|
def resolved_conversations(owner, name, _number)
|
281
303
|
data = {
|
282
304
|
zerocracy_baza: [
|
@@ -286,6 +308,14 @@ class Fbe::Graph
|
|
286
308
|
data[:"#{owner}_#{name}"] || []
|
287
309
|
end
|
288
310
|
|
311
|
+
# Returns mock issue and pull request counts.
|
312
|
+
#
|
313
|
+
# @param [String] _owner Repository owner (ignored)
|
314
|
+
# @param [String] _name Repository name (ignored)
|
315
|
+
# @return [Hash] Hash with 'issues' and 'pulls' counts
|
316
|
+
# @example
|
317
|
+
# fake.total_issues_and_pulls('owner', 'repo')
|
318
|
+
# # => {"issues"=>23, "pulls"=>19}
|
289
319
|
def total_issues_and_pulls(_owner, _name)
|
290
320
|
{
|
291
321
|
'issues' => 23,
|
@@ -293,10 +323,23 @@ class Fbe::Graph
|
|
293
323
|
}
|
294
324
|
end
|
295
325
|
|
326
|
+
# Returns mock total commit count.
|
327
|
+
#
|
328
|
+
# @param [String] _owner Repository owner (ignored)
|
329
|
+
# @param [String] _name Repository name (ignored)
|
330
|
+
# @param [String] _branch Branch name (ignored)
|
331
|
+
# @return [Integer] Always returns 1484
|
296
332
|
def total_commits(_owner, _name, _branch)
|
297
333
|
1484
|
298
334
|
end
|
299
335
|
|
336
|
+
# Returns mock issue type event data.
|
337
|
+
#
|
338
|
+
# @param [String] node_id The event node ID
|
339
|
+
# @return [Hash, nil] Event data for known IDs, nil otherwise
|
340
|
+
# @example
|
341
|
+
# fake.issue_type_event('ITAE_examplevq862Ga8lzwAAAAQZanzv')
|
342
|
+
# # => {'type'=>'IssueTypeAddedEvent', ...}
|
300
343
|
def issue_type_event(node_id)
|
301
344
|
case node_id
|
302
345
|
when 'ITAE_examplevq862Ga8lzwAAAAQZanzv'
|
@@ -362,6 +405,10 @@ class Fbe::Graph
|
|
362
405
|
|
363
406
|
private
|
364
407
|
|
408
|
+
# Generates mock conversation thread data.
|
409
|
+
#
|
410
|
+
# @param [String] id The conversation thread ID
|
411
|
+
# @return [Hash] Mock conversation data with comments
|
365
412
|
def conversation(id)
|
366
413
|
{
|
367
414
|
'id' => id,
|
data/lib/fbe/if_absent.rb
CHANGED
@@ -8,28 +8,43 @@ require 'time'
|
|
8
8
|
require_relative '../fbe'
|
9
9
|
require_relative 'fb'
|
10
10
|
|
11
|
-
# Injects a fact if it's absent in the factbase, otherwise
|
12
|
-
#
|
11
|
+
# Injects a fact if it's absent in the factbase, otherwise returns nil.
|
12
|
+
#
|
13
|
+
# Checks if a fact with the same property values already exists. If not,
|
14
|
+
# creates a new fact. System properties (_id, _time, _version) are excluded
|
15
|
+
# from the uniqueness check.
|
13
16
|
#
|
14
17
|
# Here is what you do when you want to add a fact to the factbase, but
|
15
18
|
# don't want to make a duplicate of an existing one:
|
16
19
|
#
|
17
20
|
# require 'fbe/if_absent'
|
18
|
-
# n =
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# n.when = Time.now
|
21
|
+
# n = Fbe.if_absent do |f|
|
22
|
+
# f.what = 'something'
|
23
|
+
# f.details = 'important'
|
24
|
+
# end
|
25
|
+
# return if n.nil? # Fact already existed
|
26
|
+
# n.when = Time.now # Add additional properties to the new fact
|
25
27
|
#
|
26
28
|
# This code will definitely create one fact with +what+ equals to +something+
|
27
29
|
# and +details+ equals to +important+, while the +when+ will be equal to the
|
28
30
|
# time of its first creation.
|
29
31
|
#
|
30
|
-
# @param [Factbase] fb The
|
31
|
-
# @yield [Factbase::Fact]
|
32
|
-
# @return [nil
|
32
|
+
# @param [Factbase] fb The factbase to check and insert into (defaults to Fbe.fb)
|
33
|
+
# @yield [Factbase::Fact] A proxy fact object to set properties on
|
34
|
+
# @return [nil, Factbase::Fact] nil if fact exists, otherwise the newly created fact
|
35
|
+
# @note String values are properly escaped in queries
|
36
|
+
# @note Time values are converted to UTC ISO8601 format for comparison
|
37
|
+
# @example Ensure unique user registration
|
38
|
+
# user = Fbe.if_absent do |f|
|
39
|
+
# f.type = 'user'
|
40
|
+
# f.email = 'john@example.com'
|
41
|
+
# end
|
42
|
+
# if user
|
43
|
+
# user.registered_at = Time.now
|
44
|
+
# puts "New user created"
|
45
|
+
# else
|
46
|
+
# puts "User already exists"
|
47
|
+
# end
|
33
48
|
def Fbe.if_absent(fb: Fbe.fb)
|
34
49
|
attrs = {}
|
35
50
|
f =
|
data/lib/fbe/issue.rb
CHANGED
@@ -6,18 +6,27 @@
|
|
6
6
|
require_relative '../fbe'
|
7
7
|
require_relative 'octo'
|
8
8
|
|
9
|
-
# Converts
|
9
|
+
# Converts GitHub repository and issue IDs into a formatted issue reference.
|
10
10
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
11
|
+
# Takes the +repository+ and +issue+ properties from the provided +fact+,
|
12
|
+
# queries the GitHub API to get the repository's full name, and formats it
|
13
|
+
# as a standard GitHub issue reference (e.g., "zerocracy/fbe#42").
|
14
|
+
# Results are cached globally to minimize API calls.
|
15
15
|
#
|
16
|
-
# @param [Factbase::Fact] fact The fact
|
17
|
-
# @param [Judges::Options] options The options
|
18
|
-
# @param [Hash] global The hash for global caching
|
19
|
-
# @param [Loog] loog The logging facility
|
20
|
-
# @return [String]
|
16
|
+
# @param [Factbase::Fact] fact The fact containing repository and issue properties
|
17
|
+
# @param [Judges::Options] options The options from judges tool (uses $options global)
|
18
|
+
# @param [Hash] global The hash for global caching (uses $global)
|
19
|
+
# @param [Loog] loog The logging facility (uses $loog global)
|
20
|
+
# @return [String] Formatted issue reference (e.g., "owner/repo#123")
|
21
|
+
# @raise [RuntimeError] If fact is nil or required properties are missing
|
22
|
+
# @raise [RuntimeError] If required global variables are not set
|
23
|
+
# @note Requires 'repository' and 'issue' properties in the fact
|
24
|
+
# @note Repository names are cached to reduce GitHub API calls
|
25
|
+
# @example Format an issue reference
|
26
|
+
# issue_fact = fb.query('(eq type "issue")').first
|
27
|
+
# issue_fact.repository = 549866411 # Repository ID
|
28
|
+
# issue_fact.issue = 42 # Issue number
|
29
|
+
# puts Fbe.issue(issue_fact) # => "zerocracy/fbe#42"
|
21
30
|
def Fbe.issue(fact, options: $options, global: $global, loog: $loog)
|
22
31
|
raise 'The fact is nil' if fact.nil?
|
23
32
|
raise 'The $global is not set' if global.nil?
|
data/lib/fbe/iterate.rb
CHANGED
@@ -10,13 +10,30 @@ require_relative 'fb'
|
|
10
10
|
require_relative 'octo'
|
11
11
|
require_relative 'unmask_repos'
|
12
12
|
|
13
|
-
# Creates an instance of {Fbe::Iterate} and
|
13
|
+
# Creates an instance of {Fbe::Iterate} and evaluates it with the provided block.
|
14
14
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# @
|
15
|
+
# This is a convenience method that creates an iterator instance and evaluates
|
16
|
+
# the DSL block within its context. The iterator processes repositories defined
|
17
|
+
# in options.repositories, executing queries and managing state for each.
|
18
|
+
#
|
19
|
+
# @param [Factbase] fb The global factbase provided by the +judges+ tool (defaults to Fbe.fb)
|
20
|
+
# @param [Judges::Options] options The options from judges tool (uses $options global)
|
21
|
+
# @param [Hash] global The hash for global caching (uses $global)
|
22
|
+
# @param [Loog] loog The logging facility (uses $loog global)
|
23
|
+
# @yield Block containing DSL methods (as, by, over, etc.) to configure iteration
|
24
|
+
# @return [Object] Result of the block evaluation
|
25
|
+
# @raise [RuntimeError] If required globals are not set
|
26
|
+
# @example Iterate through repositories processing issues
|
27
|
+
# Fbe.iterate do
|
28
|
+
# as 'issues-iterator'
|
29
|
+
# by '(and (eq what "issue") (gt created_at $before))'
|
30
|
+
# repeats 5
|
31
|
+
# quota_aware
|
32
|
+
# over(timeout: 300) do |repository_id, issue_id|
|
33
|
+
# process_issue(repository_id, issue_id)
|
34
|
+
# issue_id + 1
|
35
|
+
# end
|
36
|
+
# end
|
20
37
|
def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
|
21
38
|
raise 'The fb is nil' if fb.nil?
|
22
39
|
raise 'The $global is not set' if global.nil?
|
@@ -26,24 +43,44 @@ def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
|
|
26
43
|
c.instance_eval(&)
|
27
44
|
end
|
28
45
|
|
29
|
-
#
|
46
|
+
# Repository iterator with stateful query execution.
|
47
|
+
#
|
48
|
+
# This class provides a DSL for iterating through repositories and executing
|
49
|
+
# queries while maintaining state between iterations. It tracks progress using
|
50
|
+
# "marker" facts in the factbase and supports features like:
|
51
|
+
#
|
52
|
+
# - Stateful iteration with automatic restart capability
|
53
|
+
# - GitHub API quota awareness to prevent rate limit issues
|
54
|
+
# - Configurable repeat counts per repository
|
55
|
+
# - Timeout controls for long-running operations
|
30
56
|
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
57
|
+
# The iterator executes a query for each repository, passing the previous
|
58
|
+
# result as context. If the query returns nil, it restarts from the beginning
|
59
|
+
# for that repository. Progress is persisted in the factbase to support
|
60
|
+
# resuming after interruptions.
|
61
|
+
#
|
62
|
+
# @example Processing pull requests with state management
|
63
|
+
# iterator = Fbe::Iterate.new(fb: fb, loog: loog, options: options, global: global)
|
64
|
+
# iterator.as('pull-requests')
|
65
|
+
# iterator.by('(and (eq what "pull_request") (gt number $before))')
|
66
|
+
# iterator.repeats(10)
|
67
|
+
# iterator.quota_aware
|
68
|
+
# iterator.over(timeout: 600) do |repo_id, pr_number|
|
69
|
+
# # Process pull request
|
70
|
+
# fetch_and_store_pr(repo_id, pr_number)
|
71
|
+
# pr_number # Return next PR number to process
|
72
|
+
# end
|
36
73
|
#
|
37
74
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
38
75
|
# Copyright:: Copyright (c) 2024-2025 Zerocracy
|
39
76
|
# License:: MIT
|
40
77
|
class Fbe::Iterate
|
41
|
-
#
|
78
|
+
# Creates a new iterator instance.
|
42
79
|
#
|
43
|
-
# @param [Factbase] fb The factbase
|
44
|
-
# @param [Loog] loog The logging facility
|
45
|
-
# @param [Judges::Options] options The options
|
46
|
-
# @param [Hash] global The hash for global caching
|
80
|
+
# @param [Factbase] fb The factbase for storing iteration state
|
81
|
+
# @param [Loog] loog The logging facility for debug output
|
82
|
+
# @param [Judges::Options] options The options containing repository configuration
|
83
|
+
# @param [Hash] global The hash for global caching of API responses
|
47
84
|
def initialize(fb:, loog:, options:, global:)
|
48
85
|
@fb = fb
|
49
86
|
@loog = loog
|
@@ -56,56 +93,107 @@ class Fbe::Iterate
|
|
56
93
|
@quota_aware = false
|
57
94
|
end
|
58
95
|
|
59
|
-
#
|
96
|
+
# Makes the iterator aware of GitHub API quota limits.
|
60
97
|
#
|
61
|
-
# When
|
62
|
-
#
|
63
|
-
#
|
98
|
+
# When enabled, the iterator will check quota status before processing
|
99
|
+
# each repository and gracefully stop when the quota is exhausted.
|
100
|
+
# This prevents API errors and allows for resuming later.
|
64
101
|
#
|
65
102
|
# @return [nil] Nothing is returned
|
103
|
+
# @example Enable quota awareness
|
104
|
+
# iterator.quota_aware
|
105
|
+
# iterator.over { |repo, item| ... } # Will stop if quota exhausted
|
66
106
|
def quota_aware
|
67
107
|
@quota_aware = true
|
68
108
|
end
|
69
109
|
|
70
|
-
# Sets the
|
110
|
+
# Sets the maximum number of iterations per repository.
|
111
|
+
#
|
112
|
+
# Controls how many times the query will be executed for each repository
|
113
|
+
# before moving to the next one. Useful for limiting processing scope.
|
71
114
|
#
|
72
|
-
# @param [Integer] repeats The
|
115
|
+
# @param [Integer] repeats The maximum iterations per repository
|
73
116
|
# @return [nil] Nothing is returned
|
117
|
+
# @raise [RuntimeError] If repeats is nil or not positive
|
118
|
+
# @example Process up to 100 items per repository
|
119
|
+
# iterator.repeats(100)
|
74
120
|
def repeats(repeats)
|
75
121
|
raise 'Cannot set "repeats" to nil' if repeats.nil?
|
76
122
|
raise 'The "repeats" must be a positive integer' unless repeats.positive?
|
77
123
|
@repeats = repeats
|
78
124
|
end
|
79
125
|
|
80
|
-
# Sets the query to
|
126
|
+
# Sets the query to execute for each iteration.
|
127
|
+
#
|
128
|
+
# The query can use two special variables:
|
129
|
+
# - $before: The value from the previous iteration (or initial value)
|
130
|
+
# - $repository: The current repository ID
|
81
131
|
#
|
82
|
-
# @param [String] query The query to execute
|
132
|
+
# @param [String] query The Factbase query to execute
|
83
133
|
# @return [nil] Nothing is returned
|
134
|
+
# @raise [RuntimeError] If query is already set or nil
|
135
|
+
# @example Query for issues after a certain ID
|
136
|
+
# iterator.by('(and (eq what "issue") (gt id $before) (eq repo $repository))')
|
84
137
|
def by(query)
|
85
138
|
raise 'Query is already set' unless @query.nil?
|
86
139
|
raise 'Cannot set query to nil' if query.nil?
|
87
140
|
@query = query
|
88
141
|
end
|
89
142
|
|
90
|
-
# Sets the label
|
143
|
+
# Sets the label for tracking iteration state.
|
91
144
|
#
|
92
|
-
#
|
145
|
+
# The label is used to create marker facts in the factbase that track
|
146
|
+
# the last processed item for each repository. This enables resuming
|
147
|
+
# iteration after interruptions.
|
148
|
+
#
|
149
|
+
# @param [String] label Unique identifier for this iteration type
|
93
150
|
# @return [nil] Nothing is returned
|
151
|
+
# @raise [RuntimeError] If label is already set or nil
|
152
|
+
# @example Set label for issue processing
|
153
|
+
# iterator.as('issue-processor')
|
94
154
|
def as(label)
|
95
155
|
raise 'Label is already set' unless @label.nil?
|
96
156
|
raise 'Cannot set "label" to nil' if label.nil?
|
97
157
|
@label = label
|
98
158
|
end
|
99
159
|
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
# +
|
104
|
-
#
|
160
|
+
# Executes the iteration over all configured repositories.
|
161
|
+
#
|
162
|
+
# For each repository, retrieves the last processed value (or uses the initial
|
163
|
+
# value from +since+) and executes the configured query with it. The query
|
164
|
+
# receives two parameters: $before (the last processed value) and $repository
|
165
|
+
# (GitHub repository ID).
|
166
|
+
#
|
167
|
+
# When the query returns a non-nil result, the block is called with the
|
168
|
+
# repository ID and query result. The block must return an Integer that will
|
169
|
+
# be stored as the new "latest" value for the next iteration.
|
170
|
+
#
|
171
|
+
# When the query returns nil, the iteration for that repository restarts
|
172
|
+
# from the initial value (set by +since+), and the block is NOT called.
|
173
|
+
#
|
174
|
+
# The method tracks progress using marker facts and supports:
|
175
|
+
# - Automatic restart when query returns nil
|
176
|
+
# - Timeout to prevent infinite loops
|
177
|
+
# - GitHub API quota checking (if enabled)
|
178
|
+
# - State persistence for resuming after interruptions
|
179
|
+
#
|
180
|
+
# Processing flow for each repository:
|
181
|
+
# 1. Read the "latest" value from factbase (or use +since+ if not found)
|
182
|
+
# 2. Execute the query with $before=latest and $repository=repo_id
|
183
|
+
# 3. If query returns nil: restart from +since+ value, skip to next repo
|
184
|
+
# 4. If query returns a value: call the block with (repo_id, query_result)
|
185
|
+
# 5. Store the block's return value as the new "latest" for next iteration
|
105
186
|
#
|
106
|
-
# @param [Float] timeout
|
107
|
-
# @yield [Integer,
|
187
|
+
# @param [Float] timeout Maximum seconds to run (default: 120)
|
188
|
+
# @yield [Integer, Object] Repository ID and the result from query execution
|
189
|
+
# @yieldreturn [Integer] The value to store as "latest" for next iteration
|
108
190
|
# @return [nil] Nothing is returned
|
191
|
+
# @raise [RuntimeError] If block doesn't return an Integer
|
192
|
+
# @example Process issues incrementally
|
193
|
+
# iterator.over(timeout: 300) do |repo_id, issue_number|
|
194
|
+
# fetch_and_process_issue(repo_id, issue_number)
|
195
|
+
# issue_number + 1 # Return next issue number to process
|
196
|
+
# end
|
109
197
|
def over(timeout: 2 * 60, &)
|
110
198
|
raise 'Use "as" first' if @label.nil?
|
111
199
|
raise 'Use "by" first' if @query.nil?
|
@@ -129,7 +217,7 @@ class Fbe::Iterate
|
|
129
217
|
break
|
130
218
|
end
|
131
219
|
if Time.now - start > timeout
|
132
|
-
|
220
|
+
@loog.info("We are doing this for #{start.ago} already, won't check #{repo}")
|
133
221
|
next
|
134
222
|
end
|
135
223
|
next if restarted.include?(repo)
|
@@ -178,7 +266,7 @@ class Fbe::Iterate
|
|
178
266
|
break
|
179
267
|
end
|
180
268
|
if Time.now - start > timeout
|
181
|
-
|
269
|
+
@loog.info("We are iterating for #{start.ago} already, time to give up")
|
182
270
|
break
|
183
271
|
end
|
184
272
|
end
|
data/lib/fbe/just_one.rb
CHANGED
@@ -8,22 +8,30 @@ require 'others'
|
|
8
8
|
require_relative '../fbe'
|
9
9
|
require_relative 'fb'
|
10
10
|
|
11
|
-
#
|
12
|
-
# there) returns the existing one.
|
11
|
+
# Ensures exactly one fact exists with the specified attributes in the factbase.
|
13
12
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
13
|
+
# This method creates a new fact if none exists with the given attributes,
|
14
|
+
# or returns an existing fact if one already matches. Useful for preventing
|
15
|
+
# duplicate facts while ensuring required facts exist.
|
16
|
+
#
|
17
|
+
# @example Creating or finding a unique fact
|
18
|
+
# require 'fbe/just_one'
|
19
|
+
# fact = Fbe.just_one do |f|
|
20
|
+
# f.what = 'github_issue'
|
21
|
+
# f.issue_id = 123
|
22
|
+
# f.repository = 'zerocracy/fbe'
|
19
23
|
# end
|
24
|
+
# # Returns existing fact if one exists with these exact attributes,
|
25
|
+
# # otherwise creates and returns a new fact
|
20
26
|
#
|
21
|
-
#
|
22
|
-
#
|
27
|
+
# @example Attributes are matched exactly (case-sensitive)
|
28
|
+
# Fbe.just_one { |f| f.name = 'Test' } # Creates fact with name='Test'
|
29
|
+
# Fbe.just_one { |f| f.name = 'test' } # Creates another fact (different case)
|
23
30
|
#
|
24
|
-
# @param [Factbase] fb The
|
25
|
-
# @yield [Factbase::Fact]
|
26
|
-
# @return [Factbase::Fact] The fact
|
31
|
+
# @param [Factbase] fb The factbase to search/insert into (defaults to Fbe.fb)
|
32
|
+
# @yield [Factbase::Fact] Block to set attributes on the fact
|
33
|
+
# @return [Factbase::Fact] The existing or newly created fact
|
34
|
+
# @note System attributes (_id, _time, _version) are ignored when matching
|
27
35
|
def Fbe.just_one(fb: Fbe.fb)
|
28
36
|
attrs = {}
|
29
37
|
f =
|
@@ -8,22 +8,50 @@ require 'faraday/logging/formatter'
|
|
8
8
|
require_relative '../../fbe'
|
9
9
|
require_relative '../../fbe/middleware'
|
10
10
|
|
11
|
-
# Faraday
|
11
|
+
# Custom Faraday formatter that logs only error responses (4xx/5xx).
|
12
|
+
#
|
13
|
+
# This formatter reduces log noise by only outputting details when HTTP
|
14
|
+
# requests fail. For 403 errors with JSON responses, it shows a compact
|
15
|
+
# warning with the error message. For other errors, it logs the full
|
16
|
+
# request/response details including headers and bodies.
|
17
|
+
#
|
18
|
+
# @example Usage in Faraday middleware
|
19
|
+
# connection = Faraday.new do |f|
|
20
|
+
# f.response :logger, nil, formatter: Fbe::Middleware::Formatter
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example Log output for 403 error
|
24
|
+
# # GET https://api.github.com/repos/private/repo -> 403 / Repository access denied
|
25
|
+
#
|
26
|
+
# @example Log output for other errors (500, 404, etc)
|
27
|
+
# # GET https://api.example.com/endpoint HTTP/1.1
|
28
|
+
# # Content-Type: "application/json"
|
29
|
+
# # Authorization: "Bearer [FILTERED]"
|
30
|
+
# #
|
31
|
+
# # {"query": "data"}
|
32
|
+
# # HTTP/1.1 500
|
33
|
+
# # Content-Type: "text/html"
|
34
|
+
# #
|
35
|
+
# # Internal Server Error
|
12
36
|
#
|
13
37
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
14
38
|
# Copyright:: Copyright (c) 2024-2025 Zerocracy
|
15
39
|
# License:: MIT
|
16
40
|
class Fbe::Middleware::Formatter < Faraday::Logging::Formatter
|
17
|
-
#
|
41
|
+
# Captures HTTP request details for later use in error logging.
|
18
42
|
#
|
19
|
-
# @param [Hash] http
|
43
|
+
# @param [Hash] http Request data including method, url, headers, and body
|
44
|
+
# @return [void]
|
20
45
|
def request(http)
|
21
46
|
@req = http
|
22
47
|
end
|
23
48
|
|
24
|
-
#
|
49
|
+
# Logs HTTP response details only for error responses (4xx/5xx).
|
25
50
|
#
|
26
|
-
# @param [Hash] http
|
51
|
+
# @param [Hash] http Response data including status, headers, and body
|
52
|
+
# @return [void]
|
53
|
+
# @note Only logs when status >= 400
|
54
|
+
# @note Special handling for 403 JSON responses to show compact error message
|
27
55
|
def response(http)
|
28
56
|
return if http.status < 400
|
29
57
|
if http.status == 403 && http.response_headers['content-type'].start_with?('application/json')
|
data/lib/fbe/middleware.rb
CHANGED
@@ -5,11 +5,24 @@
|
|
5
5
|
|
6
6
|
require_relative '../fbe'
|
7
7
|
|
8
|
-
#
|
8
|
+
# Middleware components for Faraday HTTP client configuration.
|
9
|
+
#
|
10
|
+
# This module serves as a namespace for various middleware components
|
11
|
+
# that enhance Faraday HTTP client functionality with custom behaviors
|
12
|
+
# such as request/response formatting, logging, and error handling.
|
13
|
+
#
|
14
|
+
# The middleware components in this module are designed to work with
|
15
|
+
# the Faraday HTTP client library and can be plugged into the Faraday
|
16
|
+
# middleware stack to provide additional functionality.
|
17
|
+
#
|
18
|
+
# @example Using middleware in Faraday client
|
19
|
+
# Faraday.new do |conn|
|
20
|
+
# conn.use Fbe::Middleware::Formatter
|
21
|
+
# conn.adapter Faraday.default_adapter
|
22
|
+
# end
|
9
23
|
#
|
10
24
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
11
25
|
# Copyright:: Copyright (c) 2024-2025 Zerocracy
|
12
26
|
# License:: MIT
|
13
27
|
module Fbe::Middleware
|
14
|
-
# empty
|
15
28
|
end
|
data/lib/fbe/octo.rb
CHANGED
@@ -128,7 +128,21 @@ end
|
|
128
128
|
# Fake GitHub client for testing purposes.
|
129
129
|
#
|
130
130
|
# This class provides mock implementations of Octokit methods for testing.
|
131
|
-
# It returns predictable data structures that mimic GitHub API
|
131
|
+
# It returns predictable, deterministic data structures that mimic GitHub API
|
132
|
+
# responses without making actual API calls. The mock data uses consistent
|
133
|
+
# patterns:
|
134
|
+
# - IDs are generated from string names using character code sums
|
135
|
+
# - Timestamps are random but within recent past
|
136
|
+
# - Repository and user data follows GitHub's JSON structure
|
137
|
+
#
|
138
|
+
# @example Using FakeOctokit in tests
|
139
|
+
# client = Fbe::FakeOctokit.new
|
140
|
+
# repo = client.repository('octocat/hello-world')
|
141
|
+
# puts repo[:full_name] # => "octocat/hello-world"
|
142
|
+
# puts repo[:id] # => 1224 (deterministic from name)
|
143
|
+
#
|
144
|
+
# @note All methods return static or pseudo-random data
|
145
|
+
# @note No actual API calls are made
|
132
146
|
class Fbe::FakeOctokit
|
133
147
|
# Generates a random time in the past.
|
134
148
|
#
|
@@ -167,6 +181,13 @@ class Fbe::FakeOctokit
|
|
167
181
|
o
|
168
182
|
end
|
169
183
|
|
184
|
+
# Lists repositories for a user or organization.
|
185
|
+
#
|
186
|
+
# @param [String] _user The user/org name (ignored in mock)
|
187
|
+
# @return [Array<Hash>] Array of repository hashes
|
188
|
+
# @example
|
189
|
+
# client.repositories('octocat')
|
190
|
+
# # => [{:id=>123, :full_name=>"yegor256/judges", ...}, ...]
|
170
191
|
def repositories(_user = nil)
|
171
192
|
[
|
172
193
|
repository('yegor256/judges'),
|
@@ -428,6 +449,14 @@ class Fbe::FakeOctokit
|
|
428
449
|
}
|
429
450
|
end
|
430
451
|
|
452
|
+
# Lists releases for a repository.
|
453
|
+
#
|
454
|
+
# @param [String] _repo Repository name (ignored in mock)
|
455
|
+
# @param [Hash] _opts Options hash (ignored in mock)
|
456
|
+
# @return [Array<Hash>] Array of release hashes
|
457
|
+
# @example
|
458
|
+
# client.releases('octocat/Hello-World')
|
459
|
+
# # => [{:tag_name=>"0.19.0", :name=>"just a fake name", ...}, ...]
|
431
460
|
def releases(_repo, _opts = {})
|
432
461
|
[
|
433
462
|
release('https://github...'),
|
@@ -435,6 +464,13 @@ class Fbe::FakeOctokit
|
|
435
464
|
]
|
436
465
|
end
|
437
466
|
|
467
|
+
# Gets a single release.
|
468
|
+
#
|
469
|
+
# @param [String] _url Release URL (ignored in mock)
|
470
|
+
# @return [Hash] Release information
|
471
|
+
# @example
|
472
|
+
# client.release('https://api.github.com/repos/octocat/Hello-World/releases/1')
|
473
|
+
# # => {:tag_name=>"0.19.0", :name=>"just a fake name", ...}
|
438
474
|
def release(_url)
|
439
475
|
{
|
440
476
|
node_id: 'RE_kwDOL6GCO84J7Cen',
|
@@ -449,6 +485,14 @@ class Fbe::FakeOctokit
|
|
449
485
|
}
|
450
486
|
end
|
451
487
|
|
488
|
+
# Gets repository information.
|
489
|
+
#
|
490
|
+
# @param [String, Integer] name Repository name ('owner/repo') or ID
|
491
|
+
# @return [Hash] Repository information
|
492
|
+
# @raise [Octokit::NotFound] If name is 404123 or 404124 (for testing)
|
493
|
+
# @example
|
494
|
+
# client.repository('octocat/Hello-World')
|
495
|
+
# # => {:id=>1296269, :full_name=>"octocat/Hello-World", ...}
|
452
496
|
def repository(name)
|
453
497
|
raise Octokit::NotFound if [404_123, 404_124].include?(name)
|
454
498
|
{
|
@@ -488,12 +532,28 @@ class Fbe::FakeOctokit
|
|
488
532
|
}
|
489
533
|
end
|
490
534
|
|
535
|
+
# Lists pull requests associated with a commit.
|
536
|
+
#
|
537
|
+
# @param [String] repo Repository name ('owner/repo')
|
538
|
+
# @param [String] _sha Commit SHA (ignored in mock)
|
539
|
+
# @return [Array<Hash>] Array of pull request hashes
|
540
|
+
# @example
|
541
|
+
# client.commit_pulls('octocat/Hello-World', 'abc123')
|
542
|
+
# # => [{:number=>42, :state=>"open", ...}]
|
491
543
|
def commit_pulls(repo, _sha)
|
492
544
|
[
|
493
545
|
pull_request(repo, 42)
|
494
546
|
]
|
495
547
|
end
|
496
548
|
|
549
|
+
# Lists issues for a repository.
|
550
|
+
#
|
551
|
+
# @param [String] repo Repository name ('owner/repo')
|
552
|
+
# @param [Hash] _options Query options (ignored in mock)
|
553
|
+
# @return [Array<Hash>] Array of issue hashes
|
554
|
+
# @example
|
555
|
+
# client.list_issues('octocat/Hello-World', state: 'open')
|
556
|
+
# # => [{:number=>42, :title=>"Found a bug", ...}, ...]
|
497
557
|
def list_issues(repo, _options = {})
|
498
558
|
[
|
499
559
|
issue(repo, 42),
|
@@ -501,6 +561,14 @@ class Fbe::FakeOctokit
|
|
501
561
|
]
|
502
562
|
end
|
503
563
|
|
564
|
+
# Gets a single issue.
|
565
|
+
#
|
566
|
+
# @param [String] repo Repository name ('owner/repo')
|
567
|
+
# @param [Integer] number Issue number
|
568
|
+
# @return [Hash] Issue information
|
569
|
+
# @example
|
570
|
+
# client.issue('octocat/Hello-World', 42)
|
571
|
+
# # => {:id=>42, :number=>42, :created_at=>...}
|
504
572
|
def issue(repo, number)
|
505
573
|
{
|
506
574
|
id: 42,
|
@@ -515,6 +583,14 @@ class Fbe::FakeOctokit
|
|
515
583
|
}
|
516
584
|
end
|
517
585
|
|
586
|
+
# Gets a single pull request.
|
587
|
+
#
|
588
|
+
# @param [String] repo Repository name ('owner/repo')
|
589
|
+
# @param [Integer] number Pull request number
|
590
|
+
# @return [Hash] Pull request information
|
591
|
+
# @example
|
592
|
+
# client.pull_request('octocat/Hello-World', 1)
|
593
|
+
# # => {:id=>42, :number=>1, :additions=>12, ...}
|
518
594
|
def pull_request(repo, number)
|
519
595
|
{
|
520
596
|
id: 42,
|
@@ -529,6 +605,14 @@ class Fbe::FakeOctokit
|
|
529
605
|
}
|
530
606
|
end
|
531
607
|
|
608
|
+
# Lists pull requests for a repository.
|
609
|
+
#
|
610
|
+
# @param [String] _repo Repository name (ignored in mock)
|
611
|
+
# @param [Hash] _options Query options (ignored in mock)
|
612
|
+
# @return [Array<Hash>] Array of pull request hashes
|
613
|
+
# @example
|
614
|
+
# client.pull_requests('octocat/Hello-World', state: 'open')
|
615
|
+
# # => [{:number=>100, :state=>"closed", :title=>"#90: some title", ...}]
|
532
616
|
def pull_requests(_repo, _options = {})
|
533
617
|
[
|
534
618
|
{
|
@@ -1045,6 +1129,45 @@ class Fbe::FakeOctokit
|
|
1045
1129
|
]
|
1046
1130
|
end
|
1047
1131
|
|
1132
|
+
def issue_events(_repo, _number)
|
1133
|
+
[
|
1134
|
+
{
|
1135
|
+
id: 126, actor: { login: 'user', id: 411, type: 'User' },
|
1136
|
+
event: 'labeled', created_at: Time.parse('2025-05-30 14:41:00 UTC'),
|
1137
|
+
label: { name: 'bug', color: 'd73a4a' }
|
1138
|
+
},
|
1139
|
+
{
|
1140
|
+
id: 206, actor: { login: 'user', id: 411, type: 'User' },
|
1141
|
+
event: 'mentioned', created_at: Time.parse('2025-05-30 14:41:10 UTC')
|
1142
|
+
},
|
1143
|
+
{
|
1144
|
+
id: 339, actor: { login: 'user2', id: 422, type: 'User' },
|
1145
|
+
event: 'subscribed', created_at: Time.parse('2025-05-30 14:41:10 UTC')
|
1146
|
+
},
|
1147
|
+
{
|
1148
|
+
id: 490, actor: { login: 'github-actions[bot]', id: 41_898_282, type: 'Bot' },
|
1149
|
+
event: 'renamed', created_at: Time.parse('2025-05-30 14:41:30 UTC'),
|
1150
|
+
rename: { from: 'some title', to: 'some title 2' }
|
1151
|
+
},
|
1152
|
+
{
|
1153
|
+
id: 505, actor: { login: 'user', id: 411, type: 'User' },
|
1154
|
+
event: 'subscribed', created_at: Time.parse('2025-05-30 16:18:24 UTC')
|
1155
|
+
},
|
1156
|
+
{
|
1157
|
+
id: 608, actor: { login: 'user2', id: 422, type: 'User', test: 123 },
|
1158
|
+
event: 'assigned', created_at: Time.parse('2025-05-30 17:59:08 UTC'),
|
1159
|
+
assignee: { login: 'user2', id: 422, type: 'User' },
|
1160
|
+
assigner: { login: 'user', id: 411, type: 'User' }
|
1161
|
+
},
|
1162
|
+
{
|
1163
|
+
id: 776, actor: { login: 'user2', id: 422, type: 'User' },
|
1164
|
+
event: 'referenced', commit_id: '4621af032170f43d',
|
1165
|
+
commit_url: 'https://api.github.com/repos/foo/foo/commits/4621af032170f43d',
|
1166
|
+
created_at: Time.parse('2025-05-30 19:57:50 UTC')
|
1167
|
+
}
|
1168
|
+
]
|
1169
|
+
end
|
1170
|
+
|
1048
1171
|
def pull_request_comments(_name, _number)
|
1049
1172
|
[
|
1050
1173
|
{
|
data/lib/fbe/overwrite.rb
CHANGED
@@ -6,19 +6,27 @@
|
|
6
6
|
require_relative '../fbe'
|
7
7
|
require_relative 'fb'
|
8
8
|
|
9
|
-
# Overwrites a property in the fact.
|
9
|
+
# Overwrites a property in the fact by recreating the entire fact.
|
10
10
|
#
|
11
11
|
# If the property doesn't exist in the fact, it will be added. If it does
|
12
|
-
# exist,
|
13
|
-
#
|
12
|
+
# exist, the entire fact will be destroyed, a new fact created with all
|
13
|
+
# existing properties, and the specified property set with the new value.
|
14
14
|
#
|
15
15
|
# It is important that the fact has the +_id+ property. If it doesn't,
|
16
16
|
# an exception will be raised.
|
17
17
|
#
|
18
|
-
# @param [Factbase::Fact] fact The fact to modify
|
18
|
+
# @param [Factbase::Fact] fact The fact to modify (must have _id property)
|
19
19
|
# @param [String] property The name of the property to set
|
20
|
-
# @param [Any] value The value to set
|
21
|
-
# @
|
20
|
+
# @param [Any] value The value to set (can be any type)
|
21
|
+
# @param [Factbase] fb The factbase to use (defaults to Fbe.fb)
|
22
|
+
# @return [Factbase::Fact] Returns new fact if recreated, or original if unchanged
|
23
|
+
# @raise [RuntimeError] If fact is nil, has no _id, or property is not a String
|
24
|
+
# @note This operation preserves all other properties during recreation
|
25
|
+
# @note If property already has the same single value, no changes are made
|
26
|
+
# @example Update a user's status
|
27
|
+
# user = fb.query('(eq login "john")').first
|
28
|
+
# updated_user = Fbe.overwrite(user, 'status', 'active')
|
29
|
+
# # All properties preserved, only 'status' is set to 'active'
|
22
30
|
def Fbe.overwrite(fact, property, value, fb: Fbe.fb)
|
23
31
|
raise 'The fact is nil' if fact.nil?
|
24
32
|
raise 'The fb is nil' if fb.nil?
|
data/lib/fbe/regularly.rb
CHANGED
@@ -6,15 +6,28 @@
|
|
6
6
|
require_relative '../fbe'
|
7
7
|
require_relative 'fb'
|
8
8
|
|
9
|
-
# Run the block provided every X days.
|
9
|
+
# Run the block provided every X days based on PMP configuration.
|
10
|
+
#
|
11
|
+
# Executes a block periodically based on PMP (Project Management Plan) settings.
|
12
|
+
# The block will only run if it hasn't been executed within the specified interval.
|
13
|
+
# Creates a fact recording when the judge was last run.
|
10
14
|
#
|
11
15
|
# @param [String] area The name of the PMP area
|
12
|
-
# @param [
|
13
|
-
# @param [
|
14
|
-
# @param [Factbase] fb The factbase
|
15
|
-
# @param [String] judge The name of the judge
|
16
|
-
# @param [Loog] loog The logging facility
|
16
|
+
# @param [String] p_every_days PMP property name for interval (defaults to 7 days if not in PMP)
|
17
|
+
# @param [String] p_since_days PMP property name for since period (defaults to 28 days if not in PMP)
|
18
|
+
# @param [Factbase] fb The factbase (defaults to Fbe.fb)
|
19
|
+
# @param [String] judge The name of the judge (uses $judge global)
|
20
|
+
# @param [Loog] loog The logging facility (uses $loog global)
|
21
|
+
# @yield [Factbase::Fact] Fact to populate with judge execution details
|
17
22
|
# @return [nil] Nothing
|
23
|
+
# @raise [RuntimeError] If required parameters or globals are nil
|
24
|
+
# @note Skips execution if judge was run within the interval period
|
25
|
+
# @note The 'since' property is added to the fact when p_since_days is provided
|
26
|
+
# @example Run a cleanup task every 3 days
|
27
|
+
# Fbe.regularly('cleanup', 'days_between_cleanups', 'cleanup_history_days') do |f|
|
28
|
+
# f.total_cleaned = cleanup_old_records
|
29
|
+
# # PMP might have: days_between_cleanups=3, cleanup_history_days=30
|
30
|
+
# end
|
18
31
|
def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &)
|
19
32
|
raise 'The area is nil' if area.nil?
|
20
33
|
raise 'The p_every_days is nil' if p_every_days.nil?
|
data/lib/fbe/repeatedly.rb
CHANGED
@@ -7,14 +7,29 @@ require_relative '../fbe'
|
|
7
7
|
require_relative 'fb'
|
8
8
|
require_relative 'overwrite'
|
9
9
|
|
10
|
-
# Run the block provided every X hours.
|
10
|
+
# Run the block provided every X hours based on PMP configuration.
|
11
|
+
#
|
12
|
+
# Similar to Fbe.regularly but works with hour intervals instead of days.
|
13
|
+
# Executes a block periodically, maintaining a single fact that tracks the
|
14
|
+
# last execution time. The fact is overwritten on each run rather than
|
15
|
+
# creating new facts.
|
11
16
|
#
|
12
17
|
# @param [String] area The name of the PMP area
|
13
|
-
# @param [
|
14
|
-
# @param [Factbase] fb The factbase
|
15
|
-
# @param [String] judge The name of the judge
|
16
|
-
# @param [Loog] loog The logging facility
|
18
|
+
# @param [String] p_every_hours PMP property name for interval (defaults to 24 hours if not in PMP)
|
19
|
+
# @param [Factbase] fb The factbase (defaults to Fbe.fb)
|
20
|
+
# @param [String] judge The name of the judge (uses $judge global)
|
21
|
+
# @param [Loog] loog The logging facility (uses $loog global)
|
22
|
+
# @yield [Factbase::Fact] The judge fact to populate with execution details
|
17
23
|
# @return [nil] Nothing
|
24
|
+
# @raise [RuntimeError] If required parameters or globals are nil
|
25
|
+
# @note Skips execution if judge was run within the interval period
|
26
|
+
# @note Overwrites the 'when' property of existing judge fact
|
27
|
+
# @example Run a monitoring task every 6 hours
|
28
|
+
# Fbe.repeatedly('monitoring', 'hours_between_checks') do |f|
|
29
|
+
# f.servers_checked = check_all_servers
|
30
|
+
# f.issues_found = count_issues
|
31
|
+
# # PMP might have: hours_between_checks=6
|
32
|
+
# end
|
18
33
|
def Fbe.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog, &)
|
19
34
|
raise 'The area is nil' if area.nil?
|
20
35
|
raise 'The p_every_hours is nil' if p_every_hours.nil?
|
data/lib/fbe/sec.rb
CHANGED
@@ -6,15 +6,22 @@
|
|
6
6
|
require 'tago'
|
7
7
|
require_relative '../fbe'
|
8
8
|
|
9
|
-
# Converts number of seconds into
|
9
|
+
# Converts number of seconds into human-readable time format.
|
10
10
|
#
|
11
11
|
# The number of seconds is taken from the +fact+ provided, usually stored
|
12
|
-
# there in the +seconds+ property. The seconds are formatted
|
13
|
-
# days
|
12
|
+
# there in the +seconds+ property. The seconds are formatted into a
|
13
|
+
# human-readable string like "3 days ago" or "5 hours ago" using the
|
14
|
+
# tago gem.
|
14
15
|
#
|
15
|
-
# @param [Factbase::Fact] fact The fact
|
16
|
-
# @param [String] prop The property
|
17
|
-
# @return [String]
|
16
|
+
# @param [Factbase::Fact] fact The fact containing the seconds property
|
17
|
+
# @param [String, Symbol] prop The property name with seconds (defaults to :seconds)
|
18
|
+
# @return [String] Human-readable time interval (e.g., "2 weeks ago", "3 hours ago")
|
19
|
+
# @raise [RuntimeError] If the specified property doesn't exist in the fact
|
20
|
+
# @note Uses the tago gem's ago method for formatting
|
21
|
+
# @example Format elapsed time from a fact
|
22
|
+
# build_fact = fb.query('(eq type "build")').first
|
23
|
+
# build_fact.duration = 7200 # 2 hours in seconds
|
24
|
+
# puts Fbe.sec(build_fact, :duration) # => "2 hours ago"
|
18
25
|
def Fbe.sec(fact, prop = :seconds)
|
19
26
|
s = fact[prop.to_s]
|
20
27
|
raise "There is no #{prop.inspect} property" if s.nil?
|
data/lib/fbe/unmask_repos.rb
CHANGED
@@ -6,31 +6,52 @@
|
|
6
6
|
require_relative '../fbe'
|
7
7
|
require_relative 'octo'
|
8
8
|
|
9
|
-
# Converts mask to
|
9
|
+
# Converts a repository mask pattern to a regular expression.
|
10
10
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
11
|
+
# @example Basic wildcard matching
|
12
|
+
# Fbe.mask_to_regex('zerocracy/*')
|
13
|
+
# # => /zerocracy\/.*/i
|
14
14
|
#
|
15
|
-
# @
|
16
|
-
#
|
15
|
+
# @example Specific repository (no wildcard)
|
16
|
+
# Fbe.mask_to_regex('zerocracy/fbe')
|
17
|
+
# # => /zerocracy\/fbe/i
|
18
|
+
#
|
19
|
+
# @param [String] mask Repository mask in format 'org/repo' where repo can contain '*'
|
20
|
+
# @return [Regexp] Case-insensitive regular expression for matching repositories
|
21
|
+
# @raise [RuntimeError] If organization part contains asterisk
|
17
22
|
def Fbe.mask_to_regex(mask)
|
18
23
|
org, repo = mask.split('/')
|
19
24
|
raise "Org '#{org}' can't have an asterisk" if org.include?('*')
|
20
25
|
Regexp.compile("#{org}/#{repo.gsub('*', '.*')}", Regexp::IGNORECASE)
|
21
26
|
end
|
22
27
|
|
23
|
-
#
|
28
|
+
# Resolves repository masks to actual GitHub repository names.
|
29
|
+
#
|
30
|
+
# Takes a comma-separated list of repository masks from options and expands
|
31
|
+
# wildcards by querying GitHub API. Supports inclusion and exclusion patterns.
|
32
|
+
# Archived repositories are automatically filtered out.
|
33
|
+
#
|
34
|
+
# @example Basic usage with wildcards
|
35
|
+
# # options.repositories = "zerocracy/fbe,zerocracy/ab*"
|
36
|
+
# repos = Fbe.unmask_repos
|
37
|
+
# # => ["zerocracy/fbe", "zerocracy/abc", "zerocracy/abcd"]
|
38
|
+
#
|
39
|
+
# @example Using exclusion patterns
|
40
|
+
# # options.repositories = "zerocracy/*,-zerocracy/private*"
|
41
|
+
# repos = Fbe.unmask_repos
|
42
|
+
# # Returns all zerocracy repos except those starting with 'private'
|
24
43
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# matching these masks.
|
44
|
+
# @example Empty result handling
|
45
|
+
# # options.repositories = "nonexistent/*"
|
46
|
+
# Fbe.unmask_repos # Raises error: "No repos found matching: nonexistent/*"
|
29
47
|
#
|
30
|
-
# @param [Judges::Options] options
|
31
|
-
# @param [Hash] global
|
32
|
-
# @param [Loog] loog
|
33
|
-
# @return [Array<String>]
|
48
|
+
# @param [Judges::Options] options Options containing 'repositories' field with masks
|
49
|
+
# @param [Hash] global Global cache for storing API responses
|
50
|
+
# @param [Loog] loog Logger for debug output
|
51
|
+
# @return [Array<String>] Shuffled list of repository full names (e.g., 'org/repo')
|
52
|
+
# @raise [RuntimeError] If no repositories match the provided masks
|
53
|
+
# @note Exclusion patterns must start with '-' (e.g., '-org/pattern*')
|
54
|
+
# @note Results are shuffled to distribute load when processing
|
34
55
|
def Fbe.unmask_repos(options: $options, global: $global, loog: $loog)
|
35
56
|
repos = []
|
36
57
|
octo = Fbe.octo(loog:, global:, options:)
|
data/lib/fbe/who.rb
CHANGED
@@ -6,19 +6,26 @@
|
|
6
6
|
require_relative '../fbe'
|
7
7
|
require_relative 'octo'
|
8
8
|
|
9
|
-
# Converts
|
9
|
+
# Converts a GitHub user ID into a formatted username string.
|
10
10
|
#
|
11
11
|
# The ID of the user (integer) is expected to be stored in the +who+ property of the
|
12
|
-
# provided +fact+. This function makes a live request to GitHub API
|
13
|
-
#
|
14
|
-
# will be converted to
|
12
|
+
# provided +fact+. This function makes a live request to GitHub API to
|
13
|
+
# retrieve the username. The result is cached globally to minimize API calls.
|
14
|
+
# For example, the ID +526301+ will be converted to +"@yegor256"+.
|
15
15
|
#
|
16
|
-
# @param [Factbase::Fact] fact The fact
|
17
|
-
# @param [String] prop The property
|
18
|
-
# @param [Judges::Options] options The options
|
19
|
-
# @param [Hash] global The hash for global caching
|
20
|
-
# @param [Loog] loog The logging facility
|
21
|
-
# @return [String]
|
16
|
+
# @param [Factbase::Fact] fact The fact containing the GitHub user ID
|
17
|
+
# @param [String, Symbol] prop The property name with the ID (defaults to :who)
|
18
|
+
# @param [Judges::Options] options The options from judges tool (uses $options global)
|
19
|
+
# @param [Hash] global The hash for global caching (uses $global)
|
20
|
+
# @param [Loog] loog The logging facility (uses $loog global)
|
21
|
+
# @return [String] Formatted username with @ prefix (e.g., "@yegor256")
|
22
|
+
# @raise [RuntimeError] If the specified property doesn't exist in the fact
|
23
|
+
# @note Results are cached to reduce GitHub API calls
|
24
|
+
# @note Subject to GitHub API rate limits
|
25
|
+
# @example Convert user ID to username
|
26
|
+
# contributor = fb.query('(eq type "contributor")').first
|
27
|
+
# contributor.author_id = 526301
|
28
|
+
# puts Fbe.who(contributor, :author_id) # => "@yegor256"
|
22
29
|
def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog)
|
23
30
|
id = fact[prop.to_s]
|
24
31
|
raise "There is no #{prop.inspect} property" if id.nil?
|
data/lib/fbe.rb
CHANGED
data/test/fbe/test_octo.rb
CHANGED
@@ -274,4 +274,23 @@ class TestOcto < Fbe::Test
|
|
274
274
|
assert_raises(Octokit::NotFound) { o.repository(404_123) }
|
275
275
|
assert_raises(Octokit::NotFound) { o.repository(404_124) }
|
276
276
|
end
|
277
|
+
|
278
|
+
def test_fetches_fake_issue_events_has_assigned_event
|
279
|
+
o = Fbe.octo(loog: Loog::NULL, global: {}, options: Judges::Options.new({ 'testing' => true }))
|
280
|
+
result = o.issue_events('foo/foo', 123)
|
281
|
+
assert_instance_of(Array, result)
|
282
|
+
assert_equal(7, result.size)
|
283
|
+
event = result.find { _1[:event] == 'assigned' }
|
284
|
+
assert_equal(608, event[:id])
|
285
|
+
assert_pattern do
|
286
|
+
event => {
|
287
|
+
id: Integer,
|
288
|
+
actor: { login: 'user2', id: 422, type: 'User' },
|
289
|
+
event: 'assigned',
|
290
|
+
created_at: Time,
|
291
|
+
assignee: { login: 'user2', id: 422, type: 'User' },
|
292
|
+
assigner: { login: 'user', id: 411, type: 'User' }
|
293
|
+
}
|
294
|
+
end
|
295
|
+
end
|
277
296
|
end
|