◐ Shell
clean mode source ↗

chore: backport release action (#26143) · coder/coder@7ef8201

1+

package main

2+3+

import (

4+

"regexp"

5+

"sort"

6+

"strconv"

7+

"strings"

8+

)

9+10+

// commitEntry represents a single non-merge commit.

11+

type commitEntry struct {

12+

SHA string

13+

FullSHA string

14+

Title string

15+

Timestamp int64

16+

}

17+18+

// cherryPickPRRe matches cherry-pick bot titles like

19+

// "chore: foo bar (cherry-pick #42) (#43)".

20+

var cherryPickPRRe = regexp.MustCompile(`\(cherry-pick #(\d+)\)\s*\(#\d+\)$`)

21+22+

// humanizedAreas maps conventional commit scopes to human-readable area

23+

// names. Order matters: more specific prefixes must come first so that

24+

// the first partial match wins.

25+

var humanizedAreas = []struct {

26+

Prefix string

27+

Area string

28+

}{

29+

{"agent/agentssh", "Agent SSH"},

30+

{"coderd/database", "Database"},

31+

{"enterprise/audit", "Auditing"},

32+

{"enterprise/cli", "CLI"},

33+

{"enterprise/coderd", "Server"},

34+

{"enterprise/dbcrypt", "Database"},

35+

{"enterprise/derpmesh", "Networking"},

36+

{"enterprise/provisionerd", "Provisioner"},

37+

{"enterprise/tailnet", "Networking"},

38+

{"enterprise/wsproxy", "Workspace Proxy"},

39+

{"agent", "Agent"},

40+

{"cli", "CLI"},

41+

{"coderd", "Server"},

42+

{"codersdk", "SDK"},

43+

{"docs", "Documentation"},

44+

{"enterprise", "Enterprise"},

45+

{"examples", "Examples"},

46+

{"helm", "Helm"},

47+

{"install.sh", "Installer"},

48+

{"provisionersdk", "SDK"},

49+

{"provisionerd", "Provisioner"},

50+

{"provisioner", "Provisioner"},

51+

{"pty", "CLI"},

52+

{"scaletest", "Scale Testing"},

53+

{"site", "Dashboard"},

54+

{"support", "Support"},

55+

{"tailnet", "Networking"},

56+

}

57+58+

// commitLog returns non-merge commits in the given range, filtering

59+

// out left-side commits (already in the base) and deduplicating

60+

// cherry-picks using git's --cherry-mark.

61+

func commitLog(commitRange string) ([]commitEntry, error) {

62+

// Use --left-right --cherry-mark to identify equivalent

63+

// (cherry-picked) commits and left-side-only commits.

64+

out, err := gitOutput("log", "--no-merges", "--left-right", "--cherry-mark",

65+

"--pretty=format:%m %ct %h %H %s", commitRange)

66+

if err != nil {

67+

return nil, err

68+

}

69+

if out == "" {

70+

return nil, nil

71+

}

72+73+

// Collect cherry-pick equivalent commits (marked with '=') so

74+

// we can skip duplicates. We keep only the right-side version.

75+

seen := make(map[string]bool)

76+77+

var entries []commitEntry

78+

for _, line := range strings.Split(out, "\n") {

79+

line = strings.TrimSpace(line)

80+

if line == "" {

81+

continue

82+

}

83+

// Format: %m %ct %h %H %s

84+

// mark timestamp shortSHA fullSHA title...

85+

parts := strings.SplitN(line, " ", 5)

86+

if len(parts) < 5 {

87+

continue

88+

}

89+

mark := parts[0]

90+

ts, _ := strconv.ParseInt(parts[1], 10, 64)

91+

shortSHA := parts[2]

92+

fullSHA := parts[3]

93+

title := parts[4]

94+95+

// Skip left-side commits (already in the old version).

96+

if mark == "<" {

97+

continue

98+

}

99+

// Skip cherry-pick equivalents that we've already seen

100+

// (marked '=' by --cherry-mark).

101+

if mark == "=" {

102+

if seen[title] {

103+

continue

104+

}

105+

seen[title] = true

106+

}

107+108+

// Normalize cherry-pick bot titles:

109+

// "chore: foo (cherry-pick #42) (#43)" → "chore: foo (#42)"

110+

if m := cherryPickPRRe.FindStringSubmatch(title); m != nil {

111+

title = title[:cherryPickPRRe.FindStringIndex(title)[0]] + "(#" + m[1] + ")"

112+

}

113+114+

entries = append(entries, commitEntry{

115+

SHA: shortSHA,

116+

FullSHA: fullSHA,

117+

Title: title,

118+

Timestamp: ts,

119+

})

120+

}

121+122+

// Sort by conventional commit prefix, then by timestamp

123+

// (matching the bash script's sort -k3,3 -k1,1n).

124+

sort.SliceStable(entries, func(i, j int) bool {

125+

pi := commitSortPrefix(entries[i].Title)

126+

pj := commitSortPrefix(entries[j].Title)

127+

if pi != pj {

128+

return pi < pj

129+

}

130+

return entries[i].Timestamp < entries[j].Timestamp

131+

})

132+133+

return entries, nil

134+

}

135+136+

// commitSortPrefix extracts the first word of a title for sorting.

137+

func commitSortPrefix(title string) string {

138+

idx := strings.IndexAny(title, " (:")

139+

if idx < 0 {

140+

return title

141+

}

142+

return title[:idx]

143+

}

144+145+

// conventionalPrefixRe extracts prefix, scope, and rest from a

146+

// conventional commit title. Does NOT match breaking "!" suffix;

147+

// those titles are left as-is (matching bash behavior).

148+

var conventionalPrefixRe = regexp.MustCompile(`^([a-z]+)(\((.+)\))?:\s*(.*)$`)

149+150+

// humanizeTitle converts a conventional commit title to a

151+

// human-readable form, e.g. "feat(site): add bar" -> "Dashboard: Add bar".

152+

func humanizeTitle(title string) string {

153+

m := conventionalPrefixRe.FindStringSubmatch(title)

154+

if m == nil {

155+

return title

156+

}

157+

scope := m[3] // may be empty

158+

rest := m[4]

159+

if rest == "" {

160+

return title

161+

}

162+

// Capitalize the first letter of the rest.

163+

rest = strings.ToUpper(rest[:1]) + rest[1:]

164+165+

if scope == "" {

166+

return rest

167+

}

168+169+

// Look up scope in humanizedAreas (first partial match wins).

170+

for _, ha := range humanizedAreas {

171+

if strings.HasPrefix(scope, ha.Prefix) {

172+

return ha.Area + ": " + rest

173+

}

174+

}

175+

// Scope not found in map; return as-is.

176+

return title

177+

}

178+179+

// breakingCommitRe matches conventional commit "!:" breaking changes.

180+

var breakingCommitRe = regexp.MustCompile(`^[a-zA-Z]+(\(.+\))?!:`)

181+182+

// categorizeCommit determines the release note section for a commit.

183+

// The priority order matches the bash script: breaking title first,

184+

// then labels (breaking, security, experimental), then prefix.

185+

func categorizeCommit(title string, labels []string) string {

186+

// Check breaking title first (matches bash behavior).

187+

if breakingCommitRe.MatchString(title) {

188+

return "breaking"

189+

}

190+191+

// Label-based categorization.

192+

for _, l := range labels {

193+

if l == "release/breaking" {

194+

return "breaking"

195+

}

196+

if l == "security" {

197+

return "security"

198+

}

199+

if l == "release/experimental" {

200+

return "experimental"

201+

}

202+

}

203+204+

// Extract the conventional commit prefix (e.g. "feat", "fix(scope)").

205+

prefixRe := regexp.MustCompile(`^([a-z]+)(\(.+\))?[!]?:`)

206+

m := prefixRe.FindStringSubmatch(title)

207+

if m == nil {

208+

return "other"

209+

}

210+211+

validPrefixes := []string{

212+

"feat", "fix", "docs", "refactor", "perf",

213+

"test", "build", "ci", "chore", "revert",

214+

}

215+

for _, p := range validPrefixes {

216+

if m[1] == p {

217+

return p

218+

}

219+

}

220+

return "other"

221+

}