From deb48115013c18993a6af4b046fa848eb270fb77 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 22:52:51 +0000 Subject: [PATCH 1/9] deps: update dependency go to v1.25.0 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 41f54c3..997a693 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/apricote/releaser-pleaser go 1.23.2 -toolchain go1.24.6 +toolchain go1.25.0 require ( github.com/blang/semver/v4 v4.0.0 From 2f7e8b9afeca39bb258ef7d8909d559f8aa86c32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:23:12 +0200 Subject: [PATCH 2/9] deps: update actions/checkout digest to 08eba0b (#220) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +++--- .github/workflows/docs.yaml | 2 +- .github/workflows/mirror.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/releaser-pleaser.yaml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f2a8386..79d9779 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index f1fb5d1..912615e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -13,7 +13,7 @@ jobs: id-token: write # To update the deployment status steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: lfs: "true" diff --git a/.github/workflows/mirror.yaml b/.github/workflows/mirror.yaml index e287aed..d7feadb 100644 --- a/.github/workflows/mirror.yaml +++ b/.github/workflows/mirror.yaml @@ -11,7 +11,7 @@ jobs: REMOTE: mirror steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: # Need all to fetch all tags so we can push them fetch-depth: 0 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6da204d..bcc947d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 diff --git a/.github/workflows/releaser-pleaser.yaml b/.github/workflows/releaser-pleaser.yaml index aa5097f..6e79306 100644 --- a/.github/workflows/releaser-pleaser.yaml +++ b/.github/workflows/releaser-pleaser.yaml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: ref: main From 6237c9b666f2b46b4f491536d7a8ac396d4c61af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:23:41 +0200 Subject: [PATCH 3/9] deps: update codecov/codecov-action digest to fdcc847 (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 79d9779..9d137dc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,7 +38,7 @@ jobs: run: go test -v -race -coverpkg=./... -coverprofile=coverage.txt ./... - name: Upload results to Codecov - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5 + uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5 with: token: ${{ secrets.CODECOV_TOKEN }} From 1e9e0aa5d908a2dff5f6997186badaf29aaf76f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattis=20Kr=C3=A4mer?= Date: Sat, 23 Aug 2025 22:05:52 +0200 Subject: [PATCH 4/9] feat: add updater for package.json (#213) --- action.yml | 5 ++ cmd/rp/cmd/run.go | 23 +++++--- docs/reference/github-action.md | 11 ++-- docs/reference/gitlab-cicd-component.md | 1 + internal/git/git.go | 3 +- internal/updater/changelog.go | 2 +- internal/updater/changelog_test.go | 25 +++++---- internal/updater/generic.go | 2 +- internal/updater/generic_test.go | 20 ++++--- internal/updater/packagejson.go | 30 +++++++++++ internal/updater/packagejson_test.go | 70 +++++++++++++++++++++++++ internal/updater/updater.go | 2 +- internal/updater/updater_test.go | 17 +++--- templates/run.yml | 7 ++- 14 files changed, 174 insertions(+), 44 deletions(-) create mode 100644 internal/updater/packagejson.go create mode 100644 internal/updater/packagejson_test.go diff --git a/action.yml b/action.yml index 225e3cc..5a3d233 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,10 @@ inputs: description: 'List of files that are scanned for version references.' required: false default: "" + update-package-json: + description: 'Update version field in package.json file.' + required: false + default: "false" # Remember to update docs/reference/github-action.md outputs: {} runs: @@ -27,6 +31,7 @@ runs: - --forge=github - --branch=${{ inputs.branch }} - --extra-files="${{ inputs.extra-files }}" + - ${{ inputs.update-package-json == 'true' && '--update-package-json' || '' }} env: GITHUB_TOKEN: "${{ inputs.token }}" GITHUB_USER: "oauth2" diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index ec11e24..fa48cc0 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -21,21 +21,22 @@ var runCmd = &cobra.Command{ } var ( - flagForge string - flagBranch string - flagOwner string - flagRepo string - flagExtraFiles string + flagForge string + flagBranch string + flagOwner string + flagRepo string + flagExtraFiles string + flagUpdatePackageJson bool ) func init() { rootCmd.AddCommand(runCmd) - runCmd.PersistentFlags().StringVar(&flagForge, "forge", "", "") runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "") runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "") runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "") runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "") + runCmd.PersistentFlags().BoolVar(&flagUpdatePackageJson, "update-package-json", false, "") } func run(cmd *cobra.Command, _ []string) error { @@ -48,6 +49,7 @@ func run(cmd *cobra.Command, _ []string) error { "branch", flagBranch, "owner", flagOwner, "repo", flagRepo, + "update-package-json", flagUpdatePackageJson, ) var f forge.Forge @@ -81,6 +83,13 @@ func run(cmd *cobra.Command, _ []string) error { extraFiles := parseExtraFiles(flagExtraFiles) + updaters := []updater.NewUpdater{updater.Generic} + + if flagUpdatePackageJson { + logger.DebugContext(ctx, "package.json updater enabled") + updaters = append(updaters, updater.PackageJson) + } + releaserPleaser := rp.New( f, logger, @@ -88,7 +97,7 @@ func run(cmd *cobra.Command, _ []string) error { conventionalcommits.NewParser(logger), versioning.SemVer, extraFiles, - []updater.NewUpdater{updater.Generic}, + updaters, ) return releaserPleaser.Run(ctx) diff --git a/docs/reference/github-action.md b/docs/reference/github-action.md index eec9789..3a849dc 100644 --- a/docs/reference/github-action.md +++ b/docs/reference/github-action.md @@ -14,11 +14,12 @@ The action does not support floating tags (e.g. `v1`) right now ([#31](https://g The following inputs are supported by the `apricote/releaser-pleaser` GitHub Action. -| Input | Description | Default | Example | -| ------------- | :----------------------------------------------------- | --------------: | -------------------------------------------------------------------: | -| `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}}` | -| `extra-files` | List of files that are scanned for version references. | `""` |
version/version.go
deploy/deployment.yaml
| +| Input | Description | Default | Example | +| --------------------- | :------------------------------------------------------ | --------------: | -------------------------------------------------------------------: | +| `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}}` | +| `extra-files` | List of files that are scanned for version references. | `""` |
version/version.go
deploy/deployment.yaml
| +| `update-package-json` | Update version field in package.json file. | `false` | `true` | ## Outputs diff --git a/docs/reference/gitlab-cicd-component.md b/docs/reference/gitlab-cicd-component.md index b22d5b2..01a6fc2 100644 --- a/docs/reference/gitlab-cicd-component.md +++ b/docs/reference/gitlab-cicd-component.md @@ -23,5 +23,6 @@ The following inputs are supported by the component. | `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` | | `extra-files` | List of files that are scanned for version references. | `""` |
version/version.go
deploy/deployment.yaml
| +| `update-package-json` | Update version field in package.json file. | `false` | `true` | | `stage` | Stage the job runs in. Must exists. | `build` | `test` | | `needs` | Other jobs the releaser-pleaser job depends on. | `[]` |
- validate-foo
- prepare-bar
| diff --git a/internal/git/git.go b/internal/git/git.go index d1db11b..b9c750e 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -6,6 +6,7 @@ import ( "io" "log/slog" "os" + "path/filepath" "time" "github.com/go-git/go-git/v5" @@ -144,7 +145,7 @@ func (r *Repository) UpdateFile(_ context.Context, path string, create bool, upd updatedContent := string(content) for _, update := range updaters { - updatedContent, err = update(updatedContent) + updatedContent, err = update(updatedContent, filepath.Base(path)) if err != nil { return fmt.Errorf("failed to run updater on file %s", path) } diff --git a/internal/updater/changelog.go b/internal/updater/changelog.go index 8bdb9f6..8d6d68c 100644 --- a/internal/updater/changelog.go +++ b/internal/updater/changelog.go @@ -15,7 +15,7 @@ var ( ) func Changelog(info ReleaseInfo) Updater { - return func(content string) (string, error) { + return func(content string, filename 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") diff --git a/internal/updater/changelog_test.go b/internal/updater/changelog_test.go index 917cd14..35878b9 100644 --- a/internal/updater/changelog_test.go +++ b/internal/updater/changelog_test.go @@ -9,11 +9,12 @@ import ( func TestChangelogUpdater_UpdateContent(t *testing.T) { tests := []updaterTestCase{ { - name: "empty file", - content: "", - info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"}, - want: "# Changelog\n\n## v1.0.0\n", - wantErr: assert.NoError, + 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: "well-formatted changelog", @@ -27,7 +28,8 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) { ### Bazuuum `, - info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"}, + filename: "CHANGELOG.md", + info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"}, want: `# Changelog ## v1.0.0 @@ -45,11 +47,12 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) { wantErr: assert.NoError, }, { - 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, + 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, }, } for _, tt := range tests { diff --git a/internal/updater/generic.go b/internal/updater/generic.go index b8d73b0..1883c1a 100644 --- a/internal/updater/generic.go +++ b/internal/updater/generic.go @@ -8,7 +8,7 @@ import ( var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`) func Generic(info ReleaseInfo) Updater { - return func(content string) (string, error) { + return func(content string, filename string) (string, error) { // We strip the "v" prefix to avoid adding/removing it from the users input. version := strings.TrimPrefix(info.Version, "v") diff --git a/internal/updater/generic_test.go b/internal/updater/generic_test.go index e0a8d1d..4cc8952 100644 --- a/internal/updater/generic_test.go +++ b/internal/updater/generic_test.go @@ -9,8 +9,9 @@ import ( func TestGenericUpdater_UpdateContent(t *testing.T) { tests := []updaterTestCase{ { - name: "single line", - content: "v1.0.0 // x-releaser-pleaser-version", + name: "single line", + content: "v1.0.0 // x-releaser-pleaser-version", + filename: "version.txt", info: ReleaseInfo{ Version: "v1.2.0", }, @@ -18,8 +19,9 @@ 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", + name: "multiline line", + content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n", + filename: "version.txt", info: ReleaseInfo{ Version: "v1.2.0", }, @@ -27,8 +29,9 @@ func TestGenericUpdater_UpdateContent(t *testing.T) { wantErr: assert.NoError, }, { - name: "invalid existing version", - content: "1.0 // x-releaser-pleaser-version", + name: "invalid existing version", + content: "1.0 // x-releaser-pleaser-version", + filename: "version.txt", info: ReleaseInfo{ Version: "v1.2.0", }, @@ -36,8 +39,9 @@ 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", + name: "complicated line", + content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar", + filename: "version.txt", info: ReleaseInfo{ Version: "v1.2.0", }, diff --git a/internal/updater/packagejson.go b/internal/updater/packagejson.go new file mode 100644 index 0000000..58f2cc3 --- /dev/null +++ b/internal/updater/packagejson.go @@ -0,0 +1,30 @@ +package updater + +import ( + "regexp" + "strings" +) + +// 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 + } + // We strip the "v" prefix to match npm versioning convention + version := strings.TrimPrefix(info.Version, "v") + + // Regex to match "version": "..." with flexible whitespace and quote styles + versionRegex := regexp.MustCompile(`("version"\s*:\s*)"[^"]*"`) + + // Check if the file contains a version field + if !versionRegex.MatchString(content) { + return content, nil + } + + // Replace the version value while preserving the original formatting + updatedContent := versionRegex.ReplaceAllString(content, `${1}"`+version+`"`) + + return updatedContent, nil + } +} diff --git a/internal/updater/packagejson_test.go b/internal/updater/packagejson_test.go new file mode 100644 index 0000000..4fd196e --- /dev/null +++ b/internal/updater/packagejson_test.go @@ -0,0 +1,70 @@ +package updater + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPackageJsonUpdater(t *testing.T) { + tests := []updaterTestCase{ + { + name: "simple package.json", + content: `{"name":"test","version":"1.0.0"}`, + filename: "package.json", + info: ReleaseInfo{ + Version: "v2.0.5", + }, + want: `{"name":"test","version":"2.0.5"}`, + 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", + info: ReleaseInfo{ + Version: "v2.0.0", + }, + want: "{\n \"name\": \"test\",\n \"version\": \"2.0.0\",\n \"dependencies\": {\n \"foo\": \"^1.0.0\"\n }\n}", + wantErr: assert.NoError, + }, + { + name: "invalid json", + content: `not json`, + filename: "package.json", + info: ReleaseInfo{ + Version: "v2.0.0", + }, + want: `not json`, + wantErr: assert.NoError, + }, + { + name: "json without version", + content: `{"name":"test"}`, + filename: "package.json", + info: ReleaseInfo{ + Version: "v2.0.0", + }, + want: `{"name":"test"}`, + wantErr: assert.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fmt.Println("Running updater test for PackageJson") + runUpdaterTest(t, PackageJson, tt) + }) + } +} diff --git a/internal/updater/updater.go b/internal/updater/updater.go index fb773b4..f5fd677 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -5,7 +5,7 @@ type ReleaseInfo struct { ChangelogEntry string } -type Updater func(string) (string, error) +type Updater func(content string, filename string) (string, error) type NewUpdater func(ReleaseInfo) Updater diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index 0c0c40e..17162ef 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -8,19 +8,20 @@ import ( ) type updaterTestCase struct { - name string - content string - info ReleaseInfo - want string - wantErr assert.ErrorAssertionFunc + name string + content string + filename string + info ReleaseInfo + want string + wantErr assert.ErrorAssertionFunc } func runUpdaterTest(t *testing.T, constructor NewUpdater, tt updaterTestCase) { t.Helper() - got, err := constructor(tt.info)(tt.content) - if !tt.wantErr(t, err, fmt.Sprintf("Updater(%v, %v)", tt.content, tt.info)) { + 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)) { return } - assert.Equalf(t, tt.want, got, "Updater(%v, %v)", tt.content, tt.info) + assert.Equalf(t, tt.want, got, "Updater(%v, %v, %v)", tt.content, tt.filename, tt.info) } diff --git a/templates/run.yml b/templates/run.yml index c18a330..66037b1 100644 --- a/templates/run.yml +++ b/templates/run.yml @@ -12,6 +12,10 @@ spec: description: 'List of files that are scanned for version references.' default: "" + update-package-json: + description: 'Update version field in package.json file.' + default: "false" + stage: default: build description: 'Defines the build stage' @@ -49,4 +53,5 @@ releaser-pleaser: rp run \ --forge=gitlab \ --branch=$[[ inputs.branch ]] \ - --extra-files="$[[ inputs.extra-files ]]" + --extra-files="$[[ inputs.extra-files ]]" \ + $([[ inputs.update-package-json == "true" ]] && echo "--update-package-json" || echo "") From f1aa1a2ef43aa646f8385e753b43712b9de36468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sat, 23 Aug 2025 22:14:34 +0200 Subject: [PATCH 5/9] 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. --- action.yml | 12 +++--- cmd/rp/cmd/run.go | 53 +++++++++++++++++++------ cmd/rp/cmd/run_test.go | 40 +++++++++++++++++++ docs/SUMMARY.md | 1 + docs/guides/updating-arbitrary-files.md | 10 +++-- docs/reference/github-action.md | 16 ++++---- docs/reference/gitlab-cicd-component.md | 16 ++++---- docs/reference/glossary.md | 28 +++++++++---- docs/reference/updaters.md | 33 +++++++++++++++ internal/git/git.go | 15 ++----- internal/updater/changelog.go | 19 ++++++++- internal/updater/changelog_test.go | 37 +++++++++-------- internal/updater/generic.go | 22 +++++++++- internal/updater/generic_test.go | 32 ++++++++------- internal/updater/packagejson.go | 25 ++++++++---- internal/updater/packagejson_test.go | 44 +++++++++----------- internal/updater/updater.go | 6 ++- internal/updater/updater_test.go | 19 +++++---- releaserpleaser.go | 20 ++++------ templates/run.yml | 10 ++--- 20 files changed, 307 insertions(+), 151 deletions(-) create mode 100644 docs/reference/updaters.md diff --git a/action.yml b/action.yml index 5a3d233..651ca09 100644 --- a/action.yml +++ b/action.yml @@ -14,15 +14,15 @@ inputs: required: false default: ${{ github.token }} 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 default: "" - update-package-json: - description: 'Update version field in package.json file.' + updaters: + 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 - default: "false" + default: "" # Remember to update docs/reference/github-action.md -outputs: {} +outputs: { } runs: using: 'docker' image: docker://ghcr.io/apricote/releaser-pleaser:v0.6.1 # x-releaser-pleaser-version @@ -31,7 +31,7 @@ runs: - --forge=github - --branch=${{ inputs.branch }} - --extra-files="${{ inputs.extra-files }}" - - ${{ inputs.update-package-json == 'true' && '--update-package-json' || '' }} + - --updaters="${{ inputs.updaters }}" env: GITHUB_TOKEN: "${{ inputs.token }}" GITHUB_USER: "oauth2" diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index fa48cc0..202ba3d 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "slices" "strings" "github.com/spf13/cobra" @@ -21,12 +22,12 @@ var runCmd = &cobra.Command{ } var ( - flagForge string - flagBranch string - flagOwner string - flagRepo string - flagExtraFiles string - flagUpdatePackageJson bool + flagForge string + flagBranch string + flagOwner string + flagRepo string + flagExtraFiles string + flagUpdaters []string ) func init() { @@ -36,7 +37,7 @@ func init() { runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "") runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "") 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 { @@ -49,7 +50,6 @@ func run(cmd *cobra.Command, _ []string) error { "branch", flagBranch, "owner", flagOwner, "repo", flagRepo, - "update-package-json", flagUpdatePackageJson, ) var f forge.Forge @@ -83,11 +83,19 @@ func run(cmd *cobra.Command, _ []string) error { extraFiles := parseExtraFiles(flagExtraFiles) - updaters := []updater.NewUpdater{updater.Generic} - - if flagUpdatePackageJson { - logger.DebugContext(ctx, "package.json updater enabled") - updaters = append(updaters, updater.PackageJson) + updaterNames := parseUpdaters(flagUpdaters) + updaters := []updater.Updater{} + for _, name := range updaterNames { + switch name { + 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( @@ -122,3 +130,22 @@ func parseExtraFiles(input string) []string { 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 +} diff --git a/cmd/rp/cmd/run_test.go b/cmd/rp/cmd/run_test.go index d4cea7a..8354478 100644 --- a/cmd/rp/cmd/run_test.go +++ b/cmd/rp/cmd/run_test.go @@ -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) + }) + } +} diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b13bf24..0a22df1 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -25,6 +25,7 @@ - [Pull Request Options](reference/pr-options.md) - [GitHub Action](reference/github-action.md) - [GitLab CI/CD Component](reference/gitlab-cicd-component.md) +- [Updaters](reference/updaters.md) --- diff --git a/docs/guides/updating-arbitrary-files.md b/docs/guides/updating-arbitrary-files.md index d4b65bf..e7df9a5 100644 --- a/docs/guides/updating-arbitrary-files.md +++ b/docs/guides/updating-arbitrary-files.md @@ -10,7 +10,8 @@ In some situations it makes sense to have the current version committed in files ## 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: @@ -28,7 +29,8 @@ You need to tell `releaser-pleaser` which files it should update. This happens t ### 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 jobs: @@ -44,7 +46,8 @@ jobs: ### 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 include: @@ -61,3 +64,4 @@ include: - **Reference** - [GitHub Action](../reference/github-action.md#inputs) - [GitLab CI/CD Component](../reference/gitlab-cicd-component.md#inputs) + - [Updaters](../reference/updaters.md#generic-updater) diff --git a/docs/reference/github-action.md b/docs/reference/github-action.md index 3a849dc..c5d595a 100644 --- a/docs/reference/github-action.md +++ b/docs/reference/github-action.md @@ -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 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 The following inputs are supported by the `apricote/releaser-pleaser` GitHub Action. -| Input | Description | Default | Example | -| --------------------- | :------------------------------------------------------ | --------------: | -------------------------------------------------------------------: | -| `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}}` | -| `extra-files` | List of files that are scanned for version references. | `""` |
version/version.go
deploy/deployment.yaml
| -| `update-package-json` | Update version field in package.json file. | `false` | `true` | +| Input | Description | Default | Example | +|---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------:|---------------------------------------------------------------------:| +| `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}}` | +| `extra-files` | List of files that are scanned for version references by the generic updater. | `""` |
version/version.go
deploy/deployment.yaml
| +| `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 diff --git a/docs/reference/gitlab-cicd-component.md b/docs/reference/gitlab-cicd-component.md index 01a6fc2..3080fcf 100644 --- a/docs/reference/gitlab-cicd-component.md +++ b/docs/reference/gitlab-cicd-component.md @@ -18,11 +18,11 @@ The component does not support floating tags (e.g. The following inputs are supported by the component. -| Input | Description | Default | Example | -| ---------------------- | :-------------------------------------------------------- | ------: | -------------------------------------------------------------------: | -| `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` | -| `extra-files` | List of files that are scanned for version references. | `""` |
version/version.go
deploy/deployment.yaml
| -| `update-package-json` | Update version field in package.json file. | `false` | `true` | -| `stage` | Stage the job runs in. Must exists. | `build` | `test` | -| `needs` | Other jobs the releaser-pleaser job depends on. | `[]` |
- validate-foo
- prepare-bar
| +| Input | Description | Default | Example | +|------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------:|---------------------------------------------------------------------:| +| `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` | +| `extra-files` | List of files that are scanned for version references by the generic updater. | `""` |
version/version.go
deploy/deployment.yaml
| +| `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` | +| `needs` | Other jobs the releaser-pleaser job depends on. | `[]` |
- validate-foo
- prepare-bar
| diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 543f970..8966f55 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -2,17 +2,21 @@ ### 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](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 -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)"]() @@ -24,7 +28,8 @@ In `releaser-pleaser` Markdown is used for most texts. ### 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. @@ -32,7 +37,9 @@ Learn more in the [Pre-releases](../guides/pre-releases.md) guide. ### 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. @@ -44,4 +51,11 @@ Learn more in the [Release Notes customization](../guides/release-notes.md) guid ### 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. \ No newline at end of file diff --git a/docs/reference/updaters.md b/docs/reference/updaters.md new file mode 100644 index 0000000..6bafff4 --- /dev/null +++ b/docs/reference/updaters.md @@ -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. diff --git a/internal/git/git.go b/internal/git/git.go index b9c750e..ad5c0a3 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -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) diff --git a/internal/updater/changelog.go b/internal/updater/changelog.go index 8d6d68c..a7c7506 100644 --- a/internal/updater/changelog.go +++ b/internal/updater/changelog.go @@ -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") diff --git a/internal/updater/changelog_test.go b/internal/updater/changelog_test.go index 35878b9..c0becb5 100644 --- a/internal/updater/changelog_test.go +++ b/internal/updater/changelog_test.go @@ -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) }) } } diff --git a/internal/updater/generic.go b/internal/updater/generic.go index 1883c1a..11b21a4 100644 --- a/internal/updater/generic.go +++ b/internal/updater/generic.go @@ -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") diff --git a/internal/updater/generic_test.go b/internal/updater/generic_test.go index 4cc8952..7c007a4 100644 --- a/internal/updater/generic_test.go +++ b/internal/updater/generic_test.go @@ -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) }) } } diff --git a/internal/updater/packagejson.go b/internal/updater/packagejson.go index 58f2cc3..0fcb8a5 100644 --- a/internal/updater/packagejson.go +++ b/internal/updater/packagejson.go @@ -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 } } diff --git a/internal/updater/packagejson_test.go b/internal/updater/packagejson_test.go index 4fd196e..9bff8b7 100644 --- a/internal/updater/packagejson_test.go +++ b/internal/updater/packagejson_test.go @@ -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) }) } } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index f5fd677..6e27f37 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -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 diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index 17162ef..5a90936 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -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) } diff --git a/releaserpleaser.go b/releaserpleaser.go index a09aefe..b72b85f 100644 --- a/releaserpleaser.go +++ b/releaserpleaser.go @@ -34,10 +34,10 @@ type ReleaserPleaser struct { commitParser commitparser.CommitParser versioning versioning.Strategy 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{ forge: forge, logger: logger, @@ -281,16 +281,12 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error { // Info for updaters info := updater.ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry} - err = repo.UpdateFile(ctx, updater.ChangelogFile, true, updater.WithInfo(info, updater.Changelog)) - if err != nil { - return fmt.Errorf("failed to update changelog file: %w", 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) + for _, u := range rp.updaters { + for _, file := range u.Files() { + err = repo.UpdateFile(ctx, file, u.CreateNewFiles(), u.Update(info)) + if err != nil { + return fmt.Errorf("failed to run updater %T: %w", u, err) + } } } diff --git a/templates/run.yml b/templates/run.yml index 66037b1..79459d9 100644 --- a/templates/run.yml +++ b/templates/run.yml @@ -9,12 +9,12 @@ spec: description: "GitLab token for creating and updating release MRs." 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: "" - update-package-json: - description: 'Update version field in package.json file.' - default: "false" + updaters: + 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: "" stage: default: build @@ -54,4 +54,4 @@ releaser-pleaser: --forge=gitlab \ --branch=$[[ inputs.branch ]] \ --extra-files="$[[ inputs.extra-files ]]" \ - $([[ inputs.update-package-json == "true" ]] && echo "--update-package-json" || echo "") + --updaters="$[[ inputs.updaters ]]" From 5306e2dd35de78b281e3307bc0864ba768f015c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sat, 23 Aug 2025 22:38:24 +0200 Subject: [PATCH 6/9] fix: filter out empty updaters in input (#235) --- cmd/rp/cmd/run.go | 4 ++++ cmd/rp/cmd/run_test.go | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index 202ba3d..a70e915 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -135,6 +135,10 @@ func parseUpdaters(input []string) []string { names := []string{"changelog", "generic"} for _, u := range input { + if u == "" { + continue + } + if strings.HasPrefix(u, "-") { name := u[1:] names = slices.DeleteFunc(names, func(existingName string) bool { return existingName == name }) diff --git a/cmd/rp/cmd/run_test.go b/cmd/rp/cmd/run_test.go index 8354478..4c6ceff 100644 --- a/cmd/rp/cmd/run_test.go +++ b/cmd/rp/cmd/run_test.go @@ -89,6 +89,11 @@ func Test_parseUpdaters(t *testing.T) { input: []string{"bar", "bar", "changelog"}, want: []string{"bar", "changelog", "generic"}, }, + { + name: "remove empty entries", + input: []string{""}, + want: []string{"changelog", "generic"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 48b1894cac4aca3f04e6985a3d05cc77d5e6efb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sat, 23 Aug 2025 22:40:28 +0200 Subject: [PATCH 7/9] feat: highlight breaking changes in release notes (#234) Add a `**BREAKING**` prefix to any entries in the changelog that are marked as breaking changes. Closes #225 --- internal/changelog/changelog.md.tpl | 2 +- internal/changelog/changelog_test.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/changelog/changelog.md.tpl b/internal/changelog/changelog.md.tpl index 50907eb..0a73dd5 100644 --- a/internal/changelog/changelog.md.tpl +++ b/internal/changelog/changelog.md.tpl @@ -1,5 +1,5 @@ {{define "entry" -}} -- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}} +- {{ if .BreakingChange}}**BREAKING**: {{end}}{{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}} {{ end }} {{- if not .Formatting.HideVersionTitle }} diff --git a/internal/changelog/changelog_test.go b/internal/changelog/changelog_test.go index a969730..e4fe52f 100644 --- a/internal/changelog/changelog_test.go +++ b/internal/changelog/changelog_test.go @@ -54,6 +54,23 @@ func Test_NewChangelogEntry(t *testing.T) { want: "## [1.0.0](https://example.com/1.0.0)\n\n### Features\n\n- Foobar!\n", wantErr: assert.NoError, }, + { + name: "single breaking change", + args: args{ + analyzedCommits: []commitparser.AnalyzedCommit{ + { + Commit: git.Commit{}, + Type: "feat", + Description: "Foobar!", + BreakingChange: true, + }, + }, + version: "1.0.0", + link: "https://example.com/1.0.0", + }, + want: "## [1.0.0](https://example.com/1.0.0)\n\n### Features\n\n- **BREAKING**: Foobar!\n", + wantErr: assert.NoError, + }, { name: "single fix", args: args{ From d259921215a04117e6b97124b180d051fbf7b946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sat, 23 Aug 2025 23:00:52 +0200 Subject: [PATCH 8/9] fix(github): duplicate release pr when process is stopped at wrong moment (#236) In a timing issue, the release pull request may be created but the releaser-pleaser labels not added. On the next run releaser-pleaser then creates a second release pull request. We try to reduce the chance of this happening by checking the context cancellation at the top, and if its not cancelled we run both API requests without passing along any cancellations from the parent context. Closes #215 --- internal/forge/github/github.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/forge/github/github.go b/internal/forge/github/github.go index 3bff3e6..f950add 100644 --- a/internal/forge/github/github.go +++ b/internal/forge/github/github.go @@ -296,6 +296,13 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*rele } func (g *GitHub) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error { + // If the Pull Request is created without the labels releaser-pleaser will create a new PR in the run. The user may merge both and have duplicate entries in the changelog. + // We try to avoid this situation by checking for a cancelled context first, and then running both API calls without passing along any cancellations. + if ctx.Err() != nil { + return ctx.Err() + } + ctx = context.WithoutCancel(ctx) + ghPR, _, err := g.client.PullRequests.Create( ctx, g.options.Owner, g.options.Repo, &github.NewPullRequest{ @@ -309,7 +316,6 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePul return err } - // TODO: String ID? pr.ID = ghPR.GetNumber() err = g.SetPullRequestLabels(ctx, pr, []releasepr.Label{}, pr.Labels) From b56953ba1a99fdf8582c017275ca5667a40c9367 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 21:01:22 +0000 Subject: [PATCH 9/9] deps: update dependency go to v1.25.0 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 41f54c3..997a693 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/apricote/releaser-pleaser go 1.23.2 -toolchain go1.24.6 +toolchain go1.25.0 require ( github.com/blang/semver/v4 v4.0.0