Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sprint agile apis #471

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions board.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,14 @@ type SprintsList struct {

// Sprint represents a sprint on Jira agile board
type Sprint struct {
ID int `json:"id" structs:"id"`
Name string `json:"name" structs:"name"`
CompleteDate *time.Time `json:"completeDate" structs:"completeDate"`
EndDate *time.Time `json:"endDate" structs:"endDate"`
StartDate *time.Time `json:"startDate" structs:"startDate"`
OriginBoardID int `json:"originBoardId" structs:"originBoardId"`
Self string `json:"self" structs:"self"`
State string `json:"state" structs:"state"`
ID int `json:"id,omitempty" structs:"id,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
CompleteDate *time.Time `json:"completeDate,omitempty" structs:"completeDate,omitempty"`
EndDate *time.Time `json:"endDate,omitempty" structs:"endDate,omitempty"`
StartDate *time.Time `json:"startDate,omitempty" structs:"startDate,omitempty"`
OriginBoardID int `json:"originBoardId,omitempty" structs:"originBoardId,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
State string `json:"state,omitempty" structs:"state,omitempty"`
}
Comment on lines 67 to 76
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sprint struct has been used only in unmarshaling JSON responses from /boards API call.
So adding omitempty will not break any of current behaviours.


// BoardConfiguration represents a boardConfiguration of a jira board
Expand Down
124 changes: 124 additions & 0 deletions examples/sprint/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package main

import (
"bufio"
"fmt"
"golang.org/x/term"
"log"
"os"
"strconv"
"syscall"
"time"

jira "github.com/andygrunwald/go-jira"
)

// This example implement the behaviour of Jira's "Complete sprint" button.
// It creates a new sprint and close the sprint currently active.
// Then all issues under the closed sprint are moved to the newly created sprint.
func main() {
sc := bufio.NewScanner(os.Stdin)

fmt.Print("Jira URL: ")
sc.Scan()
jiraURL := sc.Text()

fmt.Print("Jira Username: ")
sc.Scan()
username := sc.Text()

fmt.Print("Jira Password: ")
bytePassword, _ := term.ReadPassword(int(syscall.Stdin))
password := string(bytePassword)

fmt.Print("\nJira Board ID : ")
sc.Scan()
boardIDstr := sc.Text()
boardID, err := strconv.Atoi(boardIDstr)
if err != nil {
log.Fatal(err)
}

tp := jira.BasicAuthTransport{
Username: username,
Password: password,
}

client, err := jira.NewClient(tp.Client(), jiraURL)
if err != nil {
log.Println(err)
return
}

// create a new sprint
start := time.Now()
end := start.AddDate(0, 0, 7)
s := jira.Sprint{
Name: "New Sprint",
StartDate: &start,
EndDate: &end,
OriginBoardID: boardID,
}
futureSprint, _, err := client.Sprint.Create(&s)
if err != nil {
log.Fatal(err)
}
log.Println("New sprint created ID:", futureSprint.ID)

// get the sprint currently active
sprints, _, err := client.Board.GetAllSprintsWithOptions(boardID, &jira.GetAllSprintsOptions{
State: "active",
})
if err != nil {
log.Fatal(err)
}
if len(sprints.Values) != 1 {
log.Fatal("Retrieved active sprint list has invalid length")
}
activeSprint := sprints.Values[0]
log.Println("Active sprint retrieved ID:", activeSprint.ID)

// get all non-subtask issues of the active sprint
issues, _, err := client.Sprint.GetIssuesForSprintWithOptions(activeSprint.ID, &jira.GetIssuesForSprintOptions{
Jql: "type NOT IN ('Sub-task')",
})
if err != nil {
log.Fatal(err)
}
log.Println("All non-subtask issues of the active sprint retrieved length:", len(issues))
for _, i := range issues {
log.Printf("\t(%s) %s", i.ID, i.Fields.Summary)
}

// complete the active sprint
completeParam := make(map[string]interface{})
completeParam["state"] = "closed"
resp, err := client.Sprint.UpdateSprint(activeSprint.ID, completeParam)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
log.Println("Active sprint completed")

// move the issues previously under the active sprint to the newly created sprint
var issueIDs []string
for _, i := range issues {
issueIDs = append(issueIDs, i.ID)
}
resp, err = client.Sprint.MoveIssuesToSprint(futureSprint.ID, issueIDs)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
log.Println("The issues from the active sprint have been moved to the new sprint")

// start the newly created sprint
startParam := make(map[string]interface{})
startParam["state"] = "active"
resp, err = client.Sprint.UpdateSprint(futureSprint.ID, startParam)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
log.Println("The new sprint has been started")
}
91 changes: 91 additions & 0 deletions sprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ type IssuesInSprintResult struct {
Issues []Issue `json:"issues"`
}

type GetIssuesForSprintOptions struct {
// Jql filters results to issues that match the given JQL.
Jql string `url:"jql,omitempty"`

SearchOptions
}

// MoveIssuesToSprintWithContext moves issues to a sprint, for a given sprint Id.
// Issues can only be moved to open or active sprints.
// The maximum number of issues that can be moved in one operation is 50.
Expand Down Expand Up @@ -81,6 +88,39 @@ func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, er
return s.GetIssuesForSprintWithContext(context.Background(), sprintID)
}

// GetIssuesForSprintWithOptionsWithContext returns all issues in a sprint, for a given sprint Id.
// The issues are filtered out with the given options.
// This only includes issues that the user has permission to view.
// By default, the returned issues are ordered by rank.
//
// Jira API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint
func (s *SprintService) GetIssuesForSprintWithOptionsWithContext(ctx context.Context, sprintID int, options *GetIssuesForSprintOptions) ([]Issue, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
url, err := addOptions(apiEndpoint, options)
if err != nil {
return nil, nil, err
}

req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil)

