Module: Fbe

Defined in:
lib/fbe.rb

Overview

The main and only module of this gem.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2024-2025 Zerocracy

License

MIT

Defined Under Namespace

Modules: Middleware Classes: Award, Conclude, FakeOctokit, Graph, Iterate, OffQuota, Tombstone

Constant Summary collapse

VERSION =

Current version of the gem (changed by .rultor.yml on every release)

'0.46.0'

Class Method Summary collapse

Class Method Details

.bylaws(anger: 2, love: 2, paranoia: 2) ⇒ Hash<String, String>

Generates policies/bylaws from Liquid templates.

Using the templates stored in the assets/bylaws directory, this function creates a hash where keys are bylaw names (derived from filenames) and values are the rendered formulas. Templates can use three parameters to control the strictness and generosity of the bylaws.

Examples:

Generate balanced bylaws

bylaws = Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
bylaws['bug-report-was-rewarded']
# => "award { 2 * love * paranoia }"

Generate strict bylaws with minimal rewards

bylaws = Fbe.bylaws(anger: 4, love: 1, paranoia: 3)
bylaws['dud-was-punished']
# => "award { -16 * anger }"

Parameters:

  • anger (Integer) (defaults to: 2)

    Strictness level for punishments (0-4, default: 2)

    • 0: Very lenient, minimal punishments

    • 2: Balanced approach (default)

    • 4: Very strict, maximum punishments

  • love (Integer) (defaults to: 2)

    Generosity level for rewards (0-4, default: 2)

    • 0: Minimal rewards

    • 2: Balanced rewards (default)

    • 4: Maximum rewards

  • paranoia (Integer) (defaults to: 2)

    Requirements threshold for rewards (1-4, default: 2)

    • 1: Easy to earn rewards

    • 2: Balanced requirements (default)

    • 4: Very difficult to earn rewards

Returns:

  • (Hash<String, String>)

    Hash mapping bylaw names to their formulas

Raises:

  • (RuntimeError)

    If parameters are out of valid ranges



38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/fbe/bylaws.rb', line 38

def Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
  raise "The 'anger' must be in the [0..4] interval: #{anger.inspect}" unless !anger.negative? && anger < 5
  raise "The 'love' must be in the [0..4] interval: #{love.inspect}" unless !love.negative? && love < 5
  raise "The 'paranoia' must be in the [1..4] interval: #{paranoia.inspect}" unless paranoia.positive? && paranoia < 5
  home = File.join(__dir__, '../../assets/bylaws')
  raise "The directory with templates is absent #{home.inspect}" unless File.exist?(home)
  Dir[File.join(home, '*.fe.liquid')].to_h do |f|
    formula = Liquid::Template.parse(File.read(f)).render(
      'anger' => anger, 'love' => love, 'paranoia' => paranoia
    )
    [File.basename(f).gsub(/\.fe.liquid$/, ''), formula]
  end
end

.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, epoch: $epoch || Time.now, kickoff: $kickoff || Time.now) {|Factbase::Fact| ... } ⇒ Object

Creates an instance of Conclude and evals it with the block provided.

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • judge (String) (defaults to: $judge)

    The name of the judge, from the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

  • epoch (Time) (defaults to: $epoch || Time.now)

    When the entire update started

  • kickoff (Time) (defaults to: $kickoff || Time.now)

    When the particular judge started

Yields:

  • (Factbase::Fact)

    The fact



23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/fbe/conclude.rb', line 23

def Fbe.conclude(
  fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global,
  epoch: $epoch || Time.now, kickoff: $kickoff || Time.now, &
)
  raise 'The fb is nil' if fb.nil?
  raise 'The $judge is not set' if judge.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  c = Fbe::Conclude.new(fb:, judge:, loog:, options:, global:, epoch:, kickoff:)
  c.instance_eval(&)
end

.consider(query, fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, epoch: $epoch || Time.now, kickoff: $kickoff || Time.now, lifetime_aware: true, timeout_aware: true) {|Factbase::Fact| ... } ⇒ Object

Creates an instance of Conclude and then runs “consider” in it.

Parameters:

  • query (String)

    The query

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • judge (String) (defaults to: $judge)

    The name of the judge, from the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

  • epoch (Time) (defaults to: $epoch || Time.now)

    When the entire update started

  • kickoff (Time) (defaults to: $kickoff || Time.now)

    When the particular judge started

Yields:

  • (Factbase::Fact)

    The fact



21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/fbe/consider.rb', line 21

def Fbe.consider(
  query,
  fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global,
  epoch: $epoch || Time.now, kickoff: $kickoff || Time.now,
  lifetime_aware: true, timeout_aware: true, &
)
  Fbe.conclude(fb:, judge:, loog:, options:, global:, epoch:, kickoff:) do
    on query
    timeout_unaware unless timeout_aware
    lifetime_unaware unless lifetime_aware
    consider(&)
  end
end

.copy(source, target, except: []) ⇒ Integer

Note:

Existing properties in target are preserved (not overwritten)

Makes a copy of a fact, moving all properties to a new fact.

All properties from the source will be copied to the target, except those listed in the except array. Only copies properties that don’t already exist in the target. Multi-valued properties are copied with all their values.

Examples:

Copy all properties except timestamps

source = fb.query('(eq type "user")').first
target = fb.insert
count = Fbe.copy(source, target, except: ['_time', '_id'])
puts "Copied #{count} property values"

Parameters:

  • source (Factbase::Fact)

    The source fact to copy from

  • target (Factbase::Fact)

    The target fact to copy to

  • except (Array<String>) (defaults to: [])

    List of property names to NOT copy (defaults to empty)

Returns:

  • (Integer)

    The number of property values that were copied

Raises:

  • (RuntimeError)

    If source, target, or except is nil



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/fbe/copy.rb', line 26

