Skip to content

Instantly share code, notes, and snippets.

@iangmaia
Created April 28, 2025 16:53
Show Gist options
  • Save iangmaia/6d75d796531f46fd4d4b49bcc6070873 to your computer and use it in GitHub Desktop.
Save iangmaia/6d75d796531f46fd4d4b49bcc6070873 to your computer and use it in GitHub Desktop.
Convert GitHub project draft issues to Issues in a repo
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
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