Skip to content

Commit

Permalink
Merge pull request #1 from CompanyCam/maintenance/more_documentation
Browse files Browse the repository at this point in the history
Add documentation all over and remove unused testing methods.
  • Loading branch information
jeffmcfadden authored May 22, 2024
2 parents 2323a11 + c7d7ca4 commit 0025074
Show file tree
Hide file tree
Showing 17 changed files with 308 additions and 77 deletions.
9 changes: 9 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Description
<!--
Describe your change. Include a high-level summary, as well as any detail your reviewer will need to understand its context.
-->

### Reason/Reference
<!--
Why is this change necessary? (Mentioning the project it belongs to or linking to an associated issue will suffice.)
-->
11 changes: 3 additions & 8 deletions .github/workflows/standardrb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ name: Standardrb

# Controls when the action will run.
on:
push:
paths-ignore:
- '.github/**'
- 'nginx/**'
branches: [main]
pull_request:
types: [opened, reopened, synchronize]
merge_group:
Expand All @@ -29,9 +24,9 @@ jobs:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
# Disabled for now because of an issue installing ruby-debug-ide
# with:
# bundler-cache: true # runs 'bundle install' and caches installed gems automatically
with:
ruby-version: '3.3' # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- run: |
bundle install
bundle exec standardrb
32 changes: 32 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This is a basic workflow to help you get started with Actions

name: Tests

# Controls when the action will run.
on:
pull_request:
types: [opened, reopened, synchronize]
merge_group:
types: [checks_requested]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
env:
BUNDLE_DEPLOYMENT: false
BUNDLE_FROZEN: false
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3' # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- run: |
bundle install
bundle exec rake test
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
ghx (0.0.4)
ghx (0.2.1)
faraday (~> 2.9.0)
faraday-retry (~> 2.2.1)
octokit
Expand Down
4 changes: 4 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
require "bundler"
Bundler::GemHelper.install_tasks

task :test do
Dir.glob("./test/*_test.rb").each { |file| require file }
end
18 changes: 18 additions & 0 deletions lib/ghx.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,52 @@ def symbolize_keys!
# Extra classes to support more OO interfaces to the GitHub API. Wraps both the REST API and GraphQL API. Currently
# incomplete. Functionality has been built for our existing use-cases, but nothing else.
module GHX
# Defaults to $stdout
# @return [Logger]
def self.logger
@logger ||= Logger.new($stdout)
end

# @param logger [Logger]
def self.logger=(logger)
@logger = logger
end

# Internal octokit client.
# API Key defaults to ENV["GITHUB_TOKEN"]
# @return [Octokit::Client]
def self.octokit
@octokit ||= Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
end

# @param octokit [Octokit::Client]
# @return [Octokit::Client]
def self.octokit=(octokit)
@octokit = octokit
end

# Internal graphql client.
# API Key defaults to ENV["GITHUB_TOKEN"]
# @return [GHX::GraphqlClient]
def self.graphql
@graphql ||= GHX::GraphqlClient.new(ENV["GITHUB_GRAPHQL_TOKEN"])
end

# @param graphql [GHX::GraphqlClient]
# @return [GHX::GraphqlClient]
def self.graphql=(graphql)
@graphql = graphql
end

# Internal graphql client.
# API Key defaults to ENV["GITHUB_TOKEN"]
# @return [GHX::RestClient]
def self.rest_client
@rest_client ||= GHX::RestClient.new(ENV["GITHUB_TOKEN"])
end

# @param rest_client [GHX::RestClient]
# @return [GHX::RestClient]
def self.rest_client=(rest_client)
@rest_client = rest_client
end
Expand Down
9 changes: 9 additions & 0 deletions lib/ghx/dependabot.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
module GHX
# Module for Dependabot-related classes and methods
module Dependabot
# Get Dependabot alerts for a given repository
#
# @note ONLY RETURNS THE FIRST 100 ALERTS
# @param owner [String] the owner of the repository
# @param repo [String] the repository name
# @return [Array<GHX::Dependabot::Alert>] the alerts
def self.get_alerts(owner:, repo:)
# TODO: Add pagination to get all alerts in one go

GHX.rest_client.get("repos/#{owner}/#{repo}/dependabot/alerts?state=open&per_page=100").map do |alert|
GHX::Dependabot::Alert.new(alert)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/ghx/dependabot/alert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@

module GHX
module Dependabot
# A Dependabot Alert
class Alert
attr_reader :number, :state, :dependency, :security_advisory, :security_vulnerability, :url, :html_url, :created_at, :updated_at

# @param json_data [Hash] The JSON data for the alert, from the API
def initialize(json_data)
@number = json_data["number"]
@state = json_data["state"]
Expand Down
1 change: 1 addition & 0 deletions lib/ghx/dependabot/package.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module GHX
module Dependabot
# A package is a dependency that is managed by a package manager. Referenced by a SecurityVulnerability.
class Package
attr_reader :ecosystem, :name

