Repository: ankitpokhrel/jira-cli
Pull request #642 (2023-07-07 21:49:45)

feat: affects version
@ankitpokhrel I cloned the code for fix-versions. As suggested in #587 the versions tag in the json payload could be used to set the affects version field.

I tested this for create issue only, against a cloud jira instance.

I'm not an expert in this API, (for example, in the fields.json testdata I'm unaware of this use, so not sure how/what to extend), so not in a position to say whether the PR is good or not, regardless, it should act as a starting point for this feature. 

Hope this helps,
Damian.


Patch:
diff --git a/internal/cmd/epic/create/create.go b/internal/cmd/epic/create/create.go
index 2477889..1119732 100644
--- a/internal/cmd/epic/create/create.go
+++ b/internal/cmd/epic/create/create.go
@@ -98,18 +98,19 @@ func create(cmd *cobra.Command, _ []string) {
 		defer s.Stop()
 
 		cr := jira.CreateRequest{
-			Project:      project,
-			IssueType:    jira.IssueTypeEpic,
-			Summary:      params.Summary,
-			Body:         params.Body,
-			Reporter:     params.Reporter,
-			Assignee:     params.Assignee,
-			Priority:     params.Priority,
-			Labels:       params.Labels,
-			Components:   params.Components,
-			FixVersions:  params.FixVersions,
-			CustomFields: params.CustomFields,
-			EpicField:    viper.GetString("epic.name"),
+			Project:         project,
+			IssueType:       jira.IssueTypeEpic,
+			Summary:         params.Summary,
+			Body:            params.Body,
+			Reporter:        params.Reporter,
+			Assignee:        params.Assignee,
+			Priority:        params.Priority,
+			Labels:          params.Labels,
+			Components:      params.Components,
+			FixVersions:     params.FixVersions,
+			AffectsVersions: params.AffectsVersions,
+			CustomFields:    params.CustomFields,
+			EpicField:       viper.GetString("epic.name"),
 		}
 		if projectType != jira.ProjectTypeNextGen {
 			cr.Name = params.Name
@@ -229,6 +230,9 @@ func parseFlags(flags query.FlagParser) *cmdcommon.CreateParams {
 	fixVersions, err := flags.GetStringArray("fix-version")
 	cmdutil.ExitIfError(err)
 
+	affectsVersions, err := flags.GetStringArray("affects-version")
+	cmdutil.ExitIfError(err)
+
 	custom, err := flags.GetStringToString("custom")
 	cmdutil.ExitIfError(err)
 
@@ -242,18 +246,19 @@ func parseFlags(flags query.FlagParser) *cmdcommon.CreateParams {
 	cmdutil.ExitIfError(err)
 
 	return &cmdcommon.CreateParams{
-		Name:         name,
-		Summary:      summary,
-		Body:         body,
-		Priority:     priority,
-		Reporter:     reporter,
-		Assignee:     assignee,
-		Labels:       labels,
-		Components:   components,
-		FixVersions:  fixVersions,
-		CustomFields: custom,
-		Template:     template,
-		NoInput:      noInput,
-		Debug:        debug,
+		Name:            name,
+		Summary:         summary,
+		Body:            body,
+		Priority:        priority,
+		Reporter:        reporter,
+		Assignee:        assignee,
+		Labels:          labels,
+		Components:      components,
+		FixVersions:     fixVersions,
+		AffectsVersions: affectsVersions,
+		CustomFields:    custom,
+		Template:        template,
+		NoInput:         noInput,
+		Debug:           debug,
 	}
 }
diff --git a/internal/cmd/issue/create/create.go b/internal/cmd/issue/create/create.go
index 5212998..d212268 100644
--- a/internal/cmd/issue/create/create.go
+++ b/internal/cmd/issue/create/create.go
@@ -99,19 +99,20 @@ func create(cmd *cobra.Command, _ []string) {
 		defer s.Stop()
 
 		cr := jira.CreateRequest{
-			Project:        project,
-			IssueType:      params.IssueType,
-			ParentIssueKey: params.ParentIssueKey,
-			Summary:        params.Summary,
-			Body:           params.Body,
-			Reporter:       params.Reporter,
-			Assignee:       params.Assignee,
-			Priority:       params.Priority,
-			Labels:         params.Labels,
-			Components:     params.Components,
-			FixVersions:    params.FixVersions,
-			CustomFields:   params.CustomFields,
-			EpicField:      viper.GetString("epic.link"),
+			Project:         project,
+			IssueType:       params.IssueType,
+			ParentIssueKey:  params.ParentIssueKey,
+			Summary:         params.Summary,
+			Body:            params.Body,
+			Reporter:        params.Reporter,
+			Assignee:        params.Assignee,
+			Priority:        params.Priority,
+			Labels:          params.Labels,
+			Components:      params.Components,
+			FixVersions:     params.FixVersions,
+			AffectsVersions: params.AffectsVersions,
+			CustomFields:    params.CustomFields,
+			EpicField:       viper.GetString("epic.link"),
 		}
 		cr.ForProjectType(projectType)
 		cr.ForInstallationType(installation)
@@ -343,6 +344,9 @@ func parseFlags(flags query.FlagParser) *cmdcommon.CreateParams {
 	fixVersions, err := flags.GetStringArray("fix-version")
 	cmdutil.ExitIfError(err)
 
+	affectsVersions, err := flags.GetStringArray("affects-version")
+	cmdutil.ExitIfError(err)
+
 	custom, err := flags.GetStringToString("custom")
 	cmdutil.ExitIfError(err)
 
@@ -356,19 +360,20 @@ func parseFlags(flags query.FlagParser) *cmdcommon.CreateParams {
 	cmdutil.ExitIfError(err)
 
 	return &cmdcommon.CreateParams{
-		IssueType:      issueType,
-		ParentIssueKey: parentIssueKey,
-		Summary:        summary,
-		Body:           body,
-		Priority:       priority,
-		Assignee:       assignee,
-		Labels:         labels,
-		Reporter:       reporter,
-		Components:     components,
-		FixVersions:    fixVersions,
-		CustomFields:   custom,
-		Template:       template,
-		NoInput:        noInput,
-		Debug:          debug,
+		IssueType:       issueType,
+		ParentIssueKey:  parentIssueKey,
+		Summary:         summary,
+		Body:            body,
+		Priority:        priority,
+		Assignee:        assignee,
+		Labels:          labels,
+		Reporter:        reporter,
+		Components:      components,
+		FixVersions:     fixVersions,
+		AffectsVersions: affectsVersions,
+		CustomFields:    custom,
+		Template:        template,
+		NoInput:         noInput,
+		Debug:           debug,
 	}
 }
diff --git a/internal/cmd/issue/edit/edit.go b/internal/cmd/issue/edit/edit.go
index c48e43c..c497718 100644
--- a/internal/cmd/issue/edit/edit.go
+++ b/internal/cmd/issue/edit/edit.go
@@ -127,6 +127,12 @@ func edit(cmd *cobra.Command, args []string) {
 	}
 	fixVersions = append(fixVersions, params.fixVersions...)
 
+	affectsVersions := make([]string, 0, len(issue.Fields.AffectsVersions)+len(params.affectsVersions))
+	for _, fv := range issue.Fields.AffectsVersions {
+		affectsVersions = append(affectsVersions, fv.Name)
+	}
+	affectsVersions = append(affectsVersions, params.affectsVersions...)
+
 	err = func() error {
 		s := cmdutil.Info("Updating an issue...")
 		defer s.Stop()
@@ -142,14 +148,15 @@ func edit(cmd *cobra.Command, args []string) {
 		}
 
 		edr := jira.EditRequest{
-			ParentIssueKey: parent,
-			Summary:        params.summary,
-			Body:           body,
-			Priority:       params.priority,
-			Labels:         labels,
-			Components:     components,
-			FixVersions:    fixVersions,
-			CustomFields:   params.customFields,
+			ParentIssueKey:  parent,
+			Summary:         params.summary,
+			Body:            body,
+			Priority:        params.priority,
+			Labels:          labels,
+			Components:      components,
+			FixVersions:     fixVersions,
+			AffectsVersions: affectsVersions,
+			CustomFields:    params.customFields,
 		}
 		if configuredCustomFields, err := cmdcommon.GetConfiguredCustomFields(); err == nil {
 			cmdcommon.ValidateCustomFields(edr.CustomFields, configuredCustomFields)
@@ -187,10 +194,11 @@ func getAnswers(params *editParams, issue *jira.Issue) {
 			if len(ans.Metadata) > 0 {
 				qs := getMetadataQuestions(ans.Metadata, issue)
 				ans := struct {
-					Priority    string
-					Labels      string
-					Components  string
-					FixVersions string
+					Priority        string
+					Labels          string
+					Components      string
+					FixVersions     string
+					AffectsVersions string
 				}{}
 				err := survey.Ask(qs, &ans)
 				cmdutil.ExitIfError(err)
@@ -207,6 +215,9 @@ func getAnswers(params *editParams, issue *jira.Issue) {
 				if len(ans.FixVersions) > 0 {
 					params.fixVersions = strings.Split(ans.FixVersions, ",")
 				}
+				if len(ans.AffectsVersions) > 0 {
+					params.affectsVersions = strings.Split(ans.AffectsVersions, ",")
+				}
 			}
 		}
 	}
@@ -289,17 +300,18 @@ func (ec *editCmd) askQuestions(issue *jira.Issue, originalBody string) error {
 }
 
 type editParams struct {
-	issueKey     string
-	summary      string
-	body         string
-	priority     string
-	assignee     string
-	labels       []string
-	components   []string
-	fixVersions  []string
-	customFields map[string]string
-	noInput      bool
-	debug        bool
+	issueKey        string
+	summary         string
+	body            string
+	priority        string
+	assignee        string
+	labels          []string
+	components      []string
+	fixVersions     []string
+	affectsVersions []string
+	customFields    map[string]string
+	noInput         bool
+	debug           bool
 }
 
 func parseArgsAndFlags(flags query.FlagParser, args []string, project string) *editParams {
@@ -324,6 +336,9 @@ func parseArgsAndFlags(flags query.FlagParser, args []string, project string) *e
 	fixVersions, err := flags.GetStringArray("fix-version")
 	cmdutil.ExitIfError(err)
 
+	affectsVersions, err := flags.GetStringArray("affects-version")
+	cmdutil.ExitIfError(err)
+
 	custom, err := flags.GetStringToString("custom")
 	cmdutil.ExitIfError(err)
 
@@ -334,17 +349,18 @@ func parseArgsAndFlags(flags query.FlagParser, args []string, project string) *e
 	cmdutil.ExitIfError(err)
 
 	return &editParams{
-		issueKey:     cmdutil.GetJiraIssueKey(project, args[0]),
-		summary:      summary,
-		body:         body,
-		priority:     priority,
-		assignee:     assignee,
-		labels:       labels,
-		components:   components,
-		fixVersions:  fixVersions,
-		customFields: custom,
-		noInput:      noInput,
-		debug:        debug,
+		issueKey:        cmdutil.GetJiraIssueKey(project, args[0]),
+		summary:         summary,
+		body:            body,
+		priority:        priority,
+		assignee:        assignee,
+		labels:          labels,
+		components:      components,
+		fixVersions:     fixVersions,
+		affectsVersions: affectsVersions,
+		customFields:    custom,
+		noInput:         noInput,
+		debug:           debug,
 	}
 }
 
@@ -356,6 +372,11 @@ func getMetadataQuestions(meta []string, issue *jira.Issue) []*survey.Question {
 		fixVersions = append(fixVersions, fv.Name)
 	}
 
+	affectsVersions := make([]string, 0, len(issue.Fields.AffectsVersions))
+	for _, fv := range issue.Fields.AffectsVersions {
+		affectsVersions = append(affectsVersions, fv.Name)
+	}
+
 	for _, m := range meta {
 		switch m {
 		case "Priority":
@@ -389,6 +410,15 @@ func getMetadataQuestions(meta []string, issue *jira.Issue) []*survey.Question {
 					Default: strings.Join(fixVersions, ","),
 				},
 			})
+		case "AffectsVersions":
+			qs = append(qs, &survey.Question{
+				Name: "affectsversions",
+				Prompt: &survey.Input{
+					Message: "Affects Versions",
+					Help:    "Comma separated list of affectsVersions. For eg: v1.0-beta,v2.0",
+					Default: strings.Join(affectsVersions, ","),
+				},
+			})
 		}
 	}
 
@@ -407,6 +437,7 @@ func setFlags(cmd *cobra.Command) {
 	cmd.Flags().StringArrayP("label", "l", []string{}, "Append labels")
 	cmd.Flags().StringArrayP("component", "C", []string{}, "Replace components")
 	cmd.Flags().StringArray("fix-version", []string{}, "Add/Append release info (fixVersions)")
+	cmd.Flags().StringArray("affects-version", []string{}, "Add/Append release info (affectsVersions)")
 	cmd.Flags().StringToString("custom", custom, "Edit custom fields")
 	cmd.Flags().Bool("web", false, "Open in web browser after successful update")
 	cmd.Flags().Bool("no-input", false, "Disable prompt for non-required fields")
diff --git a/internal/cmdcommon/create.go b/internal/cmdcommon/create.go
index d086976..47ef993 100644
--- a/internal/cmdcommon/create.go
+++ b/internal/cmdcommon/create.go
@@ -23,21 +23,22 @@ const (
 
 // CreateParams holds parameters for create command.
 type CreateParams struct {
-	Name           string
-	IssueType      string
-	ParentIssueKey string
-	Summary        string
-	Body           string
-	Priority       string
-	Reporter       string
-	Assignee       string
-	Labels         []string
-	Components     []string
-	FixVersions    []string
-	CustomFields   map[string]string
-	Template       string
-	NoInput        bool
-	Debug          bool
+	Name            string
+	IssueType       string
+	ParentIssueKey  string
+	Summary         string
+	Body            string
+	Priority        string
+	Reporter        string
+	Assignee        string
+	Labels          []string
+	Components      []string
+	FixVersions     []string
+	AffectsVersions []string
+	CustomFields    map[string]string
+	Template        string
+	NoInput         bool
+	Debug           bool
 }
 
 // SetCreateFlags sets flags supported by create command.
@@ -61,6 +62,7 @@ And, this field is mandatory when creating a sub-task.`)
 	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().StringArray("affects-version", []string{}, "Release info (affectsVersions)")
 	cmd.Flags().StringToString("custom", custom, "Set custom fields")
 	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")
@@ -90,7 +92,7 @@ func GetMetadata() []*survey.Question {
 			Name: "metadata",
 			Prompt: &survey.MultiSelect{
 				Message: "What would you like to add?",
-				Options: []string{"Priority", "Components", "Labels", "FixVersions"},
+				Options: []string{"Priority", "Components", "Labels", "FixVersions", "AffectsVersions"},
 			},
 		},
 	}
@@ -131,6 +133,14 @@ func GetMetadataQuestions(cat []string) []*survey.Question {
 					Help:    "Comma separated list of fixVersions. For eg: v1.0-beta,v2.0",
 				},
 			})
+		case "AffectsVersions":
+			qs = append(qs, &survey.Question{
+				Name: "affectsversions",
+				Prompt: &survey.Input{
+					Message: "Affects Versions",
+					Help:    "Comma separated list of affectsVersions. For eg: v1.0-beta,v2.0",
+				},
+			})
 		}
 	}
 
@@ -159,10 +169,11 @@ func HandleNoInput(params *CreateParams) error {
 			if len(ans.Metadata) > 0 {
 				qs := GetMetadataQuestions(ans.Metadata)
 				ans := struct {
-					Priority    string
-					Labels      string
-					Components  string
-					FixVersions string
+					Priority        string
+					Labels          string
+					Components      string
+					FixVersions     string
+					AffectsVersions string
 				}{}
 				err := survey.Ask(qs, &ans)
 				if err != nil {
@@ -181,6 +192,9 @@ func HandleNoInput(params *CreateParams) error {
 				if len(ans.FixVersions) > 0 {
 					params.FixVersions = strings.Split(ans.FixVersions, ",")
 				}
+				if len(ans.AffectsVersions) > 0 {
+					params.AffectsVersions = strings.Split(ans.AffectsVersions, ",")
+				}
 			}
 		}
 	}
diff --git a/pkg/jira/create.go b/pkg/jira/create.go
index 6785a97..0f4edbd 100644
--- a/pkg/jira/create.go
+++ b/pkg/jira/create.go
@@ -24,15 +24,16 @@ type CreateRequest struct {
 	IssueType string
 	// ParentIssueKey is required when creating a sub-task for classic project.
 	// This can also be used to attach epic for next-gen project.
-	ParentIssueKey string
-	Summary        string
-	Body           interface{} // string in v1/v2 and adf.ADF in v3
-	Reporter       string
-	Assignee       string
-	Priority       string
-	Labels         []string
-	Components     []string
-	FixVersions    []string
+	ParentIssueKey  string
+	Summary         string
+	Body            interface{} // string in v1/v2 and adf.ADF in v3
+	Reporter        string
+	Assignee        string
+	Priority        string
+	Labels          []string
+	Components      []string
+	FixVersions     []string
+	AffectsVersions []string
 	// EpicField is the dynamic epic field name
 	// that changes per jira installation.
 	EpicField string
@@ -202,6 +203,18 @@ func (*Client) getRequestData(req *CreateRequest) *createRequest {
 		}
 		data.Fields.M.FixVersions = versions
 	}
+	if len(req.AffectsVersions) > 0 {
+		versions := make([]struct {
+			Name string `json:"name,omitempty"`
+		}, 0, len(req.AffectsVersions))
+
+		for _, v := range req.AffectsVersions {
+			versions = append(versions, struct {
+				Name string `json:"name,omitempty"`
+			}{v})
+		}
+		data.Fields.M.AffectsVersions = versions
+	}
 	constructCustomFields(req.CustomFields, req.configuredCustomFields, &data)
 
 	return &data
@@ -287,7 +300,9 @@ type createFields struct {
 	FixVersions []struct {
 		Name string `json:"name,omitempty"`
 	} `json:"fixVersions,omitempty"`
-
+	AffectsVersions []struct {
+		Name string `json:"name,omitempty"`
+	} `json:"versions,omitempty"`
 	epicField    string
 	customFields customField
 }
diff --git a/pkg/jira/edit.go b/pkg/jira/edit.go
index 323fd2b..6c96c33 100644
--- a/pkg/jira/edit.go
+++ b/pkg/jira/edit.go
@@ -19,14 +19,15 @@ type EditResponse struct {
 // EditRequest struct holds request data for edit request.
 // Setting an Assignee requires an account ID.
 type EditRequest struct {
-	IssueType      string
-	ParentIssueKey string
-	Summary        string
-	Body           string
-	Priority       string
-	Labels         []string
-	Components     []string
-	FixVersions    []string
+	IssueType       string
+	ParentIssueKey  string
+	Summary         string
+	Body            string
+	Priority        string
+	Labels          []string
+	Components      []string
+	FixVersions     []string
+	AffectsVersions []string
 	// CustomFields holds all custom fields passed
 	// while editing the issue.
 	CustomFields map[string]string
@@ -99,6 +100,14 @@ type editFields struct {
 			Name string `json:"name,omitempty"`
 		} `json:"remove,omitempty"`
 	} `json:"fixVersions,omitempty"`
+	AffectsVersions []struct {
+		Add *struct {
+			Name string `json:"name,omitempty"`
+		} `json:"add,omitempty"`
+		Remove *struct {
+			Name string `json:"name,omitempty"`
+		} `json:"remove,omitempty"`
+	} `json:"versions,omitempty"`
 
 	customFields customField
 }
@@ -276,6 +285,46 @@ func getRequestDataForEdit(req *EditRequest) *editRequest {
 		update.M.FixVersions = versions
 	}
 
+	if len(req.AffectsVersions) > 0 {
+		add, sub := splitAddAndRemove(req.AffectsVersions)
+
+		versions := make([]struct {
+			Add *struct {
+				Name string `json:"name,omitempty"`
+			} `json:"add,omitempty"`
+			Remove *struct {
+				Name string `json:"name,omitempty"`
+			} `json:"remove,omitempty"`
+		}, 0, len(req.AffectsVersions))
+
+		for _, v := range sub {
+			versions = append(versions, struct {
+				Add *struct {
+					Name string `json:"name,omitempty"`
+				} `json:"add,omitempty"`
+				Remove *struct {
+					Name string `json:"name,omitempty"`
+				} `json:"remove,omitempty"`
+			}{Remove: &struct {
+				Name string `json:"name,omitempty"`
+			}{Name: v}})
+		}
+		for _, v := range add {
+			versions = append(versions, struct {
+				Add *struct {
+					Name string `json:"name,omitempty"`
+				} `json:"add,omitempty"`
+				Remove *struct {
+					Name string `json:"name,omitempty"`
+				} `json:"remove,omitempty"`
+			}{Add: &struct {
+				Name string `json:"name,omitempty"`
+			}{Name: v}})
+		}
+
+		update.M.AffectsVersions = versions
+	}
+
 	fields := struct {
 		Parent *struct {
 			Key string `json:"key,omitempty"`
diff --git a/pkg/jira/types.go b/pkg/jira/types.go
index 8dea242..ac78512 100644
--- a/pkg/jira/types.go
+++ b/pkg/jira/types.go
@@ -87,6 +87,9 @@ type IssueFields struct {
 	FixVersions []struct {
 		Name string `json:"name"`
 	} `json:"fixVersions"`
+	AffectsVersions []struct {
+		Name string `json:"name"`
+	} `json:"versions"`
 	Comment struct {
 		Comments []struct {
 			ID      string      `json:"id"`


Test patch:
diff --git a/pkg/jira/create_test.go b/pkg/jira/create_test.go
index 1409d1a..e1320d7 100644
--- a/pkg/jira/create_test.go
+++ b/pkg/jira/create_test.go
@@ -46,7 +46,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"}],"fixVersions":[{"name":"v2.0"},{"name":"v2.1-hotfix"}]}}`
+		`"components":[{"name":"BE"},{"name":"FE"}],"fixVersions":[{"name":"v2.0"},{"name":"v2.1-hotfix"}],"versions":[{"name":"v3.0"},{"name":"v3.1-hotfix"}]}}`
 	testServer := createTestServer{code: 201}
 	server := testServer.serve(t, expectedBody)
 	defer server.Close()
@@ -54,14 +54,15 @@ func TestCreate(t *testing.T) {
 	client := NewClient(Config{Server: server.URL}, WithTimeout(3*time.Second))
 
 	requestData := CreateRequest{
-		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"},
+		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"},
+		AffectsVersions: []string{"v3.0", "v3.1-hotfix"},
 	}
 	actual, err := client.CreateV2(&requestData)
 	assert.NoError(t, err)