2
2
3
3
require 'octokit'
4
4
require 'thor'
5
+ require 'yaml'
5
6
6
7
class Samvera < Thor
7
8
9
+ attr_reader :owner , :repo , :label , :project_id
10
+
8
11
desc "audit_issues" , "Audits a repository for all stale issues, labels them, and adds a comment to the issue."
9
12
option :repo , required : true , type : :string
10
13
option :updated , type : :string , default : "2021-01-01"
@@ -13,13 +16,11 @@ class Samvera < Thor
13
16
option :project_id , type : :numeric , default : 28
14
17
def audit_issues
15
18
16
- repo = options [ :repo ]
19
+ @ repo = options [ :repo ]
17
20
created = options [ :created ]
18
21
updated = options [ :updated ]
19
- label = options [ :label ]
20
-
21
- # Authenticate with GitHub
22
- client = Octokit ::Client . new ( access_token : ENV [ 'GH_TOKEN' ] )
22
+ @label = options [ :label ]
23
+ @project_id = options [ :project_id ]
23
24
24
25
# Define the search criteria
25
26
query = "repo:#{ repo } is:issue is:open created:<#{ created } updated:<#{ updated } "
@@ -63,9 +64,11 @@ class Samvera < Thor
63
64
say ( "No milestone to remove from Issue ##{ issue . number } " , :yellow )
64
65
end
65
66
66
- project_url = "https://github.com/orgs/samvera/projects/#{ project_id } "
67
- project_card = client . create_project_card ( project_url , content_id : issue . id , content_type : 'Issue' )
68
- say ( "Added Issue ##{ issue . number } to Project '#{ project_name } '" , :green )
67
+ # This will fail, as projects are only supported with the GraphQL API
68
+ # @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
69
+ # project_url = "https://github.com/orgs/samvera/projects/#{project_id}"
70
+ # project_card = client.create_project_card(project_url, content_id: issue.id, content_type: 'Issue')
71
+ # say("Added Issue ##{issue.number} to Project '#{project_url}'", :green)
69
72
rescue Octokit ::Error => e
70
73
say ( "Failed to audit Issue ##{ issue . number } : #{ e . message } " , :red )
71
74
end
@@ -86,8 +89,6 @@ class Samvera < Thor
86
89
user_login = options [ :user ]
87
90
org = options [ :org ]
88
91
89
- client = Octokit ::Client . new ( access_token : ENV [ 'GH_TOKEN' ] )
90
-
91
92
begin
92
93
team = client . team_by_name ( org , team_slug )
93
94
team_id = team [ :id ]
@@ -113,8 +114,6 @@ class Samvera < Thor
113
114
user_login = options [ :user ]
114
115
org = options [ :org ]
115
116
116
- client = Octokit ::Client . new ( access_token : ENV [ 'GH_TOKEN' ] )
117
-
118
117
begin
119
118
team = client . team_by_name ( org , team_slug )
120
119
team_id = team [ :id ]
@@ -129,5 +128,278 @@ class Samvera < Thor
129
128
say ( "The user is not a member of the team." , :yellow )
130
129
end
131
130
end
131
+
132
+ desc "audit_repo_ci" , "Audit the continuous integration (CI) configuration for a Samvera GitHub Repository"
133
+ option :repo , required : true , type : :string
134
+ option :owner , type : :string , default : "samvera"
135
+ option :label , type : :string , default : "maintenance"
136
+ option :project_id , type : :numeric , default : 28
137
+ def audit_repo_ci
138
+
139
+ @owner = options [ :owner ]
140
+ @repo = options [ :repo ]
141
+ @label = options [ :label ]
142
+ @project_id = options [ :project_id ]
143
+
144
+ repo_url = "https://github.com/#{ owner } /#{ repo } .git"
145
+ local_dir = "tmp/#{ repo } "
146
+
147
+ # Clone the repository if it doesn't already exist
148
+ unless Dir . exist? ( local_dir )
149
+ say ( "Cloning repository..." , :green )
150
+ system ( "git clone #{ repo_url } #{ local_dir } " )
151
+ end
152
+
153
+ file_path = File . join ( local_dir , '.circleci' , 'config.yml' )
154
+
155
+ if File . exist? ( file_path )
156
+ say ( "File exists: #{ file_path } " , :green )
157
+
158
+ content = File . read ( file_path )
159
+ config = YAML . load ( content )
160
+
161
+ if config . key? ( "orbs" )
162
+ say ( "Orbs are specified" , :green )
163
+
164
+ orbs = config [ "orbs" ]
165
+
166
+ if orbs . key? ( "samvera" )
167
+ say ( "samvera/circleci-orb is used" , :green )
168
+
169
+ samvera_orb = orbs [ "samvera" ]
170
+ if samvera_orb != samvera_orb_release
171
+ validation_error = "Unsupported release of samvera/circleci-orb is referenced"
172
+ handle_error ( validation_error : validation_error )
173
+ else
174
+ say ( "Latest supported release of samvera/circleci-orb is referenced" , :green )
175
+ end
176
+ else
177
+ validation_error = "samvera/circleci-orb is not used"
178
+ handle_error ( validation_error : validation_error )
179
+ end
180
+ else
181
+ validation_error = "No orbs are specified"
182
+ handle_error ( validation_error : validation_error )
183
+ end
184
+
185
+ if config . key? ( "jobs" )
186
+
187
+ jobs = config [ "jobs" ]
188
+ checks_for_master_branch = false
189
+
190
+ jobs . each_pair do |key , job |
191
+
192
+ if job . key? ( "parameters" )
193
+ parameters = job [ "parameters" ]
194
+
195
+ if parameters . key? ( "ruby_version" )
196
+ say ( "Ruby version is parameterized for #{ key } " , :green )
197
+ else
198
+ validation_error = "Ruby version is not parameterized for #{ key } "
199
+ handle_error ( validation_error : validation_error )
200
+ end
201
+
202
+ if parameters . key? ( "bundler_version" )
203
+ say ( "Bundler version is parameterized for #{ key } " , :green )
204
+ else
205
+ validation_error = "Bundler version is not parameterized for #{ key } "
206
+ handle_error ( validation_error : validation_error )
207
+ end
208
+ else
209
+ validation_error = "Parameters are not specified for job #{ key } "
210
+ handle_error ( validation_error : validation_error )
211
+ end
212
+
213
+ if job . key? ( "steps" )
214
+ steps = job [ "steps" ]
215
+
216
+ if steps . empty?
217
+ validation_error = "Steps are empty for #{ key } "
218
+ handle_error ( validation_error : validation_error )
219
+ end
220
+
221
+ steps . each do |step |
222
+ if step . is_a? ( Hash )
223
+ if step . key? ( "run" )
224
+ command = step [ "run" ]
225
+
226
+ if command . key? ( "name" )
227
+ name = command [ "name" ]
228
+
229
+ if name == "Check for a branch named 'master'"
230
+ checks_for_master_branch = true
231
+ say ( "Found a job which checks for the existence of a branch named `master`." , :green )
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ else
238
+ validation_error = "Steps are not specified for job #{ key } "
239
+ handle_error ( validation_error : validation_error )
240
+ end
241
+ end
242
+
243
+ unless checks_for_master_branch
244
+ validation_error = "No job checks for the existence of a branch named `master`."
245
+ handle_error ( validation_error : validation_error )
246
+ end
247
+ else
248
+ validation_error = "No jobs are specified"
249
+ handle_error ( validation_error : validation_error )
250
+ end
251
+
252
+ if config . key? ( "workflows" )
253
+
254
+ workflows = config [ "workflows" ]
255
+
256
+ workflows . each_pair do |key , workflow |
257
+
258
+ if workflow . key? ( "jobs" )
259
+ jobs = workflow [ "jobs" ]
260
+
261
+ jobs . each do |job |
262
+ job . each_pair do |key , arg |
263
+ if arg . key? ( "ruby_version" )
264
+ ruby_version = arg [ "ruby_version" ]
265
+
266
+ if supported_ruby_versions . include? ( ruby_version )
267
+ say ( "Supported Ruby version #{ ruby_version } is used for CircleCI" , :green )
268
+ else
269
+ validation_error = "Unsupported Ruby version #{ ruby_version } is used for CircleCI"
270
+ handle_error ( validation_error : validation_error )
271
+ end
272
+ end
273
+
274
+ if arg . key? ( "rails_version" )
275
+ rails_version = arg [ "rails_version" ]
276
+
277
+ if supported_rails_versions . include? ( rails_version )
278
+ say ( "Supported Rails version #{ rails_version } is used for CircleCI" , :green )
279
+ else
280
+ validation_error = "Unsupported Rails version #{ rails_version } is used for CircleCI"
281
+ handle_error ( validation_error : validation_error )
282
+ end
283
+ end
284
+ end
285
+ end
286
+ else
287
+ validation_error = "No workflow jobs are specified"
288
+ handle_error ( validation_error : validation_error )
289
+ end
290
+ end
291
+ else
292
+ validation_error = "No workflows are specified"
293
+ handle_error ( validation_error : validation_error )
294
+ end
295
+ else
296
+ validation_error = "File does not exist: #{ file_path } "
297
+ handle_error ( validation_error : validation_error )
298
+ end
299
+ end
300
+
301
+ private
302
+
303
+ def config
304
+ @config ||= begin
305
+ file_path = "./config/cli.yaml"
306
+ yaml_content = File . read ( file_path )
307
+ YAML . load ( yaml_content )
308
+ end
309
+ end
310
+
311
+ def samvera_orb_release
312
+ config [ "samvera_orb_release" ]
313
+ end
314
+
315
+ def supported_ruby_versions
316
+ config [ "supported_ruby_versions" ]
317
+ end
318
+
319
+ def supported_rails_versions
320
+ config [ "supported_rails_versions" ]
321
+ end
322
+
323
+ def errors
324
+ @errors ||= [ ]
325
+ end
326
+
327
+ def access_token
328
+ ENV [ 'GH_TOKEN' ]
329
+ end
330
+
331
+ def client
332
+ @client ||= Octokit ::Client . new ( access_token : access_token )
333
+ end
334
+
335
+ def repository
336
+ repository ||= client . repo ( "#{ owner } /#{ repo } " )
337
+ end
338
+
339
+ def project_url
340
+ @project_url ||= "https://github.com/orgs/samvera/projects/#{ project_id } "
341
+ end
342
+
343
+ # This will fail, as projects are only supported with the GraphQL API
344
+ # @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
345
+ def columns
346
+ client . project_columns ( project_id )
347
+ end
348
+
349
+ def column
350
+ columns . first
351
+ end
352
+
353
+ def column_id
354
+ column [ "id" ]
355
+ end
356
+
357
+ def prepare_github_issue ( issue :)
358
+ unless issue . labels . map ( &:name ) . include? ( label )
359
+
360
+ begin
361
+ client . add_labels_to_an_issue ( repository . id , issue . number , [ label ] )
362
+ say ( "Label ``#{ label } \" applied to Issue ##{ issue . number } " , :green )
363
+
364
+ # This will fail, as projects are only supported with the GraphQL API
365
+ # @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
366
+ #
367
+ # say("Using #{column_id} for Project '#{project_id}'", :green)
368
+ # project_card = client.create_project_card(column_id, content_id: issue.id, content_type: 'Issue')
369
+ # say("Added Issue ##{issue.number} to Project '#{project_id}'", :green)
370
+ rescue Octokit ::Error => e
371
+ say ( "Failed to audit Issue ##{ issue . number } : #{ e . message } " , :red )
372
+ end
373
+ end
374
+ end
375
+
376
+ def create_github_issue ( issue_title :, issue_body :)
377
+
378
+ issues = client . issues ( repository . id )
379
+ existing_issues = issues . select { |issue | issue . title == issue_title }
380
+
381
+ if !existing_issues . empty?
382
+ existing_issues . each do |issue |
383
+ say ( "Issue exists: #{ issue . html_url } " , :yellow )
384
+ prepare_github_issue ( issue : issue )
385
+ end
386
+ else
387
+ issue = self . client . create_issue ( repository . id , issue_title , issue_body )
388
+ say ( "Issue created: #{ issue . html_url } " , :green )
389
+ prepare_github_issue ( issue : issue )
390
+ end
391
+ rescue Octokit ::Error => e
392
+ say ( "Error creating issue: #{ e . message } " , :red )
393
+ end
394
+
395
+ def handle_error ( validation_error :)
396
+ say ( validation_error , :red )
397
+
398
+ unless errors . include? ( validation_error )
399
+ issue_title = "CircleCI audit error: #{ validation_error } "
400
+ create_github_issue ( issue_title : issue_title , issue_body : validation_error )
401
+ errors << validation_error
402
+ end
403
+ end
132
404
end
133
405
0 commit comments