Repository: ankitpokhrel/jira-cli
Pull request #276 (2022-01-23 20:40:36)

feat: Allow to set fixVersions on issue creation
- feat: Allow to set fixVersions on issue creation
- Update docs



Patch:
diff --git a/README.md b/README.md
index 5c65fa5..d99b1fe 100644
--- a/README.md
+++ b/README.md
@@ -278,8 +278,8 @@ The `create` command lets you create an issue.
 # Create an issue using interactive prompt
 $ jira issue create
 
-# Pass required parameters to skip prompt or use --no-input option
-$ jira issue create -tBug -s"New Bug" -yHigh -lbug -lurgent -b"Bug description"
+# Pass required parameters and use --no-input option to skip prompt
+$ jira issue create -tBug -s"New Bug" -yHigh -lbug -lurgent -b"Bug description" --fix-version v2.0 --no-input
 ```
 
 To attach epic to an issue on creation, you can use `-P/--parent` field. We call it parent because the semantics of epic
diff --git a/internal/cmd/epic/create/create.go b/internal/cmd/epic/create/create.go
index b5db471..a4adf66 100644
--- a/internal/cmd/epic/create/create.go
+++ b/internal/cmd/epic/create/create.go
@@ -101,9 +101,10 @@ func create(cmd *cobra.Command, _ []string) {
 				if len(ans.Metadata) > 0 {
 					qs = cmdcommon.GetMetadataQuestions(ans.Metadata)
 					ans := struct {
-						Priority   string
-						Labels     string
-						Components string
+						Priority    string
+						Labels      string
+						Components  string
+						FixVersions string
 					}{}
 					err := survey.Ask(qs, &ans)
 					cmdutil.ExitIfError(err)
@@ -117,6 +118,9 @@ func create(cmd *cobra.Command, _ []string) {
 					if len(ans.Components) > 0 {
 						params.components = strings.Split(ans.Components, ",")
 					}
+					if len(ans.FixVersions) > 0 {
+						params.fixVersions = strings.Split(ans.FixVersions, ",")
+					}
 				}
 			}
 		}
@@ -127,14 +131,15 @@ func create(cmd *cobra.Command, _ []string) {
 		defer s.Stop()
 
 		cr := jira.CreateRequest{
-			Project:    project,
-			IssueType:  jira.IssueTypeEpic,
-			Summary:    params.summary,
-			Body:       params.body,
-			Priority:   params.priority,
-			Labels:     params.labels,
-			Components: params.components,
-			EpicField:  viper.GetString("epic.name"),
+			Project:     project,
+			IssueType:   jira.IssueTypeEpic,
+			Summary:     params.summary,
+			Body:        params.body,
+			Priority:    params.priority,
+			Labels:      params.labels,
+			Components:  params.components,
+			FixVersions: params.fixVersions,
+			EpicField:   viper.GetString("epic.name"),
 		}
 		if projectType != jira.ProjectTypeNextGen {
 			cr.Name = params.name
@@ -234,16 +239,17 @@ func (cc *createCmd) isMandatoryParamsMissing() bool {
 }
 
 type createParams struct {
-	name       string
-	summary    string
-	body       string
-	priority   string
-	assignee   string
-	labels     []string
-	components []string
-	template   string
-	noInput    bool
-	debug      bool
+	name        string
+	summary     string
+	body        string
+	priority    string
+	assignee    string
+	labels      []string
+	components  []string
+	fixVersions []string
+	template    string
+	noInput     bool
+	debug       bool
 }
 
 func parseFlags(flags query.FlagParser) *createParams {
@@ -268,6 +274,9 @@ func parseFlags(flags query.FlagParser) *createParams {
 	components, err := flags.GetStringArray("component")
 	cmdutil.ExitIfError(err)
 
+	fixVersions, err := flags.GetStringArray("fix-version")
+	cmdutil.ExitIfError(err)
+
 	template, err := flags.GetString("template")
 	cmdutil.ExitIfError(err)
 
@@ -278,15 +287,16 @@ func parseFlags(flags query.FlagParser) *createParams {
 	cmdutil.ExitIfError(err)
 
 	return &createParams{
-		name:       name,
-		summary:    summary,
-		body:       body,
-		priority:   priority,
-		assignee:   assignee,
-		labels:     labels,
-		components: components,
-		template:   template,
-		noInput:    noInput,
-		debug:      debug,
+		name:        name,
+		summary:     summary,
+		body:        body,
+		priority:    priority,
+		assignee:    assignee,
+		labels:      labels,
+		components:  components,
+		fixVersions: fixVersions,
+		template:    template,
+		noInput:     noInput,
+		debug:       debug,
 	}
 }
diff --git a/internal/cmd/issue/create/create.go b/internal/cmd/issue/create/create.go
index a3bd0a9..6a9caab 100644
--- a/internal/cmd/issue/create/create.go
+++ b/internal/cmd/issue/create/create.go
@@ -98,9 +98,10 @@ func create(cmd *cobra.Command, _ []string) {
 				if len(ans.Metadata) > 0 {
 					qs := cmdcommon.GetMetadataQuestions(ans.Metadata)
 					ans := struct {
-						Priority   string
-						Labels     string
-						Components string
+						Priority    string
+						Labels      string
+						Components  string
+						FixVersions string
 					}{}
 					err := survey.Ask(qs, &ans)
 					cmdutil.ExitIfError(err)
@@ -114,6 +115,9 @@ func create(cmd *cobra.Command, _ []string) {
 					if len(ans.Components) > 0 {
 						params.components = strings.Split(ans.Components, ",")
 					}
+					if len(ans.FixVersions) > 0 {
+						params.fixVersions = strings.Split(ans.FixVersions, ",")
+					}
 				}
 			}
 		}
@@ -132,6 +136,7 @@ func create(cmd *cobra.Command, _ []string) {
 			Priority:       params.priority,
 			Labels:         params.labels,
 			Components:     params.components,
+			FixVersions:    params.fixVersions,
 			EpicField:      viper.GetString("epic.link"),
 		}
 		cr.ForProjectType(projectType)
@@ -346,6 +351,7 @@ type createParams struct {
 	assignee       string
 	labels         []string
 	components     []string
+	fixVersions    []string
 	template       string
 	noInput        bool
 	debug          bool
@@ -376,6 +382,9 @@ func parseFlags(flags query.FlagParser) *createParams {
 	components, err := flags.GetStringArray("component")
 	cmdutil.ExitIfError(err)
 
+	fixVersions, err := flags.GetStringArray("fix-version")
+	cmdutil.ExitIfError(err)
+
 	template, err := flags.GetString("template")
 	cmdutil.ExitIfError(err)
 
@@ -394,6 +403,7 @@ func parseFlags(flags query.FlagParser) *createParams {
 		assignee:       assignee,
 		labels:         labels,
 		components:     components,
+		fixVersions:    fixVersions,
 		template:       template,
 		noInput:        noInput,
 		debug:          debug,
diff --git a/internal/cmdcommon/create.go b/internal/cmdcommon/create.go
index afa6c51..e40b99b 100644
--- a/internal/cmdcommon/create.go
+++ b/internal/cmdcommon/create.go
@@ -31,6 +31,7 @@ And, this field is mandatory when creating a sub-task.`)
 	cmd.Flags().StringP("assignee", "a", "", prefix+" assignee (email or display name)")
 	cmd.Flags().StringArrayP("label", "l", []string{}, prefix+" labels")
 	cmd.Flags().StringArrayP("component", "C", []string{}, prefix+" components")
