Skip to content

Commit af11e77

Browse files
committed
Implementing support for auditing Circle CI configurations and parsing a configuration file
1 parent 945e67c commit af11e77

File tree

3 files changed

+296
-13
lines changed

3 files changed

+296
-13
lines changed

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
README.md
1+
README.md
2+
tmp/

cli.thor

Lines changed: 284 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
require 'octokit'
44
require 'thor'
5+
require 'yaml'
56

67
class 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
132404
end
133405

config/cli.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
samvera_orb_release: "samvera/[email protected]"
3+
supported_ruby_versions:
4+
- "3.1.6"
5+
- "3.2.5"
6+
- "3.3.5"
7+
supported_rails_versions:
8+
- "7.0.8.5"
9+
- "7.1.4.1"
10+
- "7.2.1.1"

0 commit comments

Comments
 (0)