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

Constant Summary collapse

VERSION =

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

'0.16.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 }"

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, '*.liquid')].to_h do |f|
    formula = Liquid::Template.parse(File.read(f)).render(
      'anger' => anger, 'love' => love, 'paranoia' => paranoia
    )
    [File.basename(f).gsub(/\.liquid$/, ''), formula]
  end
end

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

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

Yields:

  • (Factbase::Fact)

    The fact



20
21
22
23
24
25
26
27
28
# File 'lib/fbe/conclude.rb', line 20

def Fbe.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, &)
  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:)
  c.instance_eval(&)
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"

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') ⇒ Factbase::Fact

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'

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
# 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?
  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)
    fact[k].each do |v|
      before[k] = v
    end
  end
  fb.query("(eq #{id} #{i})").delete!
  c = fb.insert
  before.each do |k, v|
    c.send(:"#{k}=", v)
  end
  c
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

Yields:

  • Block to execute within the valve context

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.



28
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
# File 'lib/fbe/fb.rb', line 28

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('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}"
          f._job = options.job_id unless options.job_id.nil?
        end
      Factbase::Logged.new(
        Factbase::SyncFactbase.new(
          Factbase::IndexedFactbase.new(
            Factbase::CachedFactbase.new(
              fbe
            )
          )
        ),
        loog
      )
    end
end

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

Creates an instance of Graph.



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) {|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

Yields:

  • (Factbase::Fact)

    A proxy fact object to set properties on



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
# File 'lib/fbe/if_absent.rb', line 48

def Fbe.if_absent(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.to_a.first
  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"

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) { ... } ⇒ 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(timeout: 300) do |repository_id, issue_id|
    process_issue(repository_id, issue_id)
    issue_id + 1
  end
end

Yields:

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

Raises:

  • (RuntimeError)

    If required globals are not set



37
38
39
40
41
42
43
44
# File 'lib/fbe/iterate.rb', line 37

def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
  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:)
  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)

Yields:

  • (Factbase::Fact)

    Block to set attributes on the 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.to_a.first
  return before unless before.nil?
  n = fb.insert
  attrs.each { |k, v| n.send(:"#{k}=", v) }
  n
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

Raises:

  • (RuntimeError)

    If organization part contains asterisk



22
23
24
25
26
# File 'lib/fbe/unmask_repos.rb', line 22

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.



27
28
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
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
# File 'lib/fbe/octo.rb', line 27

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
      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")
        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)
          loog.info("Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect}, " \
                    "#{Octokit::Client.new(access_token: token).rate_limit.remaining} quota remaining)")
        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(Faraday::HttpCache, serializer: Marshal, shared_cache: false, logger: Loog::NULL)
            builder.use(Octokit::Response::RaiseError)
            builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter)
            builder.adapter(Faraday.default_adapter)
          end
        o.middleware = stack
        o = Verbose.new(o, log: loog)
      else
        loog.debug('The connection to GitHub API is mocked')
        o = Fbe::FakeOctokit.new
      end
      decoor(o, loog:) do
        def off_quota(threshold: 50)
          left = @origin.rate_limit.remaining
          if left < threshold
            @loog.info("Too much GitHub API quota consumed already (#{left} < #{threshold}), stopping")
            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]
          @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
      end
    end
end

.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Factbase::Fact

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
updated_user = Fbe.overwrite(user, 'status', 'active')
# All properties preserved, only 'status' is set to 'active'

Raises:

  • (RuntimeError)

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



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/fbe/overwrite.rb', line 30

def Fbe.overwrite(fact, property, value, fb: Fbe.fb)
  raise 'The fact is nil' if fact.nil?
  raise 'The fb is nil' if fb.nil?
  raise "The property is not a String but #{property.class} (#{property})" unless property.is_a?(String)
  return fact if !fact[property].nil? && fact[property].size == 1 && fact[property].first == value
  before = {}
  fact.all_properties.each do |prop|
    before[prop.to_s] = fact[prop]
  end
  id = fact['_id']&.first
  raise 'There is no _id in the fact, cannot use Fbe.overwrite' if id.nil?
  raise "No facts by _id = #{id}" if fb.query("(eq _id #{id})").delete!.zero?
  n = fb.insert
  before[property.to_s] = [value]
  before.each do |k, vv|
    next unless n[k].nil?
    vv.each do |v|
      n.send(:"#{k}=", v)
    end
  end
  n
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


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/fbe/pmp.rb', line 43

def Fbe.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog)
  others do |*args1|
    area = args1.first
    unless %w[cost scope hr time procurement risk integration quality communication].include?(area.to_s)
      raise "Invalid area #{area.inspect} (not part of PMBOK)"
    end
    others do |*args2|
      param = args2.first
      f = Fbe.fb(global:, fb:, options:, loog:).query("(and (eq what 'pmp') (eq area '#{area}'))").each.to_a.first
      raise "Unknown area #{area.inspect}" if f.nil?
      r = f[param]
      raise "Unknown property #{param.inspect} in the #{area.inspect} area" if r.nil?
      r.first
    end
  end
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

Yields:

  • (Factbase::Fact)

    Fact to populate with judge execution details

Raises:

  • (RuntimeError)

    If required parameters or globals are nil



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
# File 'lib/fbe/regularly.rb', line 31

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.to_a.first
  interval = pmp.nil? ? 7 : pmp[p_every_days].first
  unless fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{interval} days')))"
  ).each.to_a.empty?
    loog.debug("#{$judge} statistics have recently been collected, skipping now")
    return
  end
  f = fb.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
  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

Yields:

  • (Factbase::Fact)

    The judge fact to populate with execution details

Raises:

  • (RuntimeError)

    If required parameters or globals are nil



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
# File 'lib/fbe/repeatedly.rb', line 33

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.to_a.first
  hours = pmp.nil? ? 24 : pmp[p_every_hours].first
  unless fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{hours} hours')))"
  ).each.to_a.empty?
    loog.debug("#{$judge} has recently been executed, skipping now")
    return
  end
  f = fb.query("(and (eq what '#{judge}'))").each.to_a.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.to_a.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"

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) ⇒ 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/*"

Raises:

  • (RuntimeError)

    If no repositories match the provided masks



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/fbe/unmask_repos.rb', line 55

def Fbe.unmask_repos(options: $options, global: $global, loog: $loog)
  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}" if repos.empty?
  repos.shuffle!
  loog.debug("Scanning #{repos.size} repositories: #{repos.join(', ')}...")
  repos
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"

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