+	cmd.Flags().StringArray("fix-version", []string{}, "Release info (fixVersions)")
 	cmd.Flags().StringP("template", "T", "", "Path to a file to read body/description from")
 	cmd.Flags().Bool("web", false, "Open in web browser after successful creation")
 	cmd.Flags().Bool("no-input", false, "Disable prompt for non-required fields")
@@ -59,7 +60,7 @@ func GetMetadata() []*survey.Question {
 			Name: "metadata",
 			Prompt: &survey.MultiSelect{
 				Message: "What would you like to add?",
-				Options: []string{"Priority", "Components", "Labels"},
+				Options: []string{"Priority", "Components", "Labels", "FixVersions"},
 			},
 		},
 	}
@@ -92,6 +93,14 @@ func GetMetadataQuestions(cat []string) []*survey.Question {
 					Help:    "Comma separated list of labels. For eg: backend,urgent",
 				},
 			})
+		case "FixVersions":
+			qs = append(qs, &survey.Question{
+				Name: "fixversions",
+				Prompt: &survey.Input{
+					Message: "Fix Versions",
+					Help:    "Comma separated list of fixVersions. For eg: v1.0-beta,v2.0",
+				},
+			})
 		}
 	}
 
diff --git a/pkg/jira/create.go b/pkg/jira/create.go
index 848d132..32aacaa 100644
--- a/pkg/jira/create.go
+++ b/pkg/jira/create.go
@@ -28,6 +28,7 @@ type CreateRequest struct {
 	Priority       string
 	Labels         []string
 	Components     []string
+	FixVersions    []string
 	// EpicField is the dynamic epic field name
 	// that changes per jira installation.
 	EpicField string
@@ -91,61 +92,7 @@ func (c *Client) create(req *CreateRequest, ver string) (*CreateResponse, error)
 	return &out, err
 }
 
