issue-db 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 779e0ce2c1a8f2e535cc64b2cc16bf40932910840e0732248526c59f61d9581f
4
- data.tar.gz: 94a2a6179637d713752cdac2d613417bac05aa9e72cff034eb33319ca769f469
3
+ metadata.gz: '0889d29fc2e002b9183fef5943de872699603c5b42deeb4f932fefd8b123a76e'
4
+ data.tar.gz: 8f6857bcc87f3ee10d684182981fc1874480036fe24e4ea695132af58e55576e
5
5
  SHA512:
6
- metadata.gz: 98d45809be1fc30ddbe0b30df98b0a191bb10d925d6844a550cc8da423bcf39582d676be24c4ff32f3bba29a03800af48d3b8990c2925869e36e90acb01c245a
7
- data.tar.gz: c1ac7316e67afb2ecebd66f63bdeefc8d730b95fac4fbd6371e4b1f1fc71f1be10abf9e2207dc05d428121b8ec512f40c98c2bb858ba87b84b91f766e22ab8dc
6
+ metadata.gz: 445ec2095bde6974d26a473d069fd4307633b2c5732780c4bdc9010b2ac838c104efee1205440ebb59465c9c8cbb54046c4d35f8fea2a280549f4cd90bad4467
7
+ data.tar.gz: '028a3c2cbd04049f6ca2d6b9c8f16ac22e9a7cc5a0b5e4e66c8b4064548284331d7c09efd0449f65f84b46b78f8400121ad7b43a913025149d8b1030e35849cf'
data/README.md CHANGED
@@ -7,4 +7,406 @@
7
7
  [![release](https://github.com/runwaylab/issue-db/actions/workflows/release.yml/badge.svg)](https://github.com/runwaylab/issue-db/actions/workflows/release.yml)
8
8
  [![coverage](./docs/assets/coverage.svg)](./docs/assets/coverage.svg)
9
9
 
10
- A Ruby Gem to use GitHub Issues as a NoSQL JSON document db
10
+ A Ruby Gem to use GitHub Issues as a NoSQL JSON document db.
11
+
12
+ ![issue-db](./docs/assets/issue-db.png)
13
+
14
+ ## Quick Start ⚡
15
+
16
+ The `issue-db` gem uses CRUD operations to read and write data to a GitHub repository using issues as the records. The title of the issue is used as the unique key for the record and the body of the issue is used to store the data in JSON format.
17
+
18
+ Here is an extremely basic example of using the `issue-db` gem:
19
+
20
+ ```ruby
21
+ require "issue_db"
22
+
23
+ # The GitHub repository to use as the database
24
+ repo = "runwaylab/grocery-orders"
25
+
26
+ # Create a new database instance
27
+ db = IssueDB.new(repo)
28
+
29
+ # Write a new record to the database
30
+ db.create("order_number_123", { location: "London", items: [ "cookies", "espresso" ] })
31
+
32
+ # Read the newly created record from the database
33
+ record = db.read("order_number_123")
34
+
35
+ puts record.data # => {location: "London", items: ["cookies", "espresso"]}
36
+ ```
37
+
38
+ > A more detailed example can be found below.
39
+
40
+ ## Installation 🚚
41
+
42
+ You may install this Gem from either [RubyGems](https://rubygems.org/gems/issue-db) or [GitHub Packages](https://github.com/runwaylab/issue-db/pkgs/rubygems/issue-db).
43
+
44
+ RubyGems:
45
+
46
+ ```ruby
47
+ source "https://rubygems.org"
48
+
49
+ gem "issue-db", "X.X.X" # Replace X.X.X with the version you want to use
50
+ ```
51
+
52
+ GitHub Packages:
53
+
54
+ ```ruby
55
+ source "https://rubygems.pkg.github.com/runwaylab" do
56
+ gem "issue-db", "X.X.X" # Replace X.X.X with the version you want to use
57
+ end
58
+ ```
59
+
60
+ Command Line Installation:
61
+
62
+ ```sh
63
+ gem install issue-db --version "X.X.X"
64
+ ```
65
+
66
+ ## Usage 💻
67
+
68
+ The following CRUD operations are available for the `issue-db` gem:
69
+
70
+ > Note: All methods return the `IssueDB::Record` of the object which was involved in the operation
71
+
72
+ ### `db.create(key, data, options = {})`
73
+
74
+ - `key` (String) - The unique key for the record. This is the title of the GitHub issue. It must be unique within the database.
75
+ - `data` (Hash) - The data to write to the record. This can be any valid JSON data type (String, Number, Boolean, Array, Object, or nil).
76
+ - `options` (Hash) - A hash of options to configure the create operation.
77
+
78
+ Example:
79
+
80
+ ```ruby
81
+ record = db.create("order_number_123", { location: "London", items: [ "cookies", "espresso" ] })
82
+
83
+ # with the `body_before` and `body_after` options to add markdown text before and after the data in the GitHub issue body
84
+ # this can be useful if you want to add additional context to the data in the issue body for humans to read
85
+ # more on this in another section of the README below
86
+ options = { body_before: "some markdown text before the data", body_after: "some markdown text after the data" }
87
+ record = db.create("order_number_123", { location: "London", items: [ "cookies", "espresso" ] }, options)
88
+ ```
89
+
90
+ Notes:
91
+
92
+ - If the key already exists in the database, the `create` method will return the existing record without modifying it.
93
+
94
+ ### `db.read(key, options = {})`
95
+
96
+ - `key` (String) - The unique key for the record. This is the title of the GitHub issue.
97
+ - `options` (Hash) - A hash of options to configure the read operation.
98
+
99
+ Example:
100
+
101
+ ```ruby
102
+ record = db.read("order_number_123")
103
+
104
+ # with the `include_closed` option to include records that have been deleted (i.e. the GitHub issue is closed)
105
+ options = { include_closed: true }
106
+ record = db.read("order_number_123", options)
107
+ ```
108
+
109
+ ### `db.update(key, data, options = {})`
110
+
111
+ - `key` (String) - The unique key for the record. This is the title of the GitHub issue.
112
+ - `data` (Hash) - The data to write to the record. This can be any valid JSON data type (String, Number, Boolean, Array, Object, or nil).
113
+ - `options` (Hash) - A hash of options to configure the update operation.
114
+
115
+ Example:
116
+
117
+ ```ruby
118
+ record = db.update("order_number_123", { location: "London", items: [ "cookies", "espresso", "chips" ] })
119
+
120
+ # with the `body_before` and `body_after` options to add markdown text before and after the data in the GitHub issue body
121
+ # this can be useful if you want to add additional context to the data in the issue body for humans to read
122
+ # more on this in another section of the README below
123
+ options = { body_before: "# Order 123\n\nData:", body_after: "Please do not edit the body of this issue" }
124
+ record = db.update("order_number_123", { location: "London", items: [ "cookies", "espresso", "chips" ] }, options)
125
+ ```
126
+
127
+ ### `db.delete(key, options = {})`
128
+
129
+ - `key` (String) - The unique key for the record. This is the title of the GitHub issue.
130
+ - `options` (Hash) - A hash of options to configure the delete operation.
131
+
132
+ Example:
133
+
134
+ ```ruby
135
+ record = db.delete("order_number_123")
136
+ ```
137
+
138
+ ### `db.list_keys(options = {})`
139
+
140
+ - `options` (Hash) - A hash of options to configure the list operation.
141
+
142
+ Example:
143
+
144
+ ```ruby
145
+ keys = db.list_keys
146
+
147
+ # with the `include_closed` option to include records that have been deleted (i.e. the GitHub issue is closed)
148
+ options = { include_closed: true }
149
+ keys = db.list_keys(options)
150
+ ```
151
+
152
+ ### `db.list(options = {})`
153
+
154
+ - `options` (Hash) - A hash of options to configure the list operation.
155
+
156
+ Example:
157
+
158
+ ```ruby
159
+ records = db.list
160
+
161
+ # with the `include_closed` option to include records that have been deleted (i.e. the GitHub issue is closed)
162
+ options = { include_closed: true }
163
+ records = db.list(options)
164
+ ```
165
+
166
+ ### `db.refresh!`
167
+
168
+ Force a refresh of the database cache. This will make a request to the GitHub API to get the latest data from the GitHub issues in the repository.
169
+
170
+ This can be useful if you have made changes to the database outside of the gem and don't want to wait for the cache to refresh. By default, the cache refreshes every 60 seconds. Modified records (such as an `update` operation) will be refreshed automatically into the cache so that subsequent reads will return the updated data. The only time you really need to worry about refreshing the cache is if you have made changes to the database outside of the gem or if there is another service using this gem that is also making changes to the database.
171
+
172
+ Example:
173
+
174
+ ```ruby
175
+ db.refresh!
176
+ ```
177
+
178
+ ## Options 🛠
179
+
180
+ This section will go into detail around how you can configure the `issue-db` gem to behave:
181
+
182
+ ### Environment Variables 🌍
183
+
184
+ | Name | Description | Default Value |
185
+ |------|-------------|---------------|
186
+ | `LOG_LEVEL` | The log level to use for the `issue-db` gem. Can be one of `DEBUG`, `INFO`, `WARN`, `ERROR`, or `FATAL` | `INFO` |
187
+ | `ISSUE_DB_LABEL` | The label to use for the issues that are used as records in the database. This value is required and it is what this gem uses to scan a repo for the records it is aware of. | `issue-db` |
188
+ | `ISSUE_DB_CACHE_EXPIRY` | The number of seconds to cache the database in memory. The database is cached in memory to avoid making a request to the GitHub API for every operation. The default value is 60 seconds. | `60` |
189
+ | `ISSUE_DB_SLEEP` | The number of seconds to sleep between requests to the GitHub API in the event of an error | `3` |
190
+ | `ISSUE_DB_RETRIES` | The number of retries to make when there is an error making a request to the GitHub API | `10` |
191
+ | `ISSUE_DB_GITHUB_TOKEN` | The GitHub personal access token to use for authenticating with the GitHub API. You can also use a GitHub app or pass in your own authenticated Octokit.rb instance | `nil` |
192
+
193
+ ## Authentication 🔒
194
+
195
+ The `issue-db` gem uses the [`Octokit.rb`](https://github.com/octokit/octokit.rb) library under the hood for interactions with the GitHub API. You have three options for authentication when using the `issue-db` gem:
196
+
197
+ > Note: The order displayed below is also the order of priority that this Gem uses to authenticate.
198
+
199
+ 1. Pass in your own authenticated `Octokit.rb` instance to the `IssueDB.new` method
200
+ 2. Use a GitHub App by setting the `ISSUE_DB_GITHUB_APP_ID`, `ISSUE_DB_GITHUB_APP_INSTALLATION_ID`, and `ISSUE_DB_GITHUB_APP_KEY` environment variables
201
+ 3. Use a GitHub personal access token by setting the `ISSUE_DB_GITHUB_TOKEN` environment variable
202
+
203
+ > Using a GitHub App is the suggested method
204
+
205
+ Here are examples of each of these options:
206
+
207
+ ### Using a GitHub Personal Access Token
208
+
209
+ ```ruby
210
+ # Assuming you have a GitHub personal access token set as the ISSUE_DB_GITHUB_TOKEN env var
211
+ require "issue_db"
212
+
213
+ db = IssueDB.new("<org>/<repo>") # THAT'S IT! 🎉
214
+ ```
215
+
216
+ ### Using a GitHub App
217
+
218
+ This is the single best way to use the `issue-db` gem because GitHub Apps have increased rate limits, fine-grained permissions, and are more secure than using a personal access token. All you have to do is provide three environment variables and the `issue-db` gem will take care of the rest:
219
+
220
+ - `ISSUE_DB_GITHUB_APP_ID`
221
+ - `ISSUE_DB_GITHUB_APP_INSTALLATION_ID`
222
+ - `ISSUE_DB_GITHUB_APP_KEY`
223
+
224
+ Here is an example of how you can use a GitHub app with the `issue-db` gem:
225
+
226
+ ```ruby
227
+ # Assuming you have the following three environment variables set:
228
+
229
+ # 1: ISSUE_DB_GITHUB_APP_ID
230
+ # app ids are found on the App's settings page
231
+
232
+ # 2: ISSUE_DB_GITHUB_APP_INSTALLATION_ID
233
+ # installation ids look like this:
234
+ # https://github.com/organizations/<org>/settings/installations/<8_digit_id>
235
+
236
+ # 3. ISSUE_DB_GITHUB_APP_KEY
237
+ # app keys are found on the App's settings page and can be downloaded
238
+ # format: "-----BEGIN...key\n...END-----\n" (this will be one super long string and that's okay)
239
+ # make sure this key in your env is a single line string with newlines as "\n"
240
+
241
+ # With all three of these environment variables set, you can proceed with ease!
242
+ db = IssueDB.new("<org>/<repo>") # THAT'S IT! 🎉
243
+ ```
244
+
245
+ ### Using Your Own Authenticated `Octokit.rb` Instance
246
+
247
+ ```ruby
248
+ require "octokit"
249
+
250
+ # Create your own authenticated Octokit.rb instance
251
+ # You should probably set the page_size to 100 and auto_paginate to true
252
+ octokit = Octokit::Client.new(access_token: "<TOKEN>", page_size: 100)
253
+ octokit.auto_paginate = true
254
+
255
+ db = IssueDB.new("<org>/<repo>", octokit_client: octokit)
256
+ ```
257
+
258
+ ## Advanced Example 🚀
259
+
260
+ Here is a more advanced example of using the `issue-db` gem that demonstrates many different features of the gem:
261
+
262
+ ```ruby
263
+ # Assuming you have a GitHub personal access token set as the ISSUE_DB_GITHUB_TOKEN env var
264
+ require "issue_db"
265
+
266
+ # The GitHub repository to use as the database
267
+ repo = "runwaylab/grocery-orders"
268
+
269
+ # Create a new database instance
270
+ db = IssueDB.new(repo)
271
+
272
+ # Write a new record to the database where the title of the issue is the unique key
273
+ new_issue = db.create("order_number_123", { location: "London", items: [ "cookies", "espresso" ] })
274
+
275
+ # View the record data and the source data which contains the GitHub issue object
276
+ puts new_issue.data # => {location: "London", items: ["cookies", "espresso"]}
277
+ puts new_issue.source_data.state # => "open" (the GitHub issue is open so the record is active)
278
+ puts new_issue.source_data.html_url # => "https://github.com/runwaylab/grocery-orders/issues/<number>" (the URL of the GitHub issue which is the DB record)
279
+
280
+ # Update the record
281
+ updated_issue = db.update("order_number_123", { location: "London", items: [ "cookies", "espresso", "chips" ] })
282
+
283
+ # View the updated record data
284
+ puts updated_issue.data # => {location: "London", items: ["cookies", "espresso", "chips"]}
285
+
286
+ # Get the record by key
287
+ record = db.read("order_number_123")
288
+
289
+ # View the record data
290
+ puts record.data # => {location: "London", items: ["cookies", "espresso", "chips"]}
291
+
292
+ # Delete the record
293
+ deleted_record = db.delete("order_number_123")
294
+ puts deleted_record.source_data.state # => "closed" (the GitHub issue is closed as "completed" so the record is inactive)
295
+
296
+ # List all keys in the database including closed records
297
+ keys = db.list_keys({ include_closed: true })
298
+
299
+ puts keys # => ["order_number_123"]
300
+
301
+ # List all records in the database including closed records
302
+ records = db.list({ include_closed: true })
303
+
304
+ # Inspection of the first record in the list
305
+ puts records.first.data # => {location: "London", items: ["cookies", "espresso", "chips"]}
306
+ puts records.first.source_data.state # => "closed" (the GitHub issue is closed as "completed" so the record is inactive)
307
+
308
+ # Force a refresh of the database cache (useful if you have made changes to the database outside of the gem and don't want to wait for the cache to refresh)
309
+ db.refresh!
310
+ ```
311
+
312
+ ## Embedding Markdown in the Issue Body 📝
313
+
314
+ With this library, you can write markdown text into the issue body **in addition** to the JSON data. Pretty cool right?
315
+
316
+ This can be especially useful if you want to add additional context to the data in the issue body for humans to read. For example, you want to open an issue to track the status of an employee who has a laptop that is running an out of date operating system. You might want to do a `db.write()` operation with machine readable data but _also_ include a note for the employee to read. Here is an example of how you can do that:
317
+
318
+ ```ruby
319
+ body_before_text = <<~BODY
320
+ # Out of Date Operating System 🚨
321
+
322
+ <!--- HUMANS: DO NOT EDIT THIS ISSUE BODY ;) -->
323
+
324
+ It looks like your laptop is running an out of date operating system. This is a security risk.
325
+
326
+ Please update your OS as soon as possible.
327
+
328
+ ## Details
329
+ BODY
330
+
331
+ body_after_text = <<~BODY
332
+ > Thank you for your attention to this matter.
333
+ BODY
334
+
335
+ options = { body_before: body_before_text, body_after: body_after_text }
336
+ data = { operating_system: "macOS 1.2.3", last_updated_at: "2024-09-30", user: "Celeste", location: "California", out_of_date: true, employee_id: 123 }
337
+ record = db.create("Out of Date OS - EmployeeID: 123", data, options) # this assumes that employee IDs are unique in this example
338
+ ```
339
+
340
+ Running that code snippet will result in a database record (GitHub issue) being created that has the following markdown body:
341
+
342
+ ````markdown
343
+ # Out of Date Operating System 🚨
344
+
345
+ <!--- HUMANS: DO NOT EDIT THIS ISSUE BODY ;) -->
346
+
347
+ It looks like your laptop is running an out of date operating system. This is a security risk.
348
+
349
+ Please update your OS as soon as possible.
350
+
351
+ ## Details
352
+
353
+ <!--- issue-db-start -->
354
+ ```json
355
+ {
356
+ "operating_system": "macOS 1.2.3",
357
+ "last_updated_at": "2024-09-30",
358
+ "user": "Celeste",
359
+ "location": "California",
360
+ "out_of_date": true
361
+ }
362
+ ```
363
+ <!--- issue-db-end -->
364
+
365
+ > Thank you for your attention to this matter.
366
+ ````
367
+
368
+ Here is a screenshot of exactly how this issue would render in GitHub:
369
+
370
+ ![example1](./docs/assets/example1.png)
371
+
372
+ And here is a link to the actual issue in GitHub: [issue link](https://github.com/runwaylab/issue-db/issues/19)
373
+
374
+ Now the best part about this, is that we can still use the `db.read()` method flawlessly and get the data back in a machine readable format, and we can even get back the markdown text as well! Here is an example of how you can do that:
375
+
376
+ ```ruby
377
+ record = db.read("Out of Date OS - EmployeeID: 123")
378
+
379
+ puts record.data # => {"operating_system"=>"macOS 1.2.3", "last_updated_at"=>"2024-09-30", "user"=>"Celeste", "location"=>"California", "out_of_date"=>true, "employee_id"=>123}
380
+ puts record.body_before # the markdown text before the data in the issue body (as seen above, not going to repeat it here - its long)
381
+ puts record.body_after # ditto ^
382
+ # puts record.source_data.body # useful for getting the raw markdown text of the issue body as is
383
+ ```
384
+
385
+ Here is a screenshot of the output of the script above:
386
+
387
+ ![example2](./docs/assets/example2.png)
388
+
389
+ ## Record Attributes 📦
390
+
391
+ Database "items" are called Records in this library. Records are represented by a `IssueDB::Record` object. Records are actually just GitHub issues under the hood!
392
+
393
+ Records have the following attributes:
394
+
395
+ - `key` (String) - The unique key for the record. This is the title of the GitHub issue.
396
+ - `data` (Hash) - The data for the record. This can be any valid JSON data type (String, Number, Boolean, Array, Object, or nil).
397
+ - `source_data` (Octokit::Issue) - The GitHub issue object that is the source of the record. This value is returned by Octokit and is a Sawyer::Resource object.
398
+ - `body_before` (String) - The markdown text before the data in the issue body.
399
+ - `body_after` (String) - The markdown text after the data in the issue body.
400
+
401
+ Example:
402
+
403
+ ```ruby
404
+ record = db.read("order_number_123")
405
+
406
+ puts record.key # => "order_number_123"
407
+ puts record.data # => { "location"=>"London", "items"=>[ "cookies", "espresso", "chips" ] }
408
+ puts record.data["location"] # => "London"
409
+ puts record.source_data.state # => "open" (the GitHub issue is open so the record is active)
410
+ puts record.body_before # => "some markdown text before the data"
411
+ puts record.body_after # => "some markdown text after the data"
412
+ ```
data/issue-db.gemspec CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency "retryable", "~> 3.0", ">= 3.0.5"
25
25
  spec.add_dependency "octokit", "~> 9.2"
26
26
  spec.add_dependency "faraday-retry", "~> 2.2", ">= 2.2.1"
27
+ spec.add_dependency "jwt", "~> 2.9", ">= 2.9.3"
27
28
 
28
29
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
29
30
 
@@ -1,20 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "octokit"
4
+ require_relative "utils/github_app"
4
5
 
5
6
  class AuthenticationError < StandardError; end
6
7
 
7
8
  module Authentication
8
- def self.login(client = nil)
9
+ def self.login(client = nil, log = nil)
9
10
  # if the client is not nil, use the pre-provided client
10
- return client unless client.nil?
11
+ unless client.nil?
12
+ log.debug("using pre-provided client") if log
13
+ return client
14
+ end
11
15
 
12
- # if the client is nil, check for GitHub App env vars
13
- # TODO
16
+ # if the client is nil, check for GitHub App env vars first
17
+ # first, check if all three of the following env vars are set and have values
18
+ # ISSUE_DB_GITHUB_APP_ID, ISSUE_DB_GITHUB_APP_INSTALLATION_ID, ISSUE_DB_GITHUB_APP_KEY
19
+ if ENV.fetch("ISSUE_DB_GITHUB_APP_ID", nil) && ENV.fetch("ISSUE_DB_GITHUB_APP_INSTALLATION_ID", nil) && ENV.fetch("ISSUE_DB_GITHUB_APP_KEY", nil)
20
+ log.debug("using github app authentication") if log
21
+ return GitHubApp.new
22
+ end
14
23
 
15
- # if the client is nil and no GitHub App env vars were found, check for the GITHUB_TOKEN
16
- token = ENV.fetch("GITHUB_TOKEN", nil)
24
+ # if the client is nil and no GitHub App env vars were found, check for the ISSUE_DB_GITHUB_TOKEN
25
+ token = ENV.fetch("ISSUE_DB_GITHUB_TOKEN", nil)
17
26
  if token
27
+ log.debug("using github token authentication") if log
18
28
  octokit = Octokit::Client.new(access_token: token, page_size: 100)
19
29
  octokit.auto_paginate = true
20
30
  return octokit
@@ -7,8 +7,9 @@ class IssueParseError < StandardError; end
7
7
  class Record
8
8
  include Parse
9
9
 
10
- attr_reader :body_before, :data, :body_after, :source_data
10
+ attr_reader :body_before, :data, :body_after, :source_data, :key
11
11
  def initialize(data)
12
+ @key = data.title
12
13
  @source_data = data
13
14
  parse!
14
15
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class provides a wrapper around the Octokit client for GitHub App authentication.
4
+ # It handles token generation and refreshing, and delegates method calls to the Octokit client.
5
+ # Helpful: https://github.com/octokit/handbook?tab=readme-ov-file#github-app-authentication-json-web-token
6
+
7
+ # Why? In some cases, you may not want to have a static long lived token like a GitHub PAT when authenticating...
8
+ # with octokit.rb.
9
+ # Most importantly, this class will handle automatic token refreshing for you out-of-the-box. Simply provide the...
10
+ # correct environment variables, call `GitHubApp.new`, and then use the returned object as you would an Octokit client.
11
+
12
+ require "octokit"
13
+ require "jwt"
14
+
15
+ class GitHubApp
16
+ TOKEN_EXPIRATION_TIME = 2700 # 45 minutes
17
+ JWT_EXPIRATION_TIME = 600 # 10 minutes
18
+
19
+ def initialize
20
+ # app ids are found on the App's settings page
21
+ @app_id = fetch_env_var("ISSUE_DB_GITHUB_APP_ID").to_i
22
+
23
+ # installation ids look like this:
24
+ # https://github.com/organizations/<org>/settings/installations/<8_digit_id>
25
+ @installation_id = fetch_env_var("ISSUE_DB_GITHUB_APP_INSTALLATION_ID").to_i
26
+
27
+ # app keys are found on the App's settings page and can be downloaded
28
+ # format: "-----BEGIN...key\n...END-----\n"
29
+ # make sure this key in your env is a single line string with newlines as "\n"
30
+ @app_key = fetch_env_var("ISSUE_DB_GITHUB_APP_KEY").gsub(/\\+n/, "\n")
31
+
32
+ @client = nil
33
+ @token_refresh_time = nil
34
+ end
35
+
36
+ private
37
+
38
+ # Fetches the value of an environment variable and raises an error if it is not set.
39
+ # @param key [String] The name of the environment variable.
40
+ # @return [String] The value of the environment variable.
41
+ def fetch_env_var(key)
42
+ ENV.fetch(key) { raise "environment variable #{key} is not set" }
43
+ end
44
+
45
+ # Caches the octokit client if it is not nil and the token has not expired
46
+ # If it is nil or the token has expired, it creates a new client
47
+ # @return [Octokit::Client] The octokit client
48
+ def client
49
+ if @client.nil? || token_expired?
50
+ @client = create_client
51
+ end
52
+
53
+ @client
54
+ end
55
+
56
+ # A helper method for generating a JWT token for the GitHub App
57
+ # @return [String] The JWT token
58
+ def jwt_token
59
+ private_key = OpenSSL::PKey::RSA.new(@app_key)
60
+
61
+ payload = {}.tap do |opts|
62
+ opts[:iat] = Time.now.to_i - 60 # issued at time, 60 seconds in the past to allow for clock drift
63
+ opts[:exp] = opts[:iat] + JWT_EXPIRATION_TIME # JWT expiration time (10 minute maximum)
64
+ opts[:iss] = @app_id # GitHub App ID
65
+ end
66
+
67
+ JWT.encode(payload, private_key, "RS256")
68
+ end
69
+
70
+ # Creates a new octokit client and fetches a new installation access token
71
+ # @return [Octokit::Client] The octokit client
72
+ def create_client
73
+ client = ::Octokit::Client.new(bearer_token: jwt_token)
74
+ access_token = client.create_app_installation_access_token(@installation_id)[:token]
75
+ client = ::Octokit::Client.new(access_token:)
76
+ client.auto_paginate = true
77
+ client.per_page = 100
78
+ @token_refresh_time = Time.now
79
+ client
80
+ end
81
+
82
+ # GitHub App installation access tokens expire after 1h
83
+ # This method checks if the token has expired and returns true if it has
84
+ # It is very cautious and expires tokens at 45 minutes to account for clock drift
85
+ # @return [Boolean] True if the token has expired, false otherwise
86
+ def token_expired?
87
+ @token_refresh_time.nil? || (Time.now - @token_refresh_time) > TOKEN_EXPIRATION_TIME
88
+ end
89
+
90
+ # This method is called when a method is called on the GitHub class that does not exist.
91
+ # It delegates the method call to the Octokit client.
92
+ # @param method [Symbol] The name of the method being called.
93
+ # @param args [Array] The arguments passed to the method.
94
+ # @param block [Proc] An optional block passed to the method.
95
+ # @return [Object] The result of the method call on the Octokit client.
96
+ def method_missing(method, *args, &block)
97
+ client.send(method, *args, &block)
98
+ end
99
+
100
+ # This method is called to check if the GitHub class responds to a method.
101
+ # It checks if the Octokit client responds to the method.
102
+ # @param method [Symbol] The name of the method being checked.
103
+ # @param include_private [Boolean] Whether to include private methods in the check.
104
+ # @return [Boolean] True if the Octokit client responds to the method, false otherwise.
105
+ def respond_to_missing?(method, include_private = false)
106
+ client.respond_to?(method, include_private) || super
107
+ end
108
+ end
data/lib/issue_db.rb CHANGED
@@ -29,7 +29,7 @@ class IssueDB
29
29
  @log = log || RedactingLogger.new($stdout, level: ENV.fetch("LOG_LEVEL", "INFO").upcase)
30
30
  Retry.setup!(log: @log)
31
31
  @version = VERSION
32
- @client = Authentication.login(octokit_client)
32
+ @client = Authentication.login(octokit_client, @log)
33
33
  @repo = Repository.new(repo)
34
34
  @label = label || ENV.fetch("ISSUE_DB_LABEL", "issue-db")
35
35
  @cache_expiry = cache_expiry || ENV.fetch("ISSUE_DB_CACHE_EXPIRY", 60).to_i
@@ -41,7 +41,7 @@ class IssueDB
41
41
  end
42
42
 
43
43
  def read(key, options = {})
44
- db.read(key)
44
+ db.read(key, options)
45
45
  end
46
46
 
47
47
  def update(key, data, options = {})
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Version
4
- VERSION = "0.0.2"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: issue-db
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - runwaylab
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-11-28 00:00:00.000000000 Z
12
+ date: 2024-11-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redacting-logger
@@ -79,6 +79,26 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: 2.2.1
82
+ - !ruby/object:Gem::Dependency
83
+ name: jwt
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.9'
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.9.3
92
+ type: :runtime
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '2.9'
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 2.9.3
82
102
  description: 'A Ruby Gem to use GitHub Issues as a NoSQL JSON document db
83
103
 
84
104
  '
@@ -97,6 +117,7 @@ files:
97
117
  - lib/issue_db/models/record.rb
98
118
  - lib/issue_db/models/repository.rb
99
119
  - lib/issue_db/utils/generate.rb
120
+ - lib/issue_db/utils/github_app.rb
100
121
  - lib/issue_db/utils/init.rb
101
122
  - lib/issue_db/utils/parse.rb
102
123
  - lib/issue_db/utils/retry.rb