if err != nil {
return nil, nil, err
}

result := new(IssuesInSprintResult)
resp, err := s.client.Do(req, result)
if err != nil {
err = NewJiraError(resp, err)
}

return result.Issues, resp, err
}

// GetIssuesForSprintWithOptions wraps GetIssuesForSprintWithOptionsWithContext using the background context.
func (s *SprintService) GetIssuesForSprintWithOptions(sprintID int, options *GetIssuesForSprintOptions) ([]Issue, *Response, error) {
return s.GetIssuesForSprintWithOptionsWithContext(context.Background(), sprintID, options)
}

// GetIssueWithContext returns a full representation of the issue for the given issue key.
// Jira will attempt to identify the issue by the issueIdOrKey path parameter.
// This can be an issue id, or an issue key.
Expand Down Expand Up @@ -123,3 +163,54 @@ func (s *SprintService) GetIssueWithContext(ctx context.Context, issueID string,
func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
return s.GetIssueWithContext(context.Background(), issueID, options)
}

// CreateWithContext creates a sprint from a JSON representation.
//
// Jira API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/sprint-createSprint
func (s *SprintService) CreateWithContext(ctx context.Context, sprint *Sprint) (*Sprint, *Response, error) {
apiEndpoint := "rest/agile/1.0/sprint"
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, sprint)
if err != nil {
return nil, nil, err
}

created := new(Sprint)
resp, err := s.client.Do(req, created)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}

return created, resp, nil
}

// Create wraps CreateWithContext using the background context.
func (s *SprintService) Create(sprint *Sprint) (*Sprint, *Response, error) {
return s.CreateWithContext(context.Background(), sprint)
}

// UpdateSprintWithContext partially updates a sprint from a JSON representation. The sprint is found by ID.
//
// Jira API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/sprint-partiallyUpdateSprint
// Caller must close resp.Body
func (s *SprintService) UpdateSprintWithContext(ctx context.Context, sprintID int, data map[string]interface{}) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%v", sprintID)

req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, data)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}

return resp, nil
}

// UpdateSprint wraps UpdateSprintWithContext using the background context.
// Caller must close resp.Body
func (s *SprintService) UpdateSprint(sprintID int, data map[string]interface{}) (*Response, error) {
return s.UpdateSprintWithContext(context.Background(), sprintID, data)
}
Comment on lines +192 to +216
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've followed the format that caller must close response body

82 changes: 82 additions & 0 deletions sprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"reflect"
"testing"
"time"
)

func TestSprintService_MoveIssuesToSprint(t *testing.T) {
Expand Down Expand Up @@ -67,6 +68,35 @@ func TestSprintService_GetIssuesForSprint(t *testing.T) {

}

func TestSprintService_GetIssuesForSprintWithOptions(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/agile/1.0/sprint/123/issue"

raw, err := ioutil.ReadFile("./mocks/issues_in_sprint.json")
if err != nil {
t.Error(err.Error())
}
testMux.HandleFunc(testAPIEdpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEdpoint)
fmt.Fprint(w, string(raw))
})

issues, _, err := testClient.Sprint.GetIssuesForSprintWithOptions(123, &GetIssuesForSprintOptions{
Jql: "type NOT IN ('Sub-task')",
})
if err != nil {
t.Errorf("Error given: %v", err)
}
if issues == nil {
t.Error("Expected issues in sprint list. Issues list is nil")
}
if len(issues) != 1 {
t.Errorf("Expect there to be 1 issue in the sprint, found %v", len(issues))
}
}

func TestSprintService_GetIssue(t *testing.T) {
setup()
defer teardown()
Expand Down Expand Up @@ -114,3 +144,55 @@ func TestSprintService_GetIssue(t *testing.T) {
t.Errorf("Error given: %s", err)
}
}

func TestSprintService_Create(t *testing.T) {
setup()
defer teardown()

testAPIEndpoint := "/rest/agile/1.0/sprint"

testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, testAPIEndpoint)
fmt.Fprint(w, `{"id":11,"self":"http://www.example.com/jira/rest/agile/1.0/sprint/11","state":"future","name":"Test Sprint","startDate":"2022-08-09T12:34:56.000Z","endDate":"2022-08-16T12:34:56.000Z","originBoardId":17}`)
})

start := time.Date(2022, 8, 9, 12, 34, 56, 0, time.UTC)
end := time.Date(2022, 8, 16, 12, 34, 56, 0, time.UTC)
s := Sprint{
Name: "Test Sprint",
StartDate: &start,
EndDate: &end,
OriginBoardID: 17,
}
sprint, _, err := testClient.Sprint.Create(&s)
if sprint == nil {
t.Error("Expected sprint. Sprint is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}

func TestSprintService_UpdateSprint(t *testing.T) {
setup()
defer teardown()

testAPIEndpoint := "/rest/agile/1.0/sprint/11"

testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, testAPIEndpoint)
fmt.Fprint(w, `{"id":11,"self":"http://www.example.com/jira/rest/agile/1.0/sprint/11","state":"future","name":"Updated Name","startDate":"2022-08-09T12:34:56.000Z","endDate":"2022-08-16T12:34:56.000Z","originBoardId":17}`)
})

data := make(map[string]interface{})
data["name"] = "Updated Name"
resp, err := testClient.Sprint.UpdateSprint(11, data)
if resp == nil {
t.Error("Expected resp. resp is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}