From 47de2f97bc582c35cfa8857c016407f7dc8c42a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Fri, 23 Aug 2024 22:02:58 +0200 Subject: [PATCH] feat: update version references in any files (#14) --- action.yml | 7 ++++- cmd/rp/cmd/run.go | 38 ++++++++++++++++++++---- releaserpleaser.go | 71 ++++++++++++++++++++++++++++++++++++++++---- updater.go | 28 ++++++++++++++---- updater_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 updater_test.go diff --git a/action.yml b/action.yml index c84aa28..075c8b1 100644 --- a/action.yml +++ b/action.yml @@ -12,14 +12,19 @@ inputs: description: 'GitHub token for creating and grooming release PRs, defaults to using secrets.GITHUB_TOKEN' required: false default: ${{ github.token }} + extra-files: + description: 'List of files that are scanned for version references.' + required: false + default: "" outputs: {} runs: using: 'docker' - image: ghcr.io/apricote/releaser-pleaser:v0.1.0 + image: ghcr.io/apricote/releaser-pleaser:v0.1.0 # x-releaser-pleaser-version args: - run - --forge=github - --branch=${{ inputs.branch }} + - --extra-files="${{ inputs.extra-files }}" env: GITHUB_TOKEN: ${{ inputs.token }} GITHUB_USER: "oauth2" diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index 34db06f..867f949 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -1,6 +1,8 @@ package cmd import ( + "strings" + "github.com/spf13/cobra" rp "github.com/apricote/releaser-pleaser" @@ -13,10 +15,11 @@ var runCmd = &cobra.Command{ } var ( - flagForge string - flagBranch string - flagOwner string - flagRepo string + flagForge string + flagBranch string + flagOwner string + flagRepo string + flagExtraFiles string ) func init() { @@ -28,6 +31,7 @@ func init() { runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "") runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "") runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "") + runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "") } func run(cmd *cobra.Command, _ []string) error { @@ -59,7 +63,31 @@ func run(cmd *cobra.Command, _ []string) error { }) } - releaserPleaser := rp.New(forge, logger, flagBranch, rp.NewConventionalCommitsParser(), rp.SemVerNextVersion) + extraFiles := parseExtraFiles(flagExtraFiles) + + releaserPleaser := rp.New( + forge, + logger, + flagBranch, + rp.NewConventionalCommitsParser(), + rp.SemVerNextVersion, + extraFiles, + []rp.Updater{&rp.GenericUpdater{}}, + ) return releaserPleaser.Run(ctx) } + +func parseExtraFiles(input string) []string { + lines := strings.Split(input, "\n") + + extraFiles := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if len(line) > 0 { + extraFiles = append(extraFiles, line) + } + } + + return extraFiles +} diff --git a/releaserpleaser.go b/releaserpleaser.go index 7c3ab16..42fd6fe 100644 --- a/releaserpleaser.go +++ b/releaserpleaser.go @@ -3,7 +3,9 @@ package rp import ( "context" "fmt" + "io" "log/slog" + "os" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -20,15 +22,19 @@ type ReleaserPleaser struct { targetBranch string commitParser CommitParser nextVersion VersioningStrategy + extraFiles []string + updaters []Updater } -func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy) *ReleaserPleaser { +func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy, extraFiles []string, updaters []Updater) *ReleaserPleaser { return &ReleaserPleaser{ forge: forge, logger: logger, targetBranch: targetBranch, commitParser: commitParser, nextVersion: versioningStrategy, + extraFiles: extraFiles, + updaters: updaters, } } @@ -236,21 +242,74 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error { return fmt.Errorf("failed to check out branch: %w", err) } - err = RunUpdater(ctx, nextVersion, worktree) - if err != nil { - return fmt.Errorf("failed to update files with new version: %w", err) - } - changelogEntry, err := NewChangelogEntry(analyzedCommits, nextVersion, rp.forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix) if err != nil { return fmt.Errorf("failed to build changelog entry: %w", err) } + // Info for updaters + info := ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry} + err = UpdateChangelogFile(worktree, changelogEntry) if err != nil { return fmt.Errorf("failed to update changelog file: %w", err) } + updateFile := func(path string, updaters []Updater) error { + file, err := worktree.Filesystem.OpenFile(path, os.O_RDWR, 0) + if err != nil { + return err + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return err + } + + updatedContent := string(content) + + for _, updater := range updaters { + updatedContent, err = updater.UpdateContent(updatedContent, info) + if err != nil { + return fmt.Errorf("failed to run updater %T on file %s", updater, path) + } + } + + err = file.Truncate(0) + if err != nil { + return fmt.Errorf("failed to replace file content: %w", err) + } + _, err = file.Seek(0, 0) + if err != nil { + return fmt.Errorf("failed to replace file content: %w", err) + } + _, err = file.Write([]byte(updatedContent)) + if err != nil { + return fmt.Errorf("failed to replace file content: %w", err) + } + + _, err = worktree.Add(path) + if err != nil { + return fmt.Errorf("failed to add updated file to git worktree: %w", err) + } + + return nil + } + + for _, path := range rp.extraFiles { + _, err = worktree.Filesystem.Stat(path) + if err != nil { + // TODO: Check for non existing file or dirs + return fmt.Errorf("failed to run file updater because the file %s does not exist: %w", path, err) + } + + err = updateFile(path, rp.updaters) + if err != nil { + return fmt.Errorf("failed to run file updater: %w", err) + } + } + releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion) releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{ Author: GitSignature(), diff --git a/updater.go b/updater.go index ce01d21..32bd2ef 100644 --- a/updater.go +++ b/updater.go @@ -1,12 +1,28 @@ package rp import ( - "context" - - "github.com/go-git/go-git/v5" + "regexp" + "strings" ) -func RunUpdater(ctx context.Context, version string, worktree *git.Worktree) error { - // TODO: Implement updater for Go,Python,ExtraFilesMarkers - return nil +var ( + GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`) +) + +type ReleaseInfo struct { + Version string + ChangelogEntry string +} + +type Updater interface { + UpdateContent(content string, info ReleaseInfo) (string, error) +} + +type GenericUpdater struct{} + +func (u *GenericUpdater) UpdateContent(content string, info ReleaseInfo) (string, error) { + // We strip the "v" prefix to avoid adding/removing it from the users input. + version := strings.TrimPrefix(info.Version, "v") + + return GenericUpdaterSemVerRegex.ReplaceAllString(content, version+"${2}"), nil } diff --git a/updater_test.go b/updater_test.go new file mode 100644 index 0000000..f82a2ee --- /dev/null +++ b/updater_test.go @@ -0,0 +1,74 @@ +package rp + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type updaterTestCase struct { + name string + content string + info ReleaseInfo + want string + wantErr assert.ErrorAssertionFunc +} + +func runUpdaterTest(t *testing.T, updater Updater, tt updaterTestCase) { + t.Helper() + + got, err := updater.UpdateContent(tt.content, tt.info) + if !tt.wantErr(t, err, fmt.Sprintf("UpdateContent(%v, %v)", tt.content, tt.info)) { + return + } + assert.Equalf(t, tt.want, got, "UpdateContent(%v, %v)", tt.content, tt.info) +} + +func TestGenericUpdater_UpdateContent(t *testing.T) { + updater := &GenericUpdater{} + + tests := []updaterTestCase{ + { + name: "single line", + content: "v1.0.0 // x-releaser-pleaser-version", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "v1.2.0 // x-releaser-pleaser-version", + wantErr: assert.NoError, + }, + { + name: "multiline line", + content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "Foooo\n\v1.2.0\nv1.2.0 // x-releaser-pleaser-version\n", + wantErr: assert.NoError, + }, + { + name: "invalid existing version", + content: "1.0 // x-releaser-pleaser-version", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "1.0 // x-releaser-pleaser-version", + wantErr: assert.NoError, + }, + { + name: "complicated line", + content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "version: v1.2.0 => Awesome, isnt it? x-releaser-pleaser-version foobar", + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runUpdaterTest(t, updater, tt) + }) + } +}