feat: add updater for package.json (#213)

This commit is contained in:
Mattis Krämer 2025-08-23 22:05:52 +02:00 committed by GitHub
parent 6237c9b666
commit 1e9e0aa5d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 174 additions and 44 deletions

View file

@ -17,6 +17,10 @@ inputs:
description: 'List of files that are scanned for version references.' description: 'List of files that are scanned for version references.'
required: false required: false
default: "" default: ""
update-package-json:
description: 'Update version field in package.json file.'
required: false
default: "false"
# Remember to update docs/reference/github-action.md # Remember to update docs/reference/github-action.md
outputs: {} outputs: {}
runs: runs:
@ -27,6 +31,7 @@ runs:
- --forge=github - --forge=github
- --branch=${{ inputs.branch }} - --branch=${{ inputs.branch }}
- --extra-files="${{ inputs.extra-files }}" - --extra-files="${{ inputs.extra-files }}"
- ${{ inputs.update-package-json == 'true' && '--update-package-json' || '' }}
env: env:
GITHUB_TOKEN: "${{ inputs.token }}" GITHUB_TOKEN: "${{ inputs.token }}"
GITHUB_USER: "oauth2" GITHUB_USER: "oauth2"

View file

@ -21,21 +21,22 @@ var runCmd = &cobra.Command{
} }
var ( var (
flagForge string flagForge string
flagBranch string flagBranch string
flagOwner string flagOwner string
flagRepo string flagRepo string
flagExtraFiles string flagExtraFiles string
flagUpdatePackageJson bool
) )
func init() { func init() {
rootCmd.AddCommand(runCmd) rootCmd.AddCommand(runCmd)
runCmd.PersistentFlags().StringVar(&flagForge, "forge", "", "") runCmd.PersistentFlags().StringVar(&flagForge, "forge", "", "")
runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "") runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "")
runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "") runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "")
runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "") runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "")
runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "") runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
runCmd.PersistentFlags().BoolVar(&flagUpdatePackageJson, "update-package-json", false, "")
} }
func run(cmd *cobra.Command, _ []string) error { func run(cmd *cobra.Command, _ []string) error {
@ -48,6 +49,7 @@ func run(cmd *cobra.Command, _ []string) error {
"branch", flagBranch, "branch", flagBranch,
"owner", flagOwner, "owner", flagOwner,
"repo", flagRepo, "repo", flagRepo,
"update-package-json", flagUpdatePackageJson,
) )
var f forge.Forge var f forge.Forge
@ -81,6 +83,13 @@ func run(cmd *cobra.Command, _ []string) error {
extraFiles := parseExtraFiles(flagExtraFiles) 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( releaserPleaser := rp.New(
f, f,
logger, logger,
@ -88,7 +97,7 @@ func run(cmd *cobra.Command, _ []string) error {
conventionalcommits.NewParser(logger), conventionalcommits.NewParser(logger),
versioning.SemVer, versioning.SemVer,
extraFiles, extraFiles,
[]updater.NewUpdater{updater.Generic}, updaters,
) )
return releaserPleaser.Run(ctx) return releaserPleaser.Run(ctx)

View file

@ -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. The following inputs are supported by the `apricote/releaser-pleaser` GitHub Action.
| Input | Description | Default | Example | | Input | Description | Default | Example |
| ------------- | :----------------------------------------------------- | --------------: | -------------------------------------------------------------------: | | --------------------- | :------------------------------------------------------ | --------------: | -------------------------------------------------------------------: |
| `branch` | This branch is used as the target for releases. | `main` | `master` | | `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` | GitHub token for creating and updating release PRs | `$GITHUB_TOKEN` | `${{secrets.RELEASER_PLEASER_TOKEN}}` | | `token` | GitHub token for creating and updating release PRs | `$GITHUB_TOKEN` | `${{secrets.RELEASER_PLEASER_TOKEN}}` |
| `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> | | `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| `update-package-json` | Update version field in package.json file. | `false` | `true` |
## Outputs ## Outputs

View file

@ -23,5 +23,6 @@ The following inputs are supported by the component.
| `branch` | This branch is used as the target for releases. | `main` | `master` | | `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` (**required**) | GitLab access token for creating and updating release PRs | | `$RELEASER_PLEASER_TOKEN` | | `token` (**required**) | GitLab access token for creating and updating release PRs | | `$RELEASER_PLEASER_TOKEN` |
| `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> | | `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| `update-package-json` | Update version field in package.json file. | `false` | `true` |
| `stage` | Stage the job runs in. Must exists. | `build` | `test` | | `stage` | Stage the job runs in. Must exists. | `build` | `test` |
| `needs` | Other jobs the releaser-pleaser job depends on. | `[]` | <pre><code>- validate-foo<br>- prepare-bar</code></pre> | | `needs` | Other jobs the releaser-pleaser job depends on. | `[]` | <pre><code>- validate-foo<br>- prepare-bar</code></pre> |

View file

@ -6,6 +6,7 @@ import (
"io" "io"
"log/slog" "log/slog"
"os" "os"
"path/filepath"
"time" "time"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@ -144,7 +145,7 @@ func (r *Repository) UpdateFile(_ context.Context, path string, create bool, upd
updatedContent := string(content) updatedContent := string(content)
for _, update := range updaters { for _, update := range updaters {
updatedContent, err = update(updatedContent) updatedContent, err = update(updatedContent, filepath.Base(path))
if err != nil { if err != nil {
return fmt.Errorf("failed to run updater on file %s", path) return fmt.Errorf("failed to run updater on file %s", path)
} }

View file

@ -15,7 +15,7 @@ var (
) )
func Changelog(info ReleaseInfo) Updater { func Changelog(info ReleaseInfo) Updater {
return func(content string) (string, error) { return func(content string, filename string) (string, error) {
headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content) headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content)
if headerIndex == nil && len(content) != 0 { if headerIndex == nil && len(content) != 0 {
return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match") return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match")

View file

@ -9,11 +9,12 @@ import (
func TestChangelogUpdater_UpdateContent(t *testing.T) { func TestChangelogUpdater_UpdateContent(t *testing.T) {
tests := []updaterTestCase{ tests := []updaterTestCase{
{ {
name: "empty file", name: "empty file",
content: "", content: "",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"}, filename: "CHANGELOG.md",
want: "# Changelog\n\n## v1.0.0\n", info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"},
wantErr: assert.NoError, want: "# Changelog\n\n## v1.0.0\n",
wantErr: assert.NoError,
}, },
{ {
name: "well-formatted changelog", name: "well-formatted changelog",
@ -27,7 +28,8 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) {
### Bazuuum ### 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 want: `# Changelog
## v1.0.0 ## v1.0.0
@ -45,11 +47,12 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "error on invalid header", name: "error on invalid header",
content: "What even is this file?", content: "What even is this file?",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"}, filename: "CHANGELOG.md",
want: "", info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"},
wantErr: assert.Error, want: "",
wantErr: assert.Error,
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View file

@ -8,7 +8,7 @@ import (
var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`) var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`)
func Generic(info ReleaseInfo) Updater { func Generic(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. // We strip the "v" prefix to avoid adding/removing it from the users input.
version := strings.TrimPrefix(info.Version, "v") version := strings.TrimPrefix(info.Version, "v")

View file

@ -9,8 +9,9 @@ import (
func TestGenericUpdater_UpdateContent(t *testing.T) { func TestGenericUpdater_UpdateContent(t *testing.T) {
tests := []updaterTestCase{ tests := []updaterTestCase{
{ {
name: "single line", name: "single line",
content: "v1.0.0 // x-releaser-pleaser-version", content: "v1.0.0 // x-releaser-pleaser-version",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },
@ -18,8 +19,9 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "multiline line", name: "multiline line",
content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n", content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },
@ -27,8 +29,9 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "invalid existing version", name: "invalid existing version",
content: "1.0 // x-releaser-pleaser-version", content: "1.0 // x-releaser-pleaser-version",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },
@ -36,8 +39,9 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "complicated line", name: "complicated line",
content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar", content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar",
filename: "version.txt",
info: ReleaseInfo{ info: ReleaseInfo{
Version: "v1.2.0", Version: "v1.2.0",
}, },

View file

@ -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
}
}

View file

@ -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)
})
}
}

View file

@ -5,7 +5,7 @@ type ReleaseInfo struct {
ChangelogEntry string ChangelogEntry string
} }
type Updater func(string) (string, error) type Updater func(content string, filename string) (string, error)
type NewUpdater func(ReleaseInfo) Updater type NewUpdater func(ReleaseInfo) Updater

View file

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

View file

@ -12,6 +12,10 @@ spec:
description: 'List of files that are scanned for version references.' description: 'List of files that are scanned for version references.'
default: "" default: ""
update-package-json:
description: 'Update version field in package.json file.'
default: "false"
stage: stage:
default: build default: build
description: 'Defines the build stage' description: 'Defines the build stage'
@ -49,4 +53,5 @@ releaser-pleaser:
rp run \ rp run \
--forge=gitlab \ --forge=gitlab \
--branch=$[[ inputs.branch ]] \ --branch=$[[ inputs.branch ]] \
--extra-files="$[[ inputs.extra-files ]]" --extra-files="$[[ inputs.extra-files ]]" \
$([[ inputs.update-package-json == "true" ]] && echo "--update-package-json" || echo "")