ghx 0.0.3

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 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: []