octokitted 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f1a1eec4292305ffb36ee14107f171a44eb4c4ec36b681ac1cbb43239fee2ca
4
- data.tar.gz: f11e7f4e512223b975247f83720d6d9a465f5126e70e8928007c8be2e01fb350
3
+ metadata.gz: 930bcddc8ee154391c8deb09e7a2c0e483e49d6cb3091adeb70239d0388d3e5d
4
+ data.tar.gz: b64e1ba7b33e4c2eeb73975825bf5fa7cd2eaf37f63383b01427e4adddbd7e4a
5
5
  SHA512:
6
- metadata.gz: 520b1c0f16cb7ff369e287eeebeb8c27efbca078ca31fc16825682dd4bc4cd30594532c63eaf76787af5245092fa5a092a7c71f1e0faa07ea1efc48503ec973a
7
- data.tar.gz: f2b6bf07d1a3463b960daaa02aded2db981bb0640affb5b87afc78a3c7726040f907e915bb3e3c2796c404b35ec9c3d08c666e6347c8ba54dde612eaa7ec3014
6
+ metadata.gz: c2738d3e5a2bb9bb665a8ed84f76ecbe4594b753e46b27a49d504587fbceae86f4a5905bd704368373bf4868551fe5a014716555e274f22fcad95b8d29eb107c
7
+ data.tar.gz: 486cf3f74dc41658968876d79d11383c1d8bfe7577f822c4fe17660555e96f4fa5480f180acd86bd140cbd8af7c972ad4068bb74436a8d2127238ba56141afb9
data/README.md CHANGED
@@ -3,3 +3,11 @@
3
3
  [![test](https://github.com/GrantBirki/octokitted/actions/workflows/test.yml/badge.svg)](https://github.com/GrantBirki/octokitted/actions/workflows/test.yml) [![lint](https://github.com/GrantBirki/octokitted/actions/workflows/lint.yml/badge.svg)](https://github.com/GrantBirki/octokitted/actions/workflows/lint.yml) [![CodeQL](https://github.com/GrantBirki/octokitted/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/GrantBirki/octokitted/actions/workflows/codeql-analysis.yml)
4
4
 
5
5
  A self-hydrating version of Octokit for usage in CI systems - like GitHub Actions!
6
+
7
+ > **kit** or **kitted** (_verb_)
8
+ >
9
+ > Defintion: provide someone or something with the appropriate clothing or equipment.
10
+ >
11
+ > "we were all kitted out in life jackets", "our octokit client was kitted out for CI usage"
12
+
13
+ ![octokitted](./docs/assets/dalle.png)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "contracts"
4
+
5
+ class Issue
6
+ include Contracts::Core
7
+ include Contracts::Builtin
8
+
9
+ # A helper class for common operations on GitHub Issues
10
+ def initialize(octokitted)
11
+ @octokit = octokitted.octokit
12
+ @log = octokitted.log
13
+ @octokitted = octokitted
14
+ end
15
+
16
+ # Adds a set of labels to an issue or pull request
17
+ # :param labels: The labels to add to the issue (Array of strings)
18
+ # :param issue_number: The issue number to add labels to
19
+ Contract KeywordArgs[labels: ArrayOf[String], issue_number: Maybe[Numeric]] => Any
20
+ def add_labels(labels:, issue_number: nil)
21
+ issue_number = construct_issue_numer(issue_number)
22
+ @log.debug("adding labels: #{labels} to issue: #{issue_number}")
23
+
24
+ @octokit.add_labels_to_an_issue(@octokitted.org_and_repo, issue_number, labels)
25
+ end
26
+
27
+ # Removes a set of labels from an issue or pull request
28
+ # If the label does not exist, the exception is caught and logged
29
+ # :param labels: The labels to remove from the issue (Array of strings)
30
+ # :param issue_number: The issue number to remove labels from
31
+ Contract KeywordArgs[labels: ArrayOf[String], issue_number: Maybe[Numeric]] => Any
32
+ def remove_labels(labels:, issue_number: nil)
33
+ issue_number = construct_issue_numer(issue_number)
34
+ @log.debug("removing labels: #{labels} from issue: #{issue_number}")
35
+
36
+ labels.each do |label|
37
+ @octokit.remove_label(@octokitted.org_and_repo, issue_number, label)
38
+ rescue Octokit::NotFound
39
+ @log.warn("label: #{label} not found on issue: #{issue_number}")
40
+ end
41
+ end
42
+
43
+ # Adds a comment to an issue or pull request
44
+ # :param comment: The comment to add to the issue (String)
45
+ # :param issue_number: The issue number to add the comment to
46
+ Contract KeywordArgs[comment: String, issue_number: Maybe[Numeric]] => Any
47
+ def add_comment(comment:, issue_number: nil)
48
+ issue_number = construct_issue_numer(issue_number)
49
+ @log.debug("adding comment: #{comment} to issue: #{issue_number}")
50
+
51
+ @octokit.add_comment(@octokitted.org_and_repo, issue_number, comment)
52
+ end
53
+
54
+ # Closes an issue
55
+ # :param issue_number: The issue number to close
56
+ Contract KeywordArgs[issue_number: Maybe[Numeric], options: Maybe[Hash]] => Any
57
+ def close(issue_number: nil, options: {})
58
+ issue_number = construct_issue_numer(issue_number)
59
+ @log.debug("closing issue: #{issue_number}")
60
+
61
+ @octokit.close_issue(@octokitted.org_and_repo, issue_number, options)
62
+ end
63
+
64
+ private
65
+
66
+ # Helper method to construct the issue number from the auto-hydrated issue_number if it exists
67
+ # :param issue_number: The issue number to use if not nil
68
+ # :return: The issue number to use
69
+ # Note: If the issue_number is nil, we trye use the auto-hydrated issue_number...
70
+ # ... if the issue_number is not nil, we use that
71
+ Contract Maybe[Numeric] => Numeric
72
+ def construct_issue_numer(issue_number)
73
+ return @octokitted.issue_number if issue_number.nil?
74
+
75
+ return issue_number
76
+ end
77
+ end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "git"
4
+ require "contracts"
4
5
 
5
6
  class GitPlugin
6
7
  attr_reader :login
7
8
 
9
+ include Contracts::Core
10
+ include Contracts::Builtin
11
+
8
12
  # Initialize the class
13
+ Contract KeywordArgs[logger: Any, login: Maybe[String], token: Maybe[String]] => Any
9
14
  def initialize(logger:, login:, token:)
10
15
  @log = logger
11
16
  @login = login
@@ -14,25 +19,32 @@ class GitPlugin
14
19
 
15
20
  # Removes / cleans up all repos that this class has cloned
16
21
  # :param cloned_repos: An array of paths to cloned repos to remove
22
+ # :return: true to indicate success
23
+ Contract ArrayOf[String] => true
17
24
  def remove_all_clones!(cloned_repos)
18
25
  @log.debug("removing all cloned repos")
19
26
  cloned_repos.each do |path|
20
27
  @log.debug("removing cloned repo: #{path}")
21
28
  FileUtils.rm_r(path)
22
29
  end
30
+ true
23
31
  end
24
32
 
25
33
  # Removes a single cloned repo
26
34
  # :param path: The path to the cloned repo to remove (String)
35
+ # :return: true to indicate success
36
+ Contract String => true
27
37
  def remove_clone!(path)
28
38
  @log.debug("removing cloned repo: #{path}")
29
39
  FileUtils.rm_r(path)
40
+ return true
30
41
  end
31
42
 
32
43
  # Clone a repository
33
44
  # :param path: The relative path to clone the repo to - (default: ".")
34
45
  # :param options: The options to pass to the Git.clone method (default: {} - https://rubydoc.info/gems/git/Git#clone-class_method)
35
46
  # :return: Hash of the Git Object, and the path to the cloned repo
47
+ Contract KeywordArgs[org: String, repo: String, path: Maybe[String], options: Maybe[Hash]] => Hash
36
48
  def clone(org:, repo:, path: ".", options: {})
37
49
  @log.debug("cloning #{org}/#{repo}")
38
50
  git_object = Git.clone("https://#{@token}@github.com/#{org}/#{repo}.git", repo, path:, log: @log, **options)
data/lib/octokitted.rb CHANGED
@@ -2,33 +2,59 @@
2
2
 
3
3
  require "octokit"
4
4
  require "logger"
5
+ require "contracts"
5
6
 
6
7
  require_relative "octokitted/git_plugin"
8
+ require_relative "octokitted/common/issue"
7
9
 
8
10
  class Octokitted
9
11
  # A Octokitted class to interact with the GitHub API
10
- attr_reader :login, :org, :repo, :org_and_repo, :octokit, :cloned_repos
12
+ attr_reader :login,
13
+ :org,
14
+ :repo,
15
+ :org_and_repo,
16
+ :octokit,
17
+ :cloned_repos,
18
+ :log,
19
+ :github_event,
20
+ :sha,
21
+ :issue_number,
22
+ :issue
23
+
24
+ include Contracts::Core
25
+ include Contracts::Builtin
11
26
 
12
27
  # Initialize the class
28
+ # :param event_path: The path to the GitHub event data (defaults to the GITHUB_EVENT_PATH env var)
13
29
  # :param login: The login to use for GitHubAPI interactions (defaults to the owner of the token)
14
30
  # :param org: The org to use with the Octokitted class
15
31
  # :param repo: The repo to interact with with the Octokitted class
32
+ # :param issue_number: The issue/pull_request number to interact with with the Octokitted class
16
33
  # :param token: The token to use to authenticate with the GitHub API
17
34
  # :param logger: The logger to use for logging
18
- def initialize(login: nil, org: nil, repo: nil, token: nil, logger: nil)
35
+ #
36
+ # Note: If you do not provide an org, repo, token, or issue_number, Octokitted will attempt to self-hydrate...
37
+ # ... these values from the environment and the GitHub event data when you call `.new` on the class
38
+ def initialize(event_path: nil, login: nil, org: nil, repo: nil, issue_number: nil, token: nil, logger: nil)
39
+ @log = logger || setup_logger
19
40
  @cloned_repos = []
41
+ @event_path = event_path || ENV.fetch("GITHUB_EVENT_PATH", nil)
42
+ @sha = ENV.fetch("GITHUB_SHA", nil)
20
43
  org_and_repo_hash = fetch_org_and_repo
21
44
  @login = login
22
45
  @org = org || org_and_repo_hash[:org]
23
46
  @repo = repo || org_and_repo_hash[:repo]
24
47
  @token = token || fetch_token
25
48
  @octokit = setup_octokit_client
26
- @log = logger || setup_logger
27
49
  @org_and_repo = org_and_repo_hash[:org_and_repo]
50
+ @github_event = fetch_github_event(@event_path)
51
+ @issue_number = issue_number || fetch_issue_number(@github_event)
28
52
  @login = @octokit.login if @login.nil? # reset the login to the owner of the token if not provided
29
53
 
30
54
  # setup the git plugin
31
55
  @git = GitPlugin.new(logger: @log, login: @login, token: @token)
56
+ # setup the common Issue plugin
57
+ @issue = Issue.new(self)
32
58
 
33
59
  @log.debug("Octokitted initialized")
34
60
  @log.debug("login: #{@octokit.login}")
@@ -38,8 +64,9 @@ class Octokitted
38
64
 
39
65
  # Setter method for the repo instance variable
40
66
  # :param repo: The repo to set
41
- # :return: the new org/repo
67
+ # :return: it does not return as it is a setter method
42
68
  # Example: gh.repo = "test"
69
+ Contract String => Any
43
70
  def repo=(repo)
44
71
  @repo = repo
45
72
  @org_and_repo = "#{@org}/#{@repo}"
@@ -48,8 +75,9 @@ class Octokitted
48
75
 
49
76
  # Setter method for the org instance variable
50
77
  # :param org: The org to set
51
- # :return: the new org/repo
78
+ # :return: it does not return as it is a setter method
52
79
  # Example: gh.org = "test"
80
+ Contract String => Any
53
81
  def org=(org)
54
82
  @org = org
55
83
  @org_and_repo = "#{@org}/#{@repo}"
@@ -60,12 +88,17 @@ class Octokitted
60
88
  # :param path: The relative path to clone the repo to - (default: ".")
61
89
  # :param options: The options to pass (default: {} - https://rubydoc.info/gems/git/Git#clone-class_method)
62
90
  # :return: The Git object to operate with
91
+ Contract Maybe[String], Maybe[Hash] => Git::Base
63
92
  def clone(path: ".", options: {})
64
93
  result = @git.clone(org: @org, repo: @repo, path:, options:)
65
94
  @cloned_repos << result[:path]
66
95
  return result[:git_object]
67
96
  end
68
97
 
98
+ # Remove a cloned repository
99
+ # :param path: The relative path to the cloned repo to remove (String)
100
+ # :return: true to indicate success
101
+ Contract String => true
69
102
  def remove_clone!(path)
70
103
  valid = false
71
104
 
@@ -82,11 +115,16 @@ class Octokitted
82
115
 
83
116
  @git.remove_clone!(path)
84
117
  @cloned_repos.delete(path)
118
+ true
85
119
  end
86
120
 
121
+ # Remove all cloned repositories that have been cloned with this instance of Octokitted
122
+ # :return: true to indicate success
123
+ Contract None => true
87
124
  def remove_all_clones!
88
125
  @git.remove_all_clones!(@cloned_repos)
89
126
  @cloned_repos = []
127
+ true
90
128
  end
91
129
 
92
130
  private
@@ -99,6 +137,7 @@ class Octokitted
99
137
 
100
138
  # Fetch the org and repo from the environment
101
139
  # :return: A hash containing the org and repo, and the org and repo separately
140
+ Contract None => HashOf[Symbol, String]
102
141
  def fetch_org_and_repo
103
142
  org_and_repo = ENV.fetch("GITHUB_REPOSITORY", nil)
104
143
  org = nil
@@ -110,8 +149,41 @@ class Octokitted
110
149
  return { org_and_repo:, org:, repo: }
111
150
  end
112
151
 
152
+ # A helper method that attempts to self-hydrate context from the GitHub event data
153
+ # In Actions, the GITHUB_EVENT_PATH env var is set to a file containing the GitHub json event data
154
+ # If it exists, we try to load it into this class
155
+ # :param event_path: The path to the GitHub event data (defaults to the GITHUB_EVENT_PATH env var)
156
+ # :return: A Hash of the GitHub event data or nil if not found
157
+ Contract Maybe[String] => Maybe[Hash]
158
+ def fetch_github_event(event_path)
159
+ unless event_path
160
+ @log.warn("GITHUB_EVENT_PATH env var not found")
161
+ return nil
162
+ end
163
+
164
+ @log.info("GitHub Event data auto-hydrated")
165
+ return JSON.parse(File.read(event_path), symbolize_names: true)
166
+ end
167
+
168
+ # A helper method that attempts to self-hydrate the issue_number from the GitHub event data
169
+ # :param github_event: The GitHub event data (Hash)
170
+ # :return: The issue_number or nil if not found
171
+ Contract Maybe[Hash] => Maybe[Numeric]
172
+ def fetch_issue_number(github_event)
173
+ if github_event.nil?
174
+ @log.debug("GitHub event data not found - issue_number not auto-hydrated")
175
+ return nil
176
+ end
177
+
178
+ issue_number = (github_event[:issue] || github_event[:pull_request] || github_event)[:number]
179
+
180
+ @log.info("issue_number auto-hydrated - issue_number: #{issue_number}")
181
+ return issue_number
182
+ end
183
+
113
184
  # fetch the GitHub token from the environment
114
- # :return: The GitHub token
185
+ # :return: The GitHub token or nil if not found
186
+ Contract None => Maybe[String]
115
187
  def fetch_token
116
188
  # first try to use the OCTOKIT_ACCESS_TOKEN env var if it exists
117
189
  token = ENV.fetch("OCTOKIT_ACCESS_TOKEN", nil) # if running in actions
@@ -121,7 +193,10 @@ class Octokitted
121
193
  token = ENV.fetch("GITHUB_TOKEN", nil) # if running in actions
122
194
  return token unless token.nil?
123
195
 
124
- raise "No GitHub token found"
196
+ # if we get here, we don't have a token - this is okay because we can still do some things...
197
+ # ... without a token, rate limiting can be an issue
198
+ @log.warn("No GitHub token found")
199
+ return nil
125
200
  end
126
201
 
127
202
  # Setup an Octokit client
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Octokitted
4
4
  module Version
5
- VERSION = "0.0.5"
5
+ VERSION = "0.0.7"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: octokitted
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grant Birkinbine
@@ -78,6 +78,7 @@ files:
78
78
  - LICENSE
79
79
  - README.md
80
80
  - lib/octokitted.rb
81
+ - lib/octokitted/common/issue.rb
81
82
  - lib/octokitted/git_plugin.rb
82
83
  - lib/version.rb
83
84
  - octokitted.gemspec