ghx 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2062a41d9688feddf31afd4a24c6123e0662e80a57fccacd2f391df0800ca982
4
+ data.tar.gz: aa47601036fad7b097f09f5c14f6d212507b6992ee3899766aa01bf6c2bdfb9a
5
+ SHA512:
6
+ metadata.gz: 8de5718bf86af69bbddacd5c2be8b86ba396d5d52c0887379bb7f83dd88be28a0defd4b618265429b3e713c29e6f231d6d28b541eb607906045552b01e78bb59
7
+ data.tar.gz: b82f1220491758e33846e9dd74637f28194b560ca81e197927e38e6ebb881abba36d72e4f5083e0447eca37eafc3efc4b078d8704f650a957ccf2738d20b0a8e
@@ -0,0 +1,142 @@
1
+ # Sample Dependabot Alert JSON Shape:
2
+ # {
3
+ # "number": 321,
4
+ # "state": "open",
5
+ # "dependency": {
6
+ # "package": {
7
+ # "ecosystem": "npm",
8
+ # "name": "react-pdf"
9
+ # },
10
+ # "manifest_path": "yarn.lock",
11
+ # "scope": "runtime"
12
+ # },
13
+ # "security_advisory": {
14
+ # "ghsa_id": "GHSA-87hq-q4gp-9wr4",
15
+ # "cve_id": "CVE-2024-34342",
16
+ # "summary": "react-pdf vulnerable to arbitrary JavaScript execution upon opening a malicious PDF with PDF.js",
17
+ # "description": "### Summary\n\nIf PDF.js is used to load a malicious PDF, and PDF.js is configured with `isEvalSupported` set to `true` (which is the default value), unrestricted attacker-controlled JavaScript will be executed in the context of the hosting domain.\n\n### Patches\n[This patch](https://github.com/wojtekmaj/react-pdf/commit/671e6eaa2e373e404040c13cc6b668fe39839cad) forces `isEvalSupported` to `false`, removing the attack vector.\n\n### Workarounds\nSet `options.isEvalSupported` to `false`, where `options` is `Document` component prop.\n\n### References\n- [GHSA-wgrm-67xf-hhpq](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq)\n- https://github.com/mozilla/pdf.js/pull/18015\n- https://github.com/mozilla/pdf.js/commit/85e64b5c16c9aaef738f421733c12911a441cec6\n- https://bugzilla.mozilla.org/show_bug.cgi?id=1893645",
18
+ # "severity": "high",
19
+ # "identifiers": [
20
+ # {
21
+ # "value": "GHSA-87hq-q4gp-9wr4",
22
+ # "type": "GHSA"
23
+ # },
24
+ # {
25
+ # "value": "CVE-2024-34342",
26
+ # "type": "CVE"
27
+ # }
28
+ # ],
29
+ # "references": [
30
+ # {
31
+ # "url": "https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq"
32
+ # },
33
+ # {
34
+ # "url": "https://github.com/wojtekmaj/react-pdf/security/advisories/GHSA-87hq-q4gp-9wr4"
35
+ # },
36
+ # {
37
+ # "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-34342"
38
+ # },
39
+ # {
40
+ # "url": "https://github.com/mozilla/pdf.js/pull/18015"
41
+ # },
42
+ # {
43
+ # "url": "https://github.com/mozilla/pdf.js/commit/85e64b5c16c9aaef738f421733c12911a441cec6"
44
+ # },
45
+ # {
46
+ # "url": "https://github.com/wojtekmaj/react-pdf/commit/208f28dd47fe38c33ce4bac4205b2b0a0bb207fe"
47
+ # },
48
+ # {
49
+ # "url": "https://github.com/wojtekmaj/react-pdf/commit/671e6eaa2e373e404040c13cc6b668fe39839cad"
50
+ # },
51
+ # {
52
+ # "url": "https://github.com/advisories/GHSA-87hq-q4gp-9wr4"
53
+ # }
54
+ # ],
55
+ # "published_at": "2024-05-07T16:48:59Z",
56
+ # "updated_at": "2024-05-08T10:10:23Z",
57
+ # "withdrawn_at": null,
58
+ # "vulnerabilities": [
59
+ # {
60
+ # "package": {
61
+ # "ecosystem": "npm",
62
+ # "name": "react-pdf"
63
+ # },
64
+ # "severity": "high",
65
+ # "vulnerable_version_range": "< 7.7.3",
66
+ # "first_patched_version": {
67
+ # "identifier": "7.7.3"
68
+ # }
69
+ # },
70
+ # {
71
+ # "package": {
72
+ # "ecosystem": "npm",
73
+ # "name": "react-pdf"
74
+ # },
75
+ # "severity": "high",
76
+ # "vulnerable_version_range": ">= 8.0.0, < 8.0.2",
77
+ # "first_patched_version": {
78
+ # "identifier": "8.0.2"
79
+ # }
80
+ # }
81
+ # ],
82
+ # "cvss": {
83
+ # "vector_string": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L",
84
+ # "score": 7.1
85
+ # },
86
+ # "cwes": [
87
+ # {
88
+ # "cwe_id": "CWE-79",
89
+ # "name": "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
90
+ # }
91
+ # ]
92
+ # },
93
+ # "security_vulnerability": {
94
+ # "package": {
95
+ # "ecosystem": "npm",
96
+ # "name": "react-pdf"
97
+ # },
98
+ # "severity": "high",
99
+ # "vulnerable_version_range": "< 7.7.3",
100
+ # "first_patched_version": {
101
+ # "identifier": "7.7.3"
102
+ # }
103
+ # },
104
+ # "url": "https://api.github.com/repos/CompanyCam/Company-Cam-API/dependabot/alerts/321",
105
+ # "html_url": "https://github.com/CompanyCam/Company-Cam-API/security/dependabot/321",
106
+ # "created_at": "2024-05-07T16:54:48Z",
107
+ # "updated_at": "2024-05-07T16:54:48Z",
108
+ # "dismissed_at": null,
109
+ # "dismissed_by": null,
110
+ # "dismissed_reason": null,
111
+ # "dismissed_comment": null,
112
+ # "fixed_at": null,
113
+ # "auto_dismissed_at": null
114
+ # }
115
+
116
+ module GHX
117
+ module Dependabot
118
+ class Alert
119
+ attr_reader :number, :state, :dependency, :security_advisory, :security_vulnerability, :url, :html_url, :created_at, :updated_at
120
+
121
+ def initialize(json_data)
122
+ @number = json_data["number"]
123
+ @state = json_data["state"]
124
+ @dependency = json_data["dependency"]
125
+ @security_advisory = json_data["security_advisory"]
126
+ @security_vulnerability = SecurityVulnerability.new(json_data["security_vulnerability"])
127
+ @url = json_data["url"]
128
+ @html_url = json_data["html_url"]
129
+ @created_at = begin
130
+ Time.parse(json_data["created_at"])
131
+ rescue
132
+ nil
133
+ end
134
+ @updated_at = begin
135
+ Time.parse(json_data["updated_at"])
136
+ rescue
137
+ nil
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,12 @@
1
+ module GHX
2
+ module Dependabot
3
+ class Package
4
+ attr_reader :ecosystem, :name
5
+
6
+ def initialize(json_data)
7
+ @ecosystem = json_data["ecosystem"]
8
+ @name = json_data["name"]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module GHX
2
+ module Dependabot
3
+ class SecurityVulnerability
4
+ attr_reader :package, :severity, :vulnerable_version_range, :first_patched_version
5
+
6
+ def initialize(json_data)
7
+ @package = Package.new(json_data["package"])
8
+ @severity = json_data["severity"]
9
+ @vulnerable_version_range = json_data["vulnerable_version_range"]
10
+ @first_patched_version = json_data["first_patched_version"]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module GHX
2
+ module Dependabot
3
+ def self.get_alerts(owner:, repo:)
4
+ GHX.rest_get("repos/#{owner}/#{repo}/dependabot/alerts?state=open&per_page=100").map do |alert|
5
+ GHX::Dependabot::Alert.new(alert)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative "dependabot/alert"
12
+ require_relative "dependabot/package"
13
+ require_relative "dependabot/security_vulnerability"
@@ -0,0 +1,68 @@
1
+ module GHX
2
+ class GraphqlClient
3
+ def initialize(api_key)
4
+ @api_key = api_key
5
+ end
6
+
7
+ def query(query)
8
+ uri = URI("https://api.github.com/graphql")
9
+ req = Net::HTTP::Post.new(uri)
10
+ req["Authorization"] = "Bearer #{@api_key}"
11
+ req["Content-Type"] = "application/json"
12
+ req.body = {query: query}.to_json
13
+
14
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
15
+ http.request(req)
16
+ end
17
+ end
18
+
19
+ # @param [String] project_id
20
+ # @param [GithubProjectItem] project_item
21
+ # @param [DateTime] reported_at
22
+ def update_project_item_reported_at(project_item_id:, reported_at:, project_id: GithubProject::MAIN_GH_PROJECT_ID)
23
+ field_id = "PVTF_lADOALH_aM4Ac-_zzgSzAZs" # project_item.field_map["Reported At"]
24
+
25
+ gql_query = <<~GQL
26
+ mutation {
27
+ updateProjectV2ItemFieldValue(input: {
28
+ fieldId: "#{field_id}",
29
+ itemId: "#{project_item_id}",
30
+ projectId: "#{project_id}",
31
+ value: {
32
+ date: "#{reported_at.to_date}"
33
+ }
34
+ }) {
35
+ projectV2Item {
36
+ id
37
+ }
38
+ }
39
+ }
40
+ GQL
41
+
42
+ query(gql_query)
43
+ end
44
+
45
+ def update_project_item_reported_by(project_item_id:, reported_by:, project_id: GithubProject::MAIN_GH_PROJECT_ID)
46
+ field_id = "PVTF_lADOALH_aM4Ac-_zzgSzBcc" # project_item.field_map["Reporter"]
47
+
48
+ gql_query = <<~GQL
49
+ mutation {
50
+ updateProjectV2ItemFieldValue(input: {
51
+ fieldId: "#{field_id}",
52
+ itemId: "#{project_item_id}",
53
+ projectId: "#{project_id}",
54
+ value: {
55
+ text: "#{reported_by}"
56
+ }
57
+ }) {
58
+ projectV2Item {
59
+ id
60
+ }
61
+ }
62
+ }
63
+ GQL
64
+
65
+ query(gql_query)
66
+ end
67
+ end
68
+ end
data/lib/ghx/issue.rb ADDED
@@ -0,0 +1,59 @@
1
+ module GHX
2
+ class Issue
3
+ attr_accessor :owner, :repo, :number, :title, :body, :state, :state_reason, :author, :assignees, :labels, :milestone, :created_at, :updated_at, :closed_at
4
+
5
+ def self.find(owner:, repo:, number:)
6
+ response_data = GHX.rest_client.get("repos/#{owner}/#{repo}/issues/#{number}")
7
+ new(owner: owner, repo: repo, **response_data)
8
+ end
9
+
10
+ def initialize(owner:, repo:, **args)
11
+ @owner = owner
12
+ @repo = repo
13
+ update_attributes(args)
14
+ end
15
+
16
+ def save
17
+ @number.nil? ? create : update
18
+ end
19
+
20
+ private
21
+
22
+ def create
23
+ response_data = GHX.rest_client.post("repos/#{owner}/#{repo}/issues", {
24
+ title: @title,
25
+ body: @body,
26
+ labels: @labels,
27
+ milestone: @milestone,
28
+ assignees: @assignees
29
+ })
30
+ update_attributes(response_data)
31
+ self
32
+ end
33
+
34
+ def update
35
+ response_data = GHX.rest_client.patch("repos/#{owner}/#{repo}/issues/#{number}", {
36
+ title: @title,
37
+ body: @body,
38
+ labels: @labels,
39
+ milestone: @milestone,
40
+ assignees: @assignees,
41
+ state: @state,
42
+ state_reason: @state_reason
43
+ })
44
+ update_attributes(response_data)
45
+ self
46
+ end
47
+
48
+ def update_attributes(hash)
49
+ self.number = hash["number"]
50
+ self.title = hash["title"]
51
+ self.body = hash["body"]
52
+ self.state = hash["state"]
53
+ self.state_reason = hash["state_reason"]
54
+ self.labels = hash["labels"]
55
+ self.milestone = hash["milestone"]
56
+ self.assignees = hash["assignees"]
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,282 @@
1
+ module GHX
2
+ class Project
3
+ attr_reader :id
4
+
5
+ def initialize(id)
6
+ @id = id
7
+ @field_configuration = nil
8
+ end
9
+
10
+ def field_configuration
11
+ @field_configuration ||= _fetch_field_configuration
12
+ end
13
+
14
+ def _fetch_field_configuration
15
+ gql_query = <<~GQL
16
+ query {
17
+ node(id: "#{id}") {
18
+ ... on ProjectV2 {
19
+ fields(first: 100) {
20
+ nodes {
21
+ ... on ProjectV2Field {
22
+ id
23
+ name
24
+ dataType
25
+ }
26
+
27
+ ... on ProjectV2SingleSelectField {
28
+ id
29
+ name
30
+ dataType
31
+ options {
32
+ id
33
+ name
34
+ }
35
+ }
36
+
37
+ ... on ProjectV2IterationField {
38
+ id
39
+ name
40
+ dataType
41
+ }
42
+
43
+
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ GQL
50
+
51
+ client = GraphqlClient.new(ENV["GITHUB_TOKEN"])
52
+ res = client.query(gql_query)
53
+
54
+ data = JSON.parse(res.body)
55
+
56
+ data.dig("data", "node", "fields", "nodes").collect do |field|
57
+ {
58
+ id: field["id"],
59
+ name: field["name"],
60
+ data_type: field["dataType"],
61
+ options: field["options"]&.map { |option| {id: option["id"], name: option["name"]} }
62
+ }
63
+ end
64
+ end
65
+
66
+ def find_item_by_id(item_id)
67
+ # gql_query = <<~GQL
68
+ # query {
69
+ # node(id: "#{id}") {
70
+ # ... on Issue {
71
+ # number
72
+ # title
73
+ # url
74
+ # }
75
+ # }
76
+ # }
77
+ # GQL
78
+ #
79
+ # client = GraphqlClient.new(ENV["GITHUB_TOKEN"])
80
+ # res = client.query(gql_query)
81
+ #
82
+ # puts res.body
83
+ end
84
+
85
+ # In order to find an item by its issue number, we need to query the project items and filter by the issue number,
86
+ # then return the first item that matches the project ID.
87
+ def find_item_by_issue_number(number:, owner: "CompanyCam", repo: "Company-Cam-API")
88
+ gql_query = <<~GQL
89
+ query {
90
+ repository(owner: "#{owner}", name: "#{repo}") {
91
+ issue(number: #{number.to_i}) {
92
+ id
93
+ number
94
+ title
95
+ url
96
+ body
97
+ createdAt
98
+ projectItems(first: 100) {
99
+ nodes {
100
+ id
101
+ project {
102
+ id
103
+ databaseId
104
+ }
105
+ content {
106
+ ... on Issue {
107
+ number
108
+ title
109
+ url
110
+ state
111
+ assignees(first: 10) {
112
+ nodes {
113
+ login
114
+ }
115
+ }
116
+ }
117
+ }
118
+ fieldValues(first: 30) {
119
+ nodes {
120
+ ... on ProjectV2ItemFieldSingleSelectValue {
121
+ field {
122
+ ... on ProjectV2SingleSelectField {
123
+ name
124
+ }
125
+ }
126
+ name
127
+ id
128
+ }
129
+ ... on ProjectV2ItemFieldLabelValue {
130
+ labels(first: 20) {
131
+ nodes {
132
+ id
133
+ name
134
+ }
135
+ }
136
+ }
137
+ ... on ProjectV2ItemFieldTextValue {
138
+ text
139
+ id
140
+ updatedAt
141
+ creator {
142
+ url
143
+ }
144
+ field {
145
+ ... on ProjectV2Field {
146
+ id
147
+ name
148
+ }
149
+ }
150
+ }
151
+ ... on ProjectV2ItemFieldMilestoneValue {
152
+ milestone {
153
+ id
154
+ }
155
+ }
156
+ ... on ProjectV2ItemFieldRepositoryValue {
157
+ repository {
158
+ id
159
+ url
160
+ }
161
+ }
162
+ ... on ProjectV2ItemFieldDateValue {
163
+ date
164
+ field {
165
+ ... on ProjectV2FieldCommon {
166
+ id
167
+ name
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ GQL
179
+
180
+ client = GraphqlClient.new(ENV["GITHUB_TOKEN"])
181
+ res = client.query(gql_query)
182
+
183
+ data = JSON.parse(res.body)
184
+
185
+ project_item_data = data.dig("data", "repository", "issue", "projectItems", "nodes").find { |node| node["project"]["id"] == id }
186
+
187
+ ProjectItem.new(field_configuration: field_configuration, data: project_item_data)
188
+ end
189
+
190
+ def get_all_items
191
+ gql_query = <<~GQL
192
+ query {
193
+ node(id: "#{id}") {
194
+ ... on ProjectV2 {
195
+ items(last: 100) {
196
+ nodes {
197
+ id
198
+ content {
199
+ ... on Issue {
200
+ number
201
+ title
202
+ url
203
+ state
204
+ assignees(first: 10) {
205
+ nodes {
206
+ login
207
+ }
208
+ }
209
+ }
210
+ }
211
+ fieldValues(first: 20) {
212
+ nodes {
213
+ ... on ProjectV2ItemFieldSingleSelectValue {
214
+ field {
215
+ ... on ProjectV2SingleSelectField {
216
+ name
217
+ }
218
+ }
219
+ name
220
+ id
221
+ }
222
+ ... on ProjectV2ItemFieldLabelValue {
223
+ labels(first: 20) {
224
+ nodes {
225
+ id
226
+ name
227
+ }
228
+ }
229
+ }
230
+ ... on ProjectV2ItemFieldTextValue {
231
+ text
232
+ id
233
+ updatedAt
234
+ creator {
235
+ url
236
+ }
237
+ field {
238
+ ... on ProjectV2Field {
239
+ id
240
+ name
241
+ }
242
+ }
243
+ }
244
+ ... on ProjectV2ItemFieldMilestoneValue {
245
+ milestone {
246
+ id
247
+ }
248
+ }
249
+ ... on ProjectV2ItemFieldRepositoryValue {
250
+ repository {
251
+ id
252
+ url
253
+ }
254
+ }
255
+ ... on ProjectV2ItemFieldDateValue {
256
+ date
257
+ field {
258
+ ... on ProjectV2FieldCommon {
259
+ id
260
+ name
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ GQL
272
+
273
+ res = GraphqlClient.default.query(gql_query)
274
+
275
+ data = JSON.parse res.body
276
+
277
+ items = data["data"]["node"]["items"]["nodes"]
278
+
279
+ items.map { ProjectItem.new(field_configuration: field_configuration, data: _1) }
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,209 @@
1
+ module GHX
2
+ class ProjectItem
3
+ attr_accessor :id, :project_id, :issue_number, :issue_title, :issue_url, :issue_state, :field_values, :field_map
4
+
5
+ def initialize(field_configuration:, data:)
6
+ _setup_field_configuration(field_configuration)
7
+
8
+ GHX.logger.debug data
9
+
10
+ node = data
11
+ content = node["content"]
12
+
13
+ # These are fields common to all Project Items:
14
+ @id = node["id"]
15
+ @project_id = node["project"]["id"]
16
+ @issue_number = content["number"]
17
+ @issue_title = content["title"]
18
+ @issue_url = content["url"]
19
+ @issue_state = content["state"]
20
+
21
+ field_values = node["fieldValues"]["nodes"]
22
+
23
+ field_values.each do |field|
24
+ GHX.logger.debug "Field: #{field}"
25
+ next unless field["field"]
26
+ next if field["field"]["name"].to_s.empty?
27
+
28
+ name = normalized_field_value_name(field["field"]["name"])
29
+
30
+ public_send(:"#{name}=", extracted_field_value(field))
31
+ rescue NoMethodError
32
+ GHX.logger.warn "Could not find field name: #{field["field"]["name"]} in the field configuration for project item #{id}"
33
+ end
34
+ end
35
+
36
+ # Updates the given fields to the given values. Makes a GraphQL call per field to do the update.
37
+ #
38
+ # @param fields [Hash] A hash of field names to values.
39
+ def update(**fields)
40
+ fields.each do |field, value|
41
+ field_id = field_id(field)
42
+ raise "Field not found: #{field}" unless field_id
43
+
44
+ field_type = field_type(field)
45
+
46
+ case field_type
47
+ when "DATE"
48
+ update_date_field(field_id, value)
49
+ when "SINGLE_SELECT"
50
+ update_single_select_field(field_id, value)
51
+ when "TEXT"
52
+ update_text_field(field_id, value)
53
+ when "TITLE"
54
+ update_title_field(field_id, value)
55
+ else
56
+ GHX.logger.warn "Unknown field type in update: #{field_type}"
57
+ end
58
+ end
59
+ end
60
+
61
+ def update_text_field(field_id, value)
62
+ gql_query = <<~GQL
63
+ mutation {
64
+ updateProjectV2ItemFieldValue(input: {
65
+ fieldId: "#{field_id}",
66
+ itemId: "#{id}",
67
+ projectId: "#{project_id}",
68
+ value: {
69
+ text: "#{value}"
70
+ }
71
+ }) {
72
+ projectV2Item {
73
+ id
74
+ }
75
+ }
76
+ }
77
+ GQL
78
+
79
+ client = GraphqlClient.new(ENV["GITHUB_TOKEN"])
80
+ res = client.query(gql_query)
81
+ GHX.logger.debug "Update text field result"
82
+ GHX.logger.debug res
83
+ end
84
+
85
+ def update_date_field(field_id, value)
86
+ gql_query = <<~GQL
87
+ mutation {
88
+ updateProjectV2ItemFieldValue(input: {
89
+ fieldId: "#{field_id}",
90
+ itemId: "#{id}",
91
+ projectId: "#{project_id}",
92
+ value: {
93
+ date: "#{value}"
94
+ }
95
+ }) {
96
+ projectV2Item {
97
+ id
98
+ }
99
+ }
100
+ }
101
+ GQL
102
+
103
+ client = GraphqlClient.new(ENV["GITHUB_TOKEN"])
104
+ res = client.query(gql_query)
105
+ GHX.logger.debug "Update date field result"
106
+ GHX.logger.debug res
107
+ end
108
+
109
+ def update_single_select_field(field_id, value)
110
+ field_options = field_options(field_id)
111
+ raise "No options found for #{field_id}" unless field_options
112
+
113
+ option_id = field_options.find { |option| option[:name] == value }&.fetch(:id)
114
+ raise "Option not found: #{value}" unless option_id
115
+
116
+ gql_query = <<~GQL
117
+ mutation {
118
+ updateProjectV2ItemFieldValue(input: {
119
+ fieldId: "#{field_id}",
120
+ itemId: "#{id}",
121
+ projectId: "#{project_id}",
122
+ value: {
123
+ singleSelectOptionId: "#{option_id}"
124
+ }
125
+ }) {
126
+ projectV2Item {
127
+ id
128
+ }
129
+ }
130
+ }
131
+ GQL
132
+
133
+ client = GraphqlClient.new(ENV["GITHUB_TOKEN"])
134
+ res = client.query(gql_query)
135
+ GHX.logger.debug "Update single select field result"
136
+ GHX.logger.debug res.body
137
+ end
138
+
139
+ private
140
+
141
+ def _setup_field_configuration(field_configuration)
142
+ @field_configuration = field_configuration.map { |fc| fc.merge({normalized_name: normalized_field_value_name(fc[:name])}) }
143
+
144
+ # Example field_configuration:
145
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCno", :name=>"Title", :data_type=>"TITLE", :options=>nil}
146
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCns", :name=>"Assignees", :data_type=>"ASSIGNEES", :options=>nil}
147
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzAZs", :name=>"Reported At", :data_type=>"DATE", :options=>nil}
148
+ # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCnw", :name=>"Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"f971fb55", :name=>"To triage"}, {:id=>"856cdede", :name=>"Ready to Assign"}, {:id=>"f75ad846", :name=>"Assigned"}, {:id=>"47fc9ee4", :name=>"Fix In progress"}, {:id=>"5ef0dc97", :name=>"Additional Info Requested"}, {:id=>"98236657", :name=>"Done - Fixed"}, {:id=>"98aea6ad", :name=>"Done - Won't Fix"}, {:id=>"a3b4fc3a", :name=>"Duplicate"}, {:id=>"81377549", :name=>"Not a Vulnerability"}]}
149
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn0", :name=>"Labels", :data_type=>"LABELS", :options=>nil}
150
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn4", :name=>"Linked pull requests", :data_type=>"LINKED_PULL_REQUESTS", :options=>nil}
151
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn8", :name=>"Milestone", :data_type=>"MILESTONE", :options=>nil}
152
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoA", :name=>"Repository", :data_type=>"REPOSITORY", :options=>nil}
153
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoM", :name=>"Reviewers", :data_type=>"REVIEWERS", :options=>nil}
154
+ # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCuA", :name=>"Severity", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"79628723", :name=>"Informational"}, {:id=>"153889c6", :name=>"Low"}, {:id=>"093709ee", :name=>"Medium"}, {:id=>"5a00bbe7", :name=>"High"}, {:id=>"00e0bbaf", :name=>"Critical"}, {:id=>"fd986bd9", :name=>"Duplicate"}]}
155
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBcc", :name=>"Reporter", :data_type=>"TEXT", :options=>nil}
156
+ # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBho", :name=>"Resolve By", :data_type=>"DATE", :options=>nil}
157
+ # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgTKjOw", :name=>"Payout Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"53c47c02", :name=>"Ready for Invoice"}, {:id=>"0b8a4629", :name=>"Payout in Process"}, {:id=>"5f356a58", :name=>"Payout Complete"}, {:id=>"368048ac", :name=>"Ineligible for Payout"}]}
158
+ @field_configuration.each do |field|
159
+ next unless field[:name]
160
+ next if field[:name].to_s.empty?
161
+
162
+ # Example
163
+ # name = "Reported At"
164
+ # instance variable will be `@reported_at`
165
+ # attr_accessor will be `reported_at`
166
+ name = normalized_field_value_name(field[:name])
167
+ instance_variable_set(:"@#{name}", nil)
168
+ self.class.attr_accessor name.to_sym
169
+ end
170
+ end
171
+
172
+ def normalized_field_value_name(name)
173
+ name.tr(" ", "_").downcase
174
+ end
175
+
176
+ # Extracts the value from the field based on the field's data type. Thank you GraphQL for making this totally asinine.
177
+ def extracted_field_value(field)
178
+ this_field_configuration = @field_configuration.find { |f| f[:name] == field["field"]["name"] }
179
+
180
+ case this_field_configuration[:data_type]
181
+ when "DATE"
182
+ field["date"]
183
+ when "SINGLE_SELECT"
184
+ field["name"]
185
+ when "TEXT"
186
+ field["text"]
187
+ when "TITLE"
188
+ field["text"]
189
+ else
190
+ GHX.logger.warn "Unknown data type in extracted_field_value: #{this_field_configuration[:data_type]}"
191
+ end
192
+ end
193
+
194
+ def field_id(normalized_field_name)
195
+ field = @field_configuration.find { |f| f[:normalized_name] == normalized_field_name.to_s }
196
+ field[:id] if field
197
+ end
198
+
199
+ def field_type(normalized_field_name)
200
+ field = @field_configuration.find { |f| f[:normalized_name] == normalized_field_name.to_s }
201
+ field[:data_type] if field
202
+ end
203
+
204
+ def field_options(field_id)
205
+ field = @field_configuration.find { |f| f[:id] == field_id }
206
+ field[:options] if field
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,72 @@
1
+ require "net/http"
2
+
3
+ module GHX
4
+ class RestClient
5
+ attr_reader :api_key
6
+
7
+ def initialize(api_key)
8
+ @api_key = api_key
9
+ end
10
+
11
+ # @return [Hash] the JSON response
12
+ def get(path)
13
+ uri = URI.parse("https://api.github.com/#{path}")
14
+ request = Net::HTTP::Get.new(uri)
15
+ request["Accept"] = "application/vnd.github+json"
16
+ request["Authorization"] = "Bearer #{@api_key}"
17
+ request["X-Github-Api-Version"] = "2022-11-28"
18
+
19
+ req_options = {
20
+ use_ssl: uri.scheme == "https"
21
+ }
22
+
23
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
24
+ http.request(request)
25
+ end
26
+
27
+ JSON.parse(response.body)
28
+ end
29
+
30
+ # @return [Hash] the JSON response
31
+ def post(path, params)
32
+ uri = URI.parse("https://api.github.com/#{path}")
33
+ request = Net::HTTP::Post.new(uri)
34
+ request["Accept"] = "application/vnd.github+json"
35
+ request["Authorization"] = "Bearer #{@api_key}"
36
+ request["X-Github-Api-Version"] = "2022-11-28"
37
+
38
+ req_options = {
39
+ use_ssl: uri.scheme == "https"
40
+ }
41
+
42
+ request.body = params.to_json
43
+
44
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
45
+ http.request(request)
46
+ end
47
+
48
+ JSON.parse(response.body)
49
+ end
50
+
51
+ # @return [Hash] the JSON response
52
+ def patch(path, params)
53
+ uri = URI.parse("https://api.github.com/#{path}")
54
+ request = Net::HTTP::Patch.new(uri)
55
+ request["Accept"] = "application/vnd.github+json"
56
+ request["Authorization"] = "Bearer #{@api_key}"
57
+ request["X-Github-Api-Version"] = "2022-11-28"
58
+
59
+ req_options = {
60
+ use_ssl: uri.scheme == "https"
61
+ }
62
+
63
+ request.body = params.to_json
64
+
65
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
66
+ http.request(request)
67
+ end
68
+
69
+ JSON.parse(response.body)
70
+ end
71
+ end
72
+ end
data/lib/ghx.rb ADDED
@@ -0,0 +1,50 @@
1
+ require "time"
2
+ require "net/http"
3
+ require "json"
4
+ require "octokit"
5
+
6
+ require_relative "version"
7
+ require_relative "ghx/graphql_client"
8
+ require_relative "ghx/rest_client"
9
+ require_relative "ghx/dependabot"
10
+ require_relative "ghx/issue"
11
+ require_relative "ghx/project"
12
+ require_relative "ghx/project_item"
13
+
14
+ # GitHub eXtended API Interface
15
+ #
16
+ # Extra classes to support more OO interfaces to the GitHub API. Wraps both the REST API and GraphQL API. Currently
17
+ # incomplete. Functionality has been built for our existing use-cases, but nothing else.
18
+ module GHX
19
+ def self.logger
20
+ @logger ||= Logger.new($stdout)
21
+ end
22
+
23
+ def self.logger=(logger)
24
+ @logger = logger
25
+ end
26
+
27
+ def self.octokit
28
+ @octokit ||= Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
29
+ end
30
+
31
+ def self.octokit=(octokit)
32
+ @octokit = octokit
33
+ end
34
+
35
+ def self.graphql
36
+ @graphql ||= GHX::GraphqlClient.new(ENV["GITHUB_GRAPHQL_TOKEN"])
37
+ end
38
+
39
+ def self.graphql=(graphql)
40
+ @graphql = graphql
41
+ end
42
+
43
+ def self.rest_client
44
+ @rest_client ||= GHX::RestClient.new(ENV["GITHUB_TOKEN"])
45
+ end
46
+
47
+ def self.rest_client=(rest_client)
48
+ @rest_client = rest_client
49
+ end
50
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module GHX
2
+ VERSION = "0.0.3"
3
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ghx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - CompanyCam
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-05-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: octokit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.9.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.9.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday-retry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.2.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: standardrb
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: debug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: An object oriented wrapper around some GitHub API calls that aren't covered
98
+ by Octokit
99
+ email: jeff.mcfadden@companycam.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - lib/ghx.rb
105
+ - lib/ghx/dependabot.rb
106
+ - lib/ghx/dependabot/alert.rb
107
+ - lib/ghx/dependabot/package.rb
108
+ - lib/ghx/dependabot/security_vulnerability.rb
109
+ - lib/ghx/graphql_client.rb
110
+ - lib/ghx/issue.rb
111
+ - lib/ghx/project.rb
112
+ - lib/ghx/project_item.rb
113
+ - lib/ghx/rest_client.rb
114
+ - lib/version.rb
115
+ homepage: https://github.com/companycam/ghx
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.5.10
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Wrapper around some GitHub API calls
138
+ test_files: []