Expand Down
1 change: 1 addition & 0 deletions lib/ghx/dependabot/security_vulnerability.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module GHX
module Dependabot
# A SecurityVulnerability is referenced by an Alert
class SecurityVulnerability
attr_reader :package, :severity, :vulnerable_version_range, :first_patched_version

Expand Down
54 changes: 5 additions & 49 deletions lib/ghx/graphql_client.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
module GHX
# Internal class to interact with the GitHub GraphQL API
class GraphqlClient
# @param api_key [String]
def initialize(api_key)
@api_key = api_key
end

# Perform a GraphQL Query and return the result
# @param query [String] GraphQL Query
# @return [Net::HTTPResponse]
def query(query)
uri = URI("https://api.github.com/graphql")
req = Net::HTTP::Post.new(uri)
Expand All @@ -15,54 +20,5 @@ def query(query)
http.request(req)
end
end

# @param [String] project_id
# @param [GithubProjectItem] project_item
# @param [DateTime] reported_at
def update_project_item_reported_at(project_item_id:, reported_at:, project_id: GithubProject::MAIN_GH_PROJECT_ID)
field_id = "PVTF_lADOALH_aM4Ac-_zzgSzAZs" # project_item.field_map["Reported At"]

gql_query = <<~GQL
mutation {
updateProjectV2ItemFieldValue(input: {
fieldId: "#{field_id}",
itemId: "#{project_item_id}",
projectId: "#{project_id}",
value: {
date: "#{reported_at.to_date}"
}
}) {
projectV2Item {
id
}
}
}
GQL

query(gql_query)
end

def update_project_item_reported_by(project_item_id:, reported_by:, project_id: GithubProject::MAIN_GH_PROJECT_ID)
field_id = "PVTF_lADOALH_aM4Ac-_zzgSzBcc" # project_item.field_map["Reporter"]

gql_query = <<~GQL
mutation {
updateProjectV2ItemFieldValue(input: {
fieldId: "#{field_id}",
itemId: "#{project_item_id}",
projectId: "#{project_id}",
value: {
text: "#{reported_by}"
}
}) {
projectV2Item {
id
}
}
}
GQL

query(gql_query)
end
end
end
20 changes: 20 additions & 0 deletions lib/ghx/issue.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
module GHX
# A GitHub Issue
class Issue
attr_accessor :owner, :repo, :number, :title, :body, :state, :state_reason, :author, :assignees, :labels, :milestone, :created_at, :updated_at, :closed_at

# Search for issues in a repository
# @param owner [String] the owner of the repository
# @param repo [String] the repository name
# @param query [String] the search query, using GitHub's search syntax
# @return [Array<Issue>] the issues found
def self.search(owner:, repo:, query:)
data = GHX.rest_client.get("search/issues?q=#{URI.encode_www_form_component(query)}+is:issue+repo:#{owner}/#{repo}")
data.fetch("items").to_a.map do |issue_data|
Expand All @@ -13,17 +19,31 @@ def self.search(owner:, repo:, query:)
[]
end

# Find an issue by its number
# @param owner [String] the owner of the repository
# @param repo [String] the repository name
# @param number [Integer] the issue number
# @return [Issue] the issue found
def self.find(owner:, repo:, number:)
response_data = GHX.rest_client.get("repos/#{owner}/#{repo}/issues/#{number}")
new(owner: owner, repo: repo, **response_data)
end

# @param owner [String] the owner of the repository
# @param repo [String] the repository name
# @param **args [Hash] the attributes of the issue you wish to assign
# @return [Issue] the new issue
def initialize(owner:, repo:, **args)
@owner = owner
@repo = repo
update_attributes(args)
end

# Save the issue to GitHub. Handles both creating and updating.
#
# If the issue has a number, it will be updated. Otherwise, it will be created.
#
# @return [Issue] the saved issue
def save
@number.nil? ? create : update
end
Expand Down
45 changes: 30 additions & 15 deletions lib/ghx/project_item.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
module GHX
# A GitHub Project Item. This is a single item in a GitHub Project Board.
#
# ProjectsV2 are only available via the GraphQL API. This class wraps access to the API and provides a more OO interface.
#
# @note Access to ProjectItems should be done largely (if not always) through the Project class.
class ProjectItem
attr_accessor :id, :project_id, :issue_number, :issue_title, :issue_url, :issue_state, :field_values, :field_map

# @param field_configuration [Array<Hash>] An array of field configurations. These are the fields that are available to the Project Item. These are provided via the Project itself. It's much easier to access ProjectItems through the project because of this.
# @param data [Hash] The data from the GraphQL API.
def initialize(field_configuration:, data:)
_setup_field_configuration(field_configuration)

Expand Down Expand Up @@ -33,7 +40,11 @@ def initialize(field_configuration:, data:)
end
end