def Fbe.copy(source, target, except: [])
  raise 'The source is nil' if source.nil?
  raise 'The target is nil' if target.nil?
  raise 'The except is nil' if except.nil?
  copied = 0
  source.all_properties.each do |k|
    next unless target[k].nil?
    next if except.include?(k)
    source[k].each do |v|
      target.send(:"#{k}=", v)
      copied += 1
    end
  end
  copied
end

.delete(fact, *props, fb: Fbe.fb, id: '_id') ⇒ nil

Delete properties from a fact by creating a new fact without them.

This method doesn’t modify the original fact. Instead, it deletes the existing fact from the factbase and creates a new one with all properties except those specified for deletion.

Examples:

Delete multiple properties from a fact

fact = fb.query('(eq type "user")').first
new_fact = Fbe.delete(fact, 'age', 'city')
# new_fact will have all properties except 'age' and 'city'

Parameters:

  • fact (Factbase::Fact)

    The fact to delete properties from (must have an ID)

  • props (Array<String>)

    List of property names to delete

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to use (defaults to Fbe.fb)

  • id (String) (defaults to: '_id')

    The property name used as unique identifier (defaults to ‘_id’)

Returns:

  • (nil)

    Nothing

Raises:

  • (RuntimeError)

    If fact is nil, has no ID, or ID property doesn’t exist



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/fbe/delete.rb', line 25

def Fbe.delete(fact, *props, fb: Fbe.fb, id: '_id')
  raise 'The fact is nil' if fact.nil?
  return if props.all? { |k| fact[k].nil? }
  i = fact[id]
  raise "There is no #{id.inspect} in the fact" if i.nil?
  i = i.first
  before = {}
  fact.all_properties.each do |k|
    next if props.include?(k)
    before[k] = fact[k]
  end
  before.delete(id)
  fb.query("(eq #{id} #{i})").delete!
  fb.txn do |fbt|
    c = fbt.insert
    before.each do |k, vv|
      next unless c[k].nil?
      vv.each do |v|
        c.send(:"#{k}=", v)
      end
    end
  end
  nil
end

.delete_one(fact, prop, value, fb: Fbe.fb, id: '_id') ⇒ nil

Delete one value of a property.

This method doesn’t modify the original fact. Instead, it deletes the existing fact from the factbase and creates a new one with all properties except the one specified for deletion.

Parameters:

  • fact (Factbase::Fact)

    The fact to delete properties from (must have an ID)

  • prop (String)

    The property name

  • value (Any)

    The value to delete

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to use (defaults to Fbe.fb)

  • id (String) (defaults to: '_id')

    The property name used as unique identifier (defaults to ‘_id’)

Returns:

  • (nil)

    Nothing



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/fbe/delete_one.rb', line 21

def Fbe.delete_one(fact, prop, value, fb: Fbe.fb, id: '_id')
  raise 'The fact is nil' if fact.nil?
  i = fact[id]
  raise "There is no #{id.inspect} in the fact" if i.nil?
  i = i.first
  before = {}
  fact.all_properties.each do |k|
    before[k] = fact[k]
  end
  return unless before[prop]
  before[prop] = before[prop] - [value]
  before.delete(prop) if before[prop].empty?
  fb.query("(eq #{id} #{i})").delete!
  fb.txn do |fbt|
    c = fbt.insert
    before.each do |k, vv|
      next unless c[k].nil?
      vv.each do |v|
        c.send(:"#{k}=", v)
      end
    end
  end
  nil
end

.enter(badge, why, options: $options, loog: $loog) { ... } ⇒ Object

Note:

Requires $options and $loog global variables to be set

Note:

In testing mode (options.testing != nil), bypasses valve recording

Enter a new valve in the Zerocracy system.

A valve is a checkpoint or gate in the processing pipeline. This method records the entry into a valve with a reason, unless in testing mode.

Examples:

Enter a valve for processing

Fbe.enter('payment-check', 'Validating payment data') do
  # Process payment validation
  validate_payment(data)
end

Parameters:

  • badge (String)

    Unique badge identifier for the valve

  • why (String)

    The reason for entering this valve

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options if not provided)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog if not provided)

Yields:

  • Block to execute within the valve context

Returns:

  • (Object)

    The result of the yielded block

Raises:

  • (RuntimeError)

    If badge, why, or required globals are nil



28
29
30
31
32
33
34
35
36
# File 'lib/fbe/enter.rb', line 28

def Fbe.enter(badge, why, options: $options, loog: $loog, &)
  raise 'The badge is nil' if badge.nil?
  raise 'The why is nil' if why.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  return yield unless options.testing.nil?
  baza = BazaRb.new('api.zerocracy.com', 443, options.zerocracy_token, loog:)
  baza.enter(options.job_name, badge, why, options.job_id.to_i, &)
end

.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Factbase

Returns an instance of Factbase (cached).

Instead of using $fb directly, it is recommended to use this utility method. It will not only return the global factbase, but will also make sure it’s properly decorated and cached.

Parameters:

  • fb (Factbase) (defaults to: $fb)

    The global factbase provided by the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

Returns:

  • (Factbase)

    The global factbase



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/fbe/fb.rb', line 29

def Fbe.fb(fb: $fb, global: $global, options: $options, loog: $loog)
  raise 'The fb is nil' if fb.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  global[:fb] ||=
    begin
      rules = Dir.glob(File.join(File.join(__dir__, '../../rules'), '*.fe')).map { |f| File.read(f) }
      fbe = Factbase::Rules.new(
        fb,
        "(and \n#{rules.join("\n")}\n)",
        uid: '_id'
      )
      fbe =
        Factbase::Pre.new(fbe) do |f, fbt|
          max = fbt.query('(max _id)').one
          f._id = (max.nil? ? 0 : max) + 1
          f._time = Time.now
          f._version = [
            Factbase::VERSION,
            Judges::VERSION,
            options.action_version
          ].compact.join('/')
          f._job = options.job_id.to_i if options.job_id
        end
      Factbase::Impatient.new(
        Factbase::Logged.new(
          Factbase::SyncFactbase.new(
            Factbase::IndexedFactbase.new(
              Factbase::CachedFactbase.new(
                fbe
              )
            )
          ),
          loog
        ),
        timeout: 60
      )
    end