-type createFields struct {
-	Project struct {
-		Key string `json:"key"`
-	} `json:"project"`
-	IssueType struct {
-		Name string `json:"name"`
-	} `json:"issuetype"`
-	Parent *struct {
-		Key string `json:"key"`
-	} `json:"parent,omitempty"`
-	Name        string      `json:"name,omitempty"`
-	Summary     string      `json:"summary"`
-	Description interface{} `json:"description,omitempty"`
-	Priority    *struct {
-		Name string `json:"name,omitempty"`
-	} `json:"priority,omitempty"`
-	Labels     []string `json:"labels,omitempty"`
-	Components []struct {
-		Name string `json:"name,omitempty"`
-	} `json:"components,omitempty"`
-
-	epicField string
-}
-
-type createFieldsMarshaler struct {
-	M createFields
-}
-
-// MarshalJSON is a custom marshaler to handle dynamic field.
-func (cfm createFieldsMarshaler) MarshalJSON() ([]byte, error) {
-	m, err := json.Marshal(cfm.M)
-	if err != nil {
-		return m, err
-	}
-
-	var temp interface{}
-	if err := json.Unmarshal(m, &temp); err != nil {
-		return nil, err
-	}
-	dm := temp.(map[string]interface{})
-
-	if cfm.M.epicField != "" {
-		dm[cfm.M.epicField] = dm["name"]
-	}
-	delete(dm, "name")
-
-	return json.Marshal(dm)
-}
-
-type createRequest struct {
-	Update struct{}              `json:"update"`
-	Fields createFieldsMarshaler `json:"fields"`
-}
-
-func (c *Client) getRequestData(req *CreateRequest) *createRequest {
+func (*Client) getRequestData(req *CreateRequest) *createRequest {
 	if req.Labels == nil {
 		req.Labels = []string{}
 	}
@@ -201,6 +148,75 @@ func (c *Client) getRequestData(req *CreateRequest) *createRequest {
 		}
 		data.Fields.M.Components = comps
 	}
+	if len(req.FixVersions) > 0 {
+		versions := make([]struct {
+			Name string `json:"name,omitempty"`
+		}, 0, len(req.FixVersions))
+
+		for _, v := range req.FixVersions {
+			versions = append(versions, struct {
+				Name string `json:"name,omitempty"`
+			}{v})
+		}
+		data.Fields.M.FixVersions = versions
+	}
 
 	return &data
 }
+
+type createRequest struct {
+	Update struct{}              `json:"update"`
+	Fields createFieldsMarshaler `json:"fields"`
+}
+
+type createFields struct {
+	Project struct {
+		Key string `json:"key"`
+	} `json:"project"`
+	IssueType struct {
+		Name string `json:"name"`
+	} `json:"issuetype"`
+	Parent *struct {
+		Key string `json:"key"`
+	} `json:"parent,omitempty"`
+	Name        string      `json:"name,omitempty"`
+	Summary     string      `json:"summary"`
+	Description interface{} `json:"description,omitempty"`
+	Priority    *struct {
+		Name string `json:"name,omitempty"`
+	} `json:"priority,omitempty"`
+	Labels     []string `json:"labels,omitempty"`
+	Components []struct {
+		Name string `json:"name,omitempty"`
+	} `json:"components,omitempty"`
+	FixVersions []struct {
+		Name string `json:"name,omitempty"`
+	} `json:"fixVersions,omitempty"`
+
+	epicField string
+}
+
+type createFieldsMarshaler struct {
+	M createFields
+}
+
+// MarshalJSON is a custom marshaler to handle dynamic field.
+func (cfm createFieldsMarshaler) MarshalJSON() ([]byte, error) {
+	m, err := json.Marshal(cfm.M)
+	if err != nil {
+		return m, err
+	}
+
+	var temp interface{}
+	if err := json.Unmarshal(m, &temp); err != nil {
+		return nil, err
+	}
+	dm := temp.(map[string]interface{})
+
+	if cfm.M.epicField != "" {
+		dm[cfm.M.epicField] = dm["name"]
+	}
+	delete(dm, "name")
+
+	return json.Marshal(dm)
+}


Test patch:
diff --git a/pkg/jira/create_test.go b/pkg/jira/create_test.go
index e52965f..5ff4c6c 100644
--- a/pkg/jira/create_test.go
+++ b/pkg/jira/create_test.go
@@ -45,7 +45,7 @@ func (c *createTestServer) statusCode(code int) {
 func TestCreate(t *testing.T) {
 	expectedBody := `{"update":{},"fields":{"project":{"key":"TEST"},"issuetype":{"name":"Bug"},` +
 		`"summary":"Test bug","description":"Test description","priority":{"name":"Normal"},"labels":["test","dev"],` +
-		`"components":[{"name":"BE"},{"name":"FE"}]}}`
+		`"components":[{"name":"BE"},{"name":"FE"}],"fixVersions":[{"name":"v2.0"},{"name":"v2.1-hotfix"}]}}`
 	testServer := createTestServer{code: 201}
 	server := testServer.serve(t, expectedBody)
 	defer server.Close()
@@ -53,13 +53,14 @@ func TestCreate(t *testing.T) {
 	client := NewClient(Config{Server: server.URL}, WithTimeout(3))
 
 	requestData := CreateRequest{
-		Project:    "TEST",
-		IssueType:  "Bug",
-		Summary:    "Test bug",
-		Body:       "Test description",
-		Priority:   "Normal",
-		Labels:     []string{"test", "dev"},
-		Components: []string{"BE", "FE"},
+		Project:     "TEST",
+		IssueType:   "Bug",
+		Summary:     "Test bug",
+		Body:        "Test description",
+		Priority:    "Normal",
+		Labels:      []string{"test", "dev"},
+		Components:  []string{"BE", "FE"},
+		FixVersions: []string{"v2.0", "v2.1-hotfix"},
 	}
 	actual, err := client.CreateV2(&requestData)
 	assert.NoError(t, err)