Skip to content

Commit 8cfe2f1

Browse files
authored
ci: Allow release note enrichment when "closes" is present (#5398)
An interesting thing I discovered with release please is that if a commit has a reference to a closed issue, this will be called out in the changelog 🎉 For example: ``` * Invalid handling of id in foreach when using discovery components (\#5322) (61fe184), closes \#5297 ``` The only problem is this breaks the way I wrote the regex for the release not enrichment script. This PR fixes that.
1 parent fad2df2 commit 8cfe2f1

File tree

2 files changed

+136
-6
lines changed

2 files changed

+136
-6
lines changed

tools/release/enrich-release-notes/main.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import (
1313
"github.com/grafana/alloy/tools/release/internal/version"
1414
)
1515

16+
// commitPattern matches a commit SHA in markdown link format: "([abc1234](https://github.com/.../commit/...))"
17+
// It captures the short SHA from the link text, regardless of surrounding context.
18+
var commitPattern = regexp.MustCompile(`\(\[([a-f0-9]{7,40})\]\(https://github\.com/[^)]+\)\)`)
19+
1620
// commitAuthorsQuery is the GraphQL query to fetch all authors for a commit.
1721
const commitAuthorsQuery = `query($owner: String!, $repo: String!, $oid: GitObjectID!) {
1822
repository(owner: $owner, name: $repo) {
@@ -109,13 +113,20 @@ func main() {
109113
fmt.Println("✅ Release notes updated successfully")
110114
}
111115

116+
// extractCommitSHA extracts a commit SHA from a changelog line.
117+
// Returns the SHA if found, or an empty string if no commit link is present.
118+
func extractCommitSHA(line string) string {
119+
matches := commitPattern.FindStringSubmatch(line)
120+
if matches == nil {
121+
return ""
122+
}
123+
return matches[1]
124+
}
125+
112126
// addContributorInfo adds contributor usernames to changelog entries.
113127
// It extracts commit SHAs from each line and looks up the author + co-authors.
114128
func addContributorInfo(ctx context.Context, client *gh.Client, body string) string {
115129
lines := strings.Split(body, "\n")
116-
// Match commit SHA in markdown link format: "([abc1234](https://github.com/.../commit/...))"
117-
// This captures the short SHA from the link text
118-
commitPattern := regexp.MustCompile(`\(\[([a-f0-9]{7,40})\]\(https://github\.com/[^)]+\)\)\s*$`)
119130

120131
for i, line := range lines {
121132
if strings.TrimSpace(line) == "" {
@@ -124,12 +135,11 @@ func addContributorInfo(ctx context.Context, client *gh.Client, body string) str
124135

125136
fmt.Printf(" Processing line %d: %s\n", i, line)
126137

127-
matches := commitPattern.FindStringSubmatch(line)
128-
if matches == nil {
138+
sha := extractCommitSHA(line)
139+
if sha == "" {
129140
fmt.Printf(" No commit SHA found in line %d\n", i)
130141
continue
131142
}
132-
sha := matches[1]
133143

134144
contributors, err := getCommitContributors(ctx, client, sha)
135145
if err != nil {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package main
2+
3+
import "testing"
4+
5+
func TestExtractCommitSHA(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
input string
9+
expectedSHA string
10+
}{
11+
{
12+
name: "commit link at end of line",
13+
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))",
14+
expectedSHA: "1c97c2d",
15+
},
16+
{
17+
name: "commit link with closes reference after",
18+
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)",
19+
expectedSHA: "61fe184",
20+
},
21+
{
22+
name: "commit link with extra notes after",
23+
input: "* Some fix ([deadbeef](https://github.com/grafana/alloy/commit/deadbeef)) - extra notes here",
24+
expectedSHA: "deadbeef",
25+
},
26+
{
27+
name: "full 40-character SHA",
28+
input: "* Fix bug ([abc1234567890def1234567890abc1234567890](https://github.com/grafana/alloy/commit/abc1234567890def1234567890abc1234567890))",
29+
expectedSHA: "abc1234567890def1234567890abc1234567890",
30+
},
31+
{
32+
name: "no parens around link",
33+
input: "* No parens [abc1234](https://github.com/grafana/alloy/commit/abc1234)",
34+
expectedSHA: "",
35+
},
36+
{
37+
name: "just a PR reference",
38+
input: "* Just a PR reference (#1234)",
39+
expectedSHA: "",
40+
},
41+
{
42+
name: "empty line",
43+
input: "",
44+
expectedSHA: "",
45+
},
46+
{
47+
name: "line with no commit info",
48+
input: "### Bug Fixes",
49+
expectedSHA: "",
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
sha := extractCommitSHA(tt.input)
56+
if sha != tt.expectedSHA {
57+
t.Errorf("extractCommitSHA(%q) = %q, want %q", tt.input, sha, tt.expectedSHA)
58+
}
59+
})
60+
}
61+
}
62+
63+
func TestFormatAttribution(t *testing.T) {
64+
tests := []struct {
65+
name string
66+
usernames []string
67+
expected string
68+
}{
69+
{
70+
name: "single user",
71+
usernames: []string{"alice"},
72+
expected: "(@alice)",
73+
},
74+
{
75+
name: "multiple users",
76+
usernames: []string{"alice", "bob", "charlie"},
77+
expected: "(@alice, @bob, @charlie)",
78+
},
79+
}
80+
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
result := formatAttribution(tt.usernames)
84+
if result != tt.expected {
85+
t.Errorf("formatAttribution(%v) = %q, want %q", tt.usernames, result, tt.expected)
86+
}
87+
})
88+
}
89+
}
90+
91+
func TestDeriveDocTag(t *testing.T) {
92+
tests := []struct {
93+
name string
94+
tag string
95+
expected string
96+
}{
97+
{
98+
name: "standard release",
99+
tag: "v1.15.2",
100+
expected: "v1.15",
101+
},
102+
{
103+
name: "release candidate",
104+
tag: "v1.2.3-rc.0",
105+
expected: "v1.2",
106+
},
107+
}
108+
109+
for _, tt := range tests {
110+
t.Run(tt.name, func(t *testing.T) {
111+
result, err := deriveDocTag(tt.tag)
112+
if err != nil {
113+
t.Fatalf("deriveDocTag(%q) returned error: %v", tt.tag, err)
114+
}
115+
if result != tt.expected {
116+
t.Errorf("deriveDocTag(%q) = %q, want %q", tt.tag, result, tt.expected)
117+
}
118+
})
119+
}
120+
}

0 commit comments

Comments
 (0)