Skip to content

Commit

Permalink
Implementing support for auditing Circle CI configurations and parsin…
Browse files Browse the repository at this point in the history
…g a configuration file
  • Loading branch information
jrgriffiniii committed Oct 18, 2024
1 parent 945e67c commit af11e77
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 13 deletions.
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
README.md
README.md
tmp/
296 changes: 284 additions & 12 deletions cli.thor
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

require 'octokit'
require 'thor'
require 'yaml'

class Samvera < Thor

attr_reader :owner, :repo, :label, :project_id

desc "audit_issues", "Audits a repository for all stale issues, labels them, and adds a comment to the issue."
option :repo, required: true, type: :string
option :updated, type: :string, default: "2021-01-01"
Expand All @@ -13,13 +16,11 @@ class Samvera < Thor
option :project_id, type: :numeric, default: 28
def audit_issues

repo = options[:repo]
@repo = options[:repo]
created = options[:created]
updated = options[:updated]
label = options[:label]

# Authenticate with GitHub
client = Octokit::Client.new(access_token: ENV['GH_TOKEN'])
@label = options[:label]
@project_id = options[:project_id]

# Define the search criteria
query = "repo:#{repo} is:issue is:open created:<#{created} updated:<#{updated}"
Expand Down Expand Up @@ -63,9 +64,11 @@ class Samvera < Thor
say("No milestone to remove from Issue ##{issue.number}", :yellow)
end

project_url = "https://github.com/orgs/samvera/projects/#{project_id}"
project_card = client.create_project_card(project_url, content_id: issue.id, content_type: 'Issue')
say("Added Issue ##{issue.number} to Project '#{project_name}'", :green)
# This will fail, as projects are only supported with the GraphQL API
# @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
# project_url = "https://github.com/orgs/samvera/projects/#{project_id}"
# project_card = client.create_project_card(project_url, content_id: issue.id, content_type: 'Issue')
# say("Added Issue ##{issue.number} to Project '#{project_url}'", :green)
rescue Octokit::Error => e
say("Failed to audit Issue ##{issue.number}: #{e.message}", :red)
end
Expand All @@ -86,8 +89,6 @@ class Samvera < Thor
user_login = options[:user]
org = options[:org]

client = Octokit::Client.new(access_token: ENV['GH_TOKEN'])

begin
team = client.team_by_name(org, team_slug)
team_id = team[:id]
Expand All @@ -113,8 +114,6 @@ class Samvera < Thor
user_login = options[:user]
org = options[:org]

client = Octokit::Client.new(access_token: ENV['GH_TOKEN'])

begin
team = client.team_by_name(org, team_slug)
team_id = team[:id]
Expand All @@ -129,5 +128,278 @@ class Samvera < Thor
say("The user is not a member of the team.", :yellow)
end
end

desc "audit_repo_ci", "Audit the continuous integration (CI) configuration for a Samvera GitHub Repository"
option :repo, required: true, type: :string
option :owner, type: :string, default: "samvera"
option :label, type: :string, default: "maintenance"
option :project_id, type: :numeric, default: 28
def audit_repo_ci

@owner = options[:owner]
@repo = options[:repo]
@label = options[:label]
@project_id = options[:project_id]

repo_url = "https://github.com/#{owner}/#{repo}.git"
local_dir = "tmp/#{repo}"

# Clone the repository if it doesn't already exist
unless Dir.exist?(local_dir)
say("Cloning repository...", :green)
system("git clone #{repo_url} #{local_dir}")
end

file_path = File.join(local_dir, '.circleci', 'config.yml')

if File.exist?(file_path)
say("File exists: #{file_path}", :green)

content = File.read(file_path)
config = YAML.load(content)

if config.key?("orbs")
say("Orbs are specified", :green)

orbs = config["orbs"]

if orbs.key?("samvera")
say("samvera/circleci-orb is used", :green)

samvera_orb = orbs["samvera"]
if samvera_orb != samvera_orb_release
validation_error = "Unsupported release of samvera/circleci-orb is referenced"
handle_error(validation_error: validation_error)
else
say("Latest supported release of samvera/circleci-orb is referenced", :green)
end
else
validation_error = "samvera/circleci-orb is not used"
handle_error(validation_error: validation_error)
end
else
validation_error = "No orbs are specified"
handle_error(validation_error: validation_error)
end

if config.key?("jobs")

jobs = config["jobs"]
checks_for_master_branch = false

jobs.each_pair do |key, job|

if job.key?("parameters")
parameters = job["parameters"]

if parameters.key?("ruby_version")
say("Ruby version is parameterized for #{key}", :green)
else
validation_error = "Ruby version is not parameterized for #{key}"
handle_error(validation_error: validation_error)
end

if parameters.key?("bundler_version")
say("Bundler version is parameterized for #{key}", :green)
else
validation_error = "Bundler version is not parameterized for #{key}"
handle_error(validation_error: validation_error)
end
else
validation_error = "Parameters are not specified for job #{key}"
handle_error(validation_error: validation_error)
end

