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

@ -14,15 +14,15 @@ inputs:
required: false required: false
default: ${{ github.token }} default: ${{ github.token }}
extra-files: extra-files:
description: 'List of files that are scanned for version references.' description: 'List of files that are scanned for version references by the generic updater.'
required: false required: false
default: "" default: ""
update-package-json: updaters:
description: 'Update version field in package.json file.' description: "List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic"
required: false required: false
default: "false" default: ""
# Remember to update docs/reference/github-action.md # Remember to update docs/reference/github-action.md
outputs: {} outputs: { }
runs: runs:
using: 'docker' using: 'docker'
image: docker://ghcr.io/apricote/releaser-pleaser:v0.6.1 # x-releaser-pleaser-version image: docker://ghcr.io/apricote/releaser-pleaser:v0.6.1 # x-releaser-pleaser-version
@ -31,7 +31,7 @@ runs:
- --forge=github - --forge=github
- --branch=${{ inputs.branch }} - --branch=${{ inputs.branch }}
- --extra-files="${{ inputs.extra-files }}" - --extra-files="${{ inputs.extra-files }}"
- ${{ inputs.update-package-json == 'true' && '--update-package-json' || '' }} - --updaters="${{ inputs.updaters }}"
env: env:
GITHUB_TOKEN: "${{ inputs.token }}" GITHUB_TOKEN: "${{ inputs.token }}"
GITHUB_USER: "oauth2" GITHUB_USER: "oauth2"

View file

