From a0a064d387b33e96ae70e8c1aaff4591ca2e0019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sat, 31 Aug 2024 15:23:21 +0200 Subject: [PATCH] refactor: move things to packages (#39) --- cmd/rp/cmd/run.go | 25 +- git.go | 52 ---- git_test.go | 1 - go.mod | 2 +- .../changelog/changelog.go | 14 +- .../changelog/changelog.md.tpl | 0 .../changelog/changelog_test.go | 35 +-- internal/commitparser/commitparser.go | 17 ++ .../conventionalcommits.go | 42 +--- .../conventionalcommits_test.go | 43 ++-- internal/forge/forge.go | 61 +++++ forge.go => internal/forge/github/github.go | 202 +++++----------- internal/forge/gitlab/gitlab.go | 31 +++ internal/git/git.go | 227 ++++++++++++++++++ internal/pointer/pointer.go | 5 + internal/releasepr/label.go | 1 + .../releasepr/releasepr.go | 70 ++---- .../releasepr/releasepr.md.tpl | 0 .../releasepr/releasepr_test.go | 2 +- internal/testutils/git.go | 126 ---------- internal/updater/changelog.go | 32 +++ internal/updater/changelog_test.go | 60 +++++ internal/updater/generic.go | 17 ++ internal/updater/generic_test.go | 53 ++++ internal/updater/updater.go | 19 ++ internal/updater/updater_test.go | 26 ++ .../versioning/semver.go | 39 ++- .../versioning/semver_test.go | 214 +++++++++-------- internal/versioning/versioning.go | 56 +++++ releaserpleaser.go | 167 +++---------- updater.go | 47 ---- updater_test.go | 129 ---------- 32 files changed, 923 insertions(+), 892 deletions(-) delete mode 100644 git.go delete mode 100644 git_test.go rename changelog.go => internal/changelog/changelog.go (73%) rename changelog.md.tpl => internal/changelog/changelog.md.tpl (100%) rename changelog_test.go => internal/changelog/changelog_test.go (79%) create mode 100644 internal/commitparser/commitparser.go rename commits.go => internal/commitparser/conventionalcommits/conventionalcommits.go (61%) rename commits_test.go => internal/commitparser/conventionalcommits/conventionalcommits_test.go (62%) create mode 100644 internal/forge/forge.go rename forge.go => internal/forge/github/github.go (66%) create mode 100644 internal/forge/gitlab/gitlab.go create mode 100644 internal/git/git.go create mode 100644 internal/pointer/pointer.go create mode 100644 internal/releasepr/label.go rename releasepr.go => internal/releasepr/releasepr.go (81%) rename releasepr.md.tpl => internal/releasepr/releasepr.md.tpl (100%) rename releasepr_test.go => internal/releasepr/releasepr_test.go (99%) delete mode 100644 internal/testutils/git.go create mode 100644 internal/updater/changelog.go create mode 100644 internal/updater/changelog_test.go create mode 100644 internal/updater/generic.go create mode 100644 internal/updater/generic_test.go create mode 100644 internal/updater/updater.go create mode 100644 internal/updater/updater_test.go rename versioning.go => internal/versioning/semver.go (68%) rename versioning_test.go => internal/versioning/semver_test.go (55%) create mode 100644 internal/versioning/versioning.go delete mode 100644 updater.go delete mode 100644 updater_test.go diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index 7d5862b..de10927 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -6,6 +6,11 @@ import ( "github.com/spf13/cobra" rp "github.com/apricote/releaser-pleaser" + "github.com/apricote/releaser-pleaser/internal/commitparser/conventionalcommits" + "github.com/apricote/releaser-pleaser/internal/forge" + "github.com/apricote/releaser-pleaser/internal/forge/github" + "github.com/apricote/releaser-pleaser/internal/updater" + "github.com/apricote/releaser-pleaser/internal/versioning" ) var runCmd = &cobra.Command{ @@ -41,9 +46,9 @@ func run(cmd *cobra.Command, _ []string) error { "repo", flagRepo, ) - var forge rp.Forge + var f forge.Forge - forgeOptions := rp.ForgeOptions{ + forgeOptions := forge.Options{ Repository: flagRepo, BaseBranch: flagBranch, } @@ -53,23 +58,23 @@ func run(cmd *cobra.Command, _ []string) error { // f = rp.NewGitLab(forgeOptions) case "github": logger.DebugContext(ctx, "using forge GitHub") - forge = rp.NewGitHub(logger, &rp.GitHubOptions{ - ForgeOptions: forgeOptions, - Owner: flagOwner, - Repo: flagRepo, + f = github.New(logger, &github.Options{ + Options: forgeOptions, + Owner: flagOwner, + Repo: flagRepo, }) } extraFiles := parseExtraFiles(flagExtraFiles) releaserPleaser := rp.New( - forge, + f, logger, flagBranch, - rp.NewConventionalCommitsParser(), - rp.SemVerNextVersion, + conventionalcommits.NewParser(), + versioning.SemVerNextVersion, extraFiles, - []rp.Updater{&rp.GenericUpdater{}}, + []updater.NewUpdater{updater.Generic}, ) return releaserPleaser.Run(ctx) diff --git a/git.go b/git.go deleted file mode 100644 index 9131570..0000000 --- a/git.go +++ /dev/null @@ -1,52 +0,0 @@ -package rp - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/go-git/go-git/v5" - "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" -) - -const ( - GitRemoteName = "origin" -) - -type Tag struct { - Hash string - Name string -} - -func CloneRepo(ctx context.Context, cloneURL, branch string, auth transport.AuthMethod) (*git.Repository, error) { - dir, err := os.MkdirTemp("", "releaser-pleaser.*") - if err != nil { - return nil, fmt.Errorf("failed to create temporary directory for repo clone: %w", err) - } - - // TODO: Log tmpdir - fmt.Printf("Clone tmpdir: %s\n", dir) - repo, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ - URL: cloneURL, - RemoteName: GitRemoteName, - ReferenceName: plumbing.NewBranchReferenceName(branch), - SingleBranch: false, - Auth: auth, - }) - if err != nil { - return nil, fmt.Errorf("failed to clone repository: %w", err) - } - - return repo, nil -} - -func GitSignature() *object.Signature { - return &object.Signature{ - Name: "releaser-pleaser", - Email: "", - When: time.Now(), - } -} diff --git a/git_test.go b/git_test.go deleted file mode 100644 index 0c37ea1..0000000 --- a/git_test.go +++ /dev/null @@ -1 +0,0 @@ -package rp diff --git a/go.mod b/go.mod index 76b6fcb..9add194 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ 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 @@ -22,6 +21,7 @@ require ( 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/changelog.go b/internal/changelog/changelog.go similarity index 73% rename from changelog.go rename to internal/changelog/changelog.go index 286faf4..1d0fd67 100644 --- a/changelog.go +++ b/internal/changelog/changelog.go @@ -1,15 +1,12 @@ -package rp +package changelog import ( "bytes" _ "embed" "html/template" "log" -) -const ( - ChangelogFile = "CHANGELOG.md" - ChangelogHeader = "# Changelog" + "github.com/apricote/releaser-pleaser/internal/commitparser" ) var ( @@ -27,9 +24,9 @@ func init() { } } -func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) { - features := make([]AnalyzedCommit, 0) - fixes := make([]AnalyzedCommit, 0) +func NewChangelogEntry(commits []commitparser.AnalyzedCommit, version, link, prefix, suffix string) (string, error) { + features := make([]commitparser.AnalyzedCommit, 0) + fixes := make([]commitparser.AnalyzedCommit, 0) for _, commit := range commits { switch commit.Type { @@ -54,5 +51,4 @@ func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix s } return changelog.String(), nil - } diff --git a/changelog.md.tpl b/internal/changelog/changelog.md.tpl similarity index 100% rename from changelog.md.tpl rename to internal/changelog/changelog.md.tpl diff --git a/changelog_test.go b/internal/changelog/changelog_test.go similarity index 79% rename from changelog_test.go rename to internal/changelog/changelog_test.go index 3fabff8..384e30f 100644 --- a/changelog_test.go +++ b/internal/changelog/changelog_test.go @@ -1,9 +1,12 @@ -package rp +package changelog import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/apricote/releaser-pleaser/internal/commitparser" + "github.com/apricote/releaser-pleaser/internal/git" ) func ptr[T any](input T) *T { @@ -12,7 +15,7 @@ func ptr[T any](input T) *T { func Test_NewChangelogEntry(t *testing.T) { type args struct { - analyzedCommits []AnalyzedCommit + analyzedCommits []commitparser.AnalyzedCommit version string link string prefix string @@ -27,7 +30,7 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "empty", args: args{ - analyzedCommits: []AnalyzedCommit{}, + analyzedCommits: []commitparser.AnalyzedCommit{}, version: "1.0.0", link: "https://example.com/1.0.0", }, @@ -37,9 +40,9 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "single feature", args: args{ - analyzedCommits: []AnalyzedCommit{ + analyzedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{}, + Commit: git.Commit{}, Type: "feat", Description: "Foobar!", }, @@ -53,9 +56,9 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "single fix", args: args{ - analyzedCommits: []AnalyzedCommit{ + analyzedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{}, + Commit: git.Commit{}, Type: "fix", Description: "Foobar!", }, @@ -69,25 +72,25 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "multiple commits with scopes", args: args{ - analyzedCommits: []AnalyzedCommit{ + analyzedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{}, + Commit: git.Commit{}, Type: "feat", Description: "Blabla!", }, { - Commit: Commit{}, + Commit: git.Commit{}, Type: "feat", Description: "So awesome!", Scope: ptr("awesome"), }, { - Commit: Commit{}, + Commit: git.Commit{}, Type: "fix", Description: "Foobar!", }, { - Commit: Commit{}, + Commit: git.Commit{}, Type: "fix", Description: "So sad!", Scope: ptr("sad"), @@ -112,9 +115,9 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "prefix", args: args{ - analyzedCommits: []AnalyzedCommit{ + analyzedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{}, + Commit: git.Commit{}, Type: "fix", Description: "Foobar!", }, @@ -135,9 +138,9 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "suffix", args: args{ - analyzedCommits: []AnalyzedCommit{ + analyzedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{}, + Commit: git.Commit{}, Type: "fix", Description: "Foobar!", }, diff --git a/internal/commitparser/commitparser.go b/internal/commitparser/commitparser.go new file mode 100644 index 0000000..484d733 --- /dev/null +++ b/internal/commitparser/commitparser.go @@ -0,0 +1,17 @@ +package commitparser + +import ( + "github.com/apricote/releaser-pleaser/internal/git" +) + +type CommitParser interface { + Analyze(commits []git.Commit) ([]AnalyzedCommit, error) +} + +type AnalyzedCommit struct { + git.Commit + Type string + Description string + Scope *string + BreakingChange bool +} diff --git a/commits.go b/internal/commitparser/conventionalcommits/conventionalcommits.go similarity index 61% rename from commits.go rename to internal/commitparser/conventionalcommits/conventionalcommits.go index f0c64e9..665a02d 100644 --- a/commits.go +++ b/internal/commitparser/conventionalcommits/conventionalcommits.go @@ -1,54 +1,32 @@ -package rp +package conventionalcommits import ( "fmt" "github.com/leodido/go-conventionalcommits" "github.com/leodido/go-conventionalcommits/parser" + + "github.com/apricote/releaser-pleaser/internal/commitparser" + "github.com/apricote/releaser-pleaser/internal/git" ) -type Commit struct { - Hash string - Message string - - PullRequest *PullRequest -} - -type PullRequest struct { - ID int - Title string - Description string -} - -type AnalyzedCommit struct { - Commit - Type string - Description string - Scope *string - BreakingChange bool -} - -type CommitParser interface { - Analyze(commits []Commit) ([]AnalyzedCommit, error) -} - -type ConventionalCommitsParser struct { +type Parser struct { machine conventionalcommits.Machine } -func NewConventionalCommitsParser() *ConventionalCommitsParser { +func NewParser() *Parser { parserMachine := parser.NewMachine( parser.WithBestEffort(), parser.WithTypes(conventionalcommits.TypesConventional), ) - return &ConventionalCommitsParser{ + return &Parser{ machine: parserMachine, } } -func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit, error) { - analyzedCommits := make([]AnalyzedCommit, 0, len(commits)) +func (c *Parser) Analyze(commits []git.Commit) ([]commitparser.AnalyzedCommit, error) { + analyzedCommits := make([]commitparser.AnalyzedCommit, 0, len(commits)) for _, commit := range commits { msg, err := c.machine.Parse([]byte(commit.Message)) @@ -63,7 +41,7 @@ func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit, commitVersionBump := conventionalCommit.VersionBump(conventionalcommits.DefaultStrategy) if commitVersionBump > conventionalcommits.UnknownVersion { // We only care about releasable commits - analyzedCommits = append(analyzedCommits, AnalyzedCommit{ + analyzedCommits = append(analyzedCommits, commitparser.AnalyzedCommit{ Commit: commit, Type: conventionalCommit.Type, Description: conventionalCommit.Description, diff --git a/commits_test.go b/internal/commitparser/conventionalcommits/conventionalcommits_test.go similarity index 62% rename from commits_test.go rename to internal/commitparser/conventionalcommits/conventionalcommits_test.go index e58a718..837035b 100644 --- a/commits_test.go +++ b/internal/commitparser/conventionalcommits/conventionalcommits_test.go @@ -1,27 +1,30 @@ -package rp +package conventionalcommits import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/apricote/releaser-pleaser/internal/commitparser" + "github.com/apricote/releaser-pleaser/internal/git" ) func TestAnalyzeCommits(t *testing.T) { tests := []struct { name string - commits []Commit - expectedCommits []AnalyzedCommit + commits []git.Commit + expectedCommits []commitparser.AnalyzedCommit wantErr assert.ErrorAssertionFunc }{ { name: "empty commits", - commits: []Commit{}, - expectedCommits: []AnalyzedCommit{}, + commits: []git.Commit{}, + expectedCommits: []commitparser.AnalyzedCommit{}, wantErr: assert.NoError, }, { name: "malformed commit message", - commits: []Commit{ + commits: []git.Commit{ { Message: "aksdjaklsdjka", }, @@ -31,17 +34,17 @@ func TestAnalyzeCommits(t *testing.T) { }, { name: "drops unreleasable", - commits: []Commit{ + commits: []git.Commit{ { Message: "chore: foobar", }, }, - expectedCommits: []AnalyzedCommit{}, + expectedCommits: []commitparser.AnalyzedCommit{}, wantErr: assert.NoError, }, { name: "highest bump (patch)", - commits: []Commit{ + commits: []git.Commit{ { Message: "chore: foobar", }, @@ -49,9 +52,9 @@ func TestAnalyzeCommits(t *testing.T) { Message: "fix: blabla", }, }, - expectedCommits: []AnalyzedCommit{ + expectedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{Message: "fix: blabla"}, + Commit: git.Commit{Message: "fix: blabla"}, Type: "fix", Description: "blabla", }, @@ -60,7 +63,7 @@ func TestAnalyzeCommits(t *testing.T) { }, { name: "highest bump (minor)", - commits: []Commit{ + commits: []git.Commit{ { Message: "fix: blabla", }, @@ -68,14 +71,14 @@ func TestAnalyzeCommits(t *testing.T) { Message: "feat: foobar", }, }, - expectedCommits: []AnalyzedCommit{ + expectedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{Message: "fix: blabla"}, + Commit: git.Commit{Message: "fix: blabla"}, Type: "fix", Description: "blabla", }, { - Commit: Commit{Message: "feat: foobar"}, + Commit: git.Commit{Message: "feat: foobar"}, Type: "feat", Description: "foobar", }, @@ -85,7 +88,7 @@ func TestAnalyzeCommits(t *testing.T) { { name: "highest bump (major)", - commits: []Commit{ + commits: []git.Commit{ { Message: "fix: blabla", }, @@ -93,14 +96,14 @@ func TestAnalyzeCommits(t *testing.T) { Message: "feat!: foobar", }, }, - expectedCommits: []AnalyzedCommit{ + expectedCommits: []commitparser.AnalyzedCommit{ { - Commit: Commit{Message: "fix: blabla"}, + Commit: git.Commit{Message: "fix: blabla"}, Type: "fix", Description: "blabla", }, { - Commit: Commit{Message: "feat!: foobar"}, + Commit: git.Commit{Message: "feat!: foobar"}, Type: "feat", Description: "foobar", BreakingChange: true, @@ -111,7 +114,7 @@ func TestAnalyzeCommits(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - analyzedCommits, err := NewConventionalCommitsParser().Analyze(tt.commits) + analyzedCommits, err := NewParser().Analyze(tt.commits) if !tt.wantErr(t, err) { return } diff --git a/internal/forge/forge.go b/internal/forge/forge.go new file mode 100644 index 0000000..a5f418d --- /dev/null +++ b/internal/forge/forge.go @@ -0,0 +1,61 @@ +package forge + +import ( + "context" + + "github.com/go-git/go-git/v5/plumbing/transport" + + "github.com/apricote/releaser-pleaser/internal/git" + "github.com/apricote/releaser-pleaser/internal/releasepr" +) + +type Forge interface { + RepoURL() string + CloneURL() string + ReleaseURL(version string) string + + GitAuth() transport.AuthMethod + + // LatestTags returns the last stable tag created on the main branch. If there is a more recent pre-release tag, + // that is also returned. If no tag is found, it returns nil. + LatestTags(context.Context) (git.Releases, error) + + // CommitsSince returns all commits to main branch after the Tag. The tag can be `nil`, in which case this + // function should return all commits. + CommitsSince(context.Context, *git.Tag) ([]git.Commit, error) + + // EnsureLabelsExist verifies that all desired labels are available on the repository. If labels are missing, they + // are created them. + EnsureLabelsExist(context.Context, []releasepr.Label) error + + // PullRequestForBranch returns the open pull request between the branch and Options.BaseBranch. If no open PR + // exists, it returns nil. + PullRequestForBranch(context.Context, string) (*releasepr.ReleasePullRequest, error) + + // CreatePullRequest opens a new pull/merge request for the ReleasePullRequest. + CreatePullRequest(context.Context, *releasepr.ReleasePullRequest) error + + // UpdatePullRequest updates the pull/merge request identified through the ID of + // the ReleasePullRequest to the current description and title. + UpdatePullRequest(context.Context, *releasepr.ReleasePullRequest) error + + // SetPullRequestLabels updates the pull/merge request identified through the ID of + // the ReleasePullRequest to the current labels. + SetPullRequestLabels(ctx context.Context, pr *releasepr.ReleasePullRequest, remove, add []releasepr.Label) error + + // ClosePullRequest closes the pull/merge request identified through the ID of + // the ReleasePullRequest, as it is no longer required. + ClosePullRequest(context.Context, *releasepr.ReleasePullRequest) error + + // PendingReleases returns a list of ReleasePullRequest. The list should contain all pull/merge requests that are + // merged and have the matching label. + PendingReleases(context.Context, releasepr.Label) ([]*releasepr.ReleasePullRequest, error) + + // CreateRelease creates a release on the Forge, pointing at the commit with the passed in details. + CreateRelease(ctx context.Context, commit git.Commit, title, changelog string, prerelease, latest bool) error +} + +type Options struct { + Repository string + BaseBranch string +} diff --git a/forge.go b/internal/forge/github/github.go similarity index 66% rename from forge.go rename to internal/forge/github/github.go index 1086564..46f203a 100644 --- a/forge.go +++ b/internal/forge/github/github.go @@ -1,4 +1,4 @@ -package rp +package github import ( "context" @@ -13,75 +13,27 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/google/go-github/v63/github" + + "github.com/apricote/releaser-pleaser/internal/forge" + "github.com/apricote/releaser-pleaser/internal/git" + "github.com/apricote/releaser-pleaser/internal/pointer" + "github.com/apricote/releaser-pleaser/internal/releasepr" ) const ( - GitHubPerPageMax = 100 - GitHubPRStateOpen = "open" - GitHubPRStateClosed = "closed" - GitHubEnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential - GitHubEnvUsername = "GITHUB_USER" - GitHubEnvRepository = "GITHUB_REPOSITORY" - GitHubLabelColor = "dedede" + PerPageMax = 100 + PRStateOpen = "open" + PRStateClosed = "closed" + EnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential + EnvUsername = "GITHUB_USER" + EnvRepository = "GITHUB_REPOSITORY" + LabelColor = "dedede" ) -type Forge interface { - RepoURL() string - CloneURL() string - ReleaseURL(version string) string - - GitAuth() transport.AuthMethod - - // LatestTags returns the last stable tag created on the main branch. If there is a more recent pre-release tag, - // that is also returned. If no tag is found, it returns nil. - LatestTags(context.Context) (Releases, error) - - // CommitsSince returns all commits to main branch after the Tag. The tag can be `nil`, in which case this - // function should return all commits. - CommitsSince(context.Context, *Tag) ([]Commit, error) - - // EnsureLabelsExist verifies that all desired labels are available on the repository. If labels are missing, they - // are created them. - EnsureLabelsExist(context.Context, []Label) error - - // PullRequestForBranch returns the open pull request between the branch and ForgeOptions.BaseBranch. If no open PR - // exists, it returns nil. - PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error) - - // CreatePullRequest opens a new pull/merge request for the ReleasePullRequest. - CreatePullRequest(context.Context, *ReleasePullRequest) error - - // UpdatePullRequest updates the pull/merge request identified through the ID of - // the ReleasePullRequest to the current description and title. - UpdatePullRequest(context.Context, *ReleasePullRequest) error - - // SetPullRequestLabels updates the pull/merge request identified through the ID of - // the ReleasePullRequest to the current labels. - SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []Label) error - - // ClosePullRequest closes the pull/merge request identified through the ID of - // the ReleasePullRequest, as it is no longer required. - ClosePullRequest(context.Context, *ReleasePullRequest) error - - // PendingReleases returns a list of ReleasePullRequest. The list should contain all pull/merge requests that are - // merged and have the matching label. - PendingReleases(context.Context, Label) ([]*ReleasePullRequest, error) - - // CreateRelease creates a release on the Forge, pointing at the commit with the passed in details. - CreateRelease(ctx context.Context, commit Commit, title, changelog string, prerelease, latest bool) error -} - -type ForgeOptions struct { - Repository string - BaseBranch string -} - -var _ Forge = &GitHub{} - -// var _ Forge = &GitLab{} +var _ forge.Forge = &GitHub{} type GitHub struct { - options *GitHubOptions + options *Options client *github.Client log *slog.Logger @@ -106,24 +58,24 @@ func (g *GitHub) GitAuth() transport.AuthMethod { } } -func (g *GitHub) LatestTags(ctx context.Context) (Releases, error) { +func (g *GitHub) LatestTags(ctx context.Context) (git.Releases, error) { g.log.DebugContext(ctx, "listing all tags in github repository") page := 1 - var releases Releases + var releases git.Releases for { tags, resp, err := g.client.Repositories.ListTags( ctx, g.options.Owner, g.options.Repo, - &github.ListOptions{Page: page, PerPage: GitHubPerPageMax}, + &github.ListOptions{Page: page, PerPage: PerPageMax}, ) if err != nil { - return Releases{}, err + return git.Releases{}, err } for _, ghTag := range tags { - tag := &Tag{ + tag := &git.Tag{ Hash: ghTag.GetCommit().GetSHA(), Name: ghTag.GetName(), } @@ -160,7 +112,7 @@ func (g *GitHub) LatestTags(ctx context.Context) (Releases, error) { return releases, nil } -func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) { +func (g *GitHub) CommitsSince(ctx context.Context, tag *git.Tag) ([]git.Commit, error) { var repositoryCommits []*github.RepositoryCommit var err error if tag != nil { @@ -173,9 +125,9 @@ func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) { return nil, err } - var commits = make([]Commit, 0, len(repositoryCommits)) + var commits = make([]git.Commit, 0, len(repositoryCommits)) for _, ghCommit := range repositoryCommits { - commit := Commit{ + commit := git.Commit{ Hash: ghCommit.GetSHA(), Message: ghCommit.GetCommit().GetMessage(), } @@ -190,7 +142,7 @@ func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) { return commits, nil } -func (g *GitHub) commitsSinceTag(ctx context.Context, tag *Tag) ([]*github.RepositoryCommit, error) { +func (g *GitHub) commitsSinceTag(ctx context.Context, tag *git.Tag) ([]*github.RepositoryCommit, error) { head := g.options.BaseBranch log := g.log.With("base", tag.Hash, "head", head) log.Debug("comparing commits", "base", tag.Hash, "head", head) @@ -204,7 +156,7 @@ func (g *GitHub) commitsSinceTag(ctx context.Context, tag *Tag) ([]*github.Repos ctx, g.options.Owner, g.options.Repo, tag.Hash, head, &github.ListOptions{ Page: page, - PerPage: GitHubPerPageMax, + PerPage: PerPageMax, }) if err != nil { return nil, err @@ -244,7 +196,7 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm SHA: head, ListOptions: github.ListOptions{ Page: page, - PerPage: GitHubPerPageMax, + PerPage: PerPageMax, }, }) if err != nil { @@ -254,7 +206,7 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm if repositoryCommits == nil && resp.LastPage > 0 { // Pre-initialize slice on first request log.Debug("found commits", "pages", resp.LastPage) - repositoryCommits = make([]*github.RepositoryCommit, 0, resp.LastPage*GitHubPerPageMax) + repositoryCommits = make([]*github.RepositoryCommit, 0, resp.LastPage*PerPageMax) } repositoryCommits = append(repositoryCommits, commits...) @@ -269,7 +221,7 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm return repositoryCommits, nil } -func (g *GitHub) prForCommit(ctx context.Context, commit Commit) (*PullRequest, error) { +func (g *GitHub) prForCommit(ctx context.Context, commit git.Commit) (*git.PullRequest, error) { // We naively look up the associated PR for each commit through the "List pull requests associated with a commit" // endpoint. This requires len(commits) requests. // Using the "List pull requests" endpoint might be faster, as it allows us to fetch 100 arbitrary PRs per request, @@ -285,7 +237,7 @@ func (g *GitHub) prForCommit(ctx context.Context, commit Commit) (*PullRequest, ctx, g.options.Owner, g.options.Repo, commit.Hash, &github.ListOptions{ Page: page, - PerPage: GitHubPerPageMax, + PerPage: PerPageMax, }) if err != nil { return nil, err @@ -314,7 +266,7 @@ func (g *GitHub) prForCommit(ctx context.Context, commit Commit) (*PullRequest, return gitHubPRToPullRequest(pullrequest), nil } -func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error { +func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []releasepr.Label) error { existingLabels := make([]string, 0, len(labels)) page := 1 @@ -325,7 +277,7 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error { ctx, g.options.Owner, g.options.Repo, &github.ListOptions{ Page: page, - PerPage: GitHubPerPageMax, + PerPage: PerPageMax, }) if err != nil { return err @@ -347,8 +299,8 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error { _, _, err := g.client.Issues.CreateLabel( ctx, g.options.Owner, g.options.Repo, &github.Label{ - Name: Pointer(string(label)), - Color: Pointer(GitHubLabelColor), + Name: pointer.Pointer(string(label)), + Color: pointer.Pointer(LabelColor), }, ) if err != nil { @@ -360,13 +312,13 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error { return nil } -func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*ReleasePullRequest, error) { +func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*releasepr.ReleasePullRequest, error) { page := 1 for { prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit(ctx, g.options.Owner, g.options.Repo, branch, &github.ListOptions{ Page: page, - PerPage: GitHubPerPageMax, + PerPage: PerPageMax, }) if err != nil { var ghErr *github.ErrorResponse @@ -379,7 +331,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele } for _, pr := range prs { - if pr.GetBase().GetRef() == g.options.BaseBranch && pr.GetHead().GetRef() == branch && pr.GetState() == GitHubPRStateOpen { + if pr.GetBase().GetRef() == g.options.BaseBranch && pr.GetHead().GetRef() == branch && pr.GetState() == PRStateOpen { return gitHubPRToReleasePullRequest(pr), nil } } @@ -393,7 +345,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele return nil, nil } -func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest) error { +func (g *GitHub) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error { ghPR, _, err := g.client.PullRequests.Create( ctx, g.options.Owner, g.options.Repo, &github.NewPullRequest{ @@ -410,7 +362,7 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest) // TODO: String ID? pr.ID = ghPR.GetNumber() - err = g.SetPullRequestLabels(ctx, pr, []Label{}, pr.Labels) + err = g.SetPullRequestLabels(ctx, pr, []releasepr.Label{}, pr.Labels) if err != nil { return err } @@ -418,7 +370,7 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest) return nil } -func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest) error { +func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error { _, _, err := g.client.PullRequests.Edit( ctx, g.options.Owner, g.options.Repo, pr.ID, &github.PullRequest{ @@ -433,7 +385,7 @@ func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest) return nil } -func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []Label) error { +func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *releasepr.ReleasePullRequest, remove, add []releasepr.Label) error { for _, label := range remove { _, err := g.client.Issues.RemoveLabelForIssue( ctx, g.options.Owner, g.options.Repo, @@ -460,11 +412,11 @@ func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullReques return nil } -func (g *GitHub) ClosePullRequest(ctx context.Context, pr *ReleasePullRequest) error { +func (g *GitHub) ClosePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error { _, _, err := g.client.PullRequests.Edit( ctx, g.options.Owner, g.options.Repo, pr.ID, &github.PullRequest{ - State: Pointer(GitHubPRStateClosed), + State: pointer.Pointer(PRStateClosed), }, ) if err != nil { @@ -474,20 +426,20 @@ func (g *GitHub) ClosePullRequest(ctx context.Context, pr *ReleasePullRequest) e return nil } -func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*ReleasePullRequest, error) { +func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel releasepr.Label) ([]*releasepr.ReleasePullRequest, error) { page := 1 - var prs []*ReleasePullRequest + var prs []*releasepr.ReleasePullRequest for { ghPRs, resp, err := g.client.PullRequests.List( ctx, g.options.Owner, g.options.Repo, &github.PullRequestListOptions{ - State: GitHubPRStateClosed, + State: PRStateClosed, Base: g.options.BaseBranch, ListOptions: github.ListOptions{ Page: page, - PerPage: GitHubPerPageMax, + PerPage: PerPageMax, }, }) if err != nil { @@ -497,7 +449,7 @@ func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*Re if prs == nil && resp.LastPage > 0 { // Pre-initialize slice on first request g.log.Debug("found pending releases", "pages", resp.LastPage) - prs = make([]*ReleasePullRequest, 0, (resp.LastPage-1)*GitHubPerPageMax) + prs = make([]*releasepr.ReleasePullRequest, 0, (resp.LastPage-1)*PerPageMax) } for _, pr := range ghPRs { @@ -526,7 +478,7 @@ func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*Re return prs, nil } -func (g *GitHub) CreateRelease(ctx context.Context, commit Commit, title, changelog string, preRelease, latest bool) error { +func (g *GitHub) CreateRelease(ctx context.Context, commit git.Commit, title, changelog string, preRelease, latest bool) error { makeLatest := "" if latest { makeLatest = "true" @@ -551,29 +503,29 @@ func (g *GitHub) CreateRelease(ctx context.Context, commit Commit, title, change return nil } -func gitHubPRToPullRequest(pr *github.PullRequest) *PullRequest { - return &PullRequest{ +func gitHubPRToPullRequest(pr *github.PullRequest) *git.PullRequest { + return &git.PullRequest{ ID: pr.GetNumber(), Title: pr.GetTitle(), Description: pr.GetBody(), } } -func gitHubPRToReleasePullRequest(pr *github.PullRequest) *ReleasePullRequest { - labels := make([]Label, 0, len(pr.Labels)) +func gitHubPRToReleasePullRequest(pr *github.PullRequest) *releasepr.ReleasePullRequest { + labels := make([]releasepr.Label, 0, len(pr.Labels)) for _, label := range pr.Labels { - labelName := Label(label.GetName()) - if slices.Contains(KnownLabels, Label(label.GetName())) { + labelName := releasepr.Label(label.GetName()) + if slices.Contains(releasepr.KnownLabels, releasepr.Label(label.GetName())) { labels = append(labels, labelName) } } - var releaseCommit *Commit + var releaseCommit *git.Commit if pr.MergeCommitSHA != nil { - releaseCommit = &Commit{Hash: pr.GetMergeCommitSHA()} + releaseCommit = &git.Commit{Hash: pr.GetMergeCommitSHA()} } - return &ReleasePullRequest{ + return &releasepr.ReleasePullRequest{ ID: pr.GetNumber(), Title: pr.GetTitle(), Description: pr.GetBody(), @@ -584,16 +536,16 @@ func gitHubPRToReleasePullRequest(pr *github.PullRequest) *ReleasePullRequest { } } -func (g *GitHubOptions) autodiscover() { - if apiToken := os.Getenv(GitHubEnvAPIToken); apiToken != "" { +func (g *Options) autodiscover() { + if apiToken := os.Getenv(EnvAPIToken); apiToken != "" { g.APIToken = apiToken } // TODO: Check if there is a better solution for cloning/pushing locally - if username := os.Getenv(GitHubEnvUsername); username != "" { + if username := os.Getenv(EnvUsername); username != "" { g.Username = username } - if envRepository := os.Getenv(GitHubEnvRepository); envRepository != "" { + if envRepository := os.Getenv(EnvRepository); envRepository != "" { // GITHUB_REPOSITORY=apricote/releaser-pleaser parts := strings.Split(envRepository, "/") if len(parts) == 2 { @@ -604,8 +556,8 @@ func (g *GitHubOptions) autodiscover() { } } -type GitHubOptions struct { - ForgeOptions +type Options struct { + forge.Options Owner string Repo string @@ -614,7 +566,7 @@ type GitHubOptions struct { Username string } -func NewGitHub(log *slog.Logger, options *GitHubOptions) *GitHub { +func New(log *slog.Logger, options *Options) *GitHub { options.autodiscover() client := github.NewClient(nil) @@ -631,29 +583,3 @@ func NewGitHub(log *slog.Logger, options *GitHubOptions) *GitHub { return gh } - -type GitLab struct { - options ForgeOptions -} - -func (g *GitLab) autodiscover() { - // Read settings from GitLab-CI env vars -} - -func NewGitLab(options ForgeOptions) *GitLab { - gl := &GitLab{ - options: options, - } - - gl.autodiscover() - - return gl -} - -func (g *GitLab) RepoURL() string { - return fmt.Sprintf("https://gitlab.com/%s", g.options.Repository) -} - -func Pointer[T any](value T) *T { - return &value -} diff --git a/internal/forge/gitlab/gitlab.go b/internal/forge/gitlab/gitlab.go new file mode 100644 index 0000000..98a4252 --- /dev/null +++ b/internal/forge/gitlab/gitlab.go @@ -0,0 +1,31 @@ +package gitlab + +import ( + "fmt" + + "github.com/apricote/releaser-pleaser/internal/forge" +) + +// var _ forge.Forge = &GitLab{} + +type GitLab struct { + options forge.Options +} + +func (g *GitLab) autodiscover() { + // Read settings from GitLab-CI env vars +} + +func New(options forge.Options) *GitLab { + gl := &GitLab{ + options: options, + } + + gl.autodiscover() + + return gl +} + +func (g *GitLab) RepoURL() string { + return fmt.Sprintf("https://gitlab.com/%s", g.options.Repository) +} diff --git a/internal/git/git.go b/internal/git/git.go new file mode 100644 index 0000000..09fd5c9 --- /dev/null +++ b/internal/git/git.go @@ -0,0 +1,227 @@ +package git + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "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 ( + remoteName = "origin" +) + +type Commit struct { + Hash string + Message string + + PullRequest *PullRequest +} + +type PullRequest struct { + ID int + Title string + Description string +} + +type Tag struct { + Hash string + Name string +} + +type Releases struct { + Latest *Tag + Stable *Tag +} + +func CloneRepo(ctx context.Context, logger *slog.Logger, cloneURL, branch string, auth transport.AuthMethod) (*Repository, error) { + dir, err := os.MkdirTemp("", "releaser-pleaser.*") + if err != nil { + return nil, fmt.Errorf("failed to create temporary directory for repo clone: %w", err) + } + + repo, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ + URL: cloneURL, + RemoteName: remoteName, + ReferenceName: plumbing.NewBranchReferenceName(branch), + SingleBranch: false, + Auth: auth, + }) + if err != nil { + return nil, fmt.Errorf("failed to clone repository: %w", err) + } + + return &Repository{r: repo, logger: logger, auth: auth}, nil +} + +type Repository struct { + r *git.Repository + logger *slog.Logger + auth transport.AuthMethod +} + +func (r *Repository) DeleteBranch(ctx context.Context, branch string) error { + if b, _ := r.r.Branch(branch); b != nil { + r.logger.DebugContext(ctx, "deleting local branch", "branch.name", branch) + if err := r.r.DeleteBranch(branch); err != nil { + return err + } + } + + return nil +} + +func (r *Repository) Checkout(_ context.Context, branch string) error { + worktree, err := r.r.Worktree() + if err != nil { + return err + } + + if err = worktree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branch), + Create: true, + }); err != nil { + return fmt.Errorf("failed to check out branch: %w", err) + } + + return nil +} + +func (r *Repository) UpdateFile(_ context.Context, path string, updaters []updater.Updater) error { + worktree, err := r.r.Worktree() + if err != nil { + return err + } + + 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 _, update := range updaters { + updatedContent, err = update(updatedContent) + if err != nil { + return fmt.Errorf("failed to run updater on file %s", 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 +} + +func (r *Repository) Commit(_ context.Context, message string) (Commit, error) { + worktree, err := r.r.Worktree() + if err != nil { + return Commit{}, err + } + + releaseCommitHash, err := worktree.Commit(message, &git.CommitOptions{ + Author: signature(), + Committer: signature(), + }) + if err != nil { + return Commit{}, fmt.Errorf("failed to commit changes: %w", err) + } + + return Commit{ + Hash: releaseCommitHash.String(), + Message: message, + }, nil +} + +func (r *Repository) HasChangesWithRemote(ctx context.Context, branch string) (bool, error) { + remoteRef, err := r.r.Reference(plumbing.NewRemoteReferenceName(remoteName, branch), false) + if err != nil { + if err.Error() == "reference not found" { + // No remote branch means that there are changes + return true, nil + } + + return false, err + } + + remoteCommit, err := r.r.CommitObject(remoteRef.Hash()) + if err != nil { + return false, err + } + + localRef, err := r.r.Reference(plumbing.NewBranchReferenceName(branch), false) + if err != nil { + return false, err + } + + localCommit, err := r.r.CommitObject(localRef.Hash()) + if err != nil { + return false, err + } + + diff, err := localCommit.PatchContext(ctx, remoteCommit) + if err != nil { + return false, err + } + + hasChanges := len(diff.FilePatches()) > 0 + + return hasChanges, nil +} + +func (r *Repository) ForcePush(ctx context.Context, branch string) error { + pushRefSpec := config.RefSpec(fmt.Sprintf( + "+%s:%s", + plumbing.NewBranchReferenceName(branch), + // This needs to be the local branch name, not the remotes/origin ref + // See https://stackoverflow.com/a/75727620 + plumbing.NewBranchReferenceName(branch), + )) + + r.logger.DebugContext(ctx, "pushing branch", "branch.name", branch, "refspec", pushRefSpec.String()) + return r.r.PushContext(ctx, &git.PushOptions{ + RemoteName: remoteName, + RefSpecs: []config.RefSpec{pushRefSpec}, + Force: true, + Auth: r.auth, + }) +} + +func signature() *object.Signature { + return &object.Signature{ + Name: "releaser-pleaser", + Email: "", + When: time.Now(), + } +} diff --git a/internal/pointer/pointer.go b/internal/pointer/pointer.go new file mode 100644 index 0000000..1c62f74 --- /dev/null +++ b/internal/pointer/pointer.go @@ -0,0 +1,5 @@ +package pointer + +func Pointer[T any](value T) *T { + return &value +} diff --git a/internal/releasepr/label.go b/internal/releasepr/label.go new file mode 100644 index 0000000..518eb87 --- /dev/null +++ b/internal/releasepr/label.go @@ -0,0 +1 @@ +package releasepr diff --git a/releasepr.go b/internal/releasepr/releasepr.go similarity index 81% rename from releasepr.go rename to internal/releasepr/releasepr.go index e177010..5bf3649 100644 --- a/releasepr.go +++ b/internal/releasepr/releasepr.go @@ -1,4 +1,4 @@ -package rp +package releasepr import ( "bytes" @@ -12,8 +12,10 @@ import ( "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/text" + "github.com/apricote/releaser-pleaser/internal/git" "github.com/apricote/releaser-pleaser/internal/markdown" - east "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast" + ast2 "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast" + "github.com/apricote/releaser-pleaser/internal/versioning" ) var ( @@ -33,7 +35,7 @@ func init() { // ReleasePullRequest // -// TODO: Reuse [PullRequest] +// TODO: Reuse [git.PullRequest] type ReleasePullRequest struct { ID int Title string @@ -41,9 +43,12 @@ type ReleasePullRequest struct { Labels []Label Head string - ReleaseCommit *Commit + ReleaseCommit *git.Commit } +// Label is the string identifier of a pull/merge request label on the forge. +type Label string + func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) { rp := &ReleasePullRequest{ Head: head, @@ -61,50 +66,9 @@ func NewReleasePullRequest(head, branch, version, changelogEntry string) (*Relea type ReleaseOverrides struct { Prefix string Suffix string - NextVersionType NextVersionType + NextVersionType versioning.NextVersionType } -type NextVersionType int - -const ( - NextVersionTypeUndefined NextVersionType = iota - NextVersionTypeNormal - NextVersionTypeRC - NextVersionTypeBeta - NextVersionTypeAlpha -) - -func (n NextVersionType) String() string { - switch n { - case NextVersionTypeUndefined: - return "undefined" - case NextVersionTypeNormal: - return "normal" - case NextVersionTypeRC: - return "rc" - case NextVersionTypeBeta: - return "beta" - case NextVersionTypeAlpha: - return "alpha" - default: - return "" - } -} - -func (n NextVersionType) IsPrerelease() bool { - switch n { - case NextVersionTypeRC, NextVersionTypeBeta, NextVersionTypeAlpha: - return true - case NextVersionTypeUndefined, NextVersionTypeNormal: - return false - default: - return false - } -} - -// Label is the string identifier of a pull/merge request label on the forge. -type Label string - const ( LabelNextVersionTypeNormal Label = "rp-next-version::normal" LabelNextVersionTypeRC Label = "rp-next-version::rc" @@ -158,13 +122,13 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R switch label { // Versioning case LabelNextVersionTypeNormal: - overrides.NextVersionType = NextVersionTypeNormal + overrides.NextVersionType = versioning.NextVersionTypeNormal case LabelNextVersionTypeRC: - overrides.NextVersionType = NextVersionTypeRC + overrides.NextVersionType = versioning.NextVersionTypeRC case LabelNextVersionTypeBeta: - overrides.NextVersionType = NextVersionTypeBeta + overrides.NextVersionType = versioning.NextVersionTypeBeta case LabelNextVersionTypeAlpha: - overrides.NextVersionType = NextVersionTypeAlpha + overrides.NextVersionType = versioning.NextVersionTypeAlpha case LabelReleasePending, LabelReleaseTagged: // These labels have no effect on the versioning. break @@ -213,18 +177,18 @@ func (pr *ReleasePullRequest) ChangelogText() (string, error) { gm := markdown.New() descriptionAST := gm.Parser().Parse(text.NewReader(source)) - var section *east.Section + var section *ast2.Section err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } - if n.Type() != ast.TypeBlock || n.Kind() != east.KindSection { + if n.Type() != ast.TypeBlock || n.Kind() != ast2.KindSection { return ast.WalkContinue, nil } - anySection, ok := n.(*east.Section) + anySection, ok := n.(*ast2.Section) if !ok { return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n) } diff --git a/releasepr.md.tpl b/internal/releasepr/releasepr.md.tpl similarity index 100% rename from releasepr.md.tpl rename to internal/releasepr/releasepr.md.tpl diff --git a/releasepr_test.go b/internal/releasepr/releasepr_test.go similarity index 99% rename from releasepr_test.go rename to internal/releasepr/releasepr_test.go index b1ffbb2..92f338d 100644 --- a/releasepr_test.go +++ b/internal/releasepr/releasepr_test.go @@ -1,4 +1,4 @@ -package rp +package releasepr import ( "testing" diff --git a/internal/testutils/git.go b/internal/testutils/git.go deleted file mode 100644 index f5721b6..0000000 --- a/internal/testutils/git.go +++ /dev/null @@ -1,126 +0,0 @@ -package testutils - -import ( - "testing" - "time" - - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/storage/memory" - "github.com/stretchr/testify/require" -) - -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 { - return func(t *testing.T, repo *git.Repository) error { - t.Helper() - - require.NotEmpty(t, message, "commit message is required") - - opts := &commitOptions{} - for _, opt := range options { - opt(opts) - } - - wt, err := repo.Worktree() - require.NoError(t, err) - - // Yeet all files - if opts.cleanFiles { - files, err := wt.Filesystem.ReadDir(".") - require.NoError(t, err, "failed to get current files") - - for _, fileInfo := range files { - err = wt.Filesystem.Remove(fileInfo.Name()) - require.NoError(t, err, "failed to remove file %q", fileInfo.Name()) - } - } - - // Create new files - for _, fileInfo := range opts.files { - file, err := wt.Filesystem.Create(fileInfo.path) - require.NoError(t, err, "failed to create file %q", fileInfo.path) - - _, err = file.Write([]byte(fileInfo.content)) - file.Close() - require.NoError(t, err, "failed to write content to file %q", fileInfo.path) - } - - // Commit - commitHash, err := wt.Commit(message, &git.CommitOptions{ - All: true, - AllowEmptyCommits: true, - Author: author, - Committer: author, - }) - require.NoError(t, err, "failed to commit") - - // Create tags - for _, tagName := range opts.tags { - _, err = repo.CreateTag(tagName, commitHash, nil) - require.NoError(t, err, "failed to create tag %q", tagName) - } - - return nil - } -} - -func WithFile(path, content string) CommitOption { - return func(opts *commitOptions) { - opts.files = append(opts.files, commitFile{path: path, content: content}) - } -} - -func WithCleanFiles() CommitOption { - return func(opts *commitOptions) { - opts.cleanFiles = true - } -} - -func WithTag(name string) CommitOption { - return func(opts *commitOptions) { - opts.tags = append(opts.tags, name) - } -} - -func WithTestRepo(commits ...Commit) Repo { - return func(t *testing.T) *git.Repository { - t.Helper() - - repo, err := git.Init(memory.NewStorage(), memfs.New()) - require.NoError(t, err, "failed to create in-memory repository") - - // Make initial commit - err = WithCommit("chore: init")(t, repo) - require.NoError(t, err, "failed to create init commit") - - for i, commit := range commits { - err = commit(t, repo) - require.NoError(t, err, "failed to create commit %d", i) - } - - return repo - } -} diff --git a/internal/updater/changelog.go b/internal/updater/changelog.go new file mode 100644 index 0000000..8bdb9f6 --- /dev/null +++ b/internal/updater/changelog.go @@ -0,0 +1,32 @@ +package updater + +import ( + "fmt" + "regexp" +) + +const ( + ChangelogHeader = "# Changelog" + ChangelogFile = "CHANGELOG.md" +) + +var ( + ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`) +) + +func Changelog(info ReleaseInfo) Updater { + 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") + } + if headerIndex != nil { + // Remove the header from the content + content = content[headerIndex[1]:] + } + + content = ChangelogHeader + "\n\n" + info.ChangelogEntry + content + + return content, nil + } +} diff --git a/internal/updater/changelog_test.go b/internal/updater/changelog_test.go new file mode 100644 index 0000000..917cd14 --- /dev/null +++ b/internal/updater/changelog_test.go @@ -0,0 +1,60 @@ +package updater + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +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: "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, Changelog, tt) + }) + } +} diff --git a/internal/updater/generic.go b/internal/updater/generic.go new file mode 100644 index 0000000..b8d73b0 --- /dev/null +++ b/internal/updater/generic.go @@ -0,0 +1,17 @@ +package updater + +import ( + "regexp" + "strings" +) + +var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`) + +func Generic(info ReleaseInfo) Updater { + 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") + + return GenericUpdaterSemVerRegex.ReplaceAllString(content, version+"${2}"), nil + } +} diff --git a/internal/updater/generic_test.go b/internal/updater/generic_test.go new file mode 100644 index 0000000..e0a8d1d --- /dev/null +++ b/internal/updater/generic_test.go @@ -0,0 +1,53 @@ +package updater + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenericUpdater_UpdateContent(t *testing.T) { + 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, Generic, tt) + }) + } +} diff --git a/internal/updater/updater.go b/internal/updater/updater.go new file mode 100644 index 0000000..fb773b4 --- /dev/null +++ b/internal/updater/updater.go @@ -0,0 +1,19 @@ +package updater + +type ReleaseInfo struct { + Version string + ChangelogEntry string +} + +type Updater func(string) (string, error) + +type NewUpdater func(ReleaseInfo) Updater + +func WithInfo(info ReleaseInfo, constructors ...NewUpdater) []Updater { + updaters := make([]Updater, 0, len(constructors)) + for _, constructor := range constructors { + updaters = append(updaters, constructor(info)) + } + + return updaters +} diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go new file mode 100644 index 0000000..0c0c40e --- /dev/null +++ b/internal/updater/updater_test.go @@ -0,0 +1,26 @@ +package updater + +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, 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)) { + return + } + assert.Equalf(t, tt.want, got, "Updater(%v, %v)", tt.content, tt.info) +} diff --git a/versioning.go b/internal/versioning/semver.go similarity index 68% rename from versioning.go rename to internal/versioning/semver.go index d18d480..49dc019 100644 --- a/versioning.go +++ b/internal/versioning/semver.go @@ -1,23 +1,18 @@ -package rp +package versioning import ( "fmt" "strings" "github.com/blang/semver/v4" - "github.com/leodido/go-conventionalcommits" + + "github.com/apricote/releaser-pleaser/internal/commitparser" + "github.com/apricote/releaser-pleaser/internal/git" ) -type Releases struct { - Latest *Tag - Stable *Tag -} +var _ Strategy = SemVerNextVersion -type VersioningStrategy = func(Releases, conventionalcommits.VersionBump, NextVersionType) (string, error) - -var _ VersioningStrategy = SemVerNextVersion - -func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) { +func SemVerNextVersion(r git.Releases, versionBump VersionBump, nextVersionType NextVersionType) (string, error) { latest, err := parseSemverWithDefault(r.Latest) if err != nil { return "", fmt.Errorf("failed to parse latest version: %w", err) @@ -36,13 +31,13 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, } switch versionBump { - case conventionalcommits.UnknownVersion: + case UnknownVersion: return "", fmt.Errorf("invalid latest bump (unknown)") - case conventionalcommits.PatchVersion: + case PatchVersion: err = next.IncrementPatch() - case conventionalcommits.MinorVersion: + case MinorVersion: err = next.IncrementMinor() - case conventionalcommits.MajorVersion: + case MajorVersion: err = next.IncrementMajor() } if err != nil { @@ -68,18 +63,18 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, return "v" + next.String(), nil } -func VersionBumpFromCommits(commits []AnalyzedCommit) conventionalcommits.VersionBump { - bump := conventionalcommits.UnknownVersion +func BumpFromCommits(commits []commitparser.AnalyzedCommit) VersionBump { + bump := UnknownVersion for _, commit := range commits { - entryBump := conventionalcommits.UnknownVersion + entryBump := UnknownVersion switch { case commit.BreakingChange: - entryBump = conventionalcommits.MajorVersion + entryBump = MajorVersion case commit.Type == "feat": - entryBump = conventionalcommits.MinorVersion + entryBump = MinorVersion case commit.Type == "fix": - entryBump = conventionalcommits.PatchVersion + entryBump = PatchVersion } if entryBump > bump { @@ -97,7 +92,7 @@ func setPRVersion(version *semver.Version, prType string, count uint64) { } } -func parseSemverWithDefault(tag *Tag) (semver.Version, error) { +func parseSemverWithDefault(tag *git.Tag) (semver.Version, error) { version := "v0.0.0" if tag != nil { version = tag.Name diff --git a/versioning_test.go b/internal/versioning/semver_test.go similarity index 55% rename from versioning_test.go rename to internal/versioning/semver_test.go index b6a0995..db22c88 100644 --- a/versioning_test.go +++ b/internal/versioning/semver_test.go @@ -1,17 +1,19 @@ -package rp +package versioning import ( "fmt" "testing" - "github.com/leodido/go-conventionalcommits" "github.com/stretchr/testify/assert" + + "github.com/apricote/releaser-pleaser/internal/commitparser" + "github.com/apricote/releaser-pleaser/internal/git" ) func TestReleases_NextVersion(t *testing.T) { type args struct { - releases Releases - versionBump conventionalcommits.VersionBump + releases git.Releases + versionBump VersionBump nextVersionType NextVersionType } tests := []struct { @@ -23,11 +25,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "simple bump (major)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MajorVersion, + versionBump: MajorVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v2.0.0", @@ -36,11 +38,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "simple bump (minor)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MinorVersion, + versionBump: MinorVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v1.2.0", @@ -49,11 +51,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "simple bump (patch)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v1.1.2", @@ -62,11 +64,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "normal to prerelease (major)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MajorVersion, + versionBump: MajorVersion, nextVersionType: NextVersionTypeRC, }, want: "v2.0.0-rc.0", @@ -75,11 +77,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "normal to prerelease (minor)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MinorVersion, + versionBump: MinorVersion, nextVersionType: NextVersionTypeRC, }, want: "v1.2.0-rc.0", @@ -88,11 +90,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "normal to prerelease (patch)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeRC, }, want: "v1.1.2-rc.0", @@ -101,11 +103,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease bump (major)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v2.0.0-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v2.0.0-rc.0"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MajorVersion, + versionBump: MajorVersion, nextVersionType: NextVersionTypeRC, }, want: "v2.0.0-rc.1", @@ -114,11 +116,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease bump (minor)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.2.0-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.2.0-rc.0"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MinorVersion, + versionBump: MinorVersion, nextVersionType: NextVersionTypeRC, }, want: "v1.2.0-rc.1", @@ -127,11 +129,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease bump (patch)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.2-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.2-rc.0"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeRC, }, want: "v1.1.2-rc.1", @@ -140,11 +142,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease different bump (major)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.2.0-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.2.0-rc.0"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MajorVersion, + versionBump: MajorVersion, nextVersionType: NextVersionTypeRC, }, want: "v2.0.0-rc.1", @@ -153,11 +155,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease different bump (minor)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.2-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.2-rc.0"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.MinorVersion, + versionBump: MinorVersion, nextVersionType: NextVersionTypeRC, }, want: "v1.2.0-rc.1", @@ -166,11 +168,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease to prerelease", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-alpha.2"}, - Stable: &Tag{Name: "v1.1.0"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1-alpha.2"}, + Stable: &git.Tag{Name: "v1.1.0"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeRC, }, want: "v1.1.1-rc.0", @@ -179,11 +181,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease to normal (explicit)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-alpha.2"}, - Stable: &Tag{Name: "v1.1.0"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1-alpha.2"}, + Stable: &git.Tag{Name: "v1.1.0"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeNormal, }, want: "v1.1.1", @@ -192,11 +194,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "prerelease to normal (implicit)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-alpha.2"}, - Stable: &Tag{Name: "v1.1.0"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1-alpha.2"}, + Stable: &git.Tag{Name: "v1.1.0"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v1.1.1", @@ -205,11 +207,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "nil tag (major)", args: args{ - releases: Releases{ + releases: git.Releases{ Latest: nil, Stable: nil, }, - versionBump: conventionalcommits.MajorVersion, + versionBump: MajorVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v1.0.0", @@ -218,11 +220,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "nil tag (minor)", args: args{ - releases: Releases{ + releases: git.Releases{ Latest: nil, Stable: nil, }, - versionBump: conventionalcommits.MinorVersion, + versionBump: MinorVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v0.1.0", @@ -231,11 +233,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "nil tag (patch)", args: args{ - releases: Releases{ + releases: git.Releases{ Latest: nil, Stable: nil, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v0.0.1", @@ -244,11 +246,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "nil stable release (major)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.0"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1-rc.0"}, Stable: nil, }, - versionBump: conventionalcommits.MajorVersion, + versionBump: MajorVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v2.0.0", @@ -257,11 +259,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "nil stable release (minor)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.0"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1-rc.0"}, Stable: nil, }, - versionBump: conventionalcommits.MinorVersion, + versionBump: MinorVersion, nextVersionType: NextVersionTypeUndefined, }, want: "v1.2.0", @@ -270,11 +272,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "nil stable release (patch)", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.0"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1-rc.0"}, Stable: nil, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeUndefined, }, // TODO: Is this actually correct our should it be v1.1.1? @@ -284,11 +286,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "error on invalid tag semver", args: args{ - releases: Releases{ - Latest: &Tag{Name: "foodazzle"}, - Stable: &Tag{Name: "foodazzle"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "foodazzle"}, + Stable: &git.Tag{Name: "foodazzle"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeRC, }, want: "", @@ -297,11 +299,11 @@ func TestReleases_NextVersion(t *testing.T) { { name: "error on invalid tag prerelease", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.foo"}, - Stable: &Tag{Name: "v1.1.1-rc.foo"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1-rc.foo"}, + Stable: &git.Tag{Name: "v1.1.1-rc.foo"}, }, - versionBump: conventionalcommits.PatchVersion, + versionBump: PatchVersion, nextVersionType: NextVersionTypeRC, }, want: "", @@ -310,12 +312,12 @@ func TestReleases_NextVersion(t *testing.T) { { name: "error on invalid bump", args: args{ - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, + releases: git.Releases{ + Latest: &git.Tag{Name: "v1.1.1"}, + Stable: &git.Tag{Name: "v1.1.1"}, }, - versionBump: conventionalcommits.UnknownVersion, + versionBump: UnknownVersion, nextVersionType: NextVersionTypeUndefined, }, want: "", @@ -336,53 +338,53 @@ func TestReleases_NextVersion(t *testing.T) { func TestVersionBumpFromCommits(t *testing.T) { tests := []struct { name string - analyzedCommits []AnalyzedCommit - want conventionalcommits.VersionBump + analyzedCommits []commitparser.AnalyzedCommit + want VersionBump }{ { name: "no entries (unknown)", - analyzedCommits: []AnalyzedCommit{}, - want: conventionalcommits.UnknownVersion, + analyzedCommits: []commitparser.AnalyzedCommit{}, + want: UnknownVersion, }, { name: "non-release type (unknown)", - analyzedCommits: []AnalyzedCommit{{Type: "docs"}}, - want: conventionalcommits.UnknownVersion, + analyzedCommits: []commitparser.AnalyzedCommit{{Type: "docs"}}, + want: UnknownVersion, }, { name: "single breaking (major)", - analyzedCommits: []AnalyzedCommit{{BreakingChange: true}}, - want: conventionalcommits.MajorVersion, + analyzedCommits: []commitparser.AnalyzedCommit{{BreakingChange: true}}, + want: MajorVersion, }, { name: "single feat (minor)", - analyzedCommits: []AnalyzedCommit{{Type: "feat"}}, - want: conventionalcommits.MinorVersion, + analyzedCommits: []commitparser.AnalyzedCommit{{Type: "feat"}}, + want: MinorVersion, }, { name: "single fix (patch)", - analyzedCommits: []AnalyzedCommit{{Type: "fix"}}, - want: conventionalcommits.PatchVersion, + analyzedCommits: []commitparser.AnalyzedCommit{{Type: "fix"}}, + want: PatchVersion, }, { name: "multiple entries (major)", - analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}}, - want: conventionalcommits.MajorVersion, + analyzedCommits: []commitparser.AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}}, + want: MajorVersion, }, { name: "multiple entries (minor)", - analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}}, - want: conventionalcommits.MinorVersion, + analyzedCommits: []commitparser.AnalyzedCommit{{Type: "fix"}, {Type: "feat"}}, + want: MinorVersion, }, { name: "multiple entries (patch)", - analyzedCommits: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}}, - want: conventionalcommits.PatchVersion, + analyzedCommits: []commitparser.AnalyzedCommit{{Type: "docs"}, {Type: "fix"}}, + want: PatchVersion, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, VersionBumpFromCommits(tt.analyzedCommits), "VersionBumpFromCommits(%v)", tt.analyzedCommits) + assert.Equalf(t, tt.want, BumpFromCommits(tt.analyzedCommits), "BumpFromCommits(%v)", tt.analyzedCommits) }) } } diff --git a/internal/versioning/versioning.go b/internal/versioning/versioning.go new file mode 100644 index 0000000..3bf8138 --- /dev/null +++ b/internal/versioning/versioning.go @@ -0,0 +1,56 @@ +package versioning + +import ( + "github.com/leodido/go-conventionalcommits" + + "github.com/apricote/releaser-pleaser/internal/git" +) + +type Strategy = func(git.Releases, VersionBump, NextVersionType) (string, error) + +type VersionBump conventionalcommits.VersionBump + +const ( + UnknownVersion VersionBump = iota + PatchVersion + MinorVersion + MajorVersion +) + +type NextVersionType int + +const ( + NextVersionTypeUndefined NextVersionType = iota + NextVersionTypeNormal + NextVersionTypeRC + NextVersionTypeBeta + NextVersionTypeAlpha +) + +func (n NextVersionType) String() string { + switch n { + case NextVersionTypeUndefined: + return "undefined" + case NextVersionTypeNormal: + return "normal" + case NextVersionTypeRC: + return "rc" + case NextVersionTypeBeta: + return "beta" + case NextVersionTypeAlpha: + return "alpha" + default: + return "" + } +} + +func (n NextVersionType) IsPrerelease() bool { + switch n { + case NextVersionTypeRC, NextVersionTypeBeta, NextVersionTypeAlpha: + return true + case NextVersionTypeUndefined, NextVersionTypeNormal: + return false + default: + return false + } +} diff --git a/releaserpleaser.go b/releaserpleaser.go index f0bf6cd..54064a4 100644 --- a/releaserpleaser.go +++ b/releaserpleaser.go @@ -3,13 +3,15 @@ package rp import ( "context" "fmt" - "io" "log/slog" - "os" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" + "github.com/apricote/releaser-pleaser/internal/changelog" + "github.com/apricote/releaser-pleaser/internal/commitparser" + "github.com/apricote/releaser-pleaser/internal/forge" + "github.com/apricote/releaser-pleaser/internal/git" + "github.com/apricote/releaser-pleaser/internal/releasepr" + "github.com/apricote/releaser-pleaser/internal/updater" + "github.com/apricote/releaser-pleaser/internal/versioning" ) const ( @@ -17,16 +19,16 @@ const ( ) type ReleaserPleaser struct { - forge Forge + forge forge.Forge logger *slog.Logger targetBranch string - commitParser CommitParser - nextVersion VersioningStrategy + commitParser commitparser.CommitParser + nextVersion versioning.Strategy extraFiles []string - updaters []Updater + updaters []updater.NewUpdater } -func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy, extraFiles []string, updaters []Updater) *ReleaserPleaser { +func New(forge forge.Forge, logger *slog.Logger, targetBranch string, commitParser commitparser.CommitParser, versioningStrategy versioning.Strategy, extraFiles []string, updaters []updater.NewUpdater) *ReleaserPleaser { return &ReleaserPleaser{ forge: forge, logger: logger, @@ -40,7 +42,8 @@ func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser Com func (rp *ReleaserPleaser) EnsureLabels(ctx context.Context) error { // TODO: Wrap Error - return rp.forge.EnsureLabelsExist(ctx, KnownLabels) + + return rp.forge.EnsureLabelsExist(ctx, releasepr.KnownLabels) } func (rp *ReleaserPleaser) Run(ctx context.Context) error { @@ -75,7 +78,7 @@ func (rp *ReleaserPleaser) runCreatePendingReleases(ctx context.Context) error { logger := rp.logger.With("method", "runCreatePendingReleases") logger.InfoContext(ctx, "checking for pending releases") - prs, err := rp.forge.PendingReleases(ctx, LabelReleasePending) + prs, err := rp.forge.PendingReleases(ctx, releasepr.LabelReleasePending) if err != nil { return err } @@ -97,7 +100,7 @@ func (rp *ReleaserPleaser) runCreatePendingReleases(ctx context.Context) error { return nil } -func (rp *ReleaserPleaser) createPendingRelease(ctx context.Context, pr *ReleasePullRequest) error { +func (rp *ReleaserPleaser) createPendingRelease(ctx context.Context, pr *releasepr.ReleasePullRequest) error { logger := rp.logger.With( "method", "createPendingRelease", "pr.id", pr.ID, @@ -129,7 +132,7 @@ func (rp *ReleaserPleaser) createPendingRelease(ctx context.Context, pr *Release logger.DebugContext(ctx, "created release", "release.title", version, "release.url", rp.forge.ReleaseURL(version)) logger.DebugContext(ctx, "updating pr labels") - err = rp.forge.SetPullRequestLabels(ctx, pr, []Label{LabelReleasePending}, []Label{LabelReleaseTagged}) + err = rp.forge.SetPullRequestLabels(ctx, pr, []releasepr.Label{releasepr.LabelReleasePending}, []releasepr.Label{releasepr.LabelReleaseTagged}) if err != nil { return err } @@ -144,14 +147,13 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error { logger := rp.logger.With("method", "runReconcileReleasePR") rpBranch := fmt.Sprintf(PullRequestBranchFormat, rp.targetBranch) - rpBranchRef := plumbing.NewBranchReferenceName(rpBranch) pr, err := rp.forge.PullRequestForBranch(ctx, rpBranch) if err != nil { return err } - var releaseOverrides ReleaseOverrides + var releaseOverrides releasepr.ReleaseOverrides if pr != nil { logger = logger.With("pr.id", pr.ID, "pr.title", pr.Title) @@ -215,7 +217,7 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error { return nil } - versionBump := VersionBumpFromCommits(analyzedCommits) + versionBump := versioning.BumpFromCommits(analyzedCommits) // TODO: Set version in release pr nextVersion, err := rp.nextVersion(releases, versionBump, releaseOverrides.NextVersionType) if err != nil { @@ -224,161 +226,68 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error { logger.InfoContext(ctx, "next version", "version", nextVersion) logger.DebugContext(ctx, "cloning repository", "clone.url", rp.forge.CloneURL()) - repo, err := CloneRepo(ctx, rp.forge.CloneURL(), rp.targetBranch, rp.forge.GitAuth()) + repo, err := git.CloneRepo(ctx, logger, rp.forge.CloneURL(), rp.targetBranch, rp.forge.GitAuth()) if err != nil { return fmt.Errorf("failed to clone repository: %w", err) } - worktree, err := repo.Worktree() - if err != nil { + + if err = repo.DeleteBranch(ctx, rpBranch); err != nil { return err } - if branch, _ := repo.Branch(rpBranch); branch != nil { - logger.DebugContext(ctx, "deleting previous releaser-pleaser branch locally", "branch.name", rpBranch) - if err = repo.DeleteBranch(rpBranch); err != nil { - return err - } + if err = repo.Checkout(ctx, rpBranch); err != nil { + return err } - if err = worktree.Checkout(&git.CheckoutOptions{ - Branch: rpBranchRef, - Create: true, - }); err != nil { - return fmt.Errorf("failed to check out branch: %w", err) - } - - changelogEntry, err := NewChangelogEntry(analyzedCommits, nextVersion, rp.forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix) + changelogEntry, err := changelog.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} + info := updater.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 = repo.UpdateFile(ctx, updater.ChangelogFile, updater.WithInfo(info, updater.Changelog)) 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) + // TODO: Check for missing files + err = repo.UpdateFile(ctx, path, updater.WithInfo(info, 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(), - Committer: GitSignature(), - }) + releaseCommit, err := repo.Commit(ctx, releaseCommitMessage) if err != nil { return fmt.Errorf("failed to commit changes: %w", err) } - logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommitHash.String(), "commit.message", releaseCommitMessage) - - newReleasePRChanges := true + logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommit.Hash, "commit.message", releaseCommit.Message) // Check if anything changed in comparison to the remote branch (if exists) - if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName(GitRemoteName, rpBranch), false); err != nil { - if err.Error() != "reference not found" { - // "reference not found" is expected and we should always push - return err - } - } else { - remoteCommit, err := repo.CommitObject(remoteRef.Hash()) - if err != nil { - return err - } - - localCommit, err := repo.CommitObject(releaseCommitHash) - if err != nil { - return err - } - - diff, err := localCommit.PatchContext(ctx, remoteCommit) - if err != nil { - return err - } - - newReleasePRChanges = len(diff.FilePatches()) > 0 + newReleasePRChanges, err := repo.HasChangesWithRemote(ctx, rpBranch) + if err != nil { + return err } if newReleasePRChanges { - pushRefSpec := config.RefSpec(fmt.Sprintf( - "+%s:%s", - rpBranchRef, - // This needs to be the local branch name, not the remotes/origin ref - // See https://stackoverflow.com/a/75727620 - rpBranchRef, - )) - logger.DebugContext(ctx, "pushing branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String()) - if err = repo.PushContext(ctx, &git.PushOptions{ - RemoteName: GitRemoteName, - RefSpecs: []config.RefSpec{pushRefSpec}, - Force: true, - Auth: rp.forge.GitAuth(), - }); err != nil { + err = repo.ForcePush(ctx, rpBranch) + if err != nil { return fmt.Errorf("failed to push branch: %w", err) } - logger.InfoContext(ctx, "pushed branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String()) + logger.InfoContext(ctx, "pushed branch", "commit.hash", releaseCommit.Hash, "branch.name", rpBranch) } else { logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push") } // Open/Update PR if pr == nil { - pr, err = NewReleasePullRequest(rpBranch, rp.targetBranch, nextVersion, changelogEntry) + pr, err = releasepr.NewReleasePullRequest(rpBranch, rp.targetBranch, nextVersion, changelogEntry) if err != nil { return err } diff --git a/updater.go b/updater.go deleted file mode 100644 index 3aaedae..0000000 --- a/updater.go +++ /dev/null @@ -1,47 +0,0 @@ -package rp - -import ( - "fmt" - "regexp" - "strings" -) - -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 -} 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) - }) - } -}