if job.key?("steps")
steps = job["steps"]

if steps.empty?
validation_error = "Steps are empty for #{key}"
handle_error(validation_error: validation_error)
end

steps.each do |step|
if step.is_a?(Hash)
if step.key?("run")
command = step["run"]

if command.key?("name")
name = command["name"]

if name == "Check for a branch named 'master'"
checks_for_master_branch = true
say("Found a job which checks for the existence of a branch named `master`.", :green)
end
end
end
end
end
else
validation_error = "Steps are not specified for job #{key}"
handle_error(validation_error: validation_error)
end
end

unless checks_for_master_branch
validation_error = "No job checks for the existence of a branch named `master`."
handle_error(validation_error: validation_error)
end
else
validation_error = "No jobs are specified"
handle_error(validation_error: validation_error)
end

if config.key?("workflows")

workflows = config["workflows"]

workflows.each_pair do |key, workflow|

if workflow.key?("jobs")
jobs = workflow["jobs"]

jobs.each do |job|
job.each_pair do |key, arg|
if arg.key?("ruby_version")
ruby_version = arg["ruby_version"]

if supported_ruby_versions.include?(ruby_version)
say("Supported Ruby version #{ruby_version} is used for CircleCI", :green)
else
validation_error = "Unsupported Ruby version #{ruby_version} is used for CircleCI"
handle_error(validation_error: validation_error)
end
end

if arg.key?("rails_version")
rails_version = arg["rails_version"]

if supported_rails_versions.include?(rails_version)
say("Supported Rails version #{rails_version} is used for CircleCI", :green)
else
validation_error = "Unsupported Rails version #{rails_version} is used for CircleCI"
handle_error(validation_error: validation_error)
end
end
end
end
else
validation_error = "No workflow jobs are specified"
handle_error(validation_error: validation_error)
end
end
else
validation_error = "No workflows are specified"
handle_error(validation_error: validation_error)
end
else
validation_error = "File does not exist: #{file_path}"
handle_error(validation_error: validation_error)
end
end

private

def config
@config ||= begin
file_path = "./config/cli.yaml"
yaml_content = File.read(file_path)
YAML.load(yaml_content)
end
end

def samvera_orb_release
config["samvera_orb_release"]
end

def supported_ruby_versions
config["supported_ruby_versions"]
end

def supported_rails_versions
config["supported_rails_versions"]
end

def errors
@errors ||= []
end

def access_token
ENV['GH_TOKEN']
end

def client
@client ||= Octokit::Client.new(access_token: access_token)
end

def repository
repository ||= client.repo("#{owner}/#{repo}")
end

def project_url
@project_url ||= "https://github.com/orgs/samvera/projects/#{project_id}"
end

# This will fail, as projects are only supported with the GraphQL API
# @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
def columns
client.project_columns(project_id)
end

def column
columns.first
end

def column_id
column["id"]
end

def prepare_github_issue(issue:)
unless issue.labels.map(&:name).include?(label)

begin
client.add_labels_to_an_issue(repository.id, issue.number, [label])
say("Label ``#{label}\" applied to Issue ##{issue.number}", :green)

# This will fail, as projects are only supported with the GraphQL API
# @see https://docs.github.com/en/rest/projects/projects?apiVersion=2022-11-28
#
# say("Using #{column_id} for Project '#{project_id}'", :green)
# project_card = client.create_project_card(column_id, content_id: issue.id, content_type: 'Issue')
# say("Added Issue ##{issue.number} to Project '#{project_id}'", :green)
rescue Octokit::Error => e
say("Failed to audit Issue ##{issue.number}: #{e.message}", :red)
end
end
end

def create_github_issue(issue_title:, issue_body:)

issues = client.issues(repository.id)
existing_issues = issues.select { |issue| issue.title == issue_title }

if !existing_issues.empty?
existing_issues.each do |issue|
say("Issue exists: #{issue.html_url}", :yellow)
prepare_github_issue(issue: issue)
end
else
issue = self.client.create_issue(repository.id, issue_title, issue_body)
say("Issue created: #{issue.html_url}", :green)
prepare_github_issue(issue: issue)
end
rescue Octokit::Error => e
say("Error creating issue: #{e.message}", :red)
end

def handle_error(validation_error:)
say(validation_error, :red)

unless errors.include?(validation_error)
issue_title = "CircleCI audit error: #{validation_error}"
create_github_issue(issue_title: issue_title, issue_body: validation_error)
errors << validation_error
end
end
end

10 changes: 10 additions & 0 deletions config/cli.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
samvera_orb_release: "samvera/circleci-orb@1.0"
supported_ruby_versions:
- "3.1.6"
- "3.2.5"
- "3.3.5"
supported_rails_versions:
- "7.0.8.5"
- "7.1.4.1"
- "7.2.1.1"

0 comments on commit af11e77

Please sign in to comment.