22
33require 'octokit'
44require 'thor'
5+ require 'yaml'
56
67class Samvera < Thor
78
9+ attr_reader :owner , :repo , :label , :project_id
10+
811 desc "audit_issues" , "Audits a repository for all stale issues, labels them, and adds a comment to the issue."
912 option :repo , required : true , type : :string
1013 option :updated , type : :string , default : "2021-01-01"
@@ -13,13 +16,11 @@ class Samvera < Thor
1316 option :project_id , type : :numeric , default : 28
1417 def audit_issues
1518
16- repo = options [ :repo ]
19+ @ repo = options [ :repo ]
1720 created = options [ :created ]
1821 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 ]
2324
2425 # Define the search criteria
2526 query = "repo:#{ repo } is:issue is:open created:<#{ created } updated:<#{ updated } "
@@ -63,9 +64,11 @@ class Samvera < Thor
6364 say ( "No milestone to remove from Issue ##{ issue . number } " , :yellow )
6465 end
6566
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)
6972 rescue Octokit ::Error => e
7073 say ( "Failed to audit Issue ##{ issue . number } : #{ e . message } " , :red )
7174 end
@@ -86,8 +89,6 @@ class Samvera < Thor
8689 user_login = options [ :user ]
8790 org = options [ :org ]
8891
89- client = Octokit ::Client . new ( access_token : ENV [ 'GH_TOKEN' ] )
90-
9192 begin
9293 team = client . team_by_name ( org , team_slug )
9394 team_id = team [ :id ]
@@ -113,8 +114,6 @@ class Samvera < Thor
113114 user_login = options [ :user ]
114115 org = options [ :org ]
115116
116- client = Octokit ::Client . new ( access_token : ENV [ 'GH_TOKEN' ] )
117-
118117 begin
119118 team = client . team_by_name ( org , team_slug )
120119 team_id = team [ :id ]
@@ -129,5 +128,278 @@ class Samvera < Thor
129128 say ( "The user is not a member of the team." , :yellow )
130129 end
131130 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
132404end
133405
0 commit comments