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 run: pnpm install
working-directory: website working-directory: website
- name: Generate release notes
run: go run ./cmd/release --notes > "${RUNNER_TEMP}/release-notes.md"
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: latest version: latest
args: release --clean --draft args: release --clean --release-notes=${{ runner.temp }}/release-notes.md
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GH_GORELEASER_TOKEN: ${{secrets.GH_GORELEASER_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: 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 - Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml
preconditions: preconditions:
- sh: test $(git rev-parse --abbrev-ref HEAD) = "main" - sh: test $(git rev-parse --abbrev-ref HEAD) = "main"

View File

@@ -43,20 +43,29 @@ var changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
// Flags // Flags
var ( var (
versionFlag bool versionFlag bool
notesFlag bool
) )
func init() { func init() {
pflag.BoolVarP(&versionFlag, "version", "v", false, "resolved version number") 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() pflag.Parse()
} }
func main() { func main() {
if err := release(); err != nil { if err := run(); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
} }
func run() error {
if notesFlag {
return notes(os.Stdout)
}
return release()
}
func release() error { func release() error {
if len(pflag.Args()) != 1 { if len(pflag.Args()) != 1 {
return errors.New("error: expected version number") 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 artifacts automatically when a new Git tag is pushed to `main` branch (raw
executables and DEB and RPM packages). 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 Raw executables can also be reproduced and verified locally by
checking out a specific tag and calling `goreleaser build`, using the Go version checking out a specific tag and calling `goreleaser build`, using the Go version
defined in the above GitHub Actions. defined in the above GitHub Actions.