feat(release): generate the GitHub release body from the changelog (#2987)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Valentin Maerten
2026-08-21 15:31:52 +02:00
committed by GitHub
parent dd4463ec60
commit 30c998136d
6 changed files with 141 additions and 3 deletions

View File

@@ -46,12 +46,15 @@ jobs:
run: pnpm install
working-directory: website
- name: Generate release notes
run: go run ./cmd/release --notes > "${RUNNER_TEMP}/release-notes.md"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
with:
distribution: goreleaser-pro
version: latest
args: release --clean --draft
args: release --clean --release-notes=${{ runner.temp }}/release-notes.md
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GH_GORELEASER_TOKEN: ${{secrets.GH_GORELEASER_TOKEN}}

View File

@@ -217,7 +217,6 @@ tasks:
Please wait for the CI to finish and then do the following:
- Copy the changelog for v{{.VERSION}} to the GitHub release
- Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml
preconditions:
- sh: test $(git rev-parse --abbrev-ref HEAD) = "main"

View File

@@ -43,20 +43,29 @@ var changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
// Flags
var (
versionFlag bool
notesFlag bool
)
func init() {
pflag.BoolVarP(&versionFlag, "version", "v", false, "resolved version number")
pflag.BoolVar(&notesFlag, "notes", false, "changelog section of the current version, for the GitHub release body")
pflag.Parse()
}
func main() {
if err := release(); err != nil {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func run() error {
if notesFlag {
return notes(os.Stdout)
}
return release()
}
func release() error {
if len(pflag.Args()) != 1 {
return errors.New("error: expected version number")

48
cmd/release/notes.go Normal file
View File

@@ -0,0 +1,48 @@
package main
import (
"fmt"
"io"
"os"
"strings"
"github.com/Masterminds/semver/v3"
)
func notes(w io.Writer) error {
version, err := getVersion(versionFile)
if err != nil {
return err
}
b, err := os.ReadFile(changelogSource)
if err != nil {
return err
}
section := changelogSection(string(b), version)
if section == "" {
return nil
}
_, err = fmt.Fprintln(w, section)
return err
}
func changelogSection(changelog string, version *semver.Version) string {
heading := fmt.Sprintf("## v%s ", version)
var section []string
var found bool
for line := range strings.SplitSeq(changelog, "\n") {
if strings.HasPrefix(line, "## ") {
if found {
break
}
found = strings.HasPrefix(line, heading)
continue
}
if found {
section = append(section, line)
}
}
return strings.TrimSpace(strings.Join(section, "\n"))
}

75
cmd/release/notes_test.go Normal file
View File

@@ -0,0 +1,75 @@
package main
import (
"testing"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
)
func TestChangelogSection(t *testing.T) {
t.Parallel()
const changelog = `# Changelog
## v3.53.1 - 2026-08-18
### 🚀 Features
- Something new (#1 by @someone).
### 🐛 Fixes
- Something fixed (#2 by @someone).
## v3.53.10 - 2026-08-17
- An entry of a version whose heading starts with "## v3.53.1".
## v3.5.0 - 2026-01-01
- An older entry.
`
tests := []struct {
name string
version string
expected string
}{
{
name: "section with sub-headings",
version: "3.53.1",
expected: `### 🚀 Features
- Something new (#1 by @someone).
### 🐛 Fixes
- Something fixed (#2 by @someone).`,
},
{
name: "last section of the file",
version: "3.5.0",
expected: "- An older entry.",
},
{
// A patch release may ship without changelog entries.
name: "missing section",
version: "3.53.2",
expected: "",
},
{
name: "heading of another version starts with this one",
version: "3.53.10",
expected: `- An entry of a version whose heading starts with "## v3.53.1".`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
version := semver.MustParse(test.version)
assert.Equal(t, test.expected, changelogSection(changelog, version))
})
}
}

View File

@@ -16,6 +16,10 @@ of the Taskfile.
artifacts automatically when a new Git tag is pushed to `main` branch (raw
executables and DEB and RPM packages).
The body of the GitHub release is the section of the `CHANGELOG.md` matching the
version being released, extracted by `go run ./cmd/release --notes`. A version
without changelog entries is released with an empty body.
Raw executables can also be reproduced and verified locally by
checking out a specific tag and calling `goreleaser build`, using the Go version
defined in the above GitHub Actions.