2023-09-17 13:34:03 -04:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2023-10-02 14:38:49 -04:00
|
|
|
"fmt"
|
2023-09-17 13:34:03 -04:00
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/andygrunwald/go-jira"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type IssueReader struct {
|
2023-10-02 14:38:49 -04:00
|
|
|
auth jira.BasicAuthTransport //auth data
|
|
|
|
|
client *jira.Client //jira client
|
|
|
|
|
score map[string]int //score information
|
|
|
|
|
settings *DashSettings
|
|
|
|
|
available bool //flag letting us know if the client is currentl available
|
2023-09-17 13:34:03 -04:00
|
|
|
}
|
|
|
|
|
|
2023-10-02 14:38:49 -04:00
|
|
|
func NewIssueReader(user, pass string, s *DashSettings) *IssueReader {
|
2023-09-17 13:34:03 -04:00
|
|
|
ir := &IssueReader{
|
|
|
|
|
auth: jira.BasicAuthTransport{
|
|
|
|
|
Username: strings.TrimSpace(user),
|
|
|
|
|
Password: strings.TrimSpace(pass),
|
|
|
|
|
},
|
2023-10-02 14:38:49 -04:00
|
|
|
score: make(map[string]int),
|
|
|
|
|
settings: s,
|
|
|
|
|
available: false,
|
2023-09-17 13:34:03 -04:00
|
|
|
}
|
|
|
|
|
|
2023-10-02 14:38:49 -04:00
|
|
|
ir.Reconnect()
|
|
|
|
|
return ir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *IssueReader) Reconnect() {
|
|
|
|
|
i.client = nil
|
|
|
|
|
if i.settings.Jira.Url != "" {
|
|
|
|
|
var err error
|
|
|
|
|
i.client, err = jira.NewClient(i.auth.Client(), strings.TrimSpace(i.settings.Jira.Url))
|
|
|
|
|
if err != nil {
|
|
|
|
|
DashLogger.Log(fmt.Sprintf("%v", err))
|
|
|
|
|
i.available = false
|
|
|
|
|
} else {
|
|
|
|
|
i.available = true
|
|
|
|
|
}
|
2023-09-17 13:34:03 -04:00
|
|
|
}
|
2023-10-02 14:38:49 -04:00
|
|
|
}
|
2023-09-17 13:34:03 -04:00
|
|
|
|
2023-10-02 14:38:49 -04:00
|
|
|
// forcibly drops the client (recoverable only through manual reconnection)
|
|
|
|
|
func (i *IssueReader) DropClient() {
|
|
|
|
|
i.client = nil
|
|
|
|
|
i.available = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *IssueReader) IsAvailable() bool {
|
|
|
|
|
return i.available
|
2023-09-17 13:34:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *IssueReader) GetScores() map[string]int {
|
|
|
|
|
return i.score
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *IssueReader) ZeroScores() {
|
|
|
|
|
for k := range i.score {
|
|
|
|
|
i.score[k] = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-02 14:38:49 -04:00
|
|
|
func (i *IssueReader) BuildBountyJQL() string {
|
|
|
|
|
var epiclink string
|
2023-09-17 13:34:03 -04:00
|
|
|
|
2023-10-02 14:38:49 -04:00
|
|
|
epiclink = "("
|
|
|
|
|
for k, epicid := range i.settings.Jira.Epics {
|
|
|
|
|
epiclink = epiclink + `"Epic Link" = ` + epicid
|
|
|
|
|
if k != len(i.settings.Jira.Epics)-1 {
|
|
|
|
|
epiclink = epiclink + " OR "
|
|
|
|
|
}
|
2023-09-17 13:34:03 -04:00
|
|
|
}
|
2023-10-02 14:38:49 -04:00
|
|
|
epiclink = epiclink + ")"
|
|
|
|
|
|
|
|
|
|
query := fmt.Sprintf(`project=%s AND %s AND Status = Accepted`, i.settings.Jira.Project, epiclink)
|
|
|
|
|
return query
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *IssueReader) RefreshScores() map[string]int {
|
2023-09-17 13:34:03 -04:00
|
|
|
|
2023-10-02 14:38:49 -04:00
|
|
|
poIssues, er := i.GetAllIssues(i.client, i.BuildBountyJQL())
|
|
|
|
|
if er != nil {
|
|
|
|
|
DashLogger.Log(fmt.Sprintf("%v\n", er))
|
|
|
|
|
} else {
|
|
|
|
|
i.ZeroScores()
|
|
|
|
|
//run through each to check if we've got bounty points to award
|
|
|
|
|
for _, issue := range poIssues {
|
|
|
|
|
teamId := i.ParseCustomTeamField(issue.Fields.Unknowns[i.settings.Jira.Teamfield])
|
|
|
|
|
i.ExtractScoreFromComments(teamId, issue.Key)
|
|
|
|
|
}
|
2023-09-17 13:34:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return i.GetScores()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// the team field is one of type string after a non-intuitive map decomposition, assert on interface
|
|
|
|
|
func (i *IssueReader) ParseCustomTeamField(teamField interface{}) string {
|
|
|
|
|
var str string = ""
|
|
|
|
|
if teamField != nil {
|
|
|
|
|
str = teamField.([]interface{})[0].(map[string]interface{})["value"].(string)
|
|
|
|
|
}
|
|
|
|
|
return str
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// given a teamid and jira issue, validate potential PO bounty scores
|
|
|
|
|
func (i *IssueReader) ExtractScoreFromComments(teamId string, key string) {
|
|
|
|
|
if i.IsValidTeam(teamId) {
|
|
|
|
|
is, _, err := i.client.Issue.Get(key, nil)
|
|
|
|
|
if err != nil {
|
2023-10-02 14:38:49 -04:00
|
|
|
DashLogger.Fatal(err)
|
2023-09-17 13:34:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
comments := is.Fields.Comments.Comments
|
|
|
|
|
for _, c := range comments {
|
|
|
|
|
if i.CommentAuthorIsPO(c) {
|
|
|
|
|
bp := i.ExtractBountyFromComment(c)
|
|
|
|
|
i.score[teamId] = i.score[teamId] + bp
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check if s exists in Scrum Teams list
|
|
|
|
|
func (i *IssueReader) IsValidTeam(s string) bool {
|
2023-10-02 14:38:49 -04:00
|
|
|
for _, t := range i.settings.Teams {
|
2023-09-17 13:34:03 -04:00
|
|
|
if strings.Compare(strings.ToLower(s), strings.ToLower(t)) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// given a jira.Comment, find and extract the associated PO Bounty points
|
|
|
|
|
func (i *IssueReader) ExtractBountyFromComment(c *jira.Comment) int {
|
|
|
|
|
result := 0
|
2023-10-02 14:38:49 -04:00
|
|
|
re := regexp.MustCompile(i.settings.Criteria.Pattern)
|
2023-09-17 13:34:03 -04:00
|
|
|
if c != nil {
|
|
|
|
|
matches := re.FindStringSubmatch(c.Body)
|
|
|
|
|
|
2023-10-02 14:38:49 -04:00
|
|
|
//our returned matches must be larger than the specified match index
|
|
|
|
|
if len(matches) > i.settings.Criteria.MatchId {
|
2023-09-17 13:34:03 -04:00
|
|
|
//submatch must be an integer, or else we simply don't update the result
|
2023-10-02 14:38:49 -04:00
|
|
|
a, err := strconv.Atoi(matches[i.settings.Criteria.MatchId])
|
2023-09-17 13:34:03 -04:00
|
|
|
if err == nil {
|
|
|
|
|
result = a
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// given a specific jira.Comment, find if it was authored by someone int he POList
|
|
|
|
|
func (i *IssueReader) CommentAuthorIsPO(c *jira.Comment) bool {
|
|
|
|
|
if c != nil {
|
2023-10-02 14:38:49 -04:00
|
|
|
for _, p := range i.settings.ProductOwners {
|
2023-09-17 13:34:03 -04:00
|
|
|
if c.Author.Name == p {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retrieve all issues as per searchString JQL
|
|
|
|
|
func (i *IssueReader) GetAllIssues(client *jira.Client, searchString string) ([]jira.Issue, error) {
|
2023-10-02 14:38:49 -04:00
|
|
|
|
|
|
|
|
if client == nil {
|
|
|
|
|
i.available = false
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-17 13:34:03 -04:00
|
|
|
last := 0
|
|
|
|
|
var issues []jira.Issue
|
|
|
|
|
for {
|
|
|
|
|
opt := &jira.SearchOptions{
|
|
|
|
|
MaxResults: 1000, // Max results can go up to 1000
|
|
|
|
|
StartAt: last,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chunk, resp, err := client.Issue.Search(searchString, opt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
total := resp.Total
|
|
|
|
|
if issues == nil {
|
|
|
|
|
issues = make([]jira.Issue, 0, total)
|
|
|
|
|
}
|
|
|
|
|
issues = append(issues, chunk...)
|
|
|
|
|
last = resp.StartAt + len(chunk)
|
|
|
|
|
if last >= total {
|
|
|
|
|
return issues, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|