How to move CPAN RT tickets to Github

Most of my new CPAN modules use Github for issue tracking because of the nice integration with pull requests. I recently wanted to migrate an older distribution to using Github, but didn't want to track tickets in two places.

A while ago, Yanick Champoux wrote Bandying tickets from RT to Github, which looked like exactly what I wanted. Unfortunately, it used the old Github API, which is no longer supported.

I've fixed it up and am sharing it here. If you have your Github user and an OAuth token in your git config file as github.user and github.token, it will use those as defaults. Ditto if you have your PAUSE credentials in a .pause file already for uploading.

If you don't have a github OAuth token, get one with this script, which is right out of the Net::Github SYNOPSIS:

#!/usr/bin/env perl
use v5.10;
use strict;
use warnings;
use autodie;

use Net::GitHub::V3;
use IO::Prompt::Tiny qw/prompt/;

my $user = prompt( "Github username:" );
my $pass = prompt( "Github password:" );
my $gh = Net::GitHub::V3->new( login => $user, pass => $pass );
my $oauth = $gh->oauth;
my $o = $oauth->create_authorization( {
    scopes => ['user', 'public_repo', 'repo', 'gist'], # just ['public_repo']
    note   => 'Net::GitHub',
} );
say $o->{token};

Here is the script that does the work. There are a bunch of heuristics that are very specific to my way of working (like getting the distribution name out of a dist.ini file), but those only set prompt defaults so you should be able to use this as is or tweak it to your needs.

#!/usr/bin/env perl
use v5.10;
use strict;
use warnings;
use Carp;
use IO::Prompt::Tiny qw/prompt/;
use Net::GitHub;
use Path::Tiny;
use RT::Client::REST::Ticket;
use RT::Client::REST;
use Syntax::Keyword::Junction qw/any/;

sub _git_config {
    my $key = shift;
    chomp( my $value = `git config --get $key` );
    croak "Unknown $key" unless $value;
    return $value;

my $pause_rc = path( $ENV{HOME}, ".pause" );
my %pause;

sub _pause_rc {
    my $key = shift;
    if ( $pause_rc->exists && !%pause ) {
        %pause = split " ", $pause_rc->slurp;
    return $pause{$key} // '';

sub _dist_name {
    # dzil only for now
    my $dist = path("dist.ini");
    if ( $dist->exists ) {
        my ($first) = $dist->lines( { count => 1 } );
        my ($name) = $first =~ m/name\s*=\s*(\S+)/;
        return $name if defined $name;
    return '';

my $github_user       = prompt( "github user: ",  _git_config("github.user") );
my $github_token      = prompt( "github token: ", _git_config("github.token") );
my $github_repo_owner = prompt( "repo owner: ",   $github_user );
my $github_repo       = prompt( "repo name: ",    path(".")->absolute->basename );

my $rt_user = prompt( "PAUSE ID: ", _pause_rc("user") );
my $rt_password =
  _pause_rc("password") ? _pause_rc("password") : prompt("PAUSE password: ");
my $rt_dist = prompt( "RT dist name: ", _dist_name() );

my $gh = Net::GitHub->new( access_token => $github_token );
$gh->set_default_user_repo( $github_repo_owner, $github_repo );
my $gh_issue = $gh->issue;

my $rt = RT::Client::REST->new( server => '' );
    username => $rt_user,
    password => $rt_password

# see which tickets we already have on the github side
my @gh_issues =
  map { /\[rt\.cpan\.org #(\d+)\]/ }
  map { $_->{title} }
  $gh_issue->repos_issues( $github_repo_owner, $github_repo, { state => 'open' } );

my @rt_tickets = $rt->search(
    type  => 'ticket',
    query => qq{
        Queue = '$rt_dist' 
        ( Status = 'new' or Status = 'open' )

for my $id (@rt_tickets) {

    if ( any(@gh_issues) eq $id ) {
        say "ticket #$id already on github";

    # get the information from RT
    my $ticket = RT::Client::REST::Ticket->new(
        rt => $rt,
        id => $id,

    # we just want the first transaction, which
    # has the original ticket description
    my $desc = $ticket->transactions->get_iterator->()->content;

    $desc =~ s/^/    /gms;

    my $subject = $ticket->subject;

    my $isu = $gh_issue->create_issue(
            "title" => "$subject [ #$id]",
            "body"  => "$id\n\n$desc",

    say "ticket #$id ($subject) copied to github";

A more sophisticated version would import each of the RT comments as github comments, but this was enough for me to use Github's issue tracker and not lose track of things that were previously in RT.

This entry was posted in git, perl programming and tagged , , , , . Bookmark the permalink. Both comments and trackbacks are currently closed.


  1. Posted March 5, 2013 at 6:30 pm | Permalink

    Sweet. I also had updated the script in my env repo ( earlier this january after rjbs poked me. I've updated the blog entry to point to that update and yours.

  2. Posted March 5, 2013 at 10:35 pm | Permalink

    I worked with this some today. I think I've got support for exporting/importing comments working, but it needs to be tested. It doesn't support attachments, or transactions which have not "content", like changing the ticket the owner.

    Before I run it with a real queue, I want to ask: What about the "requestor email"? This is an important bit of information, and as best as I can tell, Github offers no way to preserve it, at least not in a way that would cause a comment to seamlessly go to them.

    My understanding is that the workaround is to continue to keep the RT ticket around, and reply back to them through that ticket, and encourage them to continue the conversation on Github if they are interested and able. Is that right? I'll have to consider this complication as I consider importing 50+ issues.

    • Posted March 6, 2013 at 8:49 am | Permalink

      Very cool!

      Unfortunately, I think the requestor email get lost. I don't think GH lets you create a commit with someone else as an owner.

      Maybe email the requestor with the new GH issue link and tell them how to watch the issue? (Click the watch button or whatever.)

      I think it will be lossy, regardless, but I'm willing to live with that for many (but not all) dists because the contribution benefits are worth it.

    • Michael Peters
      Posted March 6, 2013 at 10:24 am | Permalink

      Mark, maybe your script could have an option to close the RT ticket with a comment pointing people to the new GitHub ticket. This would have RT send them a notice and if they were still concerned they could follow that ticket on github.

      • Posted March 6, 2013 at 12:01 pm | Permalink

        Good idea. I was going to figure out how to do a "bulk update", but scripting it may be just about as easy.

        I have a few "stalled" tickets on that would also be nice to port over, and currently aren't handled. I'm sure those will be worth automating for me.

        Regarding handling the requestor email:

        - I bet I can at least find it and stick it in the GitHub issue for reference, even if I can't reply to it directly.

        - In the long term, having a Github account seems like no higher of a bar than having an RT account. I don't think the create-by-email interface of RT is used that much, as it it's spam liability.

  3. Posted March 5, 2013 at 10:36 pm | Permalink

    I meant to post my updated version. Here it is: