Skip to content

Commit b8af95c

Browse files
committed
chore: rewrite prepare-release script as a go cmd
This change allows release engineers to generate the changelog PR via the local gh command thus inheriting the user's credentials. This fixes permissions issues in the release process. The script is added as a go tool via go.mod. Signed-off-by: crozzy <joseph.crosland@gmail.com>
1 parent 134ff1a commit b8af95c

7 files changed

Lines changed: 455 additions & 174 deletions

File tree

.github/scripts/changelog-render

Lines changed: 0 additions & 96 deletions
This file was deleted.

.github/scripts/changelog-update

Lines changed: 0 additions & 28 deletions
This file was deleted.

.github/scripts/prepare-release

Lines changed: 0 additions & 48 deletions
This file was deleted.

docs/contributor/releases.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ On rare occasions when a fix is time-sensitive, it is possible to create a relea
1414

1515
```sh
1616
NEW_VERSION=v0.999.999
17-
.github/scripts/prepare-release -b main -r upstream "$NEW_VERSION"
17+
go tool prepare-release -remote=upstream -branch=main "$NEW_VERSION"
1818
```
1919

2020
Follow the `prepare-release` command's instructions to merge changelog updates and release the tag.
@@ -42,7 +42,7 @@ Finally, prepare the release specifying the release branch.
4242
LAST_MINOR=v0.999.0
4343
NEW_VERSION=v0.999.1
4444
BRANCH=release-${LAST_MINOR%.*} # e.g. release-v0.999
45-
.github/scripts/prepare-release -b $BRANCH -r upstream $NEW_VERSION
45+
go tool prepare-release -remote=upstream -branch=$BRANCH $NEW_VERSION
4646
```
4747

4848
Follow the `prepare-release` command's instructions to merge changelog updates and release the tag.

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,5 @@ require (
6262
)
6363

6464
replace github.com/quay/claircore/updater/driver => ./updater/driver
65+
66+
tool github.com/quay/claircore/internal/cmd/prepare-release
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"regexp"
11+
"strings"
12+
"time"
13+
)
14+
15+
const (
16+
notesRef = "refs/notes/changelog"
17+
compareURL = "https://github.com/quay/claircore/compare"
18+
)
19+
20+
// getPreviousTag finds the most recent version tag merged into the current branch.
21+
func getPreviousTag() (string, error) {
22+
out, err := exec.Command("git", "tag", "--sort=-taggerdate", "--merged").Output()
23+
if err != nil {
24+
return "", fmt.Errorf("listing tags: %w", err)
25+
}
26+
for _, line := range strings.Split(string(out), "\n") {
27+
if strings.HasPrefix(line, "v") {
28+
return line, nil
29+
}
30+
}
31+
return "", fmt.Errorf("no version tags found")
32+
}
33+
34+
// renderChangelog generates the changelog content for a new release.
35+
// It reads git notes from commits between previous tag and the branch.
36+
func renderChangelog(w io.Writer, prevTag, nextTag, branch string) error {
37+
// Write header
38+
fmt.Fprintf(w, "<a name=\"%s\"></a>\n", nextTag)
39+
fmt.Fprintf(w, "## [%s] - %s\n", nextTag, time.Now().Format("2006-01-02"))
40+
fmt.Fprintf(w, "[%s]: %s/%s...%s\n\n", nextTag, compareURL, prevTag, nextTag)
41+
42+
// Get commits since prevTag on the target branch
43+
out, err := exec.Command("git", "rev-list", prevTag+".."+branch).Output()
44+
if err != nil {
45+
return fmt.Errorf("listing commits: %w", err)
46+
}
47+
48+
hasNotes := false
49+
for _, commit := range strings.Split(strings.TrimSpace(string(out)), "\n") {
50+
if commit == "" {
51+
continue
52+
}
53+
note, err := getCommitNote(commit)
54+
if err != nil || note == "" {
55+
continue
56+
}
57+
hasNotes = true
58+
formatted := formatNote(note)
59+
fmt.Fprint(w, formatted)
60+
}
61+
62+
if !hasNotes {
63+
fmt.Fprintln(w, "Nothing interesting happened this release.")
64+
fmt.Fprintln(w)
65+
}
66+
67+
return nil
68+
}
69+
70+
// getCommitNote retrieves the changelog note for a commit.
71+
func getCommitNote(commit string) (string, error) {
72+
cmd := exec.Command("git", "notes", "--ref="+notesRef, "show", commit)
73+
out, err := cmd.Output()
74+
if err != nil {
75+
// No soup for you
76+
return "", err
77+
}
78+
return string(out), nil
79+
}
80+
81+
// formatNote formats a changelog note into markdown.
82+
func formatNote(note string) string {
83+
var sb strings.Builder
84+
lines := strings.Split(strings.TrimSpace(note), "\n")
85+
86+
hasEmptyLine := false
87+
for _, line := range lines {
88+
if line == "" {
89+
hasEmptyLine = true
90+
break
91+
}
92+
}
93+
94+
if hasEmptyLine {
95+
// Do the details thing
96+
first := true
97+
inDetails := false
98+
for _, line := range lines {
99+
line = linkifyJira(line)
100+
if first {
101+
sb.WriteString("- ")
102+
sb.WriteString(line)
103+
sb.WriteString("\n")
104+
first = false
105+
} else if line == "" && !inDetails {
106+
sb.WriteString(" <details>\n")
107+
inDetails = true
108+
} else {
109+
sb.WriteString(" ")
110+
sb.WriteString(line)
111+
sb.WriteString("\n")
112+
}
113+
}
114+
if inDetails {
115+
sb.WriteString(" </details>\n")
116+
}
117+
} else {
118+
// Just print it
119+
for _, line := range lines {
120+
line = linkifyJira(line)
121+
sb.WriteString(line)
122+
sb.WriteString("\n")
123+
}
124+
}
125+
sb.WriteString("\n")
126+
127+
return sb.String()
128+
}
129+
130+
var jiraPattern = regexp.MustCompile(`(PROJQUAY|CLAIRDEV)-[0-9]+`)
131+
132+
const jiraURL = "https://redhat.atlassian.net/browse"
133+
134+
// linkifyJira replaces PROJQUAY-XXX with markdown links.
135+
func linkifyJira(s string) string {
136+
return jiraPattern.ReplaceAllStringFunc(s, func(match string) string {
137+
return fmt.Sprintf("[%s](%s/%s)", match, jiraURL, match)
138+
})
139+
}
140+
141+
// updateChangelogFile renders the new changelog section, reads the existing changelog
142+
// into memory and then writes the new changelog section then the existing changelog.
143+
func updateChangelogFile(repoRoot, prevTag, nextTag, branch string) error {
144+
changelogPath := filepath.Join(repoRoot, "CHANGELOG.md")
145+
146+
// Render new content
147+
var newChangelog strings.Builder
148+
if err := renderChangelog(&newChangelog, prevTag, nextTag, branch); err != nil {
149+
return fmt.Errorf("rendering changelog: %w", err)
150+
}
151+
152+
// Read existing changelog
153+
existing, err := os.ReadFile(changelogPath)
154+
if err != nil && !os.IsNotExist(err) {
155+
return fmt.Errorf("reading existing changelog: %w", err)
156+
}
157+
158+
// Write new changelog (prepend new content)
159+
f, err := os.Create(changelogPath)
160+
if err != nil {
161+
return fmt.Errorf("creating changelog: %w", err)
162+
}
163+
defer f.Close()
164+
165+
bw := bufio.NewWriter(f)
166+
bw.WriteString(newChangelog.String())
167+
bw.Write(existing)
168+
if err := bw.Flush(); err != nil {
169+
return fmt.Errorf("writing changelog: %w", err)
170+
}
171+
172+
return nil
173+
}

0 commit comments

Comments
 (0)