ref: 6b44a1578f84079dca6b0896926ca2134374e1f5
parent: 84844799a48e02168bd927b0b3c518b248ba0180
author: Kenny Levinsen <w@kl.wtf>
date: Mon Jun 6 20:08:54 EDT 2016
Direct status assignment Whenever a status assignment is made, the workflow graph is now generated, and the shortest path to the target status is found. Unfortunately, this seems to be the only way to just assign a status in JIRA, and even requires using private API's.
--- a/jira.go
+++ b/jira.go
@@ -274,7 +274,7 @@
switch file {
case "key":
return nil
- case "status", "transitions":
+ case "transitions":
sf.Lock()
str := string(sf.Content)
sf.Unlock()
@@ -281,6 +281,52 @@
str = strings.Replace(str, "\n", "", -1)
return TransitionIssue(jc, issue.Key, str)
+
+ case "status":
+ sf.Lock()
+ str := string(sf.Content)
+ sf.Unlock()
+ str = strings.Replace(str, "\n", "", -1)
+
+ issue, err := GetIssue(jc, fmt.Sprintf("%s-%s", iw.project, iw.issueNo))
+ if err != nil {
+ log.Printf("Could not fetch issue: %v", err)
+ return err
+ }
+ if issue.Fields == nil {
+ log.Printf("Issue missing fields")
+ return errors.New("oops")
+ }
+ if issue.Fields.Status == nil {
+ log.Printf("Issue missing status")
+ return errors.New("oops2")
+ }
+
+ wg, err := BuildWorkflow2(jc, iw.project, issue.Fields.Type.ID)
+ if err != nil {
+ log.Printf("Could not build workflow: %v", err)
+ return err
+ }
+
+ p, err := wg.Path(issue.Fields.Status.Name, str, 10000)
+ if err != nil {
+ log.Printf("Could not find path: %v", err)
+ log.Printf("Workflow: \n%s\n", wg.Dump())
+ return err
+ }
+
+ log.Printf("Workflow path: %s", strings.Join(p, ", "))
+
+ for _, s := range p {
+ err = TransitionIssue(jc, issue.Key, s)
+ if err != nil {
+ log.Printf("Could not transition issue: %v", err)
+ return err
+ }
+ }
+
+ return nil
+
default:
sf.Lock()
str := string(sf.Content)
--- a/utils.go
+++ b/utils.go
@@ -228,7 +228,9 @@
}
type CommentResult struct {
- Comments []jira.Comment `json:"comments,omitempty"`
+ Comments []struct {
+ ID string `json:"id"`
+ } `json:"comments,omitempty"`
}
func GetCommentsForIssue(jc *jira.Client, issue string) ([]string, error) {
--- /dev/null
+++ b/workflow.go
@@ -1,0 +1,279 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/andygrunwald/go-jira"
+)
+
+type thing struct {
+ Name string `json:"name"`
+}
+
+func BuildWorkflow1(jc *jira.Client, project, issueTypeNo string) (*WorkflowGraph, error) {
+ req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA 1: %v", err)
+ }
+
+ var t thing
+ if _, err = jc.Do(req, &t); err != nil {
+ return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+ }
+
+ req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/workflow?workflowName=%s", url.QueryEscape(t.Name)), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA 3: %v", err)
+ }
+
+ var wr WorkflowResponse1
+ if _, err = jc.Do(req, &wr); err != nil {
+ return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+ }
+
+ var wg WorkflowGraph
+ wg.Build1(&wr)
+ return &wg, nil
+}
+
+func BuildWorkflow2(jc *jira.Client, project, issueTypeNo string) (*WorkflowGraph, error) {
+ req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA 1: %v", err)
+ }
+
+ var t thing
+ if _, err = jc.Do(req, &t); err != nil {
+ return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+ }
+
+ req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/workflowDesigner/latest/workflows?name=%s", url.QueryEscape(t.Name)), nil)
+ if err != nil {
+ return nil, fmt.Errorf("could not query JIRA 3: %v", err)
+ }
+
+ req.Header.Set("X-Atlassian-Token", "nocheck")
+
+ var wr WorkflowResponse2
+ if _, err = jc.Do(req, &wr); err != nil {
+ return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+ }
+
+ var wg WorkflowGraph
+ wg.Build2(&wr)
+ return &wg, nil
+}
+
+type WorkflowResponse2 struct {
+ Layout struct {
+ Statuses []struct {
+ ID string `json:"statusId"`
+ TransitionID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Initial bool `json:"initial"`
+ } `json:"statuses"`
+ Transitions []struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ SourceID string `json:"sourceId"`
+ TargetID string `json:"targetId"`
+ ActionID int `json:"actionId"`
+ Global bool `json:"globalTransition"`
+ Looped bool `json:"loopedTransition"`
+ } `json:"transitions"`
+ } `json:"layout"`
+}
+
+type WorkflowResponse1 struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ ID int `json:"id"`
+ DisplayName string `json:"displayName"`
+ Admin bool `json:"admin"`
+ Sources []struct {
+ FromStatus WorkflowStatus `json:"fromStatus"`
+ Targets []struct {
+ ToStatus WorkflowStatus `json:"toStatus"`
+ TransitionName string
+ } `json:"targets"`
+ } `json:"sources"`
+}
+
+type WorkflowStatus struct {
+ StatusCategory struct {
+ Sequence int `json:"sequence"`
+ PrimaryAlias string `json:"primaryAlias"`
+ TranslatedName string `json:"translatedName"`
+ ColorName string `json:"colorName"`
+ Aliases []string `json:"aliases"`
+ Name string `json:"name"`
+ Key string `json:"key"`
+ ID int `json:"id"`
+ } `json:"statusCategory"`
+ IconURL string `json:"iconUrl"`
+ Description string `json:"description"`
+ Name string `json:"name"`
+ ID string `json:"id"`
+}
+
+func (wf *WorkflowStatus) Status() *Status {
+ return &Status{
+ Name: wf.Name,
+ ID: wf.ID,
+ Description: wf.Description,
+ }
+}
+
+type Status struct {
+ Name string
+ Description string
+ ID string
+ Edges []StatusEdge
+}
+
+type StatusEdge struct {
+ Name string
+ Status *Status
+}
+
+type WorkflowGraph struct {
+ verteces map[string]*Status
+}
+
+func (wg *WorkflowGraph) Build2(wr *WorkflowResponse2) {
+ if wg.verteces == nil {
+ wg.verteces = make(map[string]*Status)
+ }
+
+ local := make(map[string]*Status)
+ layout := wr.Layout
+
+ for _, s := range layout.Statuses {
+ l := &Status{
+ Name: s.Name,
+ Description: s.Description,
+ ID: s.ID,
+ }
+
+ wg.verteces[s.Name] = l
+ local[s.TransitionID] = l
+ }
+
+ for _, t := range layout.Transitions {
+ a := local[t.SourceID]
+ b := local[t.TargetID]
+ edge := StatusEdge{
+ Name: t.Name,
+ Status: b,
+ }
+ if t.Global {
+ for _, v := range local {
+ v.Edges = append(v.Edges, edge)
+ }
+ } else {
+ a.Edges = append(a.Edges, edge)
+ }
+ }
+}
+
+func (wg *WorkflowGraph) Build1(wr *WorkflowResponse1) {
+ if wg.verteces == nil {
+ wg.verteces = make(map[string]*Status)
+ }
+ for _, elem := range wr.Sources {
+ name := elem.FromStatus.Name
+ fromStatus, exists := wg.verteces[name]
+ if !exists {
+ fromStatus = elem.FromStatus.Status()
+ wg.verteces[fromStatus.Name] = fromStatus
+ }
+
+ for _, target := range elem.Targets {
+ targetName := target.ToStatus.Name
+ targetStatus, exists := wg.verteces[targetName]
+ if !exists {
+ targetStatus = target.ToStatus.Status()
+ wg.verteces[targetStatus.Name] = targetStatus
+ }
+ targetEdge := StatusEdge{
+ Name: target.TransitionName,
+ Status: targetStatus,
+ }
+
+ fromStatus.Edges = append(fromStatus.Edges, targetEdge)
+ }
+ }
+}
+
+func (wg *WorkflowGraph) Dump() string {
+ var ss string
+ for _, v := range wg.verteces {
+ var s string
+ for _, e := range v.Edges {
+ s += fmt.Sprintf("%s (%s), ", e.Status.Name, e.Name)
+ }
+ ss += fmt.Sprintf("Status: %s, edges: %s\n", v.Name, s)
+ }
+
+ return ss
+}
+
+type path struct {
+ from *path
+ edge StatusEdge
+}
+
+func (wg *WorkflowGraph) Path(A, B string, limit int) ([]string, error) {
+ statusA := wg.verteces[A]
+ statusB := wg.verteces[B]
+
+ if statusA == nil || statusB == nil {
+ return nil, errors.New("no such status")
+ }
+
+ var search []path
+ for _, edge := range statusA.Edges {
+ search = append(search, path{edge: edge})
+ }
+
+ for len(search) > 0 {
+ limit--
+ if limit == 0 {
+ break
+ }
+ p := search[0]
+ search = search[1:]
+
+ // FOUND!
+ if p.edge.Status == statusB {
+ var s []string
+ start := &p
+
+ for {
+ s = append(s, start.edge.Name)
+ if start.from == nil {
+ break
+ }
+
+ start = start.from
+ }
+
+ for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
+ s[i], s[j] = s[j], s[i]
+ }
+
+ return s, nil
+ }
+
+ // Add the edges to the search.
+ for _, edge := range p.edge.Status.Edges {
+ // log.Printf("%s -> %s", p.edge.Status.Name, edge.Name)
+ search = append(search, path{from: &p, edge: edge})
+ }
+ }
+
+ return nil, errors.New("path not found")
+}