refactor: let updaters define the files they want to run on (#233)

This change reverses the responsibility for which files the updaters are
run on. Now each updater can specify the list of files and wether the
files should be created when they do not exist yet. This simplifies the
handling of each update in releaserpleaser.go, as we can just iterate
over all updaters and call it for each file of that updater.

Also update the flags to allow users to easily define which updaters
should run.
This commit is contained in:
Julian Tölle 2025-08-23 22:14:34 +02:00 committed by GitHub
parent 1e9e0aa5d9
commit f1aa1a2ef4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 307 additions and 151 deletions

View file

@ -6,7 +6,6 @@ import (
"io"
"log/slog"
"os"
"path/filepath"
"time"
"github.com/go-git/go-git/v5"
@ -14,8 +13,6 @@ import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/apricote/releaser-pleaser/internal/updater"
)
const (
@ -120,7 +117,7 @@ func (r *Repository) Checkout(_ context.Context, branch string) error {
return nil
}
func (r *Repository) UpdateFile(_ context.Context, path string, create bool, updaters []updater.Updater) error {
func (r *Repository) UpdateFile(_ context.Context, path string, create bool, updateHook func(string) (string, error)) error {
worktree, err := r.r.Worktree()
if err != nil {
return err
@ -142,13 +139,9 @@ func (r *Repository) UpdateFile(_ context.Context, path string, create bool, upd
return err
}
updatedContent := string(content)
for _, update := range updaters {
updatedContent, err = update(updatedContent, filepath.Base(path))
if err != nil {
return fmt.Errorf("failed to run updater on file %s", path)
}
updatedContent, err := updateHook(string(content))
if err != nil {
return fmt.Errorf("failed to run update hook on file %s", path)
}
err = file.Truncate(0)

View file

@ -14,8 +14,23 @@ var (
ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`)
)
func Changelog(info ReleaseInfo) Updater {
return func(content string, filename string) (string, error) {
func Changelog() Updater {
return changelog{}
}
type changelog struct {
}
func (c changelog) Files() []string {
return []string{ChangelogFile}
}
func (c changelog) CreateNewFiles() bool {
return true
}
func (c changelog) Update(info ReleaseInfo) func(content string) (string, error) {
return func(content string) (string, error) {
headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content)
if headerIndex == nil && len(content) != 0 {
return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match")

View file

@ -6,15 +6,22 @@ import (
"github.com/stretchr/testify/assert"
)
func TestChangelogUpdater_UpdateContent(t *testing.T) {
func TestChangelogUpdater_Files(t *testing.T) {
assert.Equal(t, []string{"CHANGELOG.md"}, Changelog().Files())
}
func TestChangelogUpdater_CreateNewFiles(t *testing.T) {
assert.True(t, Changelog().CreateNewFiles())
}
func TestChangelogUpdater_Update(t *testing.T) {
tests := []updaterTestCase{
{
name: "empty file",
content: "",
filename: "CHANGELOG.md",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"},
want: "# Changelog\n\n## v1.0.0\n",
wantErr: assert.NoError,
name: "empty file",
content: "",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"},
want: "# Changelog\n\n## v1.0.0\n",
wantErr: assert.NoError,
},
{
name: "well-formatted changelog",
@ -28,8 +35,7 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) {
### Bazuuum
`,
filename: "CHANGELOG.md",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"},
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"},
want: `# Changelog
## v1.0.0
@ -47,17 +53,16 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "error on invalid header",
content: "What even is this file?",
filename: "CHANGELOG.md",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"},
want: "",
wantErr: assert.Error,
name: "error on invalid header",
content: "What even is this file?",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"},
want: "",
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runUpdaterTest(t, Changelog, tt)
runUpdaterTest(t, Changelog(), tt)
})
}
}

View file

@ -7,8 +7,26 @@ import (
var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`)
func Generic(info ReleaseInfo) Updater {
return func(content string, filename string) (string, error) {
func Generic(files []string) Updater {
return generic{
files: files,
}
}
type generic struct {
files []string
}
func (g generic) Files() []string {
return g.files
}
func (g generic) CreateNewFiles() bool {
return false
}
func (g generic) Update(info ReleaseInfo) func(content string) (string, error) {
return func(content string) (string, error) {
// We strip the "v" prefix to avoid adding/removing it from the users input.
version := strings.TrimPrefix(info.Version, "v")

View file

@ -6,12 +6,19 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGenericUpdater_UpdateContent(t *testing.T) {
func TestGenericUpdater_Files(t *testing.T) {
assert.Equal(t, []string{"foo.bar", "version.txt"}, Generic([]string{"foo.bar", "version.txt"}).Files())
}
func TestGenericUpdater_CreateNewFiles(t *testing.T) {
assert.False(t, Generic([]string{}).CreateNewFiles())
}
func TestGenericUpdater_Update(t *testing.T) {
tests := []updaterTestCase{
{
name: "single line",
content: "v1.0.0 // x-releaser-pleaser-version",
filename: "version.txt",
name: "single line",
content: "v1.0.0 // x-releaser-pleaser-version",
info: ReleaseInfo{
Version: "v1.2.0",
},
@ -19,9 +26,8 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "multiline line",
content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n",
filename: "version.txt",
name: "multiline line",
content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n",
info: ReleaseInfo{
Version: "v1.2.0",
},
@ -29,9 +35,8 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "invalid existing version",
content: "1.0 // x-releaser-pleaser-version",
filename: "version.txt",
name: "invalid existing version",
content: "1.0 // x-releaser-pleaser-version",
info: ReleaseInfo{
Version: "v1.2.0",
},
@ -39,9 +44,8 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "complicated line",
content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar",
filename: "version.txt",
name: "complicated line",
content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar",
info: ReleaseInfo{
Version: "v1.2.0",
},
@ -51,7 +55,7 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runUpdaterTest(t, Generic, tt)
runUpdaterTest(t, Generic([]string{"version.txt"}), tt)
})
}
}

View file

@ -6,11 +6,22 @@ import (
)
// PackageJson creates an updater that modifies the version field in package.json files
func PackageJson(info ReleaseInfo) Updater {
return func(content string, filename string) (string, error) {
if filename != "package.json" {
return content, nil // No update needed for non-package.json files
}
func PackageJson() Updater {
return packagejson{}
}
type packagejson struct{}
func (p packagejson) Files() []string {
return []string{"package.json"}
}
func (p packagejson) CreateNewFiles() bool {
return false
}
func (p packagejson) Update(info ReleaseInfo) func(content string) (string, error) {
return func(content string) (string, error) {
// We strip the "v" prefix to match npm versioning convention
version := strings.TrimPrefix(info.Version, "v")
@ -23,8 +34,6 @@ func PackageJson(info ReleaseInfo) Updater {
}
// Replace the version value while preserving the original formatting
updatedContent := versionRegex.ReplaceAllString(content, `${1}"`+version+`"`)
return updatedContent, nil
return versionRegex.ReplaceAllString(content, `${1}"`+version+`"`), nil
}
}

View file

@ -1,18 +1,24 @@
package updater
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPackageJsonUpdater(t *testing.T) {
func TestPackageJsonUpdater_Files(t *testing.T) {
assert.Equal(t, []string{"package.json"}, PackageJson().Files())
}
func TestPackageJsonUpdater_CreateNewFiles(t *testing.T) {
assert.False(t, PackageJson().CreateNewFiles())
}
func TestPackageJsonUpdater_Update(t *testing.T) {
tests := []updaterTestCase{
{
name: "simple package.json",
content: `{"name":"test","version":"1.0.0"}`,
filename: "package.json",
name: "simple package.json",
content: `{"name":"test","version":"1.0.0"}`,
info: ReleaseInfo{
Version: "v2.0.5",
},
@ -20,19 +26,8 @@ func TestPackageJsonUpdater(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "simple package.json, wrong name",
content: `{"name":"test","version":"1.0.0"}`,
filename: "nopackage.json",
info: ReleaseInfo{
Version: "v2.0.5",
},
want: `{"name":"test","version":"1.0.0"}`,
wantErr: assert.NoError,
},
{
name: "complex package.json",
content: "{\n \"name\": \"test\",\n \"version\": \"1.0.0\",\n \"dependencies\": {\n \"foo\": \"^1.0.0\"\n }\n}",
filename: "package.json",
name: "complex package.json",
content: "{\n \"name\": \"test\",\n \"version\": \"1.0.0\",\n \"dependencies\": {\n \"foo\": \"^1.0.0\"\n }\n}",
info: ReleaseInfo{
Version: "v2.0.0",
},
@ -40,9 +35,8 @@ func TestPackageJsonUpdater(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "invalid json",
content: `not json`,
filename: "package.json",
name: "invalid json",
content: `not json`,
info: ReleaseInfo{
Version: "v2.0.0",
},
@ -50,9 +44,8 @@ func TestPackageJsonUpdater(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "json without version",
content: `{"name":"test"}`,
filename: "package.json",
name: "json without version",
content: `{"name":"test"}`,
info: ReleaseInfo{
Version: "v2.0.0",
},
@ -63,8 +56,7 @@ func TestPackageJsonUpdater(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fmt.Println("Running updater test for PackageJson")
runUpdaterTest(t, PackageJson, tt)
runUpdaterTest(t, PackageJson(), tt)
})
}
}

View file

@ -5,7 +5,11 @@ type ReleaseInfo struct {
ChangelogEntry string
}
type Updater func(content string, filename string) (string, error)
type Updater interface {
Files() []string
CreateNewFiles() bool
Update(info ReleaseInfo) func(content string) (string, error)
}
type NewUpdater func(ReleaseInfo) Updater

View file

@ -8,20 +8,19 @@ import (
)
type updaterTestCase struct {
name string
content string
filename string
info ReleaseInfo
want string
wantErr assert.ErrorAssertionFunc
name string
content string
info ReleaseInfo
want string
wantErr assert.ErrorAssertionFunc
}
func runUpdaterTest(t *testing.T, constructor NewUpdater, tt updaterTestCase) {
func runUpdaterTest(t *testing.T, u Updater, tt updaterTestCase) {
t.Helper()
got, err := constructor(tt.info)(tt.content, tt.filename)
if !tt.wantErr(t, err, fmt.Sprintf("Updater(%v, %v, %v)", tt.content, tt.filename, tt.info)) {
got, err := u.Update(tt.info)(tt.content)
if !tt.wantErr(t, err, fmt.Sprintf("Updater(%v, %v)", tt.content, tt.info)) {
return
}
assert.Equalf(t, tt.want, got, "Updater(%v, %v, %v)", tt.content, tt.filename, tt.info)
assert.Equalf(t, tt.want, got, "Updater(%v, %v)", tt.content, tt.info)
}