fbe 0.14.1 → 0.16.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.
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 evals it with the block provided.
13
+ # Creates an instance of {Fbe::Iterate} and evaluates it with the provided block.
14
14
  #
15
- # @param [Factbase] fb The global factbase provided by the +judges+ tool
16
- # @param [Judges::Options] options The options coming from the +judges+ tool
17
- # @param [Hash] global The hash for global caching
18
- # @param [Loog] loog The logging facility
19
- # @yield [Factbase::Fact] The fact
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
- # An iterator.
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
- # Here, you go through all repositories defined by the +repositories+ option
32
- # in the +$options+, trying to run the provided query for each of them. If the
33
- # query returns an integer that is different from the previously seen, the
34
- # function keeps repeating the cycle. Otherwise, it will restart from the
35
- # beginning.
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
- # Ctor.
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 coming from the +judges+ tool
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
- # Make this block aware of GitHub API quota.
96
+ # Makes the iterator aware of GitHub API quota limits.
60
97
  #
61
- # When the quota is reached, the loop will gracefully stop to avoid
62
- # hitting GitHub API rate limits. This helps prevent interruptions
63
- # in long-running operations.
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 total counter of repeats to make.
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 total count of repeats to execute
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 run.
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 to use in the "marker" fact.
143
+ # Sets the label for tracking iteration state.
91
144
  #
92
- # @param [String] label The label identifier
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
- # It makes a number of repeats of going through all repositories
101
- # provided by the +repositories+ configuration option. In each "repeat"
102
- # it yields the repository ID and a number that is retrieved by the
103
- # +query+. The query is supplied with two parameters:
104
- # +$before+ (the value from the previous repeat) and +$repository+ (GitHub repo ID).
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 How many seconds to spend as a maximum
107
- # @yield [Integer, Integer] Repository ID and the next number to be considered
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
- $loog.info("We are doing this for #{start.ago} already, won't check #{repo}")
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
- $loog.info("We are iterating for #{start.ago} already, time to give up")
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
- # Injects a fact if it's absent in the factbase, otherwise (it is already
12
- # there) returns the existing one.
11
+ # Ensures exactly one fact exists with the specified attributes in the factbase.
13
12
  #
14
- # require 'fbe/just_one'
15
- # n =
16
- # Fbe.just_one do |f|
17
- # f.what = 'something'
18
- # f.details = 'important'
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
- # This code will guarantee that only one fact with +what+ equals to +something+
22
- # and +details+ equals to +important+ may exist.
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 global factbase
25
- # @yield [Factbase::Fact] The fact that was either created or found
26
- # @return [Factbase::Fact] The fact found
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 logging formatter shows verbose logs for error responses only
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
- # Log HTTP request.
41
+ # Captures HTTP request details for later use in error logging.
18
42
  #
19
- # @param [Hash] http The hash with data about HTTP request
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
- # Log HTTP response.
49
+ # Logs HTTP response details only for error responses (4xx/5xx).
25
50
  #
26
- # @param [Hash] http The hash with data about HTTP response
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')
@@ -5,11 +5,24 @@
5
5
 
6
6
  require_relative '../fbe'
7
7
 
8
- # The module.
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 responses.
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, it will be re-set (the entire fact will be destroyed, a new fact
13
- # created, and the property set with the new value).
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
- # @return [Factbase::Fact] Returns new fact or previous one
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 [Integer] p_every_days How frequently to run, every X days
13
- # @param [Integer] p_since_days Since when to collect stats, X days
14
- # @param [Factbase] fb The factbase
15
- # @param [String] judge The name of the judge, from the +judges+ tool
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?
@@ -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 [Integer] p_every_hours How frequently to run, every X hours
14
- # @param [Factbase] fb The factbase
15
- # @param [String] judge The name of the judge, from the +judges+ tool
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?