Skip to content

Commit 94f9601

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

File tree

3 files changed

+297
-13
lines changed

3 files changed

+297
-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: 285 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,279 @@ 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+
say("Unsupported release of samvera/circleci-orb is referenced", :red)
172+
# create an issue for updating to the latest release of samvera/circleci-orb
173+
else
174+
say("Latest supported release of samvera/circleci-orb is referenced", :green)
175+
end
176+
else
177+
say("samvera/circleci-orb is not used", :red)
178+
# create an issue for including the latest release of samvera/circleci-orb
179+
end
180+
else
181+
say("No orbs are specified", :red)
182+
# create an issue for including the latest release of samvera/circleci-orb
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+
say("Ruby version is not parameterized for #{key}", :red)
199+
# create an issue
200+
end
201+
202+
if parameters.key?("bundler_version")
203+
say("Bundler version is parameterized for #{key}", :green)
204+
else
205+
say("Bundler version is not parameterized for #{key}", :red)
206+
# create an issue
207+
end
208+
else
209+
say("Parameters are not specified for job #{key}", :red)
210+
# create an issue
211+
end
212+
213+
if job.key?("steps")
214+
steps = job["steps"]
215+
216+
if steps.empty?
217+
say("Steps are empty for #{key}", :red)
218+
# create an issue
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+
238+
else
239+
say("Steps are not specified for job #{key}", :red)
240+
# create an issue
241+
end
242+
end
243+
244+
unless checks_for_master_branch
245+
say("No job checks for the existence of a branch named `master`.", :red)
246+
# create issue
247+
end
248+
else
249+
say("No jobs are specified", :red)
250+
# create an issue for reviewing the CircleCI configuration
251+
end
252+
253+
if config.key?("workflows")
254+
255+
workflows = config["workflows"]
256+
257+
workflows.each_pair do |key, workflow|
258+
259+
if workflow.key?("jobs")
260+
jobs = workflow["jobs"]
261+
262+
jobs.each do |job|
263+
job.each_pair do |key, arg|
264+
if arg.key?("ruby_version")
265+
ruby_version = arg["ruby_version"]
266+
267+
if supported_ruby_versions.include?(ruby_version)
268+
say("Supported Ruby version #{ruby_version} is used for CircleCI", :green)
269+
else
270+
validation_error = "Unsupported Ruby version #{ruby_version} is used for CircleCI"
271+
handle_error(validation_error: validation_error)
272+
end
273+
end
274+
275+
if arg.key?("rails_version")
276+
rails_version = arg["rails_version"]
277+
278+
if supported_rails_versions.include?(rails_version)
279+
say("Supported Rails version #{rails_version} is used for CircleCI", :green)
280+
else
281+
validation_error = "Unsupported Rails version #{rails_version} is used for CircleCI"
282+
handle_error(validation_error: validation_error)
283+
end
284+
end
285+
end
286+
end
287+
else
288+
say("No workflow jobs are specified", :red)
289+
# create issue
290+
end
291+
end
292+
else
293+
say("No workflows are specified", :red)
294+
# create issue
295+
end
296+
else
297+
say("File does not exist: #{file_path}", :red)
298+
# create issue
299+
end
300+
end
301+
302+
private
303+
304+
def config
305+
@config ||= begin
306+
file_path = "./config/cli.yaml"
307+
yaml_content = File.read(file_path)
308+
YAML.load(yaml_content)
309+
end
310+
end
311+
312+
def samvera_orb_release
313+
config["samvera_orb_release"]
314+
end
315+
316+
def supported_ruby_versions
317+
config["supported_ruby_versions"]
318+
end
319+
320+
def supported_rails_versions
321+
config["supported_rails_versions"]
322+
end
323+
324+
def errors
325+
@errors ||= []
326+
end
327+
328+
def access_token
329+
ENV['GH_TOKEN']
330+
end
331+
332+
def client
333+
@client ||= Octokit::Client.new(access_token: access_token)
334+
end
335+
336+
def repository
337+
repository ||= client.repo("#{owner}/#{repo}")
338+
end
339+
340+
def project_url
341+
@project_url ||= "https://github.com/orgs/samvera/projects/#{project_id}"
342+
end
343+
344+
# This will fail, as projects are only supported with the GraphQL API
345+
# @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
346+
def columns
347+
client.project_columns(project_id)
348+
end
349+
350+
def column
351+
columns.first
352+
end
353+
354+
def column_id
355+
column["id"]
356+
end
357+
358+
def prepare_github_issue(issue:)
359+
unless issue.labels.map(&:name).include?(label)
360+
361+
begin
362+
client.add_labels_to_an_issue(repository.id, issue.number, [label])
363+
say("Label ``#{label}\" applied to Issue ##{issue.number}", :green)
364+
365+
# This will fail, as projects are only supported with the GraphQL API
366+
# @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
367+
#
368+
# say("Using #{column_id} for Project '#{project_id}'", :green)
369+
# project_card = client.create_project_card(column_id, content_id: issue.id, content_type: 'Issue')
370+
# say("Added Issue ##{issue.number} to Project '#{project_id}'", :green)
371+
rescue Octokit::Error => e
372+
say("Failed to audit Issue ##{issue.number}: #{e.message}", :red)
373+
end
374+
end
375+
end
376+
377+
def create_github_issue(issue_title:, issue_body:)
378+
379+
issues = client.issues(repository.id)
380+
existing_issues = issues.select { |issue| issue.title == issue_title }
381+
382+
if !existing_issues.empty?
383+
existing_issues.each do |issue|
384+
say("Issue exists: #{issue.html_url}", :yellow)
385+
prepare_github_issue(issue: issue)
386+
end
387+
else
388+
issue = self.client.create_issue(repository.id, issue_title, issue_body)
389+
say("Issue created: #{issue.html_url}", :green)
390+
prepare_github_issue(issue: issue)
391+
end
392+
rescue Octokit::Error => e
393+
say("Error creating issue: #{e.message}", :red)
394+
end
395+
396+
def handle_error(validation_error:)
397+
say(validation_error, :red)
398+
399+
unless errors.include?(validation_error)
400+
issue_title = "CircleCI audit error: #{validation_error}"
401+
create_github_issue(issue_title: issue_title, issue_body: validation_error)
402+
errors << validation_error
403+
end
404+
end
132405
end
133406

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)