From 8ebcaf2c344ef231c8b3e121d3dd68cefe84709b Mon Sep 17 00:00:00 2001 From: vidhu bala Date: Wed, 19 Aug 2026 16:12:23 +0100 Subject: [PATCH 1/7] feat(jira): add --jira-trailer flag to extract issue key from git trailer When --jira-trailer is set, the command reads lines of the form ': ' from the commit message and uses those values as the sole source of Jira issue references, skipping the full commit message and branch name scan. This avoids false positives from other trailers (e.g. Ona-Environment-Id) whose values happen to match the Jira key pattern. Co-Authored-By: Claude Sonnet 4.6 --- cmd/kosli/attestJira.go | 34 +++++++++++++-------- cmd/kosli/attestJira_test.go | 35 ++++++++++++++++++++++ cmd/kosli/root.go | 1 + internal/gitview/gitView.go | 17 +++++++++++ internal/gitview/gitView_test.go | 51 ++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 12 deletions(-) diff --git a/cmd/kosli/attestJira.go b/cmd/kosli/attestJira.go index 8b22b9760..20b3e645d 100644 --- a/cmd/kosli/attestJira.go +++ b/cmd/kosli/attestJira.go @@ -29,6 +29,7 @@ type attestJiraOptions struct { projectKeys []string issueFields string secondarySource string + trailerKey string ignoreBranchMatch bool assert bool payload JiraAttestationPayload @@ -260,6 +261,7 @@ func newAttestJiraCmd(out io.Writer) *cobra.Command { cmd.Flags().StringSliceVar(&o.projectKeys, "jira-project-key", []string{}, jiraProjectKeyFlag) cmd.Flags().StringVar(&o.issueFields, "jira-issue-fields", "", jiraIssueFieldFlag) cmd.Flags().StringVar(&o.secondarySource, "jira-secondary-source", "", jiraSecondarySourceFlag) + cmd.Flags().StringVar(&o.trailerKey, "jira-trailer", "", jiraTrailerFlag) cmd.Flags().BoolVar(&o.ignoreBranchMatch, "ignore-branch-match", false, ignoreBranchMatchFlag) cmd.Flags().BoolVar(&o.assert, "assert", false, attestationAssertFlag) @@ -301,19 +303,27 @@ func (o *attestJiraOptions) run(args []string) error { return err } - // Search commit message, branch name, and secondary source for Jira issue keys, - // filtering out false positives from multi-segment identifiers like CVE-2026-41284. - searchTexts := []string{commitInfo.Message} - if !o.ignoreBranchMatch { - searchTexts = append(searchTexts, commitInfo.Branch) - } - if o.secondarySource != "" { - searchTexts = append(searchTexts, o.secondarySource) + // Find Jira issue keys either from a named git trailer or by scanning the + // commit message, branch name, and secondary source. + var issueIDs []string + if o.trailerKey != "" { + trailerValues := gitview.GetTrailerValues(commitInfo.Message, o.trailerKey) + combinedTrailerText := strings.Join(trailerValues, "\n") + issueIDs = jira.FindJiraIssueKeys(combinedTrailerText, o.projectKeys) + logger.Debug("Checked for Jira issue references in trailer '%s' of Git commit %s: %v", o.trailerKey, commitInfo.Sha1, trailerValues) + } else { + searchTexts := []string{commitInfo.Message} + if !o.ignoreBranchMatch { + searchTexts = append(searchTexts, commitInfo.Branch) + } + if o.secondarySource != "" { + searchTexts = append(searchTexts, o.secondarySource) + } + combinedText := strings.Join(searchTexts, "\n") + issueIDs = jira.FindJiraIssueKeys(combinedText, o.projectKeys) + logger.Debug("Checked for Jira issue references in Git commit %s on branch %s commit message:\n%s", commitInfo.Sha1, commitInfo.Branch, commitInfo.Message) } - combinedText := strings.Join(searchTexts, "\n") - issueIDs := jira.FindJiraIssueKeys(combinedText, o.projectKeys) - logger.Debug("Checked for Jira issue references in Git commit %s on branch %s commit message:\n%s", commitInfo.Sha1, commitInfo.Branch, commitInfo.Message) - logger.Debug("the following Jira references are found in commit message or branch name: %v", issueIDs) + logger.Debug("the following Jira references are found: %v", issueIDs) issueLog := "" issueFoundCount := 0 diff --git a/cmd/kosli/attestJira_test.go b/cmd/kosli/attestJira_test.go index 6676932a2..62e888b40 100644 --- a/cmd/kosli/attestJira_test.go +++ b/cmd/kosli/attestJira_test.go @@ -331,6 +331,41 @@ func (suite *AttestJiraCommandTestSuite) TestAttestJiraCmd() { cmd: fmt.Sprintf("attest jira --name .foo --commit HEAD --jira-base-url https://kosli-test.atlassian.net %s", suite.defaultKosliArguments), golden: "Error: failed to parse attestation name: invalid attestation name format: .foo\n", }, + { + name: "27 can attest jira using --jira-trailer to extract issue key from commit trailer", + cmd: fmt.Sprintf(`attest jira --name bar + --jira-base-url https://kosli-test.atlassian.net + --jira-trailer Jira + --repo-root %s %s`, suite.tmpDir, suite.defaultKosliArguments), + golden: "jira attestation 'bar' is reported to trail: test-123\n", + additionalConfig: jiraTestsAdditionalConfig{ + commitMessage: "fix: some change\n\nJira: EX-1\nOna-Environment-Id: ONA-999", + }, + }, + { + name: "28 --jira-trailer with no matching trailer produces no issue IDs (non-compliant but reported)", + cmd: fmt.Sprintf(`attest jira --name bar + --jira-base-url https://kosli-test.atlassian.net + --jira-trailer Jira + --repo-root %s %s`, suite.tmpDir, suite.defaultKosliArguments), + golden: "jira attestation 'bar' is reported to trail: test-123\n", + additionalConfig: jiraTestsAdditionalConfig{ + commitMessage: "fix: some change with no jira trailer", + }, + }, + { + wantError: true, + name: "29 --jira-trailer with --assert fails when trailer is absent", + cmd: fmt.Sprintf(`attest jira --name bar + --jira-base-url https://kosli-test.atlassian.net + --jira-trailer Jira + --assert + --repo-root %s %s`, suite.tmpDir, suite.defaultKosliArguments), + golden: "jira attestation 'bar' is reported to trail: test-123\nError: no Jira references are found in commit message or branch name\n", + additionalConfig: jiraTestsAdditionalConfig{ + commitMessage: "fix: some change with no jira trailer", + }, + }, } for _, test := range tests { diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 144650b3d..68de77382 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -169,6 +169,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, jiraIssueFieldFlag = "[optional] The comma separated list of fields to include from the Jira issue. Default no fields are included. '*all' will give all fields." jiraSecondarySourceFlag = "[optional] An optional string to search for Jira ticket reference, e.g. '--jira-secondary-source ${{ github.head_ref }}'" ignoreBranchMatchFlag = "Ignore branch name when searching for Jira ticket reference." + jiraTrailerFlag = "[optional] The git trailer key to use as the sole source of Jira issue references (e.g. '--jira-trailer Jira' extracts the value of 'Jira: ' lines from the commit message). When set, the commit message body and branch name are not scanned." envDescriptionFlag = "[optional] The environment description." flowDescriptionFlag = "[optional] The Kosli flow description." trailDescriptionFlag = "[optional] The Kosli trail description." diff --git a/internal/gitview/gitView.go b/internal/gitview/gitView.go index e9365817e..a7e98a56d 100644 --- a/internal/gitview/gitView.go +++ b/internal/gitview/gitView.go @@ -316,6 +316,23 @@ func (gv *GitView) MatchPatternInCommitMessageORBranchName(pattern, commitSHA, s return matches, commitInfo, nil } +// GetTrailerValues extracts the values of all trailer lines in a commit message +// that match the given key. The key comparison is case-insensitive. Trailer lines +// have the format ": ". Returns an empty (non-nil) slice if none are found. +func GetTrailerValues(message, key string) []string { + result := []string{} + prefix := strings.ToLower(key) + ":" + for _, line := range strings.Split(message, "\n") { + if strings.HasPrefix(strings.ToLower(line), prefix) { + value := strings.TrimSpace(line[len(prefix):]) + if value != "" { + result = append(result, value) + } + } + } + return result +} + // ResolveRevision returns an explicit commit SHA1 from commit SHA or ref (e.g. HEAD~2) func (gv *GitView) ResolveRevision(commitSHAOrRef string) (string, error) { hash, err := gv.repository.ResolveRevision(plumbing.Revision(commitSHAOrRef)) diff --git a/internal/gitview/gitView_test.go b/internal/gitview/gitView_test.go index 2ff79040f..ed2fd002e 100644 --- a/internal/gitview/gitView_test.go +++ b/internal/gitview/gitView_test.go @@ -644,6 +644,57 @@ func initializeRepoAndCommit(repoPath string, commitsNumber int) (*git.Repositor return repo, w, nil } +func (suite *GitViewTestSuite) TestGetTrailerValues() { + for _, tt := range []struct { + name string + message string + key string + expected []string + }{ + { + name: "no trailers returns empty slice", + message: "fix: something\n\nsome body text", + key: "Jira", + expected: []string{}, + }, + { + name: "single matching trailer", + message: "fix: something\n\nJira: BX-123", + key: "Jira", + expected: []string{"BX-123"}, + }, + { + name: "key match is case-insensitive", + message: "fix: something\n\njira: BX-123", + key: "Jira", + expected: []string{"BX-123"}, + }, + { + name: "multiple occurrences of same key", + message: "fix: something\n\nJira: BX-123\nJira: BX-456", + key: "Jira", + expected: []string{"BX-123", "BX-456"}, + }, + { + name: "non-matching trailers are ignored", + message: "fix: something\n\nJira: BX-123\nOna-Environment-Id: ONA-456", + key: "Jira", + expected: []string{"BX-123"}, + }, + { + name: "whitespace trimmed from value", + message: "fix: something\n\nJira: BX-123 ", + key: "Jira", + expected: []string{"BX-123"}, + }, + } { + suite.Run(tt.name, func() { + result := GetTrailerValues(tt.message, tt.key) + require.Equal(suite.T(), tt.expected, result) + }) + } +} + func TestGitViewTestSuite(t *testing.T) { suite.Run(t, new(GitViewTestSuite)) } From 7f8d11d952a8fc963a4ef7a9e5c425e2bb59ae3a Mon Sep 17 00:00:00 2001 From: vidhu bala Date: Wed, 19 Aug 2026 17:46:33 +0100 Subject: [PATCH 2/7] test: register --jira-trailer in empty-flag-audit coverage Co-Authored-By: Claude Sonnet 4.6 --- cmd/kosli/testdata/empty-flag-audit-coverage.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/kosli/testdata/empty-flag-audit-coverage.json b/cmd/kosli/testdata/empty-flag-audit-coverage.json index 73765f822..3df38dc8d 100644 --- a/cmd/kosli/testdata/empty-flag-audit-coverage.json +++ b/cmd/kosli/testdata/empty-flag-audit-coverage.json @@ -212,6 +212,7 @@ "jira-pat": "string", "jira-project-key": "stringSlice", "jira-secondary-source": "string", + "jira-trailer": "string", "jira-username": "string", "name": "string", "origin-url": "string", From 2d7398a7458065c52aaedac1054df64a24970fce Mon Sep 17 00:00:00 2001 From: Vidhu Bala Date: Tue, 25 Aug 2026 15:48:58 +0100 Subject: [PATCH 3/7] Update cmd/kosli/attestJira_test.go Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- cmd/kosli/attestJira_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/kosli/attestJira_test.go b/cmd/kosli/attestJira_test.go index 71aa703fa..14e6f6ce9 100644 --- a/cmd/kosli/attestJira_test.go +++ b/cmd/kosli/attestJira_test.go @@ -371,6 +371,7 @@ func (suite *AttestJiraCommandTestSuite) TestAttestJiraCmd() { cmd: fmt.Sprintf(`attest jira --name bar --jira-base-url https://kosli-test.atlassian.net --jira-trailer Jira + --assert --repo-root %s %s`, suite.tmpDir, suite.defaultKosliArguments), golden: "jira attestation 'bar' is reported to trail: test-123\n", additionalConfig: jiraTestsAdditionalConfig{ From 11c68b5a2957037bdacafbab985aad23f638a49b Mon Sep 17 00:00:00 2001 From: vidhu bala Date: Tue, 25 Aug 2026 22:56:51 +0100 Subject: [PATCH 4/7] fix(attest jira): add --jira-trailer flag with edge case fixes and accurate help - Trim leading whitespace from trailer lines so editor-indented or git-log-formatted messages (4-space indent) match correctly - Strip trailing colon from --jira-trailer value so "Jira:" and "Jira" both produce the same prefix - Error when --jira-trailer and --jira-secondary-source are both set (they are mutually exclusive; secondary source is silently ignored in trailer mode) - Warn when --ignore-branch-match is set alongside --jira-trailer (it has no effect in trailer mode) - Warn when a trailer is found but contains no valid Jira issue keys - Thread issueSource through both --assert error messages so trailer mode names the trailer rather than "commit message or branch name" - Update Long description to document trailer mode, its interaction with --ignore-branch-match, and its use as the preferred CVE-collision fix - Add --jira-trailer example to attestJiraExample Co-Authored-By: Claude Sonnet 4.6 --- cmd/kosli/attestJira.go | 52 +++++++++++++++++++++++++++----- cmd/kosli/attestJira_test.go | 2 +- internal/gitview/gitView.go | 7 +++-- internal/gitview/gitView_test.go | 12 ++++++++ 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/cmd/kosli/attestJira.go b/cmd/kosli/attestJira.go index f7c99152f..d12b7bc67 100644 --- a/cmd/kosli/attestJira.go +++ b/cmd/kosli/attestJira.go @@ -39,8 +39,13 @@ type attestJiraOptions struct { const attestJiraShortDesc = `Report a jira attestation to an artifact or a trail in a Kosli flow. ` const attestJiraLongDesc = attestJiraShortDesc + ` -Parses the given commit's message, current branch name or the content of the ^--jira-secondary-source^ -argument for Jira issue references of the form: +By default, parses the given commit's message, current branch name, or the content of the +^--jira-secondary-source^ argument for Jira issue references of the form. +Use ^--jira-trailer^ to read issue keys exclusively from a named git trailer line instead +(e.g. ^Jira: PROJ-42^); when set, the commit message body, branch name, and +^--jira-secondary-source^ are not scanned. + +Jira issue references have the form: 'at least 2 characters long, starting with an uppercase letter project key followed by dash and one or more digits'. @@ -60,13 +65,16 @@ because ^CVE-2026^ would be followed by ^-4^. This applies across all parsed sou (commit message, branch name, and secondary source). Note: if your Jira project key collides with this pattern (e.g. a project key of ^CVE^), an issue reference that happens to be the prefix of a longer hyphenated number (such as a CVE -identifier) will be filtered out. Use ^--jira-secondary-source^ with a different identifier -format as a workaround. +identifier) will be filtered out. Use ^--jira-trailer^ to read issue keys from a dedicated +git trailer line (e.g. ^Jira: CVE-42^), which bypasses pattern-scanning entirely. +Alternatively, use ^--jira-secondary-source^ with a different identifier format. If you want to restrict the Jira issue matching to a specific project, use the ^--jira-project-key^ flag to specify your own project key. You can specify multiple project keys if needed. If the ^--ignore-branch-match^ is set, the branch name is not parsed for a match. +^--ignore-branch-match^ has no effect when ^--jira-trailer^ is set, since the branch is +never scanned in trailer mode. The found issue references will be checked against Jira to confirm their existence. The attestation is reported in all cases, and its compliance status depends on referencing @@ -191,6 +199,20 @@ kosli attest jira \ --jira-api-token yourJiraAPIToken \ --api-token yourAPIToken \ --org yourOrgName + +# read the jira issue key exclusively from a git trailer line (e.g. "Jira: PROJ-42") +# bypasses commit message and branch scanning entirely — useful when project keys +# collide with patterns like CVE identifiers +kosli attest jira \ + --name yourAttestationName \ + --flow yourFlowName \ + --trail yourTrailName \ + --jira-trailer Jira \ + --jira-base-url https://kosli.atlassian.net \ + --jira-username user@domain.com \ + --jira-api-token yourJiraAPIToken \ + --api-token yourAPIToken \ + --org yourOrgName ` func newAttestJiraCmd(out io.Writer) *cobra.Command { @@ -235,6 +257,11 @@ func newAttestJiraCmd(out io.Writer) *cobra.Command { return err } + err = MuXRequiredFlags(cmd, []string{"jira-trailer", "jira-secondary-source"}, false) + if err != nil { + return err + } + err = ValidateSliceValues(o.redactedCommitInfo, allowedCommitRedactionValues) if err != nil { return fmt.Errorf("%s for --redact-commit-info", err.Error()) @@ -310,16 +337,27 @@ func (o *attestJiraOptions) run(args []string) error { // commit message, branch name, and secondary source. var issueIDs []string if o.trailerKey != "" { + if o.ignoreBranchMatch { + logger.Warn("--ignore-branch-match has no effect when --jira-trailer is set") + } trailerValues := gitview.GetTrailerValues(commitInfo.Message, o.trailerKey) combinedTrailerText := strings.Join(trailerValues, "\n") issueIDs = jira.FindJiraIssueKeys(combinedTrailerText, o.projectKeys) logger.Debug("Checked for Jira issue references in trailer '%s' of Git commit %s: %v", o.trailerKey, commitInfo.Sha1, trailerValues) + if len(trailerValues) > 0 && len(issueIDs) == 0 { + logger.Warn("trailer '%s' was found but contained no valid Jira issue keys: %v", o.trailerKey, trailerValues) + } } else { issueIDs = jira.FindJiraIssueKeys(jiraSearchText(commitInfo, o.secondarySource, o.ignoreBranchMatch), o.projectKeys) logger.Debug("Checked for Jira issue references in Git commit %s on branch %s commit message:\n%s", commitInfo.Sha1, commitInfo.Branch, commitInfo.Message) } logger.Debug("the following Jira references are found: %v", issueIDs) + issueSource := "commit message or branch name" + if o.trailerKey != "" { + issueSource = fmt.Sprintf("trailer '%s'", o.trailerKey) + } + issueLog := "" issueFoundCount := 0 unconfirmedIDs := []string{} @@ -378,7 +416,7 @@ func (o *attestJiraOptions) run(args []string) error { if err != nil { errString = fmt.Sprintf("%s\nError: ", err.Error()) } - err = fmt.Errorf("%sno Jira references are found in commit message or branch name", errString) + err = fmt.Errorf("%sno Jira references are found in %s", errString, issueSource) } if issueFoundCount != len(issueIDs) && o.assert && !global.DryRun { @@ -391,8 +429,8 @@ func (o *attestJiraOptions) run(args []string) error { for _, reason := range unconfirmedReasons { reasonLog += fmt.Sprintf("\n\treason: %s", reason) } - err = fmt.Errorf("%s%s from references found in commit message or branch name%s%s", errString, - jiraAssertHeadline(len(issueIDs)-issueFoundCount-len(unconfirmedIDs), len(unconfirmedIDs)), issueLog, reasonLog) + err = fmt.Errorf("%s%s from references found in %s%s%s", errString, + jiraAssertHeadline(len(issueIDs)-issueFoundCount-len(unconfirmedIDs), len(unconfirmedIDs)), issueSource, issueLog, reasonLog) } return wrapAttestationError(err) } diff --git a/cmd/kosli/attestJira_test.go b/cmd/kosli/attestJira_test.go index 14e6f6ce9..57c9006aa 100644 --- a/cmd/kosli/attestJira_test.go +++ b/cmd/kosli/attestJira_test.go @@ -397,7 +397,7 @@ func (suite *AttestJiraCommandTestSuite) TestAttestJiraCmd() { --jira-trailer Jira --assert --repo-root %s %s`, suite.tmpDir, suite.defaultKosliArguments), - golden: "jira attestation 'bar' is reported to trail: test-123\nError: no Jira references are found in commit message or branch name\n", + golden: "jira attestation 'bar' is reported to trail: test-123\nError: no Jira references are found in trailer 'Jira'\n", additionalConfig: jiraTestsAdditionalConfig{ commitMessage: "fix: some change with no jira trailer", }, diff --git a/internal/gitview/gitView.go b/internal/gitview/gitView.go index b2e016511..2ef22ce7d 100644 --- a/internal/gitview/gitView.go +++ b/internal/gitview/gitView.go @@ -281,10 +281,11 @@ func getCommitURL(repoURL, commitHash string) string { // have the format ": ". Returns an empty (non-nil) slice if none are found. func GetTrailerValues(message, key string) []string { result := []string{} - prefix := strings.ToLower(key) + ":" + prefix := strings.ToLower(strings.TrimRight(key, ":")) + ":" for _, line := range strings.Split(message, "\n") { - if strings.HasPrefix(strings.ToLower(line), prefix) { - value := strings.TrimSpace(line[len(prefix):]) + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(strings.ToLower(trimmed), prefix) { + value := strings.TrimSpace(trimmed[len(prefix):]) if value != "" { result = append(result, value) } diff --git a/internal/gitview/gitView_test.go b/internal/gitview/gitView_test.go index 00a6b49f3..845baa957 100644 --- a/internal/gitview/gitView_test.go +++ b/internal/gitview/gitView_test.go @@ -531,6 +531,18 @@ func (suite *GitViewTestSuite) TestGetTrailerValues() { key: "Jira", expected: []string{"BX-123"}, }, + { + name: "leading whitespace on line is tolerated", + message: "fix: something\n\n Jira: BX-123", + key: "Jira", + expected: []string{"BX-123"}, + }, + { + name: "key supplied with trailing colon still matches", + message: "fix: something\n\nJira: BX-123", + key: "Jira:", + expected: []string{"BX-123"}, + }, } { suite.Run(tt.name, func() { result := GetTrailerValues(tt.message, tt.key) From d1665911e7c714f1095dc006b596ed5a51527a59 Mon Sep 17 00:00:00 2001 From: vidhu bala Date: Thu, 27 Aug 2026 16:51:59 +0100 Subject: [PATCH 5/7] fix(jira trailer): trim whitespace from trailer key; add branch-not-scanned test - TrimSpace the key in GetTrailerValues before TrimRight(key, ":") - Add unit test: key with surrounding whitespace still matches - Add integration test 30: --jira-trailer does not scan branch name Co-Authored-By: Claude Sonnet 4.6 --- cmd/kosli/attestJira_test.go | 14 ++++++++++++++ internal/gitview/gitView.go | 2 +- internal/gitview/gitView_test.go | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/attestJira_test.go b/cmd/kosli/attestJira_test.go index 57c9006aa..45fabe40c 100644 --- a/cmd/kosli/attestJira_test.go +++ b/cmd/kosli/attestJira_test.go @@ -402,6 +402,20 @@ func (suite *AttestJiraCommandTestSuite) TestAttestJiraCmd() { commitMessage: "fix: some change with no jira trailer", }, }, + { + wantError: true, + name: "30 --jira-trailer does not scan branch name even when it contains a Jira key", + cmd: fmt.Sprintf(`attest jira --name bar + --jira-base-url https://kosli-test.atlassian.net + --jira-trailer Jira + --assert + --repo-root %s %s`, suite.tmpDir, suite.defaultKosliArguments), + golden: "jira attestation 'bar' is reported to trail: test-123\nError: no Jira references are found in trailer 'Jira'\n", + additionalConfig: jiraTestsAdditionalConfig{ + branchName: "EX-1-some-feature", + commitMessage: "fix: some change with no jira trailer", + }, + }, } for _, test := range tests { diff --git a/internal/gitview/gitView.go b/internal/gitview/gitView.go index 2ef22ce7d..ea1f3c23c 100644 --- a/internal/gitview/gitView.go +++ b/internal/gitview/gitView.go @@ -281,7 +281,7 @@ func getCommitURL(repoURL, commitHash string) string { // have the format ": ". Returns an empty (non-nil) slice if none are found. func GetTrailerValues(message, key string) []string { result := []string{} - prefix := strings.ToLower(strings.TrimRight(key, ":")) + ":" + prefix := strings.ToLower(strings.TrimRight(strings.TrimSpace(key), ":")) + ":" for _, line := range strings.Split(message, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(strings.ToLower(trimmed), prefix) { diff --git a/internal/gitview/gitView_test.go b/internal/gitview/gitView_test.go index 845baa957..9332fa235 100644 --- a/internal/gitview/gitView_test.go +++ b/internal/gitview/gitView_test.go @@ -543,6 +543,12 @@ func (suite *GitViewTestSuite) TestGetTrailerValues() { key: "Jira:", expected: []string{"BX-123"}, }, + { + name: "key with surrounding whitespace still matches", + message: "fix: something\n\nJira: BX-123", + key: " Jira ", + expected: []string{"BX-123"}, + }, } { suite.Run(tt.name, func() { result := GetTrailerValues(tt.message, tt.key) From b2fb1c3a8257c3e18979832c46c03d3db6a86efb Mon Sep 17 00:00:00 2001 From: Vidhu Bala Date: Thu, 27 Aug 2026 18:18:42 +0100 Subject: [PATCH 6/7] Update cmd/kosli/root.go Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- cmd/kosli/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 67e658c89..d15030026 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -169,7 +169,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, jiraIssueFieldFlag = "[optional] The comma separated list of fields to include from the Jira issue. Default no fields are included. '*all' will give all fields." jiraSecondarySourceFlag = "[optional] An optional string to search for Jira ticket reference, e.g. '--jira-secondary-source ${{ github.head_ref }}'" ignoreBranchMatchFlag = "Ignore branch name when searching for Jira ticket reference." - jiraTrailerFlag = "[optional] The git trailer key to use as the sole source of Jira issue references (e.g. '--jira-trailer Jira' extracts the value of 'Jira: ' lines from the commit message). When set, the commit message body and branch name are not scanned." + jiraTrailerFlag = "[optional] The git trailer key to use as the sole source of Jira issue references (e.g. '--jira-trailer Jira' extracts the value of 'Jira: ' lines from the commit message). When set, the commit message body and branch name are not scanned. Mutually exclusive with --jira-secondary-source." envDescriptionFlag = "[optional] The environment description." flowDescriptionFlag = "[optional] The Kosli flow description." trailDescriptionFlag = "[optional] The Kosli trail description." From 678021bf14194316dc0fbf9a9426e7a838dc566e Mon Sep 17 00:00:00 2001 From: vidhu bala Date: Thu, 27 Aug 2026 18:19:25 +0100 Subject: [PATCH 7/7] test(attest jira): pin --jira-trailer/--jira-secondary-source mutual exclusion Co-Authored-By: Claude Sonnet 4.6 --- cmd/kosli/attestJira_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/attestJira_test.go b/cmd/kosli/attestJira_test.go index 45fabe40c..7739d1d35 100644 --- a/cmd/kosli/attestJira_test.go +++ b/cmd/kosli/attestJira_test.go @@ -404,7 +404,13 @@ func (suite *AttestJiraCommandTestSuite) TestAttestJiraCmd() { }, { wantError: true, - name: "30 --jira-trailer does not scan branch name even when it contains a Jira key", + name: "30 --jira-trailer and --jira-secondary-source are mutually exclusive", + cmd: fmt.Sprintf("attest jira --name bar --jira-base-url https://kosli-test.atlassian.net --jira-trailer Jira --jira-secondary-source foo --commit HEAD --repo-root %s %s", suite.tmpDir, suite.defaultKosliArguments), + golden: "Error: only one of --jira-trailer, --jira-secondary-source is allowed\n", + }, + { + wantError: true, + name: "31 --jira-trailer does not scan branch name even when branch contains a Jira key", cmd: fmt.Sprintf(`attest jira --name bar --jira-base-url https://kosli-test.atlassian.net --jira-trailer Jira