diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f2a8386..9d137dc 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 @@ -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 }} @@ -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 diff --git a/action.yml b/action.yml index 225e3cc..651ca09 100644 --- a/action.yml +++ b/action.yml @@ -14,11 +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: "" + 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: "" # 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 @@ -27,6 +31,7 @@ runs: - --forge=github - --branch=${{ inputs.branch }} - --extra-files="${{ inputs.extra-files }}" + - --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 ec11e24..a70e915 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" @@ -26,16 +27,17 @@ var ( flagOwner string flagRepo string flagExtraFiles string + flagUpdaters []string ) 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().StringSliceVar(&flagUpdaters, "updaters", []string{}, "") } func run(cmd *cobra.Command, _ []string) error { @@ -81,6 +83,21 @@ func run(cmd *cobra.Command, _ []string) error { extraFiles := parseExtraFiles(flagExtraFiles) + 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( f, logger, @@ -88,7 +105,7 @@ func run(cmd *cobra.Command, _ []string) error { conventionalcommits.NewParser(logger), versioning.SemVer, extraFiles, - []updater.NewUpdater{updater.Generic}, + updaters, ) return releaserPleaser.Run(ctx) @@ -113,3 +130,26 @@ func parseExtraFiles(input string) []string { return extraFiles } + +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 }) + } 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..4c6ceff 100644 --- a/cmd/rp/cmd/run_test.go +++ b/cmd/rp/cmd/run_test.go @@ -57,3 +57,48 @@ 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"}, + }, + { + name: "remove empty entries", + input: []string{""}, + want: []string{"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 eec9789..c5d595a 100644 --- a/docs/reference/github-action.md +++ b/docs/reference/github-action.md @@ -8,17 +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
| +| 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 b22d5b2..3080fcf 100644 --- a/docs/reference/gitlab-cicd-component.md +++ b/docs/reference/gitlab-cicd-component.md @@ -18,10 +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
| -| `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/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 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{ 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) diff --git a/internal/git/git.go b/internal/git/git.go index d1db11b..ad5c0a3 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -13,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 ( @@ -119,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 @@ -141,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) - 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 8bdb9f6..a7c7506 100644 --- a/internal/updater/changelog.go +++ b/internal/updater/changelog.go @@ -14,7 +14,22 @@ var ( ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`) ) -func Changelog(info ReleaseInfo) Updater { +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 { diff --git a/internal/updater/changelog_test.go b/internal/updater/changelog_test.go index 917cd14..c0becb5 100644 --- a/internal/updater/changelog_test.go +++ b/internal/updater/changelog_test.go @@ -6,7 +6,15 @@ 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", @@ -54,7 +62,7 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) { } 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 b8d73b0..11b21a4 100644 --- a/internal/updater/generic.go +++ b/internal/updater/generic.go @@ -7,7 +7,25 @@ import ( var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`) -func Generic(info ReleaseInfo) Updater { +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 e0a8d1d..7c007a4 100644 --- a/internal/updater/generic_test.go +++ b/internal/updater/generic_test.go @@ -6,7 +6,15 @@ 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", @@ -47,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 new file mode 100644 index 0000000..0fcb8a5 --- /dev/null +++ b/internal/updater/packagejson.go @@ -0,0 +1,39 @@ +package updater + +import ( + "regexp" + "strings" +) + +// PackageJson creates an updater that modifies the version field in 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") + + // 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 + return versionRegex.ReplaceAllString(content, `${1}"`+version+`"`), nil + } +} diff --git a/internal/updater/packagejson_test.go b/internal/updater/packagejson_test.go new file mode 100644 index 0000000..9bff8b7 --- /dev/null +++ b/internal/updater/packagejson_test.go @@ -0,0 +1,62 @@ +package updater + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +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"}`, + info: ReleaseInfo{ + Version: "v2.0.5", + }, + want: `{"name":"test","version":"2.0.5"}`, + 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}", + 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`, + info: ReleaseInfo{ + Version: "v2.0.0", + }, + want: `not json`, + wantErr: assert.NoError, + }, + { + name: "json without version", + content: `{"name":"test"}`, + info: ReleaseInfo{ + Version: "v2.0.0", + }, + want: `{"name":"test"}`, + wantErr: assert.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runUpdaterTest(t, PackageJson(), tt) + }) + } +} diff --git a/internal/updater/updater.go b/internal/updater/updater.go index fb773b4..6e27f37 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -5,7 +5,11 @@ type ReleaseInfo struct { ChangelogEntry string } -type Updater func(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 0c0c40e..5a90936 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -15,10 +15,10 @@ type updaterTestCase struct { 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) + got, err := u.Update(tt.info)(tt.content) if !tt.wantErr(t, err, fmt.Sprintf("Updater(%v, %v)", tt.content, tt.info)) { return } 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 c18a330..79459d9 100644 --- a/templates/run.yml +++ b/templates/run.yml @@ -9,7 +9,11 @@ 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: "" + + 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: @@ -49,4 +53,5 @@ releaser-pleaser: rp run \ --forge=gitlab \ --branch=$[[ inputs.branch ]] \ - --extra-files="$[[ inputs.extra-files ]]" + --extra-files="$[[ inputs.extra-files ]]" \ + --updaters="$[[ inputs.updaters ]]"