diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index 04e6aef..899752c 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -64,12 +64,12 @@ func run(cmd *cobra.Command, args []string) error { }) } - changesets, latestTag, stableTag, err := getChangesetsFromForge(ctx, f) + changesets, releases, err := getChangesetsFromForge(ctx, f) if err != nil { return fmt.Errorf("failed to get changesets: %w", err) } - err = reconcileReleasePR(ctx, f, changesets, latestTag, stableTag) + err = reconcileReleasePR(ctx, f, changesets, releases) if err != nil { return fmt.Errorf("failed to reconcile release pr: %w", err) } @@ -77,39 +77,39 @@ func run(cmd *cobra.Command, args []string) error { return nil } -func getChangesetsFromForge(ctx context.Context, forge rp.Forge) (changesets []rp.Changeset, latestTag *rp.Tag, stableTag *rp.Tag, err error) { - latestTag, stableTag, err = forge.LatestTags(ctx) +func getChangesetsFromForge(ctx context.Context, forge rp.Forge) ([]rp.Changeset, rp.Releases, error) { + releases, err := forge.LatestTags(ctx) if err != nil { - return nil, nil, nil, err + return nil, rp.Releases{}, err } - if latestTag != nil { - logger.InfoContext(ctx, "found latest tag", "tag.hash", latestTag.Hash, "tag.name", latestTag.Name) + if releases.Latest != nil { + logger.InfoContext(ctx, "found latest tag", "tag.hash", releases.Latest.Hash, "tag.name", releases.Latest.Name) + if releases.Stable != nil && releases.Latest.Hash != releases.Stable.Hash { + logger.InfoContext(ctx, "found stable tag", "tag.hash", releases.Stable.Hash, "tag.name", releases.Stable.Name) + } } else { logger.InfoContext(ctx, "no latest tag found") } - if latestTag.Hash != stableTag.Hash { - logger.InfoContext(ctx, "found stable tag", "tag.hash", stableTag.Hash, "tag.name", stableTag.Name) - } - releasableCommits, err := forge.CommitsSince(ctx, stableTag) + releasableCommits, err := forge.CommitsSince(ctx, releases.Stable) if err != nil { - return nil, nil, nil, err + return nil, rp.Releases{}, err } logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits)) - changesets, err = forge.Changesets(ctx, releasableCommits) + changesets, err := forge.Changesets(ctx, releasableCommits) if err != nil { - return nil, nil, nil, err + return nil, rp.Releases{}, err } logger.InfoContext(ctx, "Found changesets", "length", len(changesets)) - return changesets, latestTag, stableTag, nil + return changesets, releases, nil } -func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Changeset, latestTag *rp.Tag, stableTag *rp.Tag) error { +func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Changeset, releases rp.Releases) error { rpBranch := fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch) rpBranchRef := plumbing.NewBranchReferenceName(rpBranch) // Check Forge for open PR @@ -135,7 +135,7 @@ func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Cha } versionBump := rp.VersionBumpFromChangesets(changesets) - nextVersion, err := rp.NextVersion(latestTag, stableTag, versionBump, releaseOverrides.NextVersionType) + nextVersion, err := releases.NextVersion(versionBump, releaseOverrides.NextVersionType) if err != nil { return err } diff --git a/forge.go b/forge.go index 29942d8..5e07e85 100644 --- a/forge.go +++ b/forge.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "os" + "strings" "github.com/blang/semver/v4" "github.com/go-git/go-git/v5/plumbing/transport" @@ -35,7 +36,7 @@ type Forge interface { // 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) (stable *Tag, prerelease *Tag, err error) + 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. @@ -84,18 +85,20 @@ func (g *GitHub) GitAuth() transport.AuthMethod { } } -func (g *GitHub) LatestTags(ctx context.Context) (latest *Tag, stable *Tag, err error) { +func (g *GitHub) LatestTags(ctx context.Context) (Releases, error) { g.log.DebugContext(ctx, "listing all tags in github repository") page := 1 + var releases Releases + for { tags, resp, err := g.client.Repositories.ListTags( ctx, g.options.Owner, g.options.Repo, &github.ListOptions{Page: page, PerPage: GitHubPerPageMax}, ) if err != nil { - return nil, nil, err + return Releases{}, err } for _, ghTag := range tags { @@ -104,7 +107,7 @@ func (g *GitHub) LatestTags(ctx context.Context) (latest *Tag, stable *Tag, err Name: ghTag.GetName(), } - version, err := semver.Parse(tag.Name) + version, err := semver.Parse(strings.TrimPrefix(tag.Name, "v")) if err != nil { g.log.WarnContext( ctx, "unable to parse tag as semver, skipping", @@ -115,13 +118,14 @@ func (g *GitHub) LatestTags(ctx context.Context) (latest *Tag, stable *Tag, err continue } - if latest == nil { - latest = tag + if releases.Latest == nil { + releases.Latest = tag } if len(version.Pre) == 0 { // Stable version tag // We return once we have found the latest stable tag, not needed to look at every single tag. - return latest, tag, nil + releases.Stable = tag + break } } @@ -130,10 +134,9 @@ func (g *GitHub) LatestTags(ctx context.Context) (latest *Tag, stable *Tag, err } page = resp.NextPage - } - return nil, nil, nil + return releases, nil } func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) { diff --git a/versioning.go b/versioning.go index 39ebfdc..3582044 100644 --- a/versioning.go +++ b/versioning.go @@ -8,29 +8,20 @@ import ( "github.com/leodido/go-conventionalcommits" ) -func NextVersion(latestTag *Tag, stableTag *Tag, versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) { - // TODO: Validate for versioning after pre-releases - latestVersion := "v0.0.0" - if latestTag != nil { - latestVersion = latestTag.Name - } - stableVersion := "v0.0.0" - if stableTag != nil { - stableVersion = stableTag.Name - } +type Releases struct { + Latest *Tag + Stable *Tag +} - // The lib can not handle v prefixes - latestVersion = strings.TrimPrefix(latestVersion, "v") - stableVersion = strings.TrimPrefix(stableVersion, "v") - - latest, err := semver.Parse(latestVersion) +func (r Releases) NextVersion(versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) { + latest, err := parseSemverWithDefault(r.Latest) if err != nil { - return "", err + return "", fmt.Errorf("failed to parse latest version: %w", err) } - stable, err := semver.Parse(stableVersion) + stable, err := parseSemverWithDefault(r.Stable) if err != nil { - return "", err + return "", fmt.Errorf("failed to parse stable version: %w", err) } next := stable // Copy all fields @@ -99,3 +90,20 @@ func setPRVersion(version *semver.Version, prType string, count uint64) { {VersionNum: count, IsNum: true}, } } + +func parseSemverWithDefault(tag *Tag) (semver.Version, error) { + version := "v0.0.0" + if tag != nil { + version = tag.Name + } + + // The lib can not handle v prefixes + version = strings.TrimPrefix(version, "v") + + parsedVersion, err := semver.Parse(version) + if err != nil { + return semver.Version{}, fmt.Errorf("failed to parse version %q: %w", version, err) + } + + return parsedVersion, nil +} diff --git a/versioning_test.go b/versioning_test.go index a0b9e26..f25df88 100644 --- a/versioning_test.go +++ b/versioning_test.go @@ -8,24 +8,25 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNextVersion(t *testing.T) { +func TestReleases_NextVersion(t *testing.T) { type args struct { - latestTag *Tag - stableTag *Tag versionBump conventionalcommits.VersionBump nextVersionType NextVersionType } tests := []struct { - name string - args args - want string - wantErr assert.ErrorAssertionFunc + name string + releases Releases + args args + want string + wantErr assert.ErrorAssertionFunc }{ { name: "simple bump (major)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1"}, - stableTag: &Tag{Name: "v1.1.1"}, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -34,9 +35,12 @@ func TestNextVersion(t *testing.T) { }, { name: "simple bump (minor)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1"}, - stableTag: &Tag{Name: "v1.1.1"}, + versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -45,9 +49,12 @@ func TestNextVersion(t *testing.T) { }, { name: "simple bump (patch)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1"}, - stableTag: &Tag{Name: "v1.1.1"}, + versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -56,9 +63,12 @@ func TestNextVersion(t *testing.T) { }, { name: "normal to prerelease (major)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1"}, - stableTag: &Tag{Name: "v1.1.1"}, + versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeRC, }, @@ -67,9 +77,12 @@ func TestNextVersion(t *testing.T) { }, { name: "normal to prerelease (minor)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1"}, - stableTag: &Tag{Name: "v1.1.1"}, + versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeRC, }, @@ -78,9 +91,12 @@ func TestNextVersion(t *testing.T) { }, { name: "normal to prerelease (patch)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1"}, - stableTag: &Tag{Name: "v1.1.1"}, + versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -89,9 +105,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease bump (major)", + releases: Releases{ + Latest: &Tag{Name: "v2.0.0-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v2.0.0-rc.0"}, - stableTag: &Tag{Name: "v1.1.1"}, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeRC, }, @@ -100,9 +118,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease bump (minor)", + releases: Releases{ + Latest: &Tag{Name: "v1.2.0-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.2.0-rc.0"}, - stableTag: &Tag{Name: "v1.1.1"}, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeRC, }, @@ -111,9 +131,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease bump (patch)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.2-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.2-rc.0"}, - stableTag: &Tag{Name: "v1.1.1"}, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -122,9 +144,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease different bump (major)", + releases: Releases{ + Latest: &Tag{Name: "v1.2.0-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.2.0-rc.0"}, - stableTag: &Tag{Name: "v1.1.1"}, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeRC, }, @@ -133,9 +157,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease different bump (minor)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.2-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.2-rc.0"}, - stableTag: &Tag{Name: "v1.1.1"}, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeRC, }, @@ -144,9 +170,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease to prerelease", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-alpha.2"}, + Stable: &Tag{Name: "v1.1.0"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1-alpha.2"}, - stableTag: &Tag{Name: "v1.1.0"}, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -155,9 +183,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease to normal (explicit)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-alpha.2"}, + Stable: &Tag{Name: "v1.1.0"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1-alpha.2"}, - stableTag: &Tag{Name: "v1.1.0"}, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeNormal, }, @@ -166,9 +196,11 @@ func TestNextVersion(t *testing.T) { }, { name: "prerelease to normal (implicit)", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-alpha.2"}, + Stable: &Tag{Name: "v1.1.0"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1-alpha.2"}, - stableTag: &Tag{Name: "v1.1.0"}, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -177,9 +209,11 @@ func TestNextVersion(t *testing.T) { }, { name: "nil tag (major)", + releases: Releases{ + Latest: nil, + Stable: nil, + }, args: args{ - latestTag: nil, - stableTag: nil, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -188,9 +222,11 @@ func TestNextVersion(t *testing.T) { }, { name: "nil tag (minor)", + releases: Releases{ + Latest: nil, + Stable: nil, + }, args: args{ - latestTag: nil, - stableTag: nil, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -199,9 +235,11 @@ func TestNextVersion(t *testing.T) { }, { name: "nil tag (patch)", + releases: Releases{ + Latest: nil, + Stable: nil, + }, args: args{ - latestTag: nil, - stableTag: nil, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -210,9 +248,11 @@ func TestNextVersion(t *testing.T) { }, { name: "error on invalid tag semver", + releases: Releases{ + Latest: &Tag{Name: "foodazzle"}, + Stable: &Tag{Name: "foodazzle"}, + }, args: args{ - latestTag: &Tag{Name: "foodazzle"}, - stableTag: &Tag{Name: "foodazzle"}, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -221,9 +261,11 @@ func TestNextVersion(t *testing.T) { }, { name: "error on invalid tag prerelease", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-rc.foo"}, + Stable: &Tag{Name: "v1.1.1-rc.foo"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1-rc.foo"}, - stableTag: &Tag{Name: "v1.1.1-rc.foo"}, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -232,9 +274,12 @@ func TestNextVersion(t *testing.T) { }, { name: "error on invalid bump", + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, args: args{ - latestTag: &Tag{Name: "v1.1.1"}, - stableTag: &Tag{Name: "v1.1.1"}, + versionBump: conventionalcommits.UnknownVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -244,11 +289,11 @@ func TestNextVersion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NextVersion(tt.args.latestTag, tt.args.stableTag, tt.args.versionBump, tt.args.nextVersionType) - if !tt.wantErr(t, err, fmt.Sprintf("NextVersion(%v, %v, %v, %v)", tt.args.latestTag, tt.args.stableTag, tt.args.versionBump, tt.args.nextVersionType)) { + got, err := tt.releases.NextVersion(tt.args.versionBump, tt.args.nextVersionType) + if !tt.wantErr(t, err, fmt.Sprintf("Releases(%v, %v).NextVersion(%v, %v)", tt.releases.Latest, tt.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)) { return } - assert.Equalf(t, tt.want, got, "NextVersion(%v, %v, %v, %v)", tt.args.latestTag, tt.args.stableTag, tt.args.versionBump, tt.args.nextVersionType) + assert.Equalf(t, tt.want, got, "Releases(%v, %v).NextVersion(%v, %v)", tt.releases.Latest, tt.releases.Stable, tt.args.versionBump, tt.args.nextVersionType) }) } }