end

.github_graph(options: $options, global: $global, loog: $loog) ⇒ Fbe::Graph

Creates an instance of Graph.

Parameters:

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • global (Hash) (defaults to: $global)

    Hash of global options

  • loog (Loog) (defaults to: $loog)

    Logging facility

Returns:



17
18
19
20
21
22
23
24
25
# File 'lib/fbe/github_graph.rb', line 17

def Fbe.github_graph(options: $options, global: $global, loog: $loog)
  global[:github_graph] ||=
    if options.testing.nil?
      Fbe::Graph.new(token: options.github_token || ENV.fetch('GITHUB_TOKEN', nil))
    else
      loog.debug('The connection to GitHub GraphQL API is mocked')
      Fbe::Graph::Fake.new
    end
end

.if_absent(fb: Fbe.fb, always: false) {|Factbase::Fact| ... } ⇒ nil, Factbase::Fact

Note:

String values are properly escaped in queries

Note:

Time values are converted to UTC ISO8601 format for comparison

Injects a fact if it’s absent in the factbase, otherwise returns nil.

Checks if a fact with the same property values already exists. If not, creates a new fact. System properties (_id, _time, _version) are excluded from the uniqueness check.

Here is what you do when you want to add a fact to the factbase, but don’t want to make a duplicate of an existing one:

require 'fbe/if_absent'
n = Fbe.if_absent do |f|
  f.what = 'something'
  f.details = 'important'
end
return if n.nil?  # Fact already existed
n.when = Time.now # Add additional properties to the new fact

This code will definitely create one fact with what equals to something and details equals to important, while the when will be equal to the time of its first creation.

Examples:

Ensure unique user registration

user = Fbe.if_absent do |f|
  f.type = 'user'
  f.email = '[email protected]'
end
if user
  user.registered_at = Time.now
  puts "New user created"
else
  puts "User already exists"
end

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to check and insert into (defaults to Fbe.fb)

  • always (Boolean) (defaults to: false)

    If true, return the object in any case

Yields:

  • (Factbase::Fact)

    A proxy fact object to set properties on

Returns:

  • (nil, Factbase::Fact)

    nil if fact exists, otherwise the newly created fact



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/fbe/if_absent.rb', line 49

def Fbe.if_absent(fb: Fbe.fb, always: false)
  attrs = {}
  f =
    others(map: attrs) do |*args|
      k = args[0]
      if k.end_with?('=')
        k = k[0..-2].to_sym
        v = args[1]
        raise "Can't set #{k} to nil" if v.nil?
        raise "Can't set #{k} to empty string" if v.is_a?(String) && v.empty?
        @map[k] = v
      else
        @map[k.to_sym]
      end
    end
  yield f
  q = attrs.except('_id', '_time', '_version').map do |k, v|
    vv = v.to_s
    if v.is_a?(String)
      vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
    elsif v.is_a?(Time)
      vv = v.utc.iso8601
    end
    "(eq #{k} #{vv})"
  end.join(' ')
  q = "(and #{q})"
  before = fb.query(q).each.first
  return before if before && always
  return nil if before
  n = fb.insert
  attrs.each { |k, v| n.send(:"#{k}=", v) }
  n
end

.issue(fact, options: $options, global: $global, loog: $loog) ⇒ String

Note:

Requires ‘repository’ and ‘issue’ properties in the fact

Note:

Repository names are cached to reduce GitHub API calls

Converts GitHub repository and issue IDs into a formatted issue reference.

