Ruby/ Faraday - Trac to Codebase
Ruby script for importing Trac tickets saved as CSV's into CodebaseHQ - thanks to Ian Dickinson for the code.
#!/usr/bin/env ruby
#
# Quick-and-dirty script to upload tickets from a Trac instance to the trackers
# available in CodebaseHQ, using their API.
#
# Process: export the Trac tickets as a CSV. Suppose the project that we're going
# to write to in CodebaseHQ is 'foo', then either:
#
# ruby import-cbhq-tickets.rb -d foo-trac.csv foo # dry-run only
# ruby import-cbhq-tickets.rb -a foo-trac.csv foo # actual update
#
# Script assumes that your CodebaseHQ user-ID and API token are in the
# environment variables CODEBASE_USER and CODEBASE_API_TOKEN respectively.
#
# For details on the codebaseHQ API, see: http://support.codebasehq.com/kb
#
# Dependencies:
# gem install multi_xml faraday_middleware nokogiri
require 'logger'
require 'faraday_middleware'
require 'multi_xml'
require 'nokogiri'
require 'csv'
raise "Usage: ruby import-cbhq-tickets.rb (-d | -a) <file> <project-name>" unless ARGV.length == 3
raise "Usage: ruby import-cbhq-tickets.rb (-d | -a) <file> <project-name>" unless ["-a", "-d"].include?( ARGV[0] )
$user=ENV['CODEBASE_USER']
$token=ENV['CODEBASE_API_TOKEN']
# Return true if this is just a dry run, doesn't make any changes
def dry_run?
ARGV[0] == "-d"
end
# Return the input file, or raise an error
def input_file
f = ARGV[1]
raise "No such file: #{f}" unless File.exist?( f )
f
end
# Return the project name that we're updating
def project_name
ARGV[2]
end
# Create a connection to CodebaseHQ's API
conn = Faraday.new 'https://api3.codebasehq.com/', ssl: {verify: false} do |c|
c.use Faraday::Response::Logger, Logger.new('faraday.log')
c.use FaradayMiddleware::FollowRedirects, limit: 3
c.use Faraday::Response::RaiseError # raise exceptions on 40x, 50x responses
c.use Faraday::Adapter::NetHttp
c.response :xml, :content_type => /\bxml$/
end
conn.headers[:user_agent] = 'Ruby script'
conn.basic_auth($user, $token)
# Download some info about the project, so that we can do the mappings
$statuses = conn.get( "/#{project_name}/tickets/statuses" ).body
$priorities = conn.get( "/#{project_name}/tickets/priorities" ).body
$categories = conn.get( "/#{project_name}/tickets/categories" ).body
$assigned_users = conn.get( "/#{project_name}/assignments" ).body
$milestones = conn.get( "/#{project_name}/milestones" ).body
if dry_run?
puts "This is what we got back from codebaseHQ:"
puts "Status: #{$statuses.inspect}"
puts "Priority: #{$priorities.inspect}"
puts "Category: #{$categories.inspect}"
puts "Milesone: #{$milestones.inspect}"
end
$ticket_type_translations = {
"defect" => "bug",
"enhancement" => "enhancement",
"task" => "task"
}
$priority_translations = {
"critical" => "Critical",
"major" => "High",
"minor" => "Normal",
"trivial" => "Low",
"irritating" => "Normal"
}
$status_translations = {
"closed" => "Completed",
"accepted" => "Accepted",
"new" => "New",
"assigned" => "Accepted",
"reopened" => "In Progress"
}
# return the user id for a given user
def user_id_for_email( email )
u = $assigned_users["users"].find {|user| user["email_address"] == email}
raise "Unknown user email #{email}" unless u
u["id"]
end
def find_id_by_name( list, prompt, name, translation_table = nil )
if translation_table
n = name
name = translation_table[name]
raise "No translation for #{prompt} #{n}" unless name
end
c = list.find {|item| item["name"] == name }
raise "Unknown #{prompt} #{name}" unless c
c["id"]
end
# Build an XML structure to represent the ticket for codebase, using
# info from a row of the CSV of Trac data
def new_ticket_structure( row )
builder = Nokogiri::XML::Builder.new( encoding: "UTF-8" ) do |xml|
xml.ticket do
xml.summary {xml.cdata row["summary"]}
xml.description {xml.cdata row["description"]} if row["description"]
xml.send( :'ticket-type', $ticket_type_translations[ row["type"] ])
xml.send( :'reporter-id', user_id_for_email( row["reporter"] ))
xml.send( :'assignee-id', user_id_for_email( row["owner"] ))
xml.send( :'category-id', find_id_by_name( $categories["ticketing_categories"], "category", row["component"] ))
xml.send( :'priority-id', find_id_by_name( $priorities["ticketing_priorities"], "priority", row["priority"], $priority_translations ))
xml.send( :'status-id', find_id_by_name( $statuses["ticketing_statuses"], "status", row["status"], $status_translations ))
if row["milestone"]
xml.send( :'milestone-id', find_id_by_name( $milestones["ticketing_milestone"], "milestone", row["milestone"] ))
end
end
end
builder.to_xml
end
# and here we start processing the CSV of exported Trac data
csv = CSV.open( input_file, {headers: true, encoding: "UTF-8"} )
csv.each_with_index do |row,n|
payload = new_ticket_structure( row )
begin
if dry_run?
puts payload.inspect
else
conn.post do |req|
req.url "/#{project_name}/tickets"
req.headers['Content-Type'] = 'application/xml'
req.body = payload
end
end
rescue
puts "Failed update on row #{n}"
puts "#{payload}\n-----------------"
end
end