#===============================================================================
#
# DESCRIPTION: plugin for LIMS::Local::IssueTracker
# requires two gitlab boards. 'RFCs' and 'Test' both containing 'Backlog' and
# 'Completed' lists
#
#===============================================================================
package LIMS::Local::IssueTracker::Plugin::GitLab;
our $VERSION = "0.01";
use Modern::Perl;
use utf8;
use Carp;
use GitLab::API::v4;
use Moose::Role;
use namespace::autoclean;
use Data::Printer;
use JSON;
use Try::Tiny;
has 'tracker' => (
is => 'ro',
isa => 'GitLab::API::v4',
lazy => '1',
builder => '_build_gitlab'
);
has 'project' => (
is => 'ro',
isa => 'Int',
lazy => 1,
builder => '_build_project'
);
with 'LIMS::Local::Role::Trackable';
sub _build_gitlab {
my $self = shift;
# my $key = $self->config->{key};
my $url = $self->config->{url};
my $private_token = $self->config->{private_token};
my $t = GitLab::API::v4->new(url => $url, private_token => $private_token);
# my $t = GitLab::API::v4->new(url => 'https://gitlab.com', private_token => 'GGDQo53SAL4FbMFcu1mb');
return $t;
}
# set project id depending on whether we are testing
# use gitlab api and config file to determine
sub _build_project {
my $self = shift;
my $project_id; # gitlabs internal id for project
if ( not $ENV{DEBUG_GITLAB_LIVE} && ($ENV{HARNESS_ACTIVE} || $ENV{TESTRUN} )) {
my $p = $self->tracker->user_projects($self->config->{board}->{user});
($project_id) = map {$_->{id}}grep {$_->{name} =~ /Testproject/} @$p;
}
else {
my $p = $self->tracker->group_projects($self->config->{board}->{group});
($project_id) = map {$_->{id}}grep {$_->{name} =~ /HILIS4/} @$p;
}
return $project_id;
}
sub check_config {
my $self = shift;
my $config = $self->config;
# mandatory config fields
my @mandatory_issue_lists = qw/new complete rejected/;
if (
( grep { not exists $config->{$_} } qw(url private_token) )
or ( grep { not exists $config->{board}->{lists}->{$_}->{name} }
@mandatory_issue_lists )
or ( grep { not exists $config->{board}->{lists}->{$_}->{id} }
@mandatory_issue_lists )
)
{
return 0;
}
if ( not $ENV{DEBUG_GITLAB_LIVE} && ($ENV{HARNESS_ACTIVE} || $ENV{TESTRUN} )) {
if ( not exists $config->{board}->{user} ) {
return 0;
}
}
else { # not testing
if ( not exists $config->{board}->{group} ) {
return 0;
}
}
return 1;
}
sub create_issue {
my $self = shift;
my $args = shift;
my $tracker = $self->tracker;
my $new_cards_list = $self->config->{board}->{lists}->{new}->{name};
my $res;
# first check the colour is valid
my $colour = $self->config->{colours}->{ $args->{reason} };
croak "Tracker error adding label to card: $!" unless $colour;
# now make the issue
try {
$res = $tracker->create_issue(
$self->project,
{
#labels => [$new_cards_list, $colour],
labels => [$new_cards_list, $args->{reason}],
title => $args->{name},
description => $args->{desc}
}
);
}
catch { croak "Tracker error posting new card: $_"; };
return $res->{iid};
}
# returns raw status from trello (not the config list names)
sub get_status {
my $self = shift;
my $id = shift;
croak "no id" unless $id;
my $tracker = $self->tracker;
my $res = try { $tracker->issue($self->project, $id) } catch {
croak "Tracker error: can't get card $id :$_"};
if (grep {/rejected/} @{$res->{labels}}){
return 'rejected';
}
else {
return $res->{state};
}
}
# convert back to generic list name from config
sub get_status_config_name {
my $self = shift;
my $card_id = shift;
my $list = $self->get_status($card_id);
my ($key) =
grep { $self->config->{board}->{lists}->{$_}->{name} eq $list }
keys $self->config->{board}->{lists};
return $key;
}
# have we finished with it?
sub is_complete_or_rejected {
my $self = shift;
my $id = shift;
my @list_names = qw/complete rejected/;
my $status = grep { $self->is_status( $_, $id ) } @list_names;
if ($status) {
return 1;
}
else { return 0; }
}
# boolean check if card is in list
sub is_status {
my $self = shift;
my $list_name = shift;
my $id = shift;
my $list = $self->config->{board}->{lists}->{$list_name}->{name};
my $status = $self->get_status($id);
if ( $status eq $list ) {
return 1;
}
else { return 0; }
}
# takes ArrayRef of ids and returns a ArrayRef of ids in Completed list
sub list_complete {
my $self = shift;
my $ids = shift;
my @complete = grep { $self->is_status( 'complete', $_ ) } @$ids;
return \@complete;
}
# takes ArrayRef of ids and returns a ArrayRef of ids in Rejected list
sub list_rejected {
my $self = shift;
my $ids = shift;
my @rejected = grep { $self->is_status( 'rejected', $_ ) } @$ids;
return \@rejected;
}
sub move_card {
my $self = shift;
my $tracker = $self->tracker;
my $card_id = shift;
my $destination = shift || croak "requires destination: $!";
try {
$tracker->edit_issue( $self->project, $card_id,
{ state_event => $self->config->{board}->{lists}->{$destination}->{id} } );
if ( $destination eq "rejected" ) { # just a label on the closed board issues
my @labels = @{ $tracker->issue($self->project, $card_id)->{labels} };
push @labels, $destination;
$tracker->edit_issue( $self->project, $card_id,
# { labels => [ $self->config->{board}->{lists}->{$destination}->{name} ] }
{ labels => \@labels }
);
}
} catch {
croak "Tracker error: can't move card $card_id :$_";
}
}
# change status of card from closed: True to closed:False
# needed for #RfC::recreate_issue
sub unarchive_card {
croak "No archive concept in GitLab";
my $self = shift;
my $tracker = $self->trello;
my $card_id = shift;
my $res = $tracker->put( "cards/$card_id", { closed => 'false' } );
croak "Trello error: can't unarchive card $card_id :$!"
if $res->code != 200;
}
# archive_issues archives all cards from a given list
# @ARGS "listname" looks up list name from config and uses that id in api
sub archive_issues {
croak "No archive concept in GitLab";
my $self = shift;
my $tracker = $self->trello;
my $list = shift || croak "requires list name $!";
my $list_id = $self->config->{board}->{lists}->{$list}->{id};
my $res = $tracker->post("lists/$list_id/ArchiveAllCards");
croak "Trello error: can't archive cards from list $list :$!"
if $res->code != 200;
}
#returns a list of iids of all issues
sub all_iids {
my $self = shift;
my $tracker = $self->tracker;
my $issues = $tracker->issues($self->project);
my @iids = map {$_->{iid}} @$issues;
return @iids;
}
# delete issues as a clean up operation for testing
# only works in test env for safety
sub delete_all_issues {
my $self = shift;
my $tracker = $self->tracker;
if ( $ENV{HARNESS_ACTIVE} || $ENV{TESTRUN} ) {
my $issues = $tracker->issues($self->project);
my @iids = map {$_->{iid}} @$issues;
for my $id (@iids) {
$tracker->delete_issue($self->project, $id);
}
}
else {
croak "Running delete_all_issues() outside of a test is dangerous";
}
}
1;
=head1 NAME
LIMS::Local::IssueTracker::Plugin::GitLab
=head1 VERSION
$VERSION
=head1 SYNOPSIS
use LIMS::Local::IssueTracker;
$self->load_plugin( LIMS::Local::IssueTracker::Plugin::GitLab );
croak "not trackable" unless $self->does("LIMS::Local::Role::Trackable");
=head1 DESCRIPTION
Plugin for LIMS::Local::IssueTracker. Implements LIMS::Local::Role::Trackable
=head1 DIAGNOSTICS
=over 12
=item C<all_iids>
retrieves list of ids from issues board
returns array
=item C<archive_issues>
Disabled
removes cards from list
param Str listname (from yaml)
TODO wont work in GitLab as not worked out archiving yet
maybe can just label as archived and have a hidden list for archived closed issues
=item C<check_config>
validates config is usable
=item C<create_issue>
adds a card to the new GitLab list
params: { {name} , {desc} }
=item C<delete_all_issues>
deletes all issues
param none
Note: only in testmode
=item C<get_status>
returns raw list name from issue tracker
=item C<get_status_config_name>
returns config mapped list name if mapping exists
=item C<is_status>
boolean check if card is in list
=item C<list_complete>
takes ArrayRef of ids and returns a ArrayRef of ids in complete list but not rejected
=item C<list_rejected>
takes ArrayRef of ids and returns a ArrayRef of ids labelled as rejected
=item C<move_card>
transfer cards between lists
params cardid, listname
listname is from config list names
can only transfer to valid lists from config (no long_grass)
=item C<unarchive_card>
Disabled
reinstate card that has been archived
=back
=head1 CONFIGURATION AND ENVIRONMENT
Yaml configuration
---
plugin: GitLab
url: https://gitlab.com/api/v4
private_token: <FROM GITLAB>
# irrelevant in gitlab
colours:
new feature: green
modification: yellow
fix error: red
change menu: blue
board:
live:
name: HILIS4
group: hmds_software
id:
lists:
new:
name: rfc
id:
complete:
name: close # need to change status instead
id:
rejected:
name: rejected
id:
test:
user: hmds.lth
name: Testproject
id:
lists:
new:
name: rfc
id: reopen
complete:
name: closed
id: close
rejected:
name: rejected
id: close
=head1 DEPENDENCIES
GitLab::API::v4
Try::Tiny
Moose::Role
=head1 INCOMPATIBILITIES
=head1 BUGS AND LIMITATIONS
GitLab Issues have no concept of archiving so the archive functions croak for now
In the future they may be implemented so they are deleted from GitLab but recovered via the database if that is useful
=head1 AUTHOR
Garry Quested (garry.quested@nhs.net)
=head1 LICENSE AND COPYRIGHT
Copyright (c) 2019 Garry Quested (garry.quested@nhs.net). All rights reserved.
This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
=cut