From 1e56b2f28df2da7400b404a05cf03bee3632ca22 Mon Sep 17 00:00:00 2001 From: Tore Martin Hagen Date: Thu, 23 Jan 2025 12:36:01 +0100 Subject: [PATCH 1/3] Added jira-issue-fields to attest jira command --- cmd/kosli/attestJira.go | 34 +++++++++++++++++++++------ cmd/kosli/reportEvidenceCommitJira.go | 2 +- cmd/kosli/root.go | 1 + internal/jira/jira.go | 24 +++++++++++++++---- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/cmd/kosli/attestJira.go b/cmd/kosli/attestJira.go index 6a3b50e15..c59f7f27e 100644 --- a/cmd/kosli/attestJira.go +++ b/cmd/kosli/attestJira.go @@ -20,12 +20,13 @@ type JiraAttestationPayload struct { type attestJiraOptions struct { *CommonAttestationOptions - baseURL string - username string - apiToken string - pat string - assert bool - payload JiraAttestationPayload + baseURL string + username string + apiToken string + pat string + issueFields string + assert bool + payload JiraAttestationPayload } const attestJiraShortDesc = `Report a jira attestation to an artifact or a trail in a Kosli flow. ` @@ -41,6 +42,12 @@ The attestation is reported in all cases, and its compliance status depends on r existing Jira issues. If you have wrong Jira credentials or wrong Jira-base-url it will be reported as non existing Jira issue. This is because Jira returns same 404 error code in all cases. + +The ^--jira-issue-fields^ can be used to include fields from the jira issue. By default no fields +are included. ^*all^ will give all fields. Using ^--jira-issue-fields "*all" --dry-run^ will give you +the complete list so you can select the once you need. Be aware that specifying "creator" gives both "Creator" +and "creator", but specifying "Creator" gives nothing. The issue fields uses the jira API that is documented here: +https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/#api-rest-api-2-issue-issueidorkey-get-request ` + attestationBindingDesc + ` ` + commitDescription @@ -81,6 +88,18 @@ kosli attest jira \ --api-token yourAPIToken \ --org yourOrgName +# report a jira attestation about a trail and include jira issue summary, description and creator: +kosli attest jira \ + --name yourAttestationName \ + --flow yourFlowName \ + --trail yourTrailName \ + --jira-base-url https://kosli.atlassian.net \ + --jira-username user@domain.com \ + --jira-api-token yourJiraAPIToken \ + --jira-issue-fields "summary,description,creator" + --api-token yourAPIToken \ + --org yourOrgName + # report a jira attestation about an artifact which has not been reported yet in a trail: kosli attest jira \ --name yourTemplateArtifactName.yourAttestationName \ @@ -184,6 +203,7 @@ func newAttestJiraCmd(out io.Writer) *cobra.Command { cmd.Flags().StringVar(&o.username, "jira-username", "", jiraUsernameFlag) cmd.Flags().StringVar(&o.apiToken, "jira-api-token", "", jiraAPITokenFlag) cmd.Flags().StringVar(&o.pat, "jira-pat", "", jiraPATFlag) + cmd.Flags().StringVar(&o.issueFields, "jira-issue-fields", "", jiraIssueFieldFlag) cmd.Flags().BoolVar(&o.assert, "assert", false, attestationAssertFlag) err := RequireFlags(cmd, []string{"flow", "trail", "name", "commit", "jira-base-url"}) @@ -226,7 +246,7 @@ func (o *attestJiraOptions) run(args []string) error { issueLog := "" issueFoundCount := 0 for _, issueID := range issueIDs { - result, err := jc.GetJiraIssueInfo(issueID) + result, err := jc.GetJiraIssueInfo(issueID, o.issueFields) if err != nil { return err } diff --git a/cmd/kosli/reportEvidenceCommitJira.go b/cmd/kosli/reportEvidenceCommitJira.go index ae1d6cbbb..3d83f94fd 100644 --- a/cmd/kosli/reportEvidenceCommitJira.go +++ b/cmd/kosli/reportEvidenceCommitJira.go @@ -185,7 +185,7 @@ func (o *reportEvidenceCommitJiraOptions) run(args []string) error { issueLog := "" issueFoundCount := 0 for _, issueID := range issueIDs { - result, err := jc.GetJiraIssueInfo(issueID) + result, err := jc.GetJiraIssueInfo(issueID, "") if err != nil { return err } diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 5fe814958..0a292416b 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -114,6 +114,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, jiraUsernameFlag = "Jira username (for Jira Cloud)" jiraAPITokenFlag = "Jira API token (for Jira Cloud)" jiraPATFlag = "Jira personal access token (for self-hosted Jira)" + jiraIssueFieldFlag = "[optional] The comma separated list of fields to include from the Jira issue. Default no fields are included. '*all' will give all fields." envDescriptionFlag = "[optional] The environment description." flowDescriptionFlag = "[optional] The Kosli flow description." trailDescriptionFlag = "[optional] The Kosli trail description." diff --git a/internal/jira/jira.go b/internal/jira/jira.go index efe369560..77c6caca5 100644 --- a/internal/jira/jira.go +++ b/internal/jira/jira.go @@ -15,9 +15,10 @@ type JiraConfig struct { } type JiraIssueInfo struct { - IssueID string `json:"issue_id"` - IssueURL string `json:"issue_url"` - IssueExists bool `json:"issue_exists"` + IssueID string `json:"issue_id"` + IssueURL string `json:"issue_url"` + IssueExists bool `json:"issue_exists"` + IssueFields *jira.IssueFields `json:"issue_fields,omitempty"` } // NewJiraConfig returns a new JiraConfig @@ -61,7 +62,7 @@ func (jc *JiraConfig) NewJiraClient() (*jira.Client, error) { // GetJiraIssueInfo retrieve Jira issue information // if issue is not found, we still return a JiraIssueInfo object with IssueExists set to false -func (jc *JiraConfig) GetJiraIssueInfo(issueID string) (*JiraIssueInfo, error) { +func (jc *JiraConfig) GetJiraIssueInfo(issueID string, issueFields string) (*JiraIssueInfo, error) { result := &JiraIssueInfo{ IssueID: issueID, IssueExists: false, @@ -72,13 +73,26 @@ func (jc *JiraConfig) GetJiraIssueInfo(issueID string) (*JiraIssueInfo, error) { if err != nil { return result, err } - issue, response, err := jiraClient.Issue.Get(issueID, nil) + + // API will return all fields if the Fields is empty so we default to a non-existing field. + // The user can use '*all' if they want all + if issueFields == "" { + issueFields = "non-existing-key-in-jira-fields" + } + queryOptions := jira.GetQueryOptions{ + Fields: issueFields, + } + + issue, response, err := jiraClient.Issue.Get(issueID, &queryOptions) if err != nil && response.StatusCode != http.StatusNotFound { return result, err } if issue != nil { result.IssueExists = true + if issue.Fields != nil { + result.IssueFields = issue.Fields + } } return result, nil } From 2f310f60507e615f78451253fe49dd0b737eff59 Mon Sep 17 00:00:00 2001 From: Tore Martin Hagen Date: Fri, 24 Jan 2025 10:38:10 +0100 Subject: [PATCH 2/3] Removed comment about Creator --- cmd/kosli/attestJira.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/kosli/attestJira.go b/cmd/kosli/attestJira.go index c59f7f27e..0c6ce1328 100644 --- a/cmd/kosli/attestJira.go +++ b/cmd/kosli/attestJira.go @@ -45,8 +45,7 @@ This is because Jira returns same 404 error code in all cases. The ^--jira-issue-fields^ can be used to include fields from the jira issue. By default no fields are included. ^*all^ will give all fields. Using ^--jira-issue-fields "*all" --dry-run^ will give you -the complete list so you can select the once you need. Be aware that specifying "creator" gives both "Creator" -and "creator", but specifying "Creator" gives nothing. The issue fields uses the jira API that is documented here: +the complete list so you can select the once you need. The issue fields uses the jira API that is documented here: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/#api-rest-api-2-issue-issueidorkey-get-request ` + attestationBindingDesc + ` From 7a053d208170acf8cdf8e3208a0294d2dce91ca2 Mon Sep 17 00:00:00 2001 From: Tore Martin Hagen Date: Fri, 24 Jan 2025 10:51:56 +0100 Subject: [PATCH 3/3] Added test of jira fields --- cmd/kosli/attestJira_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmd/kosli/attestJira_test.go b/cmd/kosli/attestJira_test.go index 9ba15f506..b9b453c50 100644 --- a/cmd/kosli/attestJira_test.go +++ b/cmd/kosli/attestJira_test.go @@ -161,6 +161,17 @@ func (suite *AttestJiraCommandTestSuite) TestAttestJiraCmd() { commitMessage: "EX-1 test commit", }, }, + { + name: "can attest jira against a trail with summary and description from jira issue fields", + cmd: fmt.Sprintf(`attest jira --name bar + --jira-base-url https://kosli-test.atlassian.net --jira-username tore@kosli.com + --jira-issue-fields "summary,description" + --repo-root %s %s`, suite.tmpDir, suite.defaultKosliArguments), + golden: "jira attestation 'bar' is reported to trail: test-123\n", + additionalConfig: jiraTestsAdditionalConfig{ + commitMessage: "EX-1 test commit", + }, + }, { name: "can attest jira against a trail when name is not found in the trail template", cmd: fmt.Sprintf(`attest jira --name additional