Group
Extension

Net-Bugzilla-Kanbanize/lib/Net/Bugzilla/Kanbanize.pm

use strict;
use warnings;

package Net::Bugzilla::Kanbanize;
$Net::Bugzilla::Kanbanize::VERSION = '0.007';    # TRIAL
our $VERSION;

#ABSTRACT: Bugzilla and Kanbanize sync tool

use Data::Dumper;

use Net::Bugzilla::Kanbanize;

use LWP::Simple;
use JSON;

use LWP::UserAgent;
use File::HomeDir;

use HTTP::Request;
use URI::Escape;
use List::MoreUtils qw(uniq);

#XXX: https://bugzil.la/970457

sub new {
    my ( $class, $config ) = @_;

    my $self = bless { config => $config }, $class;

    return $self;
}

sub version {
    print STDERR "Version $VERSION\n";
}

#XXX: Wrong, need to be instance variables

my $APIKEY;
my $BOARD_ID;
my $BUGZILLA_TOKEN;
my $ua = LWP::UserAgent->new();

my $total;
my $count;
my $config;

sub run {
    my $self = shift;

    $config = $self->{config};

    $APIKEY = $config->kanbanize_apikey or die "Please configure an apikey";
    $BOARD_ID = $config->kanbanize_boardid
      or die "Please configure a kanbanize_boardid";
    $BUGZILLA_TOKEN = $config->bugzilla_token
      or die "Please configure a bugzilla_token";

    $ua->timeout(15);
    $ua->env_proxy;
    $ua->default_header( 'apikey' => $APIKEY );

    my %bugs;

    if (@ARGV) {
        fill_missing_bugs_info( \%bugs, @ARGV );
    }
    else {
        %bugs = get_bugs();
    }

    $count = scalar keys %bugs;

    print STDERR "Found a total of $count bugs\n";

    $total = 0;

    while ( my ( $bugid, $bug ) = each %bugs ) {
        sync_bug($bug);
    }

    return 1;
}

sub get_bugs {
    my $req =
      HTTP::Request->new( GET =>
"https://bugzilla.mozilla.org/rest/bug?token=$BUGZILLA_TOKEN&include_fields=id,status,whiteboard,summary,assigned_to&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&component=WebOps%3A Bugzilla&component=WebOps%3A Community Platform&component=WebOps%3A Engagement&component=WebOps%3A IT-Managed Tools&component=WebOps%3A Labs&component=WebOps%3A Other&component=WebOps%3A Product Delivery&component=WebOps%3A SSL and Domain Names&product=Infrastructure %26 Operations"
      );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }

    my $data = decode_json( $res->decoded_content );

    my %bugs;

    foreach my $bug ( @{ $data->{bugs} } ) {
        $bugs{ $bug->{id} } = $bug;
    }

    my @marked = get_marked_bugs();
    foreach my $bug (@marked) {
        $bugs{ $bug->{id} } = $bug;
    }

    my @cards = get_bugs_from_all_cards();

    fill_missing_bugs_info( \%bugs, @cards );

    return %bugs;
}

sub fill_missing_bugs_info {
    my ( $bugs, @bugs ) = @_;

    my @missing_bugs;

    foreach my $bugid (@bugs) {
        if ( not exists $bugs->{$bugid} ) {
            push @missing_bugs, $bugid;
        }
    }

    my $missing_bugs_ids = join ",", sort @bugs;

    my $req =
      HTTP::Request->new( GET =>
"https://bugzilla.mozilla.org/rest/bug?token=$BUGZILLA_TOKEN&include_fields=id,status,whiteboard,summary,assigned_to&id=$missing_bugs_ids"
      );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }

    my $data = decode_json( $res->decoded_content );

    my @found_bugs = @{ $data->{bugs} };

    foreach my $bug ( sort @found_bugs ) {
        $bugs->{ $bug->{id} } = $bug;
    }

    return;
}

sub get_marked_bugs {
    my $req =
      HTTP::Request->new( GET =>
"https://bugzilla.mozilla.org/rest/bug?token=$BUGZILLA_TOKEN&include_fields=id,status,whiteboard,summary,assigned_to&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[kanban]"
      );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }

    my $data = decode_json( $res->decoded_content );

    my @bugs = @{ $data->{bugs} };

    return @bugs;
}

sub get_bugs_from_all_cards {

    my $req =
      HTTP::Request->new( POST =>
"http://kanbanize.com/index.php/api/kanbanize/get_all_tasks/boardid/$BOARD_ID/format/json"
      );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }

    my $cards = decode_json( $res->decoded_content );

    my @bugs;
    foreach my $card (@$cards) {
        my $extlink = $card->{extlink};    # XXX: Smarter parsing
        if ( $extlink =~ /(\d+)$/ ) {
            my $bugid = $1;
            push @bugs, $bugid;
        }
    }

    return @bugs;
}

