diff --git a/.golangci.yaml b/.golangci.yaml deleted file mode 100644 index b3e717d..0000000 --- a/.golangci.yaml +++ /dev/null @@ -1,27 +0,0 @@ -linters: - presets: - - bugs - - error - - import - - metalinter - - module - - unused - - enable: - - testifylint - - disable: - # preset error - # These should probably be cleaned up at some point if we want to publish part of this as a library. - - err113 # Very annoying to define static errors everywhere - - wrapcheck # Very annoying to wrap errors everywhere - # preset import - - depguard - -linters-settings: - gci: - sections: - - standard - - default - - localmodule - diff --git a/action.yml b/action.yml index 075c8b1..c84aa28 100644 --- a/action.yml +++ b/action.yml @@ -12,19 +12,14 @@ 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 # x-releaser-pleaser-version + image: ghcr.io/apricote/releaser-pleaser:v0.1.0 args: - run - --forge=github - --branch=${{ inputs.branch }} - - --extra-files="${{ inputs.extra-files }}" env: GITHUB_TOKEN: ${{ inputs.token }} GITHUB_USER: "oauth2" diff --git a/changelog.go b/changelog.go index 286faf4..43568d5 100644 --- a/changelog.go +++ b/changelog.go @@ -3,8 +3,14 @@ package rp import ( "bytes" _ "embed" + "fmt" "html/template" + "io" "log" + "os" + "regexp" + + "github.com/go-git/go-git/v5" ) const ( @@ -14,6 +20,8 @@ const ( var ( changelogTemplate *template.Template + + headerRegex = regexp.MustCompile(`^# Changelog\n`) ) //go:embed changelog.md.tpl @@ -27,6 +35,60 @@ func init() { } } +func UpdateChangelogFile(wt *git.Worktree, newEntry string) error { + file, err := wt.Filesystem.OpenFile(ChangelogFile, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return err + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return err + } + + headerIndex := headerRegex.FindIndex(content) + if headerIndex == nil && len(content) != 0 { + return fmt.Errorf("unexpected format of CHANGELOG.md, header does not match") + } + if headerIndex != nil { + // Remove the header from the content + content = content[headerIndex[1]:] + } + + err = file.Truncate(0) + if err != nil { + return err + } + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return err + } + + _, err = file.Write([]byte(ChangelogHeader + "\n\n" + newEntry)) + if err != nil { + return err + } + + _, err = file.Write(content) + if err != nil { + return err + } + + // Close file to make sure it is written to disk. + err = file.Close() + if err != nil { + return err + } + + _, err = wt.Add(ChangelogFile) + if err != nil { + return err + } + + return nil +} + func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) { features := make([]AnalyzedCommit, 0) fixes := make([]AnalyzedCommit, 0) diff --git a/changelog_test.go b/changelog_test.go index 3fabff8..3d1612b 100644 --- a/changelog_test.go +++ b/changelog_test.go @@ -1,15 +1,99 @@ package rp import ( + "io" "testing" + "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/apricote/releaser-pleaser/internal/testutils" ) func ptr[T any](input T) *T { return &input } +func TestUpdateChangelogFile(t *testing.T) { + tests := []struct { + name string + repoFn testutils.Repo + entry string + expectedContent string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "empty repo", + repoFn: testutils.WithTestRepo(), + entry: "## v1.0.0\n", + expectedContent: "# Changelog\n\n## v1.0.0\n", + wantErr: assert.NoError, + }, + { + name: "repo with well-formatted changelog", + repoFn: testutils.WithTestRepo(testutils.WithCommit("feat: add changelog", testutils.WithFile(ChangelogFile, `# Changelog + +## v0.0.1 + +- Bazzle + +## v0.1.0 + +### Bazuuum +`))), + entry: "## v1.0.0\n\n- Version 1, juhu.\n", + expectedContent: `# Changelog + +## v1.0.0 + +- Version 1, juhu. + +## v0.0.1 + +- Bazzle + +## v0.1.0 + +### Bazuuum +`, + wantErr: assert.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := tt.repoFn(t) + wt, err := repo.Worktree() + require.NoError(t, err, "failed to get worktree") + + err = UpdateChangelogFile(wt, tt.entry) + if !tt.wantErr(t, err) { + return + } + + wtStatus, err := wt.Status() + require.NoError(t, err, "failed to get worktree status") + + assert.Len(t, wtStatus, 1, "worktree status does not have the expected entry number") + + changelogFileStatus := wtStatus.File(ChangelogFile) + + assert.Equal(t, git.Unmodified, changelogFileStatus.Worktree, "unexpected file status in worktree") + assert.Equal(t, git.Added, changelogFileStatus.Staging, "unexpected file status in staging") + + changelogFile, err := wt.Filesystem.Open(ChangelogFile) + require.NoError(t, err) + defer changelogFile.Close() + + changelogFileContent, err := io.ReadAll(changelogFile) + require.NoError(t, err) + + assert.Equal(t, tt.expectedContent, string(changelogFileContent)) + }) + } +} + func Test_NewChangelogEntry(t *testing.T) { type args struct { analyzedCommits []AnalyzedCommit diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index 7661af5..34db06f 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -1,8 +1,6 @@ package cmd import ( - "strings" - "github.com/spf13/cobra" rp "github.com/apricote/releaser-pleaser" @@ -15,11 +13,10 @@ var runCmd = &cobra.Command{ } var ( - flagForge string - flagBranch string - flagOwner string - flagRepo string - flagExtraFiles string + flagForge string + flagBranch string + flagOwner string + flagRepo string ) func init() { @@ -31,7 +28,6 @@ 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 { @@ -51,9 +47,9 @@ func run(cmd *cobra.Command, _ []string) error { BaseBranch: flagBranch, } - switch flagForge { // nolint:gocritic // Will become a proper switch once gitlab is added - // case "gitlab": - // f = rp.NewGitLab(forgeOptions) + switch flagForge { + //case "gitlab": + //f = rp.NewGitLab(forgeOptions) case "github": logger.DebugContext(ctx, "using forge GitHub") forge = rp.NewGitHub(logger, &rp.GitHubOptions{ @@ -63,31 +59,7 @@ func run(cmd *cobra.Command, _ []string) error { }) } - extraFiles := parseExtraFiles(flagExtraFiles) - - releaserPleaser := rp.New( - forge, - logger, - flagBranch, - rp.NewConventionalCommitsParser(), - rp.SemVerNextVersion, - extraFiles, - []rp.Updater{&rp.GenericUpdater{}}, - ) + releaserPleaser := rp.New(forge, logger, flagBranch, rp.NewConventionalCommitsParser(), rp.SemVerNextVersion) 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/forge.go b/forge.go index 1086564..c244a0c 100644 --- a/forge.go +++ b/forge.go @@ -19,7 +19,7 @@ const ( GitHubPerPageMax = 100 GitHubPRStateOpen = "open" GitHubPRStateClosed = "closed" - GitHubEnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential + GitHubEnvAPIToken = "GITHUB_TOKEN" GitHubEnvUsername = "GITHUB_USER" GitHubEnvRepository = "GITHUB_REPOSITORY" GitHubLabelColor = "dedede" diff --git a/go.mod b/go.mod index 9add194..3be3b4e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/blang/semver/v4 v4.0.0 + github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-github/v63 v63.0.0 github.com/leodido/go-conventionalcommits v0.12.0 @@ -16,12 +17,11 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/cloudflare/circl v1.4.0 // indirect + github.com/cloudflare/circl v1.3.9 // indirect github.com/cyphar/filepath-securejoin v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 4678ca7..c43a8f2 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,6 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= -github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= -github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= diff --git a/internal/markdown/extensions/section.go b/internal/markdown/extensions/section.go index dcca37d..e85808a 100644 --- a/internal/markdown/extensions/section.go +++ b/internal/markdown/extensions/section.go @@ -12,10 +12,8 @@ import ( "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast" ) -var ( - sectionStartRegex = regexp.MustCompile(`^`) - sectionEndRegex = regexp.MustCompile(`^`) -) +var sectionStartRegex = regexp.MustCompile(`^`) +var sectionEndRegex = regexp.MustCompile(`^`) const ( sectionTrigger = "" ) -type sectionParser struct{} +type sectionParser struct { +} func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) { line, _ := reader.PeekLine() @@ -76,7 +75,8 @@ func (s *sectionParser) Trigger() []byte { return []byte(sectionTrigger) } -type section struct{} +type section struct { +} // Section is an extension that allow you to use group content under a shared parent ast node. var Section = §ion{} diff --git a/internal/markdown/renderer/markdown/renderer.go b/internal/markdown/renderer/markdown/renderer.go index 69b2883..e0d4ecb 100644 --- a/internal/markdown/renderer/markdown/renderer.go +++ b/internal/markdown/renderer/markdown/renderer.go @@ -331,7 +331,7 @@ func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node a return ast.WalkStop, fmt.Errorf(": %w", err) } if err := r.writeByte(w, '\n'); err != nil { - return ast.WalkStop, fmt.Errorf(": %w", err) + return ast.WalkStop, nil } // Write the contents of the fenced code block. diff --git a/internal/testutils/git.go b/internal/testutils/git.go index f5721b6..14737a0 100644 --- a/internal/testutils/git.go +++ b/internal/testutils/git.go @@ -11,26 +11,25 @@ import ( "github.com/stretchr/testify/require" ) -var author = &object.Signature{ - Name: "releaser-pleaser", - When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC), -} +var ( + author = &object.Signature{ + Name: "releaser-pleaser", + When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC), + } +) type CommitOption func(*commitOptions) - type commitOptions struct { cleanFiles bool files []commitFile tags []string } - type commitFile struct { path string content string } type Commit func(*testing.T, *git.Repository) error - type Repo func(*testing.T) *git.Repository func WithCommit(message string, options ...CommitOption) Commit { @@ -84,6 +83,7 @@ func WithCommit(message string, options ...CommitOption) Commit { } return nil + } } diff --git a/releasepr.go b/releasepr.go index a6744c4..1d41325 100644 --- a/releasepr.go +++ b/releasepr.go @@ -155,9 +155,6 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R overrides.NextVersionType = NextVersionTypeBeta case LabelNextVersionTypeAlpha: overrides.NextVersionType = NextVersionTypeAlpha - case LabelReleasePending, LabelReleaseTagged: - // These labels have no effect on the versioning. - break } } diff --git a/releaserpleaser.go b/releaserpleaser.go index 5dfcf12..7c3ab16 100644 --- a/releaserpleaser.go +++ b/releaserpleaser.go @@ -3,9 +3,7 @@ package rp import ( "context" "fmt" - "io" "log/slog" - "os" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -22,19 +20,15 @@ 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, extraFiles []string, updaters []Updater) *ReleaserPleaser { +func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy) *ReleaserPleaser { return &ReleaserPleaser{ forge: forge, logger: logger, targetBranch: targetBranch, commitParser: commitParser, nextVersion: versioningStrategy, - extraFiles: extraFiles, - updaters: updaters, } } @@ -242,74 +236,21 @@ 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} - - 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 - } - - err = updateFile(ChangelogFile, []Updater{&ChangelogUpdater{}}) + err = UpdateChangelogFile(worktree, changelogEntry) if err != nil { return fmt.Errorf("failed to update changelog file: %w", err) } - 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 3aaedae..ce01d21 100644 --- a/updater.go +++ b/updater.go @@ -1,47 +1,12 @@ package rp import ( - "fmt" - "regexp" - "strings" + "context" + + "github.com/go-git/go-git/v5" ) -var ( - GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`) - ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`) -) - -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 -} - -type ChangelogUpdater struct{} - -func (u *ChangelogUpdater) UpdateContent(content string, info ReleaseInfo) (string, error) { - headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content) - if headerIndex == nil && len(content) != 0 { - return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match") - } - if headerIndex != nil { - // Remove the header from the content - content = content[headerIndex[1]:] - } - - content = ChangelogHeader + "\n\n" + info.ChangelogEntry + content - - return content, nil +func RunUpdater(ctx context.Context, version string, worktree *git.Worktree) error { + // TODO: Implement updater for Go,Python,ExtraFilesMarkers + return nil } diff --git a/updater_test.go b/updater_test.go deleted file mode 100644 index c0e1419..0000000 --- a/updater_test.go +++ /dev/null @@ -1,129 +0,0 @@ -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) - }) - } -} - -func TestChangelogUpdater_UpdateContent(t *testing.T) { - updater := &ChangelogUpdater{} - - 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: "well-formatted changelog", - content: `# Changelog - -## v0.0.1 - -- Bazzle - -## v0.1.0 - -### Bazuuum -`, - info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"}, - want: `# Changelog - -## v1.0.0 - -- Version 1, juhu. - -## v0.0.1 - -- Bazzle - -## v0.1.0 - -### Bazuuum -`, - 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, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - runUpdaterTest(t, updater, tt) - }) - } -} diff --git a/versioning.go b/versioning.go index d18d480..176a28d 100644 --- a/versioning.go +++ b/versioning.go @@ -45,9 +45,6 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, case conventionalcommits.MajorVersion: err = next.IncrementMajor() } - if err != nil { - return "", err - } switch nextVersionType { case NextVersionTypeUndefined, NextVersionTypeNormal: @@ -65,6 +62,10 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, setPRVersion(&next, nextVersionType.String(), id) } + if err != nil { + return "", err + } + return "v" + next.String(), nil }