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.