Takes the repository and issue properties from the provided fact, queries the GitHub API to get the repository’s full name, and formats it as a standard GitHub issue reference (e.g., “zerocracy/fbe#42”). Results are cached globally to minimize API calls.

Examples:

Format an issue reference

issue_fact = fb.query('(eq type "issue")').first
issue_fact.repository = 549866411  # Repository ID
issue_fact.issue = 42               # Issue number
puts Fbe.issue(issue_fact)  # => "zerocracy/fbe#42"

Parameters:

  • fact (Factbase::Fact)

    The fact containing repository and issue properties

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options global)

  • global (Hash) (defaults to: $global)

    The hash for global caching (uses $global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Returns:

  • (String)

    Formatted issue reference (e.g., “owner/repo#123”)

Raises:

  • (RuntimeError)

    If fact is nil or required properties are missing

  • (RuntimeError)

    If required global variables are not set



30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/fbe/issue.rb', line 30

def Fbe.issue(fact, options: $options, global: $global, loog: $loog)
  raise 'The fact is nil' if fact.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  rid = fact['repository']
  raise "There is no 'repository' property" if rid.nil?
  rid = rid.first.to_i
  issue = fact['issue']
  raise "There is no 'issue' property" if issue.nil?
  issue = issue.first.to_i
  "#{Fbe.octo(global:, options:, loog:).repo_name_by_id(rid)}##{issue}"
end

.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, epoch: $epoch || Time.now, kickoff: $kickoff || Time.now) { ... } ⇒ Object

Creates an instance of Iterate and evaluates it with the provided block.

This is a convenience method that creates an iterator instance and evaluates the DSL block within its context. The iterator processes repositories defined in options.repositories, executing queries and managing state for each.

Examples:

Iterate through repositories processing issues

Fbe.iterate do
  as 'issues_iterator'
  by '(and (eq what "issue") (gt created_at $before))'
  repeats 5
  quota_aware
  over do |repository_id, issue_id|
    process_issue(repository_id, issue_id)
    issue_id + 1
  end
end

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The global factbase provided by the judges tool (defaults to Fbe.fb)

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options global)

  • global (Hash) (defaults to: $global)

    The hash for global caching (uses $global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Yields:

  • Block containing DSL methods (as, by, over, etc.) to configure iteration

Returns:

  • (Object)

    Result of the block evaluation

Raises:

  • (RuntimeError)

    If required globals are not set



41
42
43
44
45
46
47
48
49
50
51
# File 'lib/fbe/iterate.rb', line 41

def Fbe.iterate(
  fb: Fbe.fb, loog: $loog, options: $options, global: $global,
  epoch: $epoch || Time.now, kickoff: $kickoff || Time.now, &
)
  raise 'The fb is nil' if fb.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  c = Fbe::Iterate.new(fb:, loog:, options:, global:, epoch:, kickoff:)
  c.instance_eval(&)
end

.just_one(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ Factbase::Fact

Note:

System attributes (_id, _time, _version) are ignored when matching

Ensures exactly one fact exists with the specified attributes in the factbase.

This method creates a new fact if none exists with the given attributes, or returns an existing fact if one already matches. Useful for preventing duplicate facts while ensuring required facts exist.

Examples:

Creating or finding a unique fact

require 'fbe/just_one'
fact = Fbe.just_one do |f|
  f.what = 'github_issue'
  f.issue_id = 123
  f.repository = 'zerocracy/fbe'
end
# Returns existing fact if one exists with these exact attributes,
# otherwise creates and returns a new fact

Attributes are matched exactly (case-sensitive)

Fbe.just_one { |f| f.name = 'Test' }  # Creates fact with name='Test'
Fbe.just_one { |f| f.name = 'test' }  # Creates another fact (different case)

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to search/insert into (defaults to Fbe.fb)

Yields:

  • (Factbase::Fact)

    Block to set attributes on the fact

Returns:

  • (Factbase::Fact)

    The existing or newly created fact



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/fbe/just_one.rb', line 35

def Fbe.just_one(fb: Fbe.fb)
  attrs = {}
  f =
    others(map: attrs) do |*args|
      k = args[0]
      if k.end_with?('=')
        @map[k[0..-2].to_sym] = args[1]
      else
        @map[k.to_sym]
      end
    end
  yield f
  q = attrs.except('_id', '_time', '_version').map do |k, v|
    vv = v.to_s
    if v.is_a?(String)
      vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
    elsif v.is_a?(Time)
      vv = v.utc.iso8601
    end
    "(eq #{k} #{vv})"
  end.join(' ')
  q = "(and #{q})"
  before = fb.query(q).each.first
  return before unless before.nil?
  n = fb.insert
  attrs.each { |k, v| n.send(:"#{k}=", v) }
  n
end

.kill_if(facts, fb: Fbe.fb, fid: '_id') ⇒ Object

Delete a few facts, knowing their IDs.

Parameters:

  • facts (Array)

    List of facts to kill

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to use (defaults to Fbe.fb)



13
14
15
16
17
18
19
20
21
22
23
# File 'lib/fbe/kill_if.rb', line 13

def Fbe.kill_if(facts, fb: Fbe.fb, fid: '_id')
  ids = []
  facts.each do |f|
    if block_given?
      t = yield f
      next unless t
    end
    ids << f[fid].first
  end
  fb.query("(or #{ids.map { |id| "(eq #{fid} #{id})" }.join})").delete!
end

.mask_to_regex(mask) ⇒ Regexp

Converts a repository mask pattern to a regular expression.

Examples:

Basic wildcard matching

Fbe.mask_to_regex('zerocracy/*')
# => /zerocracy\/.*/i

Specific repository (no wildcard)

Fbe.mask_to_regex('zerocracy/fbe')
# => /zerocracy\/fbe/i

Parameters:

  • mask (String)

    Repository mask in format ‘org/repo’ where repo can contain ‘*’

Returns:

  • (Regexp)

    Case-insensitive regular expression for matching repositories

Raises:

  • (RuntimeError)

    If organization part contains asterisk



24
25
26
27
28
# File 'lib/fbe/unmask_repos.rb', line 24

def Fbe.mask_to_regex(mask)
  org, repo = mask.split('/')
  raise "Org '#{org}' can't have an asterisk" if org.include?('*')
  Regexp.compile("#{org}/#{repo.gsub('*', '.*')}", Regexp::IGNORECASE)
end

.octo(options: $options, global: $global, loog: $loog) ⇒ Hash

Makes a call to the GitHub API.

It is supposed to be used instead of Octokit::Client, because it is pre-configured and enables additional features, such as retrying, logging, and caching.

Parameters:

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • global (Hash) (defaults to: $global)

    Hash of global options

  • loog (Loog) (defaults to: $loog)

    Logging facility

Options Hash (options:):

  • :github_token (String)

    GitHub API token for authentication

  • :testing (Boolean)

    When true, uses FakeOctokit for testing

  • :sqlite_cache (String)

    Path to SQLite cache file for HTTP responses

  • :sqlite_cache_maxsize (Integer)

    Maximum size of SQLite cache in bytes (default: 10MB)

Returns:

  • (Hash)

    Usually returns a JSON, as it comes from the GitHub API



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/fbe/octo.rb', line 45

def Fbe.octo(options: $options, global: $global, loog: $loog)
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  global[:octo] ||=
    begin
      loog.info("Fbe version is #{Fbe::VERSION}")
      trace = []
      if options.testing.nil?
        o = Octokit::Client.new
        token = options.github_token
        if token.nil?
          loog.debug("The 'github_token' option is not provided")
          token = ENV.fetch('GITHUB_TOKEN', nil)
          if token.nil?
            loog.debug("The 'GITHUB_TOKEN' environment variable is not set")
          else
            loog.debug("The 'GITHUB_TOKEN' environment was provided")
          end
        else
          loog.debug("The 'github_token' option was provided (#{token.length} chars)")
        end
        if token.nil?
          loog.warn('Accessing GitHub API without a token!')
        elsif token.empty?
          loog.warn('The GitHub API token is an empty string, won\'t use it')
        else
          o = Octokit::Client.new(access_token: token)
        end
        o.auto_paginate = true
        o.per_page = 100
        o.connection_options = {
          request: {
            open_timeout: 15,
            timeout: 15
          }
        }
        stack =
          Faraday::RackBuilder.new do |builder|
            builder.use(
              Faraday::Retry::Middleware,
              exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [
                Octokit::TooManyRequests, Octokit::ServiceUnavailable
              ],
              max: 4,
              interval: ENV['RACK_ENV'] == 'test' ? 0.01 : 4,
              methods: [:get],
              backoff_factor: 2
            )
            builder.use(Octokit::Response::RaiseError)
            builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter)
            builder.use(Fbe::Middleware::RateLimit)
            builder.use(Fbe::Middleware::Trace, trace, ignores: [:fresh])
            if options.sqlite_cache
              maxsize = Filesize.from(options.sqlite_cache_maxsize || '100M').to_i
              maxvsize = Filesize.from(options.sqlite_cache_maxvsize || '100K').to_i
              cache_min_age = options.sqlite_cache_min_age&.to_i
              store = Fbe::Middleware::SqliteStore.new(
                options.sqlite_cache, Fbe::VERSION, loog:, maxsize:, maxvsize:, ttl: 24, cache_min_age:
              )
              loog.info(
                "Using HTTP cache in SQLite file: #{store.path} (" \
                "#{File.exist?(store.path) ? Filesize.from(File.size(store.path).to_s).pretty : 'file is absent'}, " \
                "max size: #{Filesize.from(maxsize.to_s).pretty}, max vsize: #{Filesize.from(maxvsize.to_s).pretty})"
              )
              builder.use(
                Faraday::HttpCache,
                store:, serializer: JSON, shared_cache: false, logger: Loog::NULL
              )
            else
              loog.info("No HTTP cache in SQLite file, because 'sqlite_cache' option is not provided")
              builder.use(
                Faraday::HttpCache,
                serializer: Marshal, shared_cache: false, logger: Loog::NULL
              )
            end
            builder.adapter(Faraday.default_adapter)
          end
        o.middleware = stack
        o = Verbose.new(o, log: loog)
        unless token.nil? || token.empty?
          loog.info(
            "Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect}, " \
            "#{o.rate_limit.remaining} quota remaining)"
          )
        end
      else
        loog.debug('The connection to GitHub API is mocked')
        o = Fbe::FakeOctokit.new
      end
      o =
        decoor(o, loog:, trace:) do
          def print_trace!(all: false, max: 5)
            if @trace.empty?
              @loog.debug('GitHub API trace is empty')
            else
              grouped =
                @trace.select { |e| e[:duration] > 0.05 || all }.group_by do |entry|
                  uri = URI.parse(entry[:url])
                  query = uri.query
                  query = "?#{query.ellipsized(40)}" if query
                  "#{uri.scheme}://#{uri.host}#{uri.path}#{query}"
                end
              message = grouped
                .sort_by { |_path, entries| -entries.count }
                .map do |path, entries|
                  [
                    '  ',
                    path.gsub(%r{^https://api.github.com/}, '/'),
                    ': ',
                    entries.count,
                    " (#{entries.sum { |e| e[:duration] }.seconds})"
                  ].join
                end
                .take(max)
                .join("\n")
              @loog.info(
                "GitHub API trace (#{grouped.count} URLs vs #{@trace.count} requests, " \
                "#{@origin.rate_limit!.remaining} quota left):\n#{message}"
              )
              @trace.clear
            end
          end

          def off_quota?(threshold: 50)
            left = @origin.rate_limit!.remaining
            if left < threshold
              @loog.info("Too much GitHub API quota consumed already (#{left} < #{threshold})")
              true
            else
              @loog.debug("Still #{left} GitHub API quota left (>#{threshold})")
              false
            end
          end

          def user_name_by_id(id)
            raise 'The ID of the user is nil' if id.nil?
            raise 'The ID of the user must be an Integer' unless id.is_a?(Integer)
            json = @origin.user(id)
            name = json[:login].downcase
            @loog.debug("GitHub user ##{id} has a name: @#{name}")
            name
          end

          def repo_id_by_name(name)
            raise 'The name of the repo is nil' if name.nil?
            json = @origin.repository(name)
            id = json[:id]
            raise "Repository #{name} not found" if id.nil?
            @loog.debug("GitHub repository #{name.inspect} has an ID: ##{id}")
            id
          end

          def repo_name_by_id(id)
            raise 'The ID of the repo is nil' if id.nil?
            raise 'The ID of the repo must be an Integer' unless id.is_a?(Integer)
            json = @origin.repository(id)
            name = json[:full_name].downcase
            @loog.debug("GitHub repository ##{id} has a name: #{name}")
            name
          end

          # Disable auto pagination for octokit client called in block
          #
          # @yield [octo] Give octokit client with disabled auto pagination
          # @yieldparam [Octokit::Client, Fbe::FakeOctokit] Octokit client
          # @return [Object] Last value in block
          # @example
          #   issue =
          #      Fbe.octo.with_disable_auto_paginate do |octo|
          #        octo.list_issue('zerocracy/fbe', per_page: 1).first
          #      end
          def with_disable_auto_paginate
            ap = @origin.auto_paginate
            @origin.auto_paginate = false
            yield self if block_given?
          ensure
            @origin.auto_paginate = ap
          end
        end
      o =
        intercepted(o) do |e, m, _args, _r|
          if e == :before && m != :off_quota? && m != :print_trace! && m != :rate_limit && o.off_quota?
            raise Fbe::OffQuota, "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{m}()"
          end
        end
      o
    end
end

.over?(global: $global, options: $options, loog: $loog, epoch: $epoch || Time.now, kickoff: $kickoff || Time.now, quota_aware: true, lifetime_aware: true, timeout_aware: true) ⇒ Boolean

Check GitHub API quota, lifetime, and timeout.

Parameters:

  • global (Hash) (defaults to: $global)

    Hash of global options

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • loog (Loog) (defaults to: $loog)

    Logging facility

  • epoch (Time) (defaults to: $epoch || Time.now)

    When the entire update started

  • kickoff (Time) (defaults to: $kickoff || Time.now)

    When the particular judge started

  • quota_aware (Boolean) (defaults to: true)

    Enable or disable check of GitHub API quota

  • lifetime_aware (Boolean) (defaults to: true)

    Enable or disable check of lifetime limitations

  • timeout_aware (Boolean) (defaults to: true)

    Enable or disable check of timeout limitations

Returns:

  • (Boolean)

    check result



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/fbe/over.rb', line 20

def Fbe.over?(
  global: $global, options: $options, loog: $loog,
  epoch: $epoch || Time.now, kickoff: $kickoff || Time.now,
  quota_aware: true, lifetime_aware: true, timeout_aware: true
)
  if quota_aware && Fbe.octo(loog:, options:, global:).off_quota?(threshold: 100)
    loog.info('We are off GitHub quota, time to stop')
    return true
  end
  if lifetime_aware && options.lifetime && Time.now - epoch > options.lifetime * 0.9
    loog.info("We ran out of lifetime (#{epoch.ago} already), must stop here")
    return true
  end
  if timeout_aware && options.timeout && Time.now - kickoff > options.timeout * 0.9
    loog.info("We've spent more than #{kickoff.ago}, must stop here")
    return true
  end
  false
end

.overwrite(fact, property_or_hash, values = nil, fb: Fbe.fb, fid: '_id') ⇒ nil

Note:

This operation preserves all other properties during recreation

Note:

If property already has the same single value, no changes are made

Overwrites a property in the fact by recreating the entire fact.

If the property doesn’t exist in the fact, it will be added. If it does exist, the entire fact will be destroyed, a new fact created with all existing properties, and the specified property set with the new value.

It is important that the fact has the _id property. If it doesn’t, an exception will be raised.

Examples:

Update a user’s status

user = fb.query('(eq login "john")').first
Fbe.overwrite(user, 'status', 'active')
# All properties preserved, only 'status' is set to 'active'

Update multiple properties at once

user = fb.query('(eq login "john")').first
Fbe.overwrite(user, status: 'active', role: 'admin')
# All properties preserved, 'status' and 'role' are updated

Parameters:

  • fact (Factbase::Fact)

    The fact to modify (must have _id property)

  • property_or_hash (String, Hash)

    The name of the property to set, or a hash of properties

  • values (Any) (defaults to: nil)

    The value to set (can be any type, including array) - ignored if first param is Hash

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to use (defaults to Fbe.fb)

Returns:

  • (nil)

    Nothing

Raises:

  • (RuntimeError)

    If fact is nil, has no _id, or property is not a String



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/fbe/overwrite.rb', line 34

def Fbe.overwrite(fact, property_or_hash, values = nil, fb: Fbe.fb, fid: '_id')
  raise 'The fact is nil' if fact.nil?
  raise 'The fb is nil' if fb.nil?
  if property_or_hash.is_a?(Hash)
    before = {}
    fact.all_properties.each do |k|
      before[k.to_s] = fact[k]
    end
    modified = false
    property_or_hash.each do |k, vv|
      raise "The value for #{k} is nil" if vv.nil?
      vv = [vv] unless vv.is_a?(Array)
      next if before[k.to_s] == vv
      before[k.to_s] = vv
      modified = true
    end
    return fact unless modified
    id = fact[fid]&.first
    raise "There is no #{fid} in the fact, cannot use Fbe.overwrite" if id.nil?
    raise "No facts by #{fid} = #{id}" if fb.query("(eq #{fid} #{id})").delete!.zero?
    fb.txn do |fbt|
      n = fbt.insert
      before.each do |k, vv|
        next unless n[k].nil?
        vv.each do |v|
          n.send(:"#{k}=", v)
        end
      end
    end
    return
  end
  property = property_or_hash
  raise "The property is not a String but #{property.class} (#{property})" unless property.is_a?(String)
  raise 'The values is nil' if values.nil?
  values = [values] unless values.is_a?(Array)
  return fact if !fact[property].nil? && fact[property].one? && values.one? && fact[property].first == values.first
  if fact[property].nil?
    values.each do |v|
      fact.send(:"#{property}=", v)
    end
    return
  end
  before = {}
  fact.all_properties.each do |k|
    before[k.to_s] = fact[k]
  end
  id = fact[fid]&.first
  raise "There is no #{fid} in the fact, cannot use Fbe.overwrite" if id.nil?
  raise "No facts by #{fid} = #{id}" if fb.query("(eq #{fid} #{id})").delete!.zero?
  fb.txn do |fbt|
    n = fbt.insert
    before[property.to_s] = values
    before.each do |k, vv|
      next unless n[k].nil?
      vv.each do |v|
        n.send(:"#{k}=", v)
      end
    end
  end
  nil
end

.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ Object

Takes configuration parameter from the “PMP” fact.

The factbase may have a few facts with the what set to pmp (stands for the “project management plan”). These facts contain information that configures the project. It is expected that every fact with the what set to pmp also contains the area property, which is set to one of nine values: scope, time, cost, etc. (the nine process areas in the PMBOK).

If a proper pmp fact is not found or the property is absent in the fact, this method throws an exception. The factbase must contain PMP-related facts. Most probably, a special judge must fill it up with such a fact.

The method uses a double nested ‘others` block to create a chainable interface that allows accessing configuration like:

Fbe.pmp.hr.reward_points
Fbe.pmp.cost.hourly_rate
Fbe.pmp.time.deadline

Examples:

# Get HR reward points from PMP configuration
points = Fbe.pmp.hr.reward_points

# Get hourly rate from cost area
rate = Fbe.pmp.cost.hourly_rate

# Get deadline from time area
deadline = Fbe.pmp.time.deadline

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

Returns:

  • (Object)

    A proxy object that allows method chaining to access PMP properties



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/fbe/pmp.rb', line 45

def Fbe.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog)
  xml = Nokogiri::XML(File.read(File.join(__dir__, '../../assets/pmp.xml')))
  pmpv =
    Class.new(SimpleDelegator) do
      attr_reader :default, :type, :memo

      def initialize(value, default, type, memo)
        super(value)
        @default = default
        @type = type
        @memo = memo
      end
    end
  Class.new do
    define_method(:areas) do
      xml.xpath('/pmp/area/@name').map(&:value)
    end
    others do |*args1|
      area = args1.first.to_s
      node = xml.at_xpath("/pmp/area[@name='#{area}']")
      raise "Unknown area #{area.inspect}" if node.nil?
      Class.new do
        define_method(:properties) do
          node.xpath('p/name').map(&:text)
        end
        others do |*args2|
          param = args2.first.to_s
          f = Fbe.fb(global:, fb:, options:, loog:).query("(and (eq what 'pmp') (eq area '#{area}'))").each.first
          result = f&.[](param)&.first
          prop = node.at_xpath("p[name='#{param}']")
          default = nil
          type = nil
          memo = nil
          if prop
            default = prop.at_xpath('default').text
            type = prop.at_xpath('type').text
            memo = prop.at_xpath('memo').text
            default =
              case type
              when 'int' then default.to_i
              when 'float' then default.to_f
              when 'bool' then default == 'true'
              else default
              end
          end
          result ||= default
          result =
            case type
            when 'int' then result.to_i
            when 'float' then result.to_f
            when 'bool' then result == 'true'
            else result
            end
          pmpv.new(result, default, type, memo)
        end
      end.new
    end
  end.new
end

.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil

Note:

Skips execution if judge was run within the interval period

Note:

The ‘since’ property is added to the fact when p_since_days is provided

Run the block provided every X days based on PMP configuration.

Executes a block periodically based on PMP (Project Management Plan) settings. The block will only run if it hasn’t been executed within the specified interval. Creates a fact recording when the judge was last run.

Examples:

Run a cleanup task every 3 days

Fbe.regularly('cleanup', 'days_between_cleanups', 'cleanup_history_days') do |f|
  f.total_cleaned = cleanup_old_records
  # PMP might have: days_between_cleanups=3, cleanup_history_days=30
end

Parameters:

  • area (String)

    The name of the PMP area

  • p_every_days (String)

    PMP property name for interval (defaults to 7 days if not in PMP)

  • p_since_days (String) (defaults to: nil)

    PMP property name for since period (defaults to 28 days if not in PMP)

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase (defaults to Fbe.fb)

  • judge (String) (defaults to: $judge)

    The name of the judge (uses $judge global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Yields:

  • (Factbase::Fact)

    Fact to populate with judge execution details

Returns:

  • (nil)

    Nothing

Raises:

  • (RuntimeError)

    If required parameters or globals are nil



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/fbe/regularly.rb', line 32

def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &)
  raise 'The area is nil' if area.nil?
  raise 'The p_every_days is nil' if p_every_days.nil?
  raise 'The fb is nil' if fb.nil?
  raise 'The $judge is not set' if judge.nil?
  raise 'The $loog is not set' if loog.nil?
  pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_days}))").each.first
  interval = pmp.nil? ? 7 : pmp[p_every_days].first
  recent = fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{interval} days')))"
  ).each.first
  if recent
    loog.info(
      "#{$judge} statistics were collected #{recent.when.ago} ago, " \
      "skipping now (we run it every #{interval} days)"
    )
    return
  end
  loog.info("#{$judge} statistics weren't collected for the last #{interval} days")
  fb.txn do |fbt|
    f = fbt.insert
    f.what = judge
    f.when = Time.now
    unless p_since_days.nil?
      days = pmp.nil? ? 28 : pmp[p_since_days].first
      since = Time.now - (days * 24 * 60 * 60)
      f.since = since
    end
    yield f
  end
  nil
end

.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil

Note:

Skips execution if judge was run within the interval period

Note:

Overwrites the ‘when’ property of existing judge fact

Run the block provided every X hours based on PMP configuration.

Similar to Fbe.regularly but works with hour intervals instead of days. Executes a block periodically, maintaining a single fact that tracks the last execution time. The fact is overwritten on each run rather than creating new facts.

Examples:

Run a monitoring task every 6 hours

Fbe.repeatedly('monitoring', 'hours_between_checks') do |f|
  f.servers_checked = check_all_servers
  f.issues_found = count_issues
  # PMP might have: hours_between_checks=6
end

Parameters:

  • area (String)

    The name of the PMP area

  • p_every_hours (String)

    PMP property name for interval (defaults to 24 hours if not in PMP)

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase (defaults to Fbe.fb)

  • judge (String) (defaults to: $judge)

    The name of the judge (uses $judge global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Yields:

  • (Factbase::Fact)

    The judge fact to populate with execution details

Returns:

  • (nil)

    Nothing

Raises:

  • (RuntimeError)

    If required parameters or globals are nil



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/fbe/repeatedly.rb', line 34

def Fbe.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog, &)
  raise 'The area is nil' if area.nil?
  raise 'The p_every_hours is nil' if p_every_hours.nil?
  raise 'The fb is nil' if fb.nil?
  raise 'The $judge is not set' if judge.nil?
  raise 'The $loog is not set' if loog.nil?
  pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_hours}))").each.first
  hours = pmp.nil? ? 24 : pmp[p_every_hours].first
  recent = fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{hours} hours')))"
  ).each.first
  if recent
    loog.info("#{$judge} was executed #{recent.when.ago} ago, skipping now (we run it every #{hours} hours)")
    return
  end
  f = fb.query("(and (eq what '#{judge}'))").each.first
  if f.nil?
    f = fb.insert
    f.what = judge
  end
  Fbe.overwrite(f, 'when', Time.now)
  yield fb.query("(and (eq what '#{judge}'))").each.first
  nil
end

.sec(fact, prop = :seconds) ⇒ String

Note:

Uses the tago gem’s ago method for formatting

Converts number of seconds into human-readable time format.

The number of seconds is taken from the fact provided, usually stored there in the seconds property. The seconds are formatted into a human-readable string like “3 days ago” or “5 hours ago” using the tago gem.

Examples:

Format elapsed time from a fact

build_fact = fb.query('(eq type "build")').first
build_fact.duration = 7200  # 2 hours in seconds
puts Fbe.sec(build_fact, :duration)  # => "2 hours ago"

Parameters:

  • fact (Factbase::Fact)

    The fact containing the seconds property

  • prop (String, Symbol) (defaults to: :seconds)

    The property name with seconds (defaults to :seconds)

Returns:

  • (String)

    Human-readable time interval (e.g., “2 weeks ago”, “3 hours ago”)

Raises:

  • (RuntimeError)

    If the specified property doesn’t exist in the fact



25
26
27
28
29
30
# File 'lib/fbe/sec.rb', line 25

def Fbe.sec(fact, prop = :seconds)
  s = fact[prop.to_s]
  raise "There is no #{prop.inspect} property" if s.nil?
  s = s.first.to_i
  (Time.now + s).ago
end

.unmask_repos(options: $options, global: $global, loog: $loog, epoch: $epoch || Time.now, kickoff: $kickoff || Time.now, quota_aware: true, lifetime_aware: true, timeout_aware: true) ⇒ Array<String>

Note:

Exclusion patterns must start with ‘-’ (e.g., ‘-org/pattern*’)

Note:

Results are shuffled to distribute load when processing

Resolves repository masks to actual GitHub repository names.

Takes a comma-separated list of repository masks from options and expands wildcards by querying GitHub API. Supports inclusion and exclusion patterns. Archived repositories are automatically filtered out.

Examples:

Basic usage with wildcards

# options.repositories = "zerocracy/fbe,zerocracy/ab*"
repos = Fbe.unmask_repos
# => ["zerocracy/fbe", "zerocracy/abc", "zerocracy/abcd"]

Using exclusion patterns

# options.repositories = "zerocracy/*,-zerocracy/private*"
repos = Fbe.unmask_repos
# Returns all zerocracy repos except those starting with 'private'

Empty result handling

# options.repositories = "nonexistent/*"
Fbe.unmask_repos  # Raises error: "No repos found matching: nonexistent/*"

Parameters:

  • options (Judges::Options) (defaults to: $options)

    Options containing ‘repositories’ field with masks

  • global (Hash) (defaults to: $global)

    Global cache for storing API responses

  • loog (Loog) (defaults to: $loog)

    Logger for debug output

  • epoch (Time) (defaults to: $epoch || Time.now)

    When the entire update started

  • kickoff (Time) (defaults to: $kickoff || Time.now)

    When the particular judge started

  • quota_aware (Boolean) (defaults to: true)

    Should we stop if quota is off?

  • lifetime_aware (Boolean) (defaults to: true)

    Should we stop if lifetime is over?

  • timeout_aware (Boolean) (defaults to: true)

    Should we stop if timeout is over?

Returns:

  • (Array<String>)

    Shuffled list of repository full names (e.g., ‘org/repo’)

Raises:

  • (RuntimeError)

    If no repositories match the provided masks



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/fbe/unmask_repos.rb', line 62

def Fbe.unmask_repos(
  options: $options, global: $global, loog: $loog, epoch: $epoch || Time.now, kickoff: $kickoff || Time.now,
  quota_aware: true, lifetime_aware: true, timeout_aware: true
)
  raise 'Repositories mask is not specified' unless options.repositories
  raise 'Repositories mask is empty' if options.repositories.empty?
  return if block_given? && Fbe.over?(
    global:, options:, loog:, epoch:, kickoff:, quota_aware:, lifetime_aware:, timeout_aware:
  )
  repos = []
  octo = Fbe.octo(loog:, global:, options:)
  masks = (options.repositories || '').split(',')
  masks.reject { |m| m.start_with?('-') }.each do |mask|
    unless mask.include?('*')
      repos << mask
      next
    end
    re = Fbe.mask_to_regex(mask)
    octo.repositories(mask.split('/')[0]).each do |r|
      repos << r[:full_name] if re.match?(r[:full_name])
    end
  end
  masks.select { |m| m.start_with?('-') }.each do |mask|
    re = Fbe.mask_to_regex(mask[1..])
    repos.reject! { |r| re.match?(r) }
  end
  repos.reject! { |repo| octo.repository(repo)[:archived] }
  raise "No repos found matching: #{options.repositories.inspect}" if repos.empty?
  repos.shuffle!
  loog.debug("Scanning #{repos.size} repositories: #{repos.joined}...")
  repos.each { |repo| octo.repository(repo) }
  return repos unless block_given?
  repos.each do |repo|
    break if Fbe.over?(global:, options:, loog:, epoch:, kickoff:, quota_aware:, lifetime_aware:, timeout_aware:)
    yield repo
  end
end

.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ String

Note:

Results are cached to reduce GitHub API calls

Note:

Subject to GitHub API rate limits

Converts a GitHub user ID into a formatted username string.

The ID of the user (integer) is expected to be stored in the who property of the provided fact. This function makes a live request to GitHub API to retrieve the username. The result is cached globally to minimize API calls. For example, the ID 526301 will be converted to “@yegor256”.

Examples:

Convert user ID to username

contributor = fb.query('(eq type "contributor")').first
contributor.author_id = 526301
puts Fbe.who(contributor, :author_id)  # => "@yegor256"

Parameters:

  • fact (Factbase::Fact)

    The fact containing the GitHub user ID

  • prop (String, Symbol) (defaults to: :who)

    The property name with the ID (defaults to :who)

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options global)

  • global (Hash) (defaults to: $global)

    The hash for global caching (uses $global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Returns:

  • (String)

    Formatted username with @ prefix (e.g., “@yegor256”)

Raises:

  • (RuntimeError)

    If the specified property doesn’t exist in the fact



29
30
31
32
33
34
# File 'lib/fbe/who.rb', line 29

def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog)
  id = fact[prop.to_s]
  raise "There is no #{prop.inspect} property" if id.nil?
  id = id.first.to_i
  "@#{Fbe.octo(options:, global:, loog:).user_name_by_id(id)}"
end