sub sync_bug {
    my $bug = shift;

    #    print STDERR "Bugid: $bug->{id}\n" if $config->verbose;

    $total++;

    if ( not defined $bug ) {
        print STDERR "[$total/$count] No info for bug $bug->{id}\n";
        return;
    }

    if ( $bug->{error} ) {
        print STDERR
          "[$total/$count] No info for bug $bug->{id} (Private bug?)\n";
        return;
    }

    my $summary    = $bug->{summary};
    my $whiteboard = $bug->{whiteboard};

    my $card = parse_whiteboard($whiteboard);

    my $status = "";
    if ( not defined $card ) {
        $card = create_card($bug);

        if ( not $card ) {
            warn "Failed to create card for bug $bug->{id}";
            return;
        }

        update_whiteboard( $bug->{id}, $card->{taskid}, $whiteboard );

        $status .= "[card created]";
    }

    $card = retrieve_card( $card->{taskid} );

    my $cardid = $card->{taskid};

    my @changes = sync_card( $card, $bug );
    if (@changes) {
        $status .= " [synced]";
    }

    if ( $status ne "" or $config->verbose ) {
        printf STDERR "[%4d/%4d] Card %4d - Bug %8d - $summary $status\n",
          $total, $count, $cardid, $bug->{id};
    }

    if (@changes) {
        foreach my $change (@changes) {
            printf STDERR "[%4d/%4d] Card %4d - Bug %8d - $summary ** %s **\n",
              $total, $count, $cardid, $bug->{id}, $change;
        }
    }
}

sub retrieve_card {
    my $card_id = shift;

    my $req =
      HTTP::Request->new( POST =>
"http://kanbanize.com/index.php/api/kanbanize/get_task_details/boardid/$BOARD_ID/taskid/$card_id/format/json"
      );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }

    my $card = decode_json( $res->decoded_content );

    return $card;
}

sub sync_bugzilla {

}

sub sync_card {
    my ( $card, $bug ) = @_;

    my @updated;

    # Check Assignee
    my $bug_assigned  = $bug->{assigned_to};
    my $card_assigned = $card->{assignee};

    if (   defined $card_assigned
        && $card_assigned ne "None"
        && $bug_assigned =~ m/\@.*\.bugs$/ )
    {
        push @updated, "Update bug $bug->{id} assigned to $card_assigned";
        update_bug_assigned( $bug, $card_assigned );
    }
    elsif ($bug_assigned !~ m[^$card_assigned@]
        && $bug_assigned !~ m/\@.*\.bugs$/ )
    {
        push @updated, "Update card assigned to $bug_assigned";

        #print STDERR
        # "bug_asigned: $bug_assigned card_assigned: $card_assigned\n";
        update_card_assigned( $card, $bug_assigned );
    }

    #Check summary (XXX: Formatting assumption here)
    my $bug_summary  = "$bug->{id} - $bug->{summary}";
    my $card_summary = $card->{title};

    if ( $bug_summary ne $card_summary ) {
        update_card_summary( $card, $bug_summary );
        push @updated, "Updated card summary";
    }

    # Check status
    my $bug_status  = $bug->{status};
    my $card_status = $card->{columnname};

    # Close card on bug completion

   #warn "[$bug->{id}] bug: $bug_status card: $card_status" if $config->verbose;

    if ( ( $bug_status eq "RESOLVED" or $bug_status eq "VERIFIED" )
        and $card_status ne "Done" )
    {
        complete_card($card);
        push @updated, "Card completed";
    }

    # XXX: Should we close bug on card completion?
    if ( ( $bug_status ne "RESOLVED" and $bug_status ne "VERIFIED" )
        and $card_status eq "Done" )
    {
        if ( $bug_status eq "REOPENED" ) {
            reopen_card($card);

            #$updated++;
        }
        else {
            warn
"Bug $bug->{id} is not RESOLVED ($bug_status) but card $card->{taskid} says $card_status";
        }
    }

    # Check extlink
    my $bug_link = "https://bugzilla.mozilla.org/show_bug.cgi?id=$bug->{id}";

    if ( $card->{extlink} ne $bug_link ) {
        update_card_extlink( $card, $bug_link );
        push @updated, "Updated external link to bugzilla";
    }

    return @updated;
}

sub reopen_card {
    my $card = shift;

    warn
"[notimplemented] Should be reopening card $card->{taskid} and moving back to ready";

    return;
}

sub complete_card {
    my $card = shift;

    my $taskid = $card->{taskid};

    my $data = {
        boardid => $BOARD_ID,
        taskid  => $taskid,
        column  => 'Done',
    };

    my $req =
      HTTP::Request->new( POST =>
          "http://kanbanize.com/index.php/api/kanbanize/move_task/format/json"
      );

    $req->content( encode_json($data) );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }
}

