From 60c8f5949fec63a8528ffee970b6a7d561f963d3 Mon Sep 17 00:00:00 2001 From: Joe Harvey <51208233+jharvey10@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:37:39 -0500 Subject: [PATCH 1/2] ci: allow release note enrichment when "closes" present --- tools/release/enrich-release-notes/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/release/enrich-release-notes/main.go b/tools/release/enrich-release-notes/main.go index ba23494c71f..7248a79cc93 100644 --- a/tools/release/enrich-release-notes/main.go +++ b/tools/release/enrich-release-notes/main.go @@ -114,8 +114,8 @@ func main() { func addContributorInfo(ctx context.Context, client *gh.Client, body string) string { lines := strings.Split(body, "\n") // Match commit SHA in markdown link format: "([abc1234](https://github.com/.../commit/...))" - // This captures the short SHA from the link text - commitPattern := regexp.MustCompile(`\(\[([a-f0-9]{7,40})\]\(https://github\.com/[^)]+\)\)\s*$`) + // This captures the short SHA from the link text, regardless of surrounding context + commitPattern := regexp.MustCompile(`\(\[([a-f0-9]{7,40})\]\(https://github\.com/[^)]+\)\)`) for i, line := range lines { if strings.TrimSpace(line) == "" { From d7c266b2fce02c6f7e5f20b1c13a839e1fdb7f30 Mon Sep 17 00:00:00 2001 From: Joe Harvey <51208233+jharvey10@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:53:28 -0500 Subject: [PATCH 2/2] test: add unit tests --- tools/release/enrich-release-notes/main.go | 22 +++- .../release/enrich-release-notes/main_test.go | 120 ++++++++++++++++++ 2 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 tools/release/enrich-release-notes/main_test.go diff --git a/tools/release/enrich-release-notes/main.go b/tools/release/enrich-release-notes/main.go index 7248a79cc93..48eaec234bb 100644 --- a/tools/release/enrich-release-notes/main.go +++ b/tools/release/enrich-release-notes/main.go @@ -13,6 +13,10 @@ import ( "github.com/grafana/alloy/tools/release/internal/version" ) +// commitPattern matches a commit SHA in markdown link format: "([abc1234](https://github.com/.../commit/...))" +// It captures the short SHA from the link text, regardless of surrounding context. +var commitPattern = regexp.MustCompile(`\(\[([a-f0-9]{7,40})\]\(https://github\.com/[^)]+\)\)`) + // commitAuthorsQuery is the GraphQL query to fetch all authors for a commit. const commitAuthorsQuery = `query($owner: String!, $repo: String!, $oid: GitObjectID!) { repository(owner: $owner, name: $repo) { @@ -109,13 +113,20 @@ func main() { fmt.Println("✅ Release notes updated successfully") } +// extractCommitSHA extracts a commit SHA from a changelog line. +// Returns the SHA if found, or an empty string if no commit link is present. +func extractCommitSHA(line string) string { + matches := commitPattern.FindStringSubmatch(line) + if matches == nil { + return "" + } + return matches[1] +} + // addContributorInfo adds contributor usernames to changelog entries. // It extracts commit SHAs from each line and looks up the author + co-authors. func addContributorInfo(ctx context.Context, client *gh.Client, body string) string { lines := strings.Split(body, "\n") - // Match commit SHA in markdown link format: "([abc1234](https://github.com/.../commit/...))" - // This captures the short SHA from the link text, regardless of surrounding context - commitPattern := regexp.MustCompile(`\(\[([a-f0-9]{7,40})\]\(https://github\.com/[^)]+\)\)`) for i, line := range lines { if strings.TrimSpace(line) == "" { @@ -124,12 +135,11 @@ func addContributorInfo(ctx context.Context, client *gh.Client, body string) str fmt.Printf(" Processing line %d: %s\n", i, line) - matches := commitPattern.FindStringSubmatch(line) - if matches == nil { + sha := extractCommitSHA(line) + if sha == "" { fmt.Printf(" No commit SHA found in line %d\n", i) continue } - sha := matches[1] contributors, err := getCommitContributors(ctx, client, sha) if err != nil { diff --git a/tools/release/enrich-release-notes/main_test.go b/tools/release/enrich-release-notes/main_test.go new file mode 100644 index 00000000000..a5069a0e2c6 --- /dev/null +++ b/tools/release/enrich-release-notes/main_test.go @@ -0,0 +1,120 @@ +package main + +import "testing" + +func TestExtractCommitSHA(t *testing.T) { + tests := []struct { + name string + input string + expectedSHA string + }{ + { + name: "commit link at end of line", + input: "* HTTP/2 is no longer always disabled in loki.write ([#5267](https://github.com/grafana/alloy/issues/5267)) ([1c97c2d](https://github.com/grafana/alloy/commit/1c97c2d569fcda2f6761534150b063d1404dc388))", + expectedSHA: "1c97c2d", + }, + { + name: "commit link with closes reference after", + input: "* Invalid handling of `id` in `foreach` when using discovery components ([#5322](https://github.com/grafana/alloy/issues/5322)) ([61fe184](https://github.com/grafana/alloy/commit/61fe1845d3b109992cbb0ec99a062ac113c1a411)), closes [#5297](https://github.com/grafana/alloy/issues/5297)", + expectedSHA: "61fe184", + }, + { + name: "commit link with extra notes after", + input: "* Some fix ([deadbeef](https://github.com/grafana/alloy/commit/deadbeef)) - extra notes here", + expectedSHA: "deadbeef", + }, + { + name: "full 40-character SHA", + input: "* Fix bug ([abc1234567890def1234567890abc1234567890](https://github.com/grafana/alloy/commit/abc1234567890def1234567890abc1234567890))", + expectedSHA: "abc1234567890def1234567890abc1234567890", + }, + { + name: "no parens around link", + input: "* No parens [abc1234](https://github.com/grafana/alloy/commit/abc1234)", + expectedSHA: "", + }, + { + name: "just a PR reference", + input: "* Just a PR reference (#1234)", + expectedSHA: "", + }, + { + name: "empty line", + input: "", + expectedSHA: "", + }, + { + name: "line with no commit info", + input: "### Bug Fixes", + expectedSHA: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sha := extractCommitSHA(tt.input) + if sha != tt.expectedSHA { + t.Errorf("extractCommitSHA(%q) = %q, want %q", tt.input, sha, tt.expectedSHA) + } + }) + } +} + +func TestFormatAttribution(t *testing.T) { + tests := []struct { + name string + usernames []string + expected string + }{ + { + name: "single user", + usernames: []string{"alice"}, + expected: "(@alice)", + }, + { + name: "multiple users", + usernames: []string{"alice", "bob", "charlie"}, + expected: "(@alice, @bob, @charlie)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatAttribution(tt.usernames) + if result != tt.expected { + t.Errorf("formatAttribution(%v) = %q, want %q", tt.usernames, result, tt.expected) + } + }) + } +} + +func TestDeriveDocTag(t *testing.T) { + tests := []struct { + name string + tag string + expected string + }{ + { + name: "standard release", + tag: "v1.15.2", + expected: "v1.15", + }, + { + name: "release candidate", + tag: "v1.2.3-rc.0", + expected: "v1.2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := deriveDocTag(tt.tag) + if err != nil { + t.Fatalf("deriveDocTag(%q) returned error: %v", tt.tag, err) + } + if result != tt.expected { + t.Errorf("deriveDocTag(%q) = %q, want %q", tt.tag, result, tt.expected) + } + }) + } +}