From fcec53507443809e141802b5b9bf15a5fe4e28b6 Mon Sep 17 00:00:00 2001 From: Laszlo Gecse <120519943+lgecse@users.noreply.github.com> Date: Tue, 5 Sep 2023 21:16:08 +0200 Subject: [PATCH] Make component field configurable (#150) Signed-off-by: Laszlo Gecse Co-authored-by: Stephen Augustus --- README.md | 6 +++ cmd/root.go | 8 ++++ internal/config/config.go | 83 ++++++++++++++++++++++++++++++------ internal/jira/issue/issue.go | 32 ++++++++++++++ internal/options/options.go | 10 +++-- 5 files changed, 121 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ffbdfcd0..a68356ac 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Configuration arguments are as follows: | repo-name | string | "uwu-tools/gh-jira-issue-sync" | true | null | | jira-uri | string | "https://jira.example.com" | true | null | | jira-project | string | "SYNC" | true | null | +| jira-components | []string | ["Core","Payment"] | false | null | | since | string | "2017-07-01T13:45:00-0800" | false | "1970-01-01T00:00:00+0000" | | timeout | duration | 500ms | false | 1m | @@ -102,6 +103,11 @@ lives at a non-root URL, the path must be included. For example, `jira-project` is the key (not the name) of the project in Jira to which the issues will be synchronized. +`jira-components` is the names of the components in Jira +that will be added to the issues when synchronized. If a component +not found on the project or the set value is otherwise invalid, +an error will return. (optional) + `since` is the cutoff date issue-sync will use when searching for issues to synchronize. If an issue was last updated before this time, it will not be synchronized. Usually this is the last run of the tool. It is in diff --git a/cmd/root.go b/cmd/root.go index b5f342cc..33718e04 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -148,6 +148,14 @@ func init() { "set the key of the Jira project", ) + RootCmd.PersistentFlags().StringSliceVarP( + &opts.JiraComponents, + options.ConfigKeyJiraComponents, + "C", + nil, + "set the Jira components to be used", + ) + RootCmd.PersistentFlags().StringVarP( &opts.Since, options.ConfigKeySince, diff --git a/internal/config/config.go b/internal/config/config.go index a9950b01..5527a55c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -94,6 +94,11 @@ type Config struct { // project represents the Jira project the user has requested. project *jira.Project + // components represents the Jira components the user would like use for the sync. + // Comes from the value of the `jira-components` configuration parameter. + // Items in Jira will have the components field set to these values. + components []*jira.Component + // since is the parsed value of the `since` configuration parameter, which is the earliest that // a GitHub issue can have been updated to be retrieved. since time.Time @@ -167,6 +172,11 @@ func (c *Config) LoadJiraConfig(client *jira.Client) error { } c.project = proj + c.components, err = c.getComponents(proj) + if err != nil { + return err + } + c.fieldIDs, err = c.getFieldIDs(client) if err != nil { return err @@ -263,6 +273,11 @@ func (c *Config) GetRepo() (string, string) { return github.GetRepo(repoPath) } +// GetJiraComponents returns the Jira component the user has configured. +func (c *Config) GetJiraComponents() []*jira.Component { + return c.components +} + // SetJiraToken adds the Jira OAuth tokens in the Viper configuration, ensuring that they // are saved for future runs. func (c *Config) SetJiraToken(token *oauth1.Token) { @@ -272,20 +287,21 @@ func (c *Config) SetJiraToken(token *oauth1.Token) { // configFile is a serializable representation of the current Viper configuration. type configFile struct { - LogLevel string `json:"log-level,omitempty" mapstructure:"log-level"` - GithubToken string `json:"github-token,omitempty" mapstructure:"github-token"` - JiraUser string `json:"jira-user,omitempty" mapstructure:"jira-user"` - JiraPass string `json:"jira-pass,omitempty" mapstructure:"jira-pass"` - JiraToken string `json:"jira-token,omitempty" mapstructure:"jira-token"` - JiraSecret string `json:"jira-secret,omitempty" mapstructure:"jira-secret"` - JiraKey string `json:"jira-private-key-path,omitempty" mapstructure:"jira-private-key-path"` - JiraCKey string `json:"jira-consumer-key,omitempty" mapstructure:"jira-consumer-key"` - RepoName string `json:"repo-name,omitempty" mapstructure:"repo-name"` - JiraURI string `json:"jira-uri,omitempty" mapstructure:"jira-uri"` - JiraProject string `json:"jira-project,omitempty" mapstructure:"jira-project"` - Since string `json:"since,omitempty" mapstructure:"since"` - Confirm bool `json:"confirm,omitempty" mapstructure:"confirm"` - Timeout time.Duration `json:"timeout,omitempty" mapstructure:"timeout"` + LogLevel string `json:"log-level,omitempty" mapstructure:"log-level"` + GithubToken string `json:"github-token,omitempty" mapstructure:"github-token"` + JiraUser string `json:"jira-user,omitempty" mapstructure:"jira-user"` + JiraPass string `json:"jira-pass,omitempty" mapstructure:"jira-pass"` + JiraToken string `json:"jira-token,omitempty" mapstructure:"jira-token"` + JiraSecret string `json:"jira-secret,omitempty" mapstructure:"jira-secret"` + JiraKey string `json:"jira-private-key-path,omitempty" mapstructure:"jira-private-key-path"` + JiraCKey string `json:"jira-consumer-key,omitempty" mapstructure:"jira-consumer-key"` + RepoName string `json:"repo-name,omitempty" mapstructure:"repo-name"` + JiraURI string `json:"jira-uri,omitempty" mapstructure:"jira-uri"` + JiraProject string `json:"jira-project,omitempty" mapstructure:"jira-project"` + Since string `json:"since,omitempty" mapstructure:"since"` + JiraComponents []string `json:"jira-components,omitempty" mapstructure:"jira-components"` + Confirm bool `json:"confirm,omitempty" mapstructure:"confirm"` + Timeout time.Duration `json:"timeout,omitempty" mapstructure:"timeout"` } // SaveConfig updates the `since` parameter to now, then saves the configuration file. @@ -517,6 +533,39 @@ func (c *Config) getFieldIDs(client *jira.Client) (*fields, error) { return &fieldIDs, nil } +// getComponents resolves every component set in config against +// Jira project, and returns with these components used by issue-sync. +func (c *Config) getComponents(proj *jira.Project) ([]*jira.Component, error) { + var returnComponents []*jira.Component + + components := c.cmdConfig.GetStringSlice(options.ConfigKeyJiraComponents) + + for _, configComponent := range components { + found := false + + for j := range proj.Components { + projComponent := &proj.Components[j] + + if projComponent.Name == configComponent { + found = true + foundComponent := jira.Component{ + Name: projComponent.Name, + ID: projComponent.ID, + } + + returnComponents = append(returnComponents, &foundComponent) + } + } + + if !found { + log.Errorf("The Jira project does not have such component defined: %s", configComponent) + return nil, ReadingJiraComponentError(configComponent) + } + } + + return returnComponents, nil +} + // Errors var ( @@ -539,3 +588,9 @@ var ( func errCustomFieldIDNotFound(field string) error { return fmt.Errorf("could not find ID custom field '%s'; check that it is named correctly", field) //nolint:goerr113 } + +type ReadingJiraComponentError string + +func (r ReadingJiraComponentError) Error() string { + return fmt.Sprintf("could not find Jira component: %s; check that it is named correctly", string(r)) +} diff --git a/internal/jira/issue/issue.go b/internal/jira/issue/issue.go index 74f1ce23..01048d39 100644 --- a/internal/jira/issue/issue.go +++ b/internal/jira/issue/issue.go @@ -140,6 +140,10 @@ func DidIssueChange(cfg *config.Config, ghIssue *gogh.Issue, jIssue *gojira.Issu anyDifferent = true } + if GetMissingComponents(cfg, jIssue) != nil { + anyDifferent = true + } + if len(ghIssue.Labels) > 0 { //nolint:nestif // TODO(lint) ghLabels := githubLabelsToStrSlice(ghIssue.Labels) @@ -211,6 +215,9 @@ func UpdateIssue( ID: jIssue.ID, } + missingComponents := GetMissingComponents(cfg, jIssue) + issue.Fields.Components = append(issue.Fields.Components, missingComponents...) + _, err := jClient.UpdateIssue(issue) if err != nil { return fmt.Errorf("updating Jira issue: %w", err) @@ -233,6 +240,30 @@ func UpdateIssue( return nil } +// GetMissingComponents compares configurated components with the Jira issue +// components. Returns the components that are missing from the Jira issue. +func GetMissingComponents(cfg *config.Config, jIssue *gojira.Issue) []*gojira.Component { + var returnComponents []*gojira.Component + + components := cfg.GetJiraComponents() + for _, configComponent := range components { + found := false + + for _, issueComponent := range jIssue.Fields.Components { + if issueComponent.Name == configComponent.Name { + found = true + break + } + } + + if !found { + returnComponents = append(returnComponents, configComponent) + } + } + + return returnComponents +} + // CreateIssue generates a Jira issue from the various fields on the given GitHub issue, then // sends it to the Jira API. func CreateIssue(cfg *config.Config, issue *gogh.Issue, ghClient github.Client, jClient jira.Client) error { @@ -258,6 +289,7 @@ func CreateIssue(cfg *config.Config, issue *gogh.Issue, ghClient github.Client, Summary: issue.GetTitle(), Description: issue.GetBody(), Unknowns: unknowns, + Components: cfg.GetJiraComponents(), } jIssue := &gojira.Issue{ diff --git a/internal/options/options.go b/internal/options/options.go index 138ef8ee..e1d52266 100644 --- a/internal/options/options.go +++ b/internal/options/options.go @@ -32,10 +32,11 @@ type Options struct { JiraURI string JiraProject string // TODO(options): Should this be a time type? - Since string - Confirm bool - Timeout time.Duration - Period time.Duration + Since string + JiraComponents []string + Confirm bool + Timeout time.Duration + Period time.Duration } const ( @@ -65,6 +66,7 @@ const ( ConfigKeyJiraSecret = "jira-secret" ConfigKeyJiraConsumerKey = "jira-consumer-key" ConfigKeyJiraPrivateKeyPath = "jira-private-key-path" + ConfigKeyJiraComponents = "jira-components" // Default values //