#=============================================================================== # # 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 ( $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 ( $ENV{HARNESS_ACTIVE} || $ENV{TESTRUN} ) { # testing 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} ) { # testing 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 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 validates config is usable =item C adds a card to the new GitLab list params: { {name} , {desc} } =item C returns raw list name from issue tracker =item C returns config mapped list name if mapping exists =item C boolean check if card is in list =item C takes ArrayRef of ids and returns a ArrayRef of ids in complete list but not rejected =item C takes ArrayRef of ids and returns a ArrayRef of ids labelled as rejected =item C 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 Disabled reinstate card that has been archived =back =head1 CONFIGURATION AND ENVIRONMENT Yaml configuration --- plugin: GitLab url: https://gitlab.com/api/v4 private_token: # 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. 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