sub update_card_extlink {
    my ( $card, $extlink ) = @_;

    my $taskid = $card->{taskid};

    my $data = {
        boardid => $BOARD_ID,
        taskid  => $taskid,
        extlink => $extlink,
    };

    my $req =
      HTTP::Request->new( POST =>
          "http://kanbanize.com/index.php/api/kanbanize/edit_task/format/json"
      );

    $req->content( encode_json($data) );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }
}

sub update_bug_assigned {
    my ( $bug, $assigned ) = @_;

    $assigned .= '@mozilla.com';

    my $bugid = $bug->{id};

    my $req =
      HTTP::Request->new(
        PUT => "https://bugzilla.mozilla.org/rest/bug/$bugid" );

    $req->content("assigned_to=$assigned&token=$BUGZILLA_TOKEN");

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }
}

sub update_card_summary {
    my ( $card, $bug_summary ) = @_;

    my $taskid = $card->{taskid};

    my $data = {
        boardid => $BOARD_ID,
        taskid  => $taskid,
        title   => $bug_summary,
    };

    my $req =
      HTTP::Request->new( POST =>
          "http://kanbanize.com/index.php/api/kanbanize/edit_task/format/json"
      );

    $req->content( encode_json($data) );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);    #$res->status_line;
    }
}

sub update_card_assigned {
    my ( $card, $bug_assigned ) = @_;

    my $taskid = $card->{taskid};
    ( my $assignee = $bug_assigned ) =~ s/\@.*//;

    my $req =
      HTTP::Request->new( POST =>
"http://kanbanize.com/index.php/api/kanbanize/edit_task/format/json/boardid/$BOARD_ID/taskid/$taskid/assignee/$assignee"
      );

    $req->content("[]");

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die $res->status_line;
    }

}

sub update_whiteboard {
    my ( $bugid, $cardid, $whiteboard ) = @_;

    my $req =
      HTTP::Request->new(
        PUT => "https://bugzilla.mozilla.org/rest/bug/$bugid" );

    if ( $whiteboard =~ m/\[kanban\]/ ) {
        $whiteboard =~ s/\[kanban\]//;
    }

    $whiteboard =
      "[kanban:https://kanbanize.com/ctrl_board/$BOARD_ID/$cardid] $whiteboard";

    $req->content("whiteboard=$whiteboard&token=$BUGZILLA_TOKEN");

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die $res->status_line;
    }

}

#XXX: https://bugzil.la/970457
sub create_card {
    my $bug = shift;

    my $data = {
        'title'   => "$bug->{id} - $bug->{summary}",
        'extlink' => "https://bugzilla.mozilla.org/show_bug.cgi?id=$bug->{id}",
        'boardid' => $BOARD_ID,
    };

    my $req =
      HTTP::Request->new( POST =>
"http://kanbanize.com/index.php/api/kanbanize/create_new_task/format/json"
      );

    $req->content( encode_json($data) );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        warn "can't create card:" . $res->status_line;
        die Dumper($res);
        return;
    }

    my $card = decode_json( $res->decoded_content );

    $card->{taskid} = $card->{id};

    move_card( $card, 'Pending Triage' );

    return $card;
}

sub move_card {
    my ( $card, $lane ) = @_;

    my $data = {
        boardid => $BOARD_ID,
        taskid  => $card->{taskid},
        column  => 'Backlog',
        lane    => $lane,
    };

    my $req =
      HTTP::Request->new( POST =>
          "http://kanbanize.com/index.php/api/kanbanize/move_task/format/json"
      );

    $req->content( encode_json($data) );

    my $res = $ua->request($req);

    if ( !$res->is_success ) {
        die Dumper($res);
    }

}

sub get_bug_info {
    my $bugid = shift;
    my $data =
      get("https://bugzilla.mozilla.org/rest/bug/$bugid?token=$BUGZILLA_TOKEN");

    if ( not $data ) {
        warn "Failed getting Bug info for Bug $bugid from bugzilla\n";
        return { id => $bugid, error => "No Data" };
    }

    print STDERR "Retrieving info for Bug $bugid from bugzilla\n"
      if $config->verbose;

    $data = decode_json($data);

    return $data->{bugs}[0];
}

sub parse_whiteboard {
    my $whiteboard = shift;

    my $card;

    if ( $whiteboard =~
        m{\[kanban:https://kanbanize.com/ctrl_board/(\d+)/(\d+)\]} )
    {
        my $boardid = $1;
        my $cardid  = $2;

        $card = { taskid => $cardid };
    }

    return $card;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Net::Bugzilla::Kanbanize - Bugzilla and Kanbanize sync tool

=head1 VERSION

version 0.007

=head1 SYNOPSIS

Kanbanize Bugzilla Sync Tool

=head2 version

prints current version to STDERR

=head1 METHODS

=head2 new

This method does something experimental.

=head2 version

This method returns a reason.

=head1 AUTHOR

Philippe M. Chiasson <gozer@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2014 by Philippe M. Chiasson.

This is free software, licensed under:

  Mozilla Public License Version 2.0

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.