# Updates the given fields to the given values. Makes a GraphQL call per field to do the update.
# Updates the given fields to the given values. Makes a GraphQL call *per field* to do the update.
#
# The implementation wraps access to the various types for each field. Since GraphQL requires us to type match on all
# requests, this gives us convenience, especially for things like a select field. We can pass in the value, and the
# method will find the ID of the option and update the field for us.
#
# @param fields [Hash] A hash of field names to values.
def update(**fields)
Expand Down Expand Up @@ -138,23 +149,27 @@ def update_single_select_field(field_id, value)

private

# Parse the field_configuration and set up the instance variables and accessors.
#
# Example field_configuration:
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCno", :name=>"Title", :data_type=>"TITLE", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCns", :name=>"Assignees", :data_type=>"ASSIGNEES", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzAZs", :name=>"Reported At", :data_type=>"DATE", :options=>nil}
# {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCnw", :name=>"Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"f971fb55", :name=>"To triage"}, {:id=>"856cdede", :name=>"Ready to Assign"}, {:id=>"f75ad846", :name=>"Assigned"}, {:id=>"47fc9ee4", :name=>"Fix In progress"}, {:id=>"5ef0dc97", :name=>"Additional Info Requested"}, {:id=>"98236657", :name=>"Done - Fixed"}, {:id=>"98aea6ad", :name=>"Done - Won't Fix"}, {:id=>"a3b4fc3a", :name=>"Duplicate"}, {:id=>"81377549", :name=>"Not a Vulnerability"}]}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn0", :name=>"Labels", :data_type=>"LABELS", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn4", :name=>"Linked pull requests", :data_type=>"LINKED_PULL_REQUESTS", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn8", :name=>"Milestone", :data_type=>"MILESTONE", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoA", :name=>"Repository", :data_type=>"REPOSITORY", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoM", :name=>"Reviewers", :data_type=>"REVIEWERS", :options=>nil}
# {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCuA", :name=>"Severity", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"79628723", :name=>"Informational"}, {:id=>"153889c6", :name=>"Low"}, {:id=>"093709ee", :name=>"Medium"}, {:id=>"5a00bbe7", :name=>"High"}, {:id=>"00e0bbaf", :name=>"Critical"}, {:id=>"fd986bd9", :name=>"Duplicate"}]}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBcc", :name=>"Reporter", :data_type=>"TEXT", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBho", :name=>"Resolve By", :data_type=>"DATE", :options=>nil}
# {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgTKjOw", :name=>"Payout Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"53c47c02", :name=>"Ready for Invoice"}, {:id=>"0b8a4629", :name=>"Payout in Process"}, {:id=>"5f356a58", :name=>"Payout Complete"}, {:id=>"368048ac", :name=>"Ineligible for Payout"}]}
#
#
def _setup_field_configuration(field_configuration)
@field_configuration = field_configuration.map { |fc| fc.merge({normalized_name: normalized_field_value_name(fc[:name])}) }

# Example field_configuration:
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCno", :name=>"Title", :data_type=>"TITLE", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCns", :name=>"Assignees", :data_type=>"ASSIGNEES", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzAZs", :name=>"Reported At", :data_type=>"DATE", :options=>nil}
# {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCnw", :name=>"Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"f971fb55", :name=>"To triage"}, {:id=>"856cdede", :name=>"Ready to Assign"}, {:id=>"f75ad846", :name=>"Assigned"}, {:id=>"47fc9ee4", :name=>"Fix In progress"}, {:id=>"5ef0dc97", :name=>"Additional Info Requested"}, {:id=>"98236657", :name=>"Done - Fixed"}, {:id=>"98aea6ad", :name=>"Done - Won't Fix"}, {:id=>"a3b4fc3a", :name=>"Duplicate"}, {:id=>"81377549", :name=>"Not a Vulnerability"}]}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn0", :name=>"Labels", :data_type=>"LABELS", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn4", :name=>"Linked pull requests", :data_type=>"LINKED_PULL_REQUESTS", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn8", :name=>"Milestone", :data_type=>"MILESTONE", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoA", :name=>"Repository", :data_type=>"REPOSITORY", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoM", :name=>"Reviewers", :data_type=>"REVIEWERS", :options=>nil}
# {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCuA", :name=>"Severity", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"79628723", :name=>"Informational"}, {:id=>"153889c6", :name=>"Low"}, {:id=>"093709ee", :name=>"Medium"}, {:id=>"5a00bbe7", :name=>"High"}, {:id=>"00e0bbaf", :name=>"Critical"}, {:id=>"fd986bd9", :name=>"Duplicate"}]}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBcc", :name=>"Reporter", :data_type=>"TEXT", :options=>nil}
# {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBho", :name=>"Resolve By", :data_type=>"DATE", :options=>nil}
# {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgTKjOw", :name=>"Payout Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"53c47c02", :name=>"Ready for Invoice"}, {:id=>"0b8a4629", :name=>"Payout in Process"}, {:id=>"5f356a58", :name=>"Payout Complete"}, {:id=>"368048ac", :name=>"Ineligible for Payout"}]}
@field_configuration.each do |field|
next unless field[:name]
next if field[:name].to_s.empty?
Expand Down
Loading

0 comments on commit 0025074

Please sign in to comment.