Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation all over and remove unused testing methods. #1

Merged
merged 5 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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