@ -2,6 +2,7 @@ package cmd
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -21,12 +22,12 @@ var runCmd = &cobra.Command{
} }
var ( var (
flagForge string flagForge string
flagBranch string flagBranch string
flagOwner string flagOwner string
flagRepo string flagRepo string
flagExtraFiles string flagExtraFiles string
flagUpdatePackageJson bool flagUpdaters []string
) )
func init() { func init() {
@ -36,7 +37,7 @@ func init() {
runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "") runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "")
runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "") runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "")
runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "") runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
runCmd.PersistentFlags().BoolVar(&flagUpdatePackageJson, "update-package-json", false, "") runCmd.PersistentFlags().StringSliceVar(&flagUpdaters, "updaters", []string{}, "")
} }
func run(cmd *cobra.Command, _ []string) error { func run(cmd *cobra.Command, _ []string) error {
@ -49,7 +50,6 @@ func run(cmd *cobra.Command, _ []string) error {
"branch", flagBranch, "branch", flagBranch,
"owner", flagOwner, "owner", flagOwner,
"repo", flagRepo, "repo", flagRepo,
"update-package-json", flagUpdatePackageJson,
) )
var f forge.Forge var f forge.Forge
@ -83,11 +83,19 @@ func run(cmd *cobra.Command, _ []string) error {
extraFiles := parseExtraFiles(flagExtraFiles) extraFiles := parseExtraFiles(flagExtraFiles)
updaters := []updater.NewUpdater{updater.Generic} updaterNames := parseUpdaters(flagUpdaters)
updaters := []updater.Updater{}
if flagUpdatePackageJson { for _, name := range updaterNames {
logger.DebugContext(ctx, "package.json updater enabled") switch name {
updaters = append(updaters, updater.PackageJson) case "generic":
updaters = append(updaters, updater.Generic(extraFiles))
case "changelog":
updaters = append(updaters, updater.Changelog())
case "packagejson":
updaters = append(updaters, updater.PackageJson())
default:
return fmt.Errorf("unknown updater: %s", name)
}
} }
releaserPleaser := rp.New( releaserPleaser := rp.New(
@ -122,3 +130,22 @@ func parseExtraFiles(input string) []string {
return extraFiles return extraFiles
} }
func parseUpdaters(input []string) []string {
names := []string{"changelog", "generic"}
for _, u := range input {
if strings.HasPrefix(u, "-") {
name := u[1:]
names = slices.DeleteFunc(names, func(existingName string) bool { return existingName == name })
} else {
names = append(names, u)
}
}
// Make sure we only have unique updaters
slices.Sort(names)
names = slices.Compact(names)
return names
}

View file

@ -57,3 +57,43 @@ dir/Chart.yaml"`,
}) })
} }
} }
func Test_parseUpdaters(t *testing.T) {
tests := []struct {
name string
input []string
want []string
}{
{
name: "empty",
input: []string{},
want: []string{"changelog", "generic"},
},
{
name: "remove defaults",
input: []string{"-changelog", "-generic"},
want: []string{},
},
{
name: "remove unknown is ignored",
input: []string{"-fooo"},
want: []string{"changelog", "generic"},
},
{
name: "add new entry",
input: []string{"bar"},
want: []string{"bar", "changelog", "generic"},
},
{
name: "duplicates are removed",
input: []string{"bar", "bar", "changelog"},
want: []string{"bar", "changelog", "generic"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseUpdaters(tt.input)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -25,6 +25,7 @@
- [Pull Request Options](reference/pr-options.md) - [Pull Request Options](reference/pr-options.md)
- [GitHub Action](reference/github-action.md) - [GitHub Action](reference/github-action.md)
- [GitLab CI/CD Component](reference/gitlab-cicd-component.md) - [GitLab CI/CD Component](reference/gitlab-cicd-component.md)
- [Updaters](reference/updaters.md)
--- ---

View file

@ -10,7 +10,8 @@ In some situations it makes sense to have the current version committed in files
## Markers ## Markers
The line that needs to be updated must have the marker `x-releaser-pleaser-version` somewhere after the version that should be updated. The line that needs to be updated must have the marker
`x-releaser-pleaser-version` somewhere after the version that should be updated.
For example: For example:
@ -28,7 +29,8 @@ You need to tell `releaser-pleaser` which files it should update. This happens t
### GitHub Action ### GitHub Action
In the GitHub Action you can set the `extra-files` input with a list of the files. They need to be formatted as a single multi-line string with one file path per line: In the GitHub Action you can set the
`extra-files` input with a list of the files. They need to be formatted as a single multi-line string with one file path per line:
```yaml ```yaml
jobs: jobs:
@ -44,7 +46,8 @@ jobs:
### GitLab CI/CD Component ### GitLab CI/CD Component
In the GitLab CI/CD Component you can set the `extra-files` input with a list of files. They need to be formatted as a single multi-line string with one file path per line: In the GitLab CI/CD Component you can set the
`extra-files` input with a list of files. They need to be formatted as a single multi-line string with one file path per line:
```yaml ```yaml
include: include:
@ -61,3 +64,4 @@ include:
- **Reference** - **Reference**
- [GitHub Action](../reference/github-action.md#inputs) - [GitHub Action](../reference/github-action.md#inputs)
- [GitLab CI/CD Component](../reference/gitlab-cicd-component.md#inputs) - [GitLab CI/CD Component](../reference/gitlab-cicd-component.md#inputs)
- [Updaters](../reference/updaters.md#generic-updater)

View file

@ -8,18 +8,20 @@ The action is available as `apricote/releaser-pleaser` on GitHub.com.
The `apricote/releaser-pleaser` action is released together with `releaser-pleaser` and they share the version number. The `apricote/releaser-pleaser` action is released together with `releaser-pleaser` and they share the version number.
The action does not support floating tags (e.g. `v1`) right now ([#31](https://github.com/apricote/releaser-pleaser/issues/31)). You have to use the full version or commit SHA instead: `apricote/releaser-pleaser@v0.2.0`. The action does not support floating tags (e.g.
`v1`) right now ([#31](https://github.com/apricote/releaser-pleaser/issues/31)). You have to use the full version or commit SHA instead:
`apricote/releaser-pleaser@v0.2.0`.
## Inputs ## Inputs
The following inputs are supported by the `apricote/releaser-pleaser` GitHub Action. The following inputs are supported by the `apricote/releaser-pleaser` GitHub Action.
| Input | Description | Default | Example | | Input | Description | Default | Example |
| --------------------- | :------------------------------------------------------ | --------------: | -------------------------------------------------------------------: | |---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------:|---------------------------------------------------------------------:|
| `branch` | This branch is used as the target for releases. | `main` | `master` | | `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` | GitHub token for creating and updating release PRs | `$GITHUB_TOKEN` | `${{secrets.RELEASER_PLEASER_TOKEN}}` | | `token` | GitHub token for creating and updating release PRs | `$GITHUB_TOKEN` | `${{secrets.RELEASER_PLEASER_TOKEN}}` |
| `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> | | `extra-files` | List of files that are scanned for version references by the generic updater. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| `update-package-json` | Update version field in package.json file. | `false` | `true` | | `updaters` | List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic | `""` | `-generic,packagejson` |
## Outputs ## Outputs

View file

@ -18,11 +18,11 @@ The component does not support floating tags (e.g.
The following inputs are supported by the component. The following inputs are supported by the component.
| Input | Description | Default | Example | | Input | Description | Default | Example |
| ---------------------- | :-------------------------------------------------------- | ------: | -------------------------------------------------------------------: | |------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------:|---------------------------------------------------------------------:|
| `branch` | This branch is used as the target for releases. | `main` | `master` | | `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` (**required**) | GitLab access token for creating and updating release PRs | | `$RELEASER_PLEASER_TOKEN` | | `token` (**required**) | GitLab access token for creating and updating release PRs | | `$RELEASER_PLEASER_TOKEN` |
| `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> | | `extra-files` | List of files that are scanned for version references by the generic updater. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| `update-package-json` | Update version field in package.json file. | `false` | `true` | | `updaters` | List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic | `""` | `-generic,packagejson` |
| `stage` | Stage the job runs in. Must exists. | `build` | `test` | | `stage` | Stage the job runs in. Must exists. | `build` | `test` |
| `needs` | Other jobs the releaser-pleaser job depends on. | `[]` | <pre><code>- validate-foo<br>- prepare-bar</code></pre> | | `needs` | Other jobs the releaser-pleaser job depends on. | `[]` | <pre><code>- validate-foo<br>- prepare-bar</code></pre> |

View file

@ -2,17 +2,21 @@
### Changelog ### Changelog
The Changelog is a file in the repository (`CHANGELOG.md`) that contains the [Release Notes](#release-notes) for every release of that repository. Usually, new releases are added at the top of the file. The Changelog is a file in the repository (
`CHANGELOG.md`) that contains the [Release Notes](#release-notes) for every release of that repository. Usually, new releases are added at the top of the file.
### Conventional Commits ### Conventional Commits
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) is a specification for commit messages. It is the only supported commit message schema in `releaser-pleaser`. Follow the link to learn more. [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) is a specification for commit messages. It is the only supported commit message schema in
`releaser-pleaser`. Follow the link to learn more.
### Forge ### Forge
A **forge** is a web-based collaborative software platform for both developing and sharing computer applications.[^wp-forge] A **forge
** is a web-based collaborative software platform for both developing and sharing computer applications.[^wp-forge]
Right now only **GitHub** is supported. We plan to support **GitLab** in the future ([#4](https://github.com/apricote/releaser-pleaser/issues/4)). For other forges like Forgejo or Gitea, please open an issue and submit a pull request. Right now only **GitHub** is supported. We plan to support **GitLab
** in the future ([#4](https://github.com/apricote/releaser-pleaser/issues/4)). For other forges like Forgejo or Gitea, please open an issue and submit a pull request.
[^wp-forge]: Quote from [Wikipedia "Forge (software)"](<https://en.wikipedia.org/wiki/Forge_(software)>) [^wp-forge]: Quote from [Wikipedia "Forge (software)"](<https://en.wikipedia.org/wiki/Forge_(software)>)
@ -24,7 +28,8 @@ In `releaser-pleaser` Markdown is used for most texts.
### Pre-release ### Pre-release
Pre-releases are a concept of [SemVer](#semantic-versioning-semver). They follow the normal versioning schema but use a suffix out of `-alpha.X`, `-beta.X` and `-rc.X`. Pre-releases are a concept of [SemVer](#semantic-versioning-semver). They follow the normal versioning schema but use a suffix out of
`-alpha.X`, `-beta.X` and `-rc.X`.
Pre-releases are not considered "stable" and are usually not recommended for most users. Pre-releases are not considered "stable" and are usually not recommended for most users.
@ -32,7 +37,9 @@ Learn more in the [Pre-releases](../guides/pre-releases.md) guide.
### Release Pull Request ### Release Pull Request
A Release Pull Request is opened by `releaser-pleaser` whenever it finds releasable commits in your project. It proposes a new version number and the Changelog. Once it is merged, `releaser-pleaser` creates a matching release. A Release Pull Request is opened by
`releaser-pleaser` whenever it finds releasable commits in your project. It proposes a new version number and the Changelog. Once it is merged,
`releaser-pleaser` creates a matching release.
Learn more in the [Release Pull Request](../explanation/release-pr.md) explanation. Learn more in the [Release Pull Request](../explanation/release-pr.md) explanation.
@ -44,4 +51,11 @@ Learn more in the [Release Notes customization](../guides/release-notes.md) guid
### Semantic Versioning (SemVer) ### Semantic Versioning (SemVer)
[Semantic Versioning](https://semver.org/) is a specification for version numbers. It is the only supported versioning schema in `releaser-pleaser`. Follow the link to learn more. [Semantic Versioning](https://semver.org/) is a specification for version numbers. It is the only supported versioning schema in
`releaser-pleaser`. Follow the link to learn more.
### Updater
Updaters can update or create files that will be included in [Release Pull Request](#release-pull-request). Examples of Updaters are
`changelog` for `CHANGELOG.md`, `generic` that can update arbitrary files and
`packagejson` that knows how to update Node.JS `package.json` files.

View file

@ -0,0 +1,33 @@
# Updaters
There are different updater for different purposes available.
They each have a name and may be enabled by default. You can configure which updaters are used through the
`updaters` input on GitHub Actions and GitLab CI/CD. This is a comma-delimited list of updaters that should be enabled, for updaters that are enabled by default you can remove them by adding a minus before its name:
```
updaters: -generic,packagejson
```
## Changelog
- **Name**: `changelog`
- **Default**: enabled
This updater creates the `CHANGELOG.md` file and adds new release notes to it.
## Generic Updater
- **Name**: `generic`
- **Default**: enabled
This updater can update any file and only needs a marker on the line. It is enabled by default.
Learn more about this updater in ["Updating arbitrary files"](../guides/updating-arbitrary-files.md).
## Node.js `package.json` Updater
- **Name**: `packagejson`
- **Default**: disabled
This updater can update the `version` field in Node.js `package.json` files. The updater is disabled by default.

View file

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

View file

@ -14,8 +14,23 @@ var (
ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`) ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`)
) )
func Changelog(info ReleaseInfo) Updater { func Changelog() Updater {
return func(content string, filename string) (string, error) { 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) headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content)
if headerIndex == nil && len(content) != 0 { if headerIndex == nil && len(content) != 0 {
return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match") return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match")

View file

@ -6,15 +6,22 @@ import (
"github.com/stretchr/testify/assert" "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{ tests := []updaterTestCase{
{ {
name: "empty file", name: "empty file",
content: "", content: "",
filename: "CHANGELOG.md", info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"},
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"}, want: "# Changelog\n\n## v1.0.0\n",
want: "# Changelog\n\n## v1.0.0\n", wantErr: assert.NoError,
wantErr: assert.NoError,
}, },
{ {
name: "well-formatted changelog", name: "well-formatted changelog",
@ -28,8 +35,7 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) {
### Bazuuum ### 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 want: `# Changelog
## v1.0.0 ## v1.0.0
@ -47,17 +53,16 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "error on invalid header", name: "error on invalid header",
content: "What even is this file?", content: "What even is this file?",
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: "",
want: "", wantErr: assert.Error,
wantErr: assert.Error,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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)`) var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`)
func Generic(info ReleaseInfo) Updater { func Generic(files []string) Updater {
return func(content string, filename string) (string, error) { 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. // We strip the "v" prefix to avoid adding/removing it from the users input.
version := strings.TrimPrefix(info.Version, "v") version := strings.TrimPrefix(info.Version, "v")

View file

@ -6,12 +6,19 @@ import (
"github.com/stretchr/testify/assert" "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{ tests := []updaterTestCase{
{ {
name: "single line", name: "single line",
content: "v1.0.0 // x-releaser-pleaser-version", content: "v1.0.0 // x-releaser-pleaser-version",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },
@ -19,9 +26,8 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "multiline line", name: "multiline line",
content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n", content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },
@ -29,9 +35,8 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "invalid existing version", name: "invalid existing version",
content: "1.0 // x-releaser-pleaser-version", content: "1.0 // x-releaser-pleaser-version",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },
@ -39,9 +44,8 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "complicated line", name: "complicated line",
content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar", content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },
@ -51,7 +55,7 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 // PackageJson creates an updater that modifies the version field in package.json files
func PackageJson(info ReleaseInfo) Updater { func PackageJson() Updater {
return func(content string, filename string) (string, error) { return packagejson{}
if filename != "package.json" { }
return content, nil // No update needed for non-package.json files
} 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 // We strip the "v" prefix to match npm versioning convention
version := strings.TrimPrefix(info.Version, "v") version := strings.TrimPrefix(info.Version, "v")
@ -23,8 +34,6 @@ func PackageJson(info ReleaseInfo) Updater {
} }
// Replace the version value while preserving the original formatting // Replace the version value while preserving the original formatting
updatedContent := versionRegex.ReplaceAllString(content, `${1}"`+version+`"`) return versionRegex.ReplaceAllString(content, `${1}"`+version+`"`), nil
return updatedContent, nil
} }
} }

View file

@ -1,18 +1,24 @@
package updater package updater
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "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{ tests := []updaterTestCase{
{ {
name: "simple package.json", name: "simple package.json",
content: `{"name":"test","version":"1.0.0"}`, content: `{"name":"test","version":"1.0.0"}`,
filename: "package.json",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v2.0.5", Version: "v2.0.5",
}, },
@ -20,19 +26,8 @@ func TestPackageJsonUpdater(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "simple package.json, wrong name", name: "complex package.json",
content: `{"name":"test","version":"1.0.0"}`, content: "{\n \"name\": \"test\",\n \"version\": \"1.0.0\",\n \"dependencies\": {\n \"foo\": \"^1.0.0\"\n }\n}",
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",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v2.0.0", Version: "v2.0.0",
}, },
@ -40,9 +35,8 @@ func TestPackageJsonUpdater(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "invalid json", name: "invalid json",
content: `not json`, content: `not json`,
filename: "package.json",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v2.0.0", Version: "v2.0.0",
}, },
@ -50,9 +44,8 @@ func TestPackageJsonUpdater(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "json without version", name: "json without version",
content: `{"name":"test"}`, content: `{"name":"test"}`,
filename: "package.json",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v2.0.0", Version: "v2.0.0",
}, },
@ -63,8 +56,7 @@ func TestPackageJsonUpdater(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 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 type NewUpdater func(ReleaseInfo) Updater

View file

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

View file

@ -34,10 +34,10 @@ type ReleaserPleaser struct {
commitParser commitparser.CommitParser commitParser commitparser.CommitParser
versioning versioning.Strategy versioning versioning.Strategy
extraFiles []string extraFiles []string
updaters []updater.NewUpdater updaters []updater.Updater
} }
func New(forge forge.Forge, logger *slog.Logger, targetBranch string, commitParser commitparser.CommitParser, versioningStrategy versioning.Strategy, extraFiles []string, updaters []updater.NewUpdater) *ReleaserPleaser { func New(forge forge.Forge, logger *slog.Logger, targetBranch string, commitParser commitparser.CommitParser, versioningStrategy versioning.Strategy, extraFiles []string, updaters []updater.Updater) *ReleaserPleaser {
return &ReleaserPleaser{ return &ReleaserPleaser{
forge: forge, forge: forge,
logger: logger, logger: logger,
@ -281,16 +281,12 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
// Info for updaters // Info for updaters
info := updater.ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry} info := updater.ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry}
err = repo.UpdateFile(ctx, updater.ChangelogFile, true, updater.WithInfo(info, updater.Changelog)) for _, u := range rp.updaters {
if err != nil { for _, file := range u.Files() {
return fmt.Errorf("failed to update changelog file: %w", err) err = repo.UpdateFile(ctx, file, u.CreateNewFiles(), u.Update(info))
} if err != nil {
return fmt.Errorf("failed to run updater %T: %w", u, err)
for _, path := range rp.extraFiles { }
// TODO: Check for missing files
err = repo.UpdateFile(ctx, path, false, updater.WithInfo(info, rp.updaters...))
if err != nil {
return fmt.Errorf("failed to run file updater: %w", err)
} }
} }

View file

@ -9,12 +9,12 @@ spec:
description: "GitLab token for creating and updating release MRs." description: "GitLab token for creating and updating release MRs."
extra-files: extra-files:
description: 'List of files that are scanned for version references.' description: 'List of files that are scanned for version references by the generic updater.'
default: "" default: ""
update-package-json: updaters:
description: 'Update version field in package.json file.' description: "List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic"
default: "false" default: ""
stage: stage:
default: build default: build
@ -54,4 +54,4 @@ releaser-pleaser:
--forge=gitlab \ --forge=gitlab \
--branch=$[[ inputs.branch ]] \ --branch=$[[ inputs.branch ]] \
--extra-files="$[[ inputs.extra-files ]]" \ --extra-files="$[[ inputs.extra-files ]]" \
$([[ inputs.update-package-json == "true" ]] && echo "--update-package-json" || echo "") --updaters="$[[ inputs.updaters ]]"