Last active
June 3, 2024 13:39
-
-
Save elfassy/6321b124911e5f1859eee7b0aab73d9b to your computer and use it in GitHub Desktop.
Github Project Issues
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'bundler/inline' | |
gemfile do | |
source 'https://rubygems.org' | |
gem 'csv' | |
gem 'graphql-client' | |
gem 'progressbar' | |
gem 'pry' | |
gem 'pry-byebug' | |
end | |
require 'graphql/client/http' | |
module App | |
extend self | |
PROJECT = ENV['PROJECT'] || 6659 #id of the project board | |
GITHUB_ACCESS_TOKEN = ENV['GH_TOKEN'] | |
LABEL = ENV['LABEL'] | |
PER_PAGE = 99 | |
HTTP = GraphQL::Client::HTTP.new("https://api.github.com/graphql") do | |
def headers(_context) | |
{ | |
"Authorization" => "Bearer #{GITHUB_ACCESS_TOKEN}", | |
"Accept" => nil, | |
"Content-Type" => "application/json" | |
} | |
end | |
end | |
# Fetch latest schema on init, this will make a network request | |
Client = GraphQL::Client.new(schema: GraphQL::Client.load_schema(HTTP), execute: HTTP) | |
# GraphQL query | |
QUERY = Client.parse <<-GRAPHQL | |
query($cursor: String, $perPage: Int, $project: Int!) { | |
organization(login: "shopify") { | |
projectV2(number: $project) { | |
items(first: $perPage, after: $cursor) { | |
totalCount | |
pageInfo { | |
endCursor | |
startCursor | |
hasNextPage | |
} | |
nodes { | |
content { | |
... on Issue { | |
title | |
url | |
state | |
createdAt | |
updatedAt | |
labels(first: 20) { | |
nodes { | |
name | |
} | |
} | |
} | |
} | |
fieldValues(first: 40) { | |
nodes { | |
... on ProjectV2ItemFieldSingleSelectValue { | |
field { | |
... on ProjectV2Field { | |
name | |
} | |
... on ProjectV2SingleSelectField { | |
name | |
} | |
... on ProjectV2IterationField { | |
name | |
} | |
} | |
name | |
} | |
... on ProjectV2ItemFieldDateValue { | |
field { | |
... on ProjectV2Field { | |
name | |
} | |
... on ProjectV2SingleSelectField { | |
name | |
} | |
... on ProjectV2IterationField { | |
name | |
} | |
} | |
date | |
} | |
... on ProjectV2ItemFieldIterationValue { | |
field { | |
... on ProjectV2Field { | |
name | |
} | |
... on ProjectV2SingleSelectField { | |
name | |
} | |
... on ProjectV2IterationField { | |
name | |
} | |
} | |
title | |
} | |
... on ProjectV2ItemFieldMilestoneValue { | |
field { | |
... on ProjectV2Field { | |
name | |
} | |
... on ProjectV2SingleSelectField { | |
name | |
} | |
... on ProjectV2IterationField { | |
name | |
} | |
} | |
milestone { | |
title | |
} | |
} | |
... on ProjectV2ItemFieldNumberValue { | |
field { | |
... on ProjectV2Field { | |
name | |
} | |
... on ProjectV2SingleSelectField { | |
name | |
} | |
... on ProjectV2IterationField { | |
name | |
} | |
} | |
number | |
} | |
... on ProjectV2ItemFieldTextValue { | |
field { | |
... on ProjectV2Field { | |
name | |
id | |
} | |
... on ProjectV2SingleSelectField { | |
name | |
} | |
... on ProjectV2IterationField { | |
name | |
} | |
} | |
text | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
GRAPHQL | |
def run | |
generate_csv(fetch_issues.flatten.compact) | |
puts 'CSV file has been successfully generated and saved as output.csv' | |
end | |
private | |
def fetch_issues(cursor = nil, page_count = 1) | |
result = Client.query(QUERY, variables: { cursor: cursor, perPage: PER_PAGE, project: PROJECT }) | |
raise "GraphQL error: #{result.errors[:data].join(' ')}" if result.errors && result.errors[:data]&.any? | |
data = result.to_h.dig('data', 'organization', 'projectV2', 'items') | |
has_next_page = data&.dig('pageInfo', 'hasNextPage') | |
cursor = data&.dig('pageInfo', 'endCursor') | |
issues = data&.dig('nodes') | |
raise "No issues found: Make sure you have `read:projects, read:org, read:user, repo` permissions and configured SSO for your API key" unless issues | |
@progress_bar ||= ProgressBar.create(total: data.dig('totalCount'), format: '%t: |%B| %c / %C (%p%%)') | |
@progress_bar.progress += issues.size | |
filtered_issues = filter_issues(issues).compact | |
filtered_issues += fetch_issues(cursor, page_count + 1) if has_next_page | |
filtered_issues | |
end | |
def filter_issues(data) | |
data.map do |issue| | |
state = issue.dig('content','state') | |
next unless state != "CLOSED" | |
labels = issue.dig('content','labels','nodes')&.map { |l| l['name'] } | |
if LABEL | |
next unless labels&.include?(LABEL) | |
end | |
issue_data = issue["content"].each_with_object({}) do |(key, value), hash| | |
case key | |
when "labels" | |
hash["issue_#{key}"] = labels.join(',') | |
when "__typename" | |
hash[key] = value | |
else | |
hash["issue_#{key}"] = value | |
end | |
end | |
issue['fieldValues']['nodes'].each_with_object(issue_data) do |field_value, fields_hash| | |
field_name = field_value.dig('field','name') | |
next unless field_name | |
field_type = field_value.keys.last | |
case field_type | |
when 'milestone' | |
fields_hash['milestone'] = field_value['milestone']['title'] | |
when 'name' | |
fields_hash[field_name] = field_value['name'] | |
when 'date' | |
fields_hash[field_name] = field_value['date'] | |
when 'title' | |
fields_hash['title'] = field_value['title'] | |
when 'number' | |
fields_hash[field_name] = field_value['number'] | |
when 'text' | |
fields_hash[field_name] = field_value['text'] | |
else | |
raise "Unknown field type: #{field_type}" | |
end | |
end | |
end | |
end | |
def generate_csv(filtered_issues) | |
CSV.open('output.csv', 'w') do |csv| | |
header_keys = filtered_issues.map(&:keys).flatten.uniq | |
csv << header_keys | |
filtered_issues.each do |issue| | |
csv << header_keys.map { |key| issue[key] } | |
end | |
end | |
end | |
end | |
App.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment