Created
April 28, 2025 16:53
-
-
Save iangmaia/6d75d796531f46fd4d4b49bcc6070873 to your computer and use it in GitHub Desktop.
Convert GitHub project draft issues to Issues in a repo
This file contains hidden or 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 'graphql/client' | |
require 'graphql/client/http' | |
require 'octokit' | |
require 'json' | |
if ENV['GITHUB_TOKEN'].nil? | |
puts "Error: Please set GITHUB_TOKEN in your .env file" | |
puts "Required permissions: repo, project (read and write)" | |
exit 1 | |
end | |
# GitHub GraphQL API endpoint | |
HTTP = GraphQL::Client::HTTP.new('https://api.github.com/graphql') do | |
def headers(context) | |
{ | |
"Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}" | |
} | |
end | |
end | |
# Schema and client setup | |
Schema = GraphQL::Client.load_schema(HTTP) | |
Client = GraphQL::Client.new(schema: Schema, execute: HTTP) | |
# Maximum number of items to fetch per page in GraphQL queries | |
ITEMS_PER_PAGE = 100 | |
# GraphQL query to get draft items from a project with pagination | |
DraftItemsQuery = Client.parse <<-GRAPHQL | |
query($organization: String!, $number: Int!, $itemsCursor: String) { | |
organization(login: $organization) { | |
projectV2(number: $number) { | |
id | |
items(first: #{ITEMS_PER_PAGE}, after: $itemsCursor) { | |
pageInfo { | |
hasNextPage | |
endCursor | |
} | |
nodes { | |
id | |
type | |
fieldValues(first: #{ITEMS_PER_PAGE}) { | |
nodes { | |
... on ProjectV2ItemFieldSingleSelectValue { | |
name | |
field { | |
... on ProjectV2SingleSelectField { | |
name | |
} | |
} | |
} | |
} | |
} | |
content { | |
... on DraftIssue { | |
title | |
body | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
GRAPHQL | |
# GraphQL mutation to convert draft to issue | |
ConvertToIssueMutation = Client.parse <<-GRAPHQL | |
mutation($itemId: ID!, $repositoryId: ID!) { | |
convertProjectV2DraftIssueItemToIssue(input: { | |
itemId: $itemId, | |
repositoryId: $repositoryId, | |
}) { | |
item { | |
id | |
type | |
content { | |
... on Issue { | |
title | |
url | |
} | |
} | |
} | |
} | |
} | |
GRAPHQL | |
class DraftConverter | |
def initialize(org_name, project_number, repo_name, dry_run: true, debug: false) | |
@org_name = org_name | |
@project_number = project_number | |
@repo_name = repo_name | |
@dry_run = dry_run | |
@debug = debug | |
@client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN']) | |
end | |
def convert_all_drafts | |
all_items = fetch_all_items | |
draft_items = filter_draft_items(all_items) | |
puts "\nFound #{draft_items.length} draft items to convert." | |
draft_items.each do |item| | |
puts "- #{item['content']['title']}" | |
end | |
if @dry_run | |
puts "\nDRY RUN: No changes will be made. Run with dry_run: false to perform the conversion." | |
return | |
end | |
puts "\nConverting draft items to issues..." | |
draft_items.each_with_index do |item, index| | |
puts "\nProcessing #{index + 1} of #{draft_items.length}: #{item['content']['title']}" | |
convert_draft_to_issue(item) | |
sleep 1 | |
end | |
end | |
private | |
def fetch_all_items | |
all_items = [] | |
items_cursor = nil | |
loop do | |
result = Client.query(DraftItemsQuery, variables: { | |
organization: @org_name, | |
number: @project_number, | |
itemsCursor: items_cursor | |
}) | |
if result.errors.any? | |
puts "Error fetching drafts: #{result.errors.messages}" | |
return [] | |
end | |
project = result.data.to_h['organization']['projectV2'] | |
return [] unless project | |
items = project['items']['nodes'] | |
all_items.concat(items) | |
page_info = project['items']['pageInfo'] | |
break unless page_info['hasNextPage'] | |
items_cursor = page_info['endCursor'] | |
end | |
all_items | |
end | |
# Filter out items that are not draft issues or have a field "Status" with a value of "Done" | |
def filter_draft_items(items) | |
items.select do |item| | |
next false unless item['type'] == 'DRAFT_ISSUE' | |
status = item['fieldValues']['nodes'].find do |field_value| | |
field_value.dig('field', 'name') == 'Status' | |
end&.dig('name') | |
status != 'Done' | |
end | |
end | |
def convert_draft_to_issue(item) | |
begin | |
repo = @client.repository("#{@org_name}/#{@repo_name}") | |
if @debug | |
puts "\nAPI Request:" | |
puts JSON.pretty_generate({ | |
itemId: item['id'], | |
repositoryId: repo.node_id | |
}) | |
end | |
result = Client.query(ConvertToIssueMutation, variables: { | |
itemId: item['id'], | |
repositoryId: repo.node_id | |
}) | |
if result.errors.any? | |
puts "❌ Error: #{result.errors.messages}" | |
if @debug | |
puts JSON.pretty_generate(result.to_h) | |
end | |
else | |
converted_item = result.data.to_h.dig('convertProjectV2DraftIssueItemToIssue', 'item') | |
if converted_item && converted_item['content'] | |
puts "✅ Converted to issue: #{converted_item['content']['url']}" | |
else | |
puts "⚠️ Conversion succeeded but no issue URL returned" | |
end | |
if @debug | |
puts "\nAPI Response:" | |
puts JSON.pretty_generate(result.to_h) | |
end | |
end | |
rescue StandardError => e | |
puts "❌ Error: #{e.message}" | |
puts e.backtrace if @debug | |
end | |
end | |
end | |
converter = DraftConverter.new('ORG', 0, 'REPO', dry_run: true, debug: true) | |
converter.convert_all_drafts |
This file contains hidden or 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
source 'https://rubygems.org' | |
gem 'graphql-client' | |
gem 'octokit' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment