mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-01-13 13:21:00 +00:00
refactor: move things to packages (#39)
This commit is contained in:
parent
44184a77f9
commit
a0a064d387
32 changed files with 923 additions and 892 deletions
|
|
@ -6,6 +6,11 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
rp "github.com/apricote/releaser-pleaser"
|
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{
|
var runCmd = &cobra.Command{
|
||||||
|
|
@ -41,9 +46,9 @@ func run(cmd *cobra.Command, _ []string) error {
|
||||||
"repo", flagRepo,
|
"repo", flagRepo,
|
||||||
)
|
)
|
||||||
|
|
||||||
var forge rp.Forge
|
var f forge.Forge
|
||||||
|
|
||||||
forgeOptions := rp.ForgeOptions{
|
forgeOptions := forge.Options{
|
||||||
Repository: flagRepo,
|
Repository: flagRepo,
|
||||||
BaseBranch: flagBranch,
|
BaseBranch: flagBranch,
|
||||||
}
|
}
|
||||||
|
|
@ -53,23 +58,23 @@ func run(cmd *cobra.Command, _ []string) error {
|
||||||
// f = rp.NewGitLab(forgeOptions)
|
// f = rp.NewGitLab(forgeOptions)
|
||||||
case "github":
|
case "github":
|
||||||
logger.DebugContext(ctx, "using forge GitHub")
|
logger.DebugContext(ctx, "using forge GitHub")
|
||||||
forge = rp.NewGitHub(logger, &rp.GitHubOptions{
|
f = github.New(logger, &github.Options{
|
||||||
ForgeOptions: forgeOptions,
|
Options: forgeOptions,
|
||||||
Owner: flagOwner,
|
Owner: flagOwner,
|
||||||
Repo: flagRepo,
|
Repo: flagRepo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
extraFiles := parseExtraFiles(flagExtraFiles)
|
extraFiles := parseExtraFiles(flagExtraFiles)
|
||||||
|
|
||||||
releaserPleaser := rp.New(
|
releaserPleaser := rp.New(
|
||||||
forge,
|
f,
|
||||||
logger,
|
logger,
|
||||||
flagBranch,
|
flagBranch,
|
||||||
rp.NewConventionalCommitsParser(),
|
conventionalcommits.NewParser(),
|
||||||
rp.SemVerNextVersion,
|
versioning.SemVerNextVersion,
|
||||||
extraFiles,
|
extraFiles,
|
||||||
[]rp.Updater{&rp.GenericUpdater{}},
|
[]updater.NewUpdater{updater.Generic},
|
||||||
)
|
)
|
||||||
|
|
||||||
return releaserPleaser.Run(ctx)
|
return releaserPleaser.Run(ctx)
|
||||||
|
|
|
||||||
52
git.go
52
git.go
|
|
@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package rp
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -4,7 +4,6 @@ go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blang/semver/v4 v4.0.0
|
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/go-git/go-git/v5 v5.12.0
|
||||||
github.com/google/go-github/v63 v63.0.0
|
github.com/google/go-github/v63 v63.0.0
|
||||||
github.com/leodido/go-conventionalcommits v0.12.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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.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/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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
package rp
|
package changelog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
"github.com/apricote/releaser-pleaser/internal/commitparser"
|
||||||
ChangelogFile = "CHANGELOG.md"
|
|
||||||
ChangelogHeader = "# Changelog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -27,9 +24,9 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) {
|
func NewChangelogEntry(commits []commitparser.AnalyzedCommit, version, link, prefix, suffix string) (string, error) {
|
||||||
features := make([]AnalyzedCommit, 0)
|
features := make([]commitparser.AnalyzedCommit, 0)
|
||||||
fixes := make([]AnalyzedCommit, 0)
|
fixes := make([]commitparser.AnalyzedCommit, 0)
|
||||||
|
|
||||||
for _, commit := range commits {
|
for _, commit := range commits {
|
||||||
switch commit.Type {
|
switch commit.Type {
|
||||||
|
|
@ -54,5 +51,4 @@ func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix s
|
||||||
}
|
}
|
||||||
|
|
||||||
return changelog.String(), nil
|
return changelog.String(), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package rp
|
package changelog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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 {
|
func ptr[T any](input T) *T {
|
||||||
|
|
@ -12,7 +15,7 @@ func ptr[T any](input T) *T {
|
||||||
|
|
||||||
func Test_NewChangelogEntry(t *testing.T) {
|
func Test_NewChangelogEntry(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
analyzedCommits []AnalyzedCommit
|
analyzedCommits []commitparser.AnalyzedCommit
|
||||||
version string
|
version string
|
||||||
link string
|
link string
|
||||||
prefix string
|
prefix string
|
||||||
|
|
@ -27,7 +30,7 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
args: args{
|
args: args{
|
||||||
analyzedCommits: []AnalyzedCommit{},
|
analyzedCommits: []commitparser.AnalyzedCommit{},
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
link: "https://example.com/1.0.0",
|
link: "https://example.com/1.0.0",
|
||||||
},
|
},
|
||||||
|
|
@ -37,9 +40,9 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "single feature",
|
name: "single feature",
|
||||||
args: args{
|
args: args{
|
||||||
analyzedCommits: []AnalyzedCommit{
|
analyzedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "feat",
|
Type: "feat",
|
||||||
Description: "Foobar!",
|
Description: "Foobar!",
|
||||||
},
|
},
|
||||||
|
|
@ -53,9 +56,9 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "single fix",
|
name: "single fix",
|
||||||
args: args{
|
args: args{
|
||||||
analyzedCommits: []AnalyzedCommit{
|
analyzedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "Foobar!",
|
Description: "Foobar!",
|
||||||
},
|
},
|
||||||
|
|
@ -69,25 +72,25 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "multiple commits with scopes",
|
name: "multiple commits with scopes",
|
||||||
args: args{
|
args: args{
|
||||||
analyzedCommits: []AnalyzedCommit{
|
analyzedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "feat",
|
Type: "feat",
|
||||||
Description: "Blabla!",
|
Description: "Blabla!",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "feat",
|
Type: "feat",
|
||||||
Description: "So awesome!",
|
Description: "So awesome!",
|
||||||
Scope: ptr("awesome"),
|
Scope: ptr("awesome"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "Foobar!",
|
Description: "Foobar!",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "So sad!",
|
Description: "So sad!",
|
||||||
Scope: ptr("sad"),
|
Scope: ptr("sad"),
|
||||||
|
|
@ -112,9 +115,9 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prefix",
|
name: "prefix",
|
||||||
args: args{
|
args: args{
|
||||||
analyzedCommits: []AnalyzedCommit{
|
analyzedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "Foobar!",
|
Description: "Foobar!",
|
||||||
},
|
},
|
||||||
|
|
@ -135,9 +138,9 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "suffix",
|
name: "suffix",
|
||||||
args: args{
|
args: args{
|
||||||
analyzedCommits: []AnalyzedCommit{
|
analyzedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{},
|
Commit: git.Commit{},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "Foobar!",
|
Description: "Foobar!",
|
||||||
},
|
},
|
||||||
17
internal/commitparser/commitparser.go
Normal file
17
internal/commitparser/commitparser.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,54 +1,32 @@
|
||||||
package rp
|
package conventionalcommits
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/leodido/go-conventionalcommits"
|
"github.com/leodido/go-conventionalcommits"
|
||||||
"github.com/leodido/go-conventionalcommits/parser"
|
"github.com/leodido/go-conventionalcommits/parser"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/commitparser"
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Commit struct {
|
type Parser 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 {
|
|
||||||
machine conventionalcommits.Machine
|
machine conventionalcommits.Machine
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConventionalCommitsParser() *ConventionalCommitsParser {
|
func NewParser() *Parser {
|
||||||
parserMachine := parser.NewMachine(
|
parserMachine := parser.NewMachine(
|
||||||
parser.WithBestEffort(),
|
parser.WithBestEffort(),
|
||||||
parser.WithTypes(conventionalcommits.TypesConventional),
|
parser.WithTypes(conventionalcommits.TypesConventional),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &ConventionalCommitsParser{
|
return &Parser{
|
||||||
machine: parserMachine,
|
machine: parserMachine,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit, error) {
|
func (c *Parser) Analyze(commits []git.Commit) ([]commitparser.AnalyzedCommit, error) {
|
||||||
analyzedCommits := make([]AnalyzedCommit, 0, len(commits))
|
analyzedCommits := make([]commitparser.AnalyzedCommit, 0, len(commits))
|
||||||
|
|
||||||
for _, commit := range commits {
|
for _, commit := range commits {
|
||||||
msg, err := c.machine.Parse([]byte(commit.Message))
|
msg, err := c.machine.Parse([]byte(commit.Message))
|
||||||
|
|
@ -63,7 +41,7 @@ func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit,
|
||||||
commitVersionBump := conventionalCommit.VersionBump(conventionalcommits.DefaultStrategy)
|
commitVersionBump := conventionalCommit.VersionBump(conventionalcommits.DefaultStrategy)
|
||||||
if commitVersionBump > conventionalcommits.UnknownVersion {
|
if commitVersionBump > conventionalcommits.UnknownVersion {
|
||||||
// We only care about releasable commits
|
// We only care about releasable commits
|
||||||
analyzedCommits = append(analyzedCommits, AnalyzedCommit{
|
analyzedCommits = append(analyzedCommits, commitparser.AnalyzedCommit{
|
||||||
Commit: commit,
|
Commit: commit,
|
||||||
Type: conventionalCommit.Type,
|
Type: conventionalCommit.Type,
|
||||||
Description: conventionalCommit.Description,
|
Description: conventionalCommit.Description,
|
||||||
|
|
@ -1,27 +1,30 @@
|
||||||
package rp
|
package conventionalcommits
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/commitparser"
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAnalyzeCommits(t *testing.T) {
|
func TestAnalyzeCommits(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
commits []Commit
|
commits []git.Commit
|
||||||
expectedCommits []AnalyzedCommit
|
expectedCommits []commitparser.AnalyzedCommit
|
||||||
wantErr assert.ErrorAssertionFunc
|
wantErr assert.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty commits",
|
name: "empty commits",
|
||||||
commits: []Commit{},
|
commits: []git.Commit{},
|
||||||
expectedCommits: []AnalyzedCommit{},
|
expectedCommits: []commitparser.AnalyzedCommit{},
|
||||||
wantErr: assert.NoError,
|
wantErr: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "malformed commit message",
|
name: "malformed commit message",
|
||||||
commits: []Commit{
|
commits: []git.Commit{
|
||||||
{
|
{
|
||||||
Message: "aksdjaklsdjka",
|
Message: "aksdjaklsdjka",
|
||||||
},
|
},
|
||||||
|
|
@ -31,17 +34,17 @@ func TestAnalyzeCommits(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "drops unreleasable",
|
name: "drops unreleasable",
|
||||||
commits: []Commit{
|
commits: []git.Commit{
|
||||||
{
|
{
|
||||||
Message: "chore: foobar",
|
Message: "chore: foobar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedCommits: []AnalyzedCommit{},
|
expectedCommits: []commitparser.AnalyzedCommit{},
|
||||||
wantErr: assert.NoError,
|
wantErr: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "highest bump (patch)",
|
name: "highest bump (patch)",
|
||||||
commits: []Commit{
|
commits: []git.Commit{
|
||||||
{
|
{
|
||||||
Message: "chore: foobar",
|
Message: "chore: foobar",
|
||||||
},
|
},
|
||||||
|
|
@ -49,9 +52,9 @@ func TestAnalyzeCommits(t *testing.T) {
|
||||||
Message: "fix: blabla",
|
Message: "fix: blabla",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedCommits: []AnalyzedCommit{
|
expectedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{Message: "fix: blabla"},
|
Commit: git.Commit{Message: "fix: blabla"},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "blabla",
|
Description: "blabla",
|
||||||
},
|
},
|
||||||
|
|
@ -60,7 +63,7 @@ func TestAnalyzeCommits(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "highest bump (minor)",
|
name: "highest bump (minor)",
|
||||||
commits: []Commit{
|
commits: []git.Commit{
|
||||||
{
|
{
|
||||||
Message: "fix: blabla",
|
Message: "fix: blabla",
|
||||||
},
|
},
|
||||||
|
|
@ -68,14 +71,14 @@ func TestAnalyzeCommits(t *testing.T) {
|
||||||
Message: "feat: foobar",
|
Message: "feat: foobar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedCommits: []AnalyzedCommit{
|
expectedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{Message: "fix: blabla"},
|
Commit: git.Commit{Message: "fix: blabla"},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "blabla",
|
Description: "blabla",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Commit: Commit{Message: "feat: foobar"},
|
Commit: git.Commit{Message: "feat: foobar"},
|
||||||
Type: "feat",
|
Type: "feat",
|
||||||
Description: "foobar",
|
Description: "foobar",
|
||||||
},
|
},
|
||||||
|
|
@ -85,7 +88,7 @@ func TestAnalyzeCommits(t *testing.T) {
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "highest bump (major)",
|
name: "highest bump (major)",
|
||||||
commits: []Commit{
|
commits: []git.Commit{
|
||||||
{
|
{
|
||||||
Message: "fix: blabla",
|
Message: "fix: blabla",
|
||||||
},
|
},
|
||||||
|
|
@ -93,14 +96,14 @@ func TestAnalyzeCommits(t *testing.T) {
|
||||||
Message: "feat!: foobar",
|
Message: "feat!: foobar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedCommits: []AnalyzedCommit{
|
expectedCommits: []commitparser.AnalyzedCommit{
|
||||||
{
|
{
|
||||||
Commit: Commit{Message: "fix: blabla"},
|
Commit: git.Commit{Message: "fix: blabla"},
|
||||||
Type: "fix",
|
Type: "fix",
|
||||||
Description: "blabla",
|
Description: "blabla",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Commit: Commit{Message: "feat!: foobar"},
|
Commit: git.Commit{Message: "feat!: foobar"},
|
||||||
Type: "feat",
|
Type: "feat",
|
||||||
Description: "foobar",
|
Description: "foobar",
|
||||||
BreakingChange: true,
|
BreakingChange: true,
|
||||||
|
|
@ -111,7 +114,7 @@ func TestAnalyzeCommits(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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) {
|
if !tt.wantErr(t, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
61
internal/forge/forge.go
Normal file
61
internal/forge/forge.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package rp
|
package github
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -13,75 +13,27 @@ import (
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
"github.com/google/go-github/v63/github"
|
"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 (
|
const (
|
||||||
GitHubPerPageMax = 100
|
PerPageMax = 100
|
||||||
GitHubPRStateOpen = "open"
|
PRStateOpen = "open"
|
||||||
GitHubPRStateClosed = "closed"
|
PRStateClosed = "closed"
|
||||||
GitHubEnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential
|
EnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential
|
||||||
GitHubEnvUsername = "GITHUB_USER"
|
EnvUsername = "GITHUB_USER"
|
||||||
GitHubEnvRepository = "GITHUB_REPOSITORY"
|
EnvRepository = "GITHUB_REPOSITORY"
|
||||||
GitHubLabelColor = "dedede"
|
LabelColor = "dedede"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Forge interface {
|
var _ forge.Forge = &GitHub{}
|
||||||
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{}
|
|
||||||
|
|
||||||
type GitHub struct {
|
type GitHub struct {
|
||||||
options *GitHubOptions
|
options *Options
|
||||||
|
|
||||||
client *github.Client
|
client *github.Client
|
||||||
log *slog.Logger
|
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")
|
g.log.DebugContext(ctx, "listing all tags in github repository")
|
||||||
|
|
||||||
page := 1
|
page := 1
|
||||||
|
|
||||||
var releases Releases
|
var releases git.Releases
|
||||||
|
|
||||||
for {
|
for {
|
||||||
tags, resp, err := g.client.Repositories.ListTags(
|
tags, resp, err := g.client.Repositories.ListTags(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
&github.ListOptions{Page: page, PerPage: GitHubPerPageMax},
|
&github.ListOptions{Page: page, PerPage: PerPageMax},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Releases{}, err
|
return git.Releases{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ghTag := range tags {
|
for _, ghTag := range tags {
|
||||||
tag := &Tag{
|
tag := &git.Tag{
|
||||||
Hash: ghTag.GetCommit().GetSHA(),
|
Hash: ghTag.GetCommit().GetSHA(),
|
||||||
Name: ghTag.GetName(),
|
Name: ghTag.GetName(),
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +112,7 @@ func (g *GitHub) LatestTags(ctx context.Context) (Releases, error) {
|
||||||
return releases, nil
|
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 repositoryCommits []*github.RepositoryCommit
|
||||||
var err error
|
var err error
|
||||||
if tag != nil {
|
if tag != nil {
|
||||||
|
|
@ -173,9 +125,9 @@ func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var commits = make([]Commit, 0, len(repositoryCommits))
|
var commits = make([]git.Commit, 0, len(repositoryCommits))
|
||||||
for _, ghCommit := range repositoryCommits {
|
for _, ghCommit := range repositoryCommits {
|
||||||
commit := Commit{
|
commit := git.Commit{
|
||||||
Hash: ghCommit.GetSHA(),
|
Hash: ghCommit.GetSHA(),
|
||||||
Message: ghCommit.GetCommit().GetMessage(),
|
Message: ghCommit.GetCommit().GetMessage(),
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +142,7 @@ func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) {
|
||||||
return commits, nil
|
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
|
head := g.options.BaseBranch
|
||||||
log := g.log.With("base", tag.Hash, "head", head)
|
log := g.log.With("base", tag.Hash, "head", head)
|
||||||
log.Debug("comparing commits", "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,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
tag.Hash, head, &github.ListOptions{
|
tag.Hash, head, &github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: GitHubPerPageMax,
|
PerPage: PerPageMax,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -244,7 +196,7 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm
|
||||||
SHA: head,
|
SHA: head,
|
||||||
ListOptions: github.ListOptions{
|
ListOptions: github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: GitHubPerPageMax,
|
PerPage: PerPageMax,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -254,7 +206,7 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm
|
||||||
if repositoryCommits == nil && resp.LastPage > 0 {
|
if repositoryCommits == nil && resp.LastPage > 0 {
|
||||||
// Pre-initialize slice on first request
|
// Pre-initialize slice on first request
|
||||||
log.Debug("found commits", "pages", resp.LastPage)
|
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...)
|
repositoryCommits = append(repositoryCommits, commits...)
|
||||||
|
|
@ -269,7 +221,7 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm
|
||||||
return repositoryCommits, nil
|
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"
|
// 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.
|
// 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,
|
// 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,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
commit.Hash, &github.ListOptions{
|
commit.Hash, &github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: GitHubPerPageMax,
|
PerPage: PerPageMax,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -314,7 +266,7 @@ func (g *GitHub) prForCommit(ctx context.Context, commit Commit) (*PullRequest,
|
||||||
return gitHubPRToPullRequest(pullrequest), nil
|
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))
|
existingLabels := make([]string, 0, len(labels))
|
||||||
|
|
||||||
page := 1
|
page := 1
|
||||||
|
|
@ -325,7 +277,7 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error {
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
&github.ListOptions{
|
&github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: GitHubPerPageMax,
|
PerPage: PerPageMax,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -347,8 +299,8 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error {
|
||||||
_, _, err := g.client.Issues.CreateLabel(
|
_, _, err := g.client.Issues.CreateLabel(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
&github.Label{
|
&github.Label{
|
||||||
Name: Pointer(string(label)),
|
Name: pointer.Pointer(string(label)),
|
||||||
Color: Pointer(GitHubLabelColor),
|
Color: pointer.Pointer(LabelColor),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -360,13 +312,13 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error {
|
||||||
return nil
|
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
|
page := 1
|
||||||
|
|
||||||
for {
|
for {
|
||||||
prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit(ctx, g.options.Owner, g.options.Repo, branch, &github.ListOptions{
|
prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit(ctx, g.options.Owner, g.options.Repo, branch, &github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: GitHubPerPageMax,
|
PerPage: PerPageMax,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var ghErr *github.ErrorResponse
|
var ghErr *github.ErrorResponse
|
||||||
|
|
@ -379,7 +331,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pr := range prs {
|
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
|
return gitHubPRToReleasePullRequest(pr), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -393,7 +345,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele
|
||||||
return nil, nil
|
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(
|
ghPR, _, err := g.client.PullRequests.Create(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
&github.NewPullRequest{
|
&github.NewPullRequest{
|
||||||
|
|
@ -410,7 +362,7 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest)
|
||||||
// TODO: String ID?
|
// TODO: String ID?
|
||||||
pr.ID = ghPR.GetNumber()
|
pr.ID = ghPR.GetNumber()
|
||||||
|
|
||||||
err = g.SetPullRequestLabels(ctx, pr, []Label{}, pr.Labels)
|
err = g.SetPullRequestLabels(ctx, pr, []releasepr.Label{}, pr.Labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +370,7 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest)
|
||||||
return nil
|
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(
|
_, _, err := g.client.PullRequests.Edit(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
pr.ID, &github.PullRequest{
|
pr.ID, &github.PullRequest{
|
||||||
|
|
@ -433,7 +385,7 @@ func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest)
|
||||||
return nil
|
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 {
|
for _, label := range remove {
|
||||||
_, err := g.client.Issues.RemoveLabelForIssue(
|
_, err := g.client.Issues.RemoveLabelForIssue(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
|
|
@ -460,11 +412,11 @@ func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullReques
|
||||||
return nil
|
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(
|
_, _, err := g.client.PullRequests.Edit(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
pr.ID, &github.PullRequest{
|
pr.ID, &github.PullRequest{
|
||||||
State: Pointer(GitHubPRStateClosed),
|
State: pointer.Pointer(PRStateClosed),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -474,20 +426,20 @@ func (g *GitHub) ClosePullRequest(ctx context.Context, pr *ReleasePullRequest) e
|
||||||
return nil
|
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
|
page := 1
|
||||||
|
|
||||||
var prs []*ReleasePullRequest
|
var prs []*releasepr.ReleasePullRequest
|
||||||
|
|
||||||
for {
|
for {
|
||||||
ghPRs, resp, err := g.client.PullRequests.List(
|
ghPRs, resp, err := g.client.PullRequests.List(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
&github.PullRequestListOptions{
|
&github.PullRequestListOptions{
|
||||||
State: GitHubPRStateClosed,
|
State: PRStateClosed,
|
||||||
Base: g.options.BaseBranch,
|
Base: g.options.BaseBranch,
|
||||||
ListOptions: github.ListOptions{
|
ListOptions: github.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PerPage: GitHubPerPageMax,
|
PerPage: PerPageMax,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -497,7 +449,7 @@ func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*Re
|
||||||
if prs == nil && resp.LastPage > 0 {
|
if prs == nil && resp.LastPage > 0 {
|
||||||
// Pre-initialize slice on first request
|
// Pre-initialize slice on first request
|
||||||
g.log.Debug("found pending releases", "pages", resp.LastPage)
|
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 {
|
for _, pr := range ghPRs {
|
||||||
|
|
@ -526,7 +478,7 @@ func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*Re
|
||||||
return prs, nil
|
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 := ""
|
makeLatest := ""
|
||||||
if latest {
|
if latest {
|
||||||
makeLatest = "true"
|
makeLatest = "true"
|
||||||
|
|
@ -551,29 +503,29 @@ func (g *GitHub) CreateRelease(ctx context.Context, commit Commit, title, change
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitHubPRToPullRequest(pr *github.PullRequest) *PullRequest {
|
func gitHubPRToPullRequest(pr *github.PullRequest) *git.PullRequest {
|
||||||
return &PullRequest{
|
return &git.PullRequest{
|
||||||
ID: pr.GetNumber(),
|
ID: pr.GetNumber(),
|
||||||
Title: pr.GetTitle(),
|
Title: pr.GetTitle(),
|
||||||
Description: pr.GetBody(),
|
Description: pr.GetBody(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitHubPRToReleasePullRequest(pr *github.PullRequest) *ReleasePullRequest {
|
func gitHubPRToReleasePullRequest(pr *github.PullRequest) *releasepr.ReleasePullRequest {
|
||||||
labels := make([]Label, 0, len(pr.Labels))
|
labels := make([]releasepr.Label, 0, len(pr.Labels))
|
||||||
for _, label := range pr.Labels {
|
for _, label := range pr.Labels {
|
||||||
labelName := Label(label.GetName())
|
labelName := releasepr.Label(label.GetName())
|
||||||
if slices.Contains(KnownLabels, Label(label.GetName())) {
|
if slices.Contains(releasepr.KnownLabels, releasepr.Label(label.GetName())) {
|
||||||
labels = append(labels, labelName)
|
labels = append(labels, labelName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var releaseCommit *Commit
|
var releaseCommit *git.Commit
|
||||||
if pr.MergeCommitSHA != nil {
|
if pr.MergeCommitSHA != nil {
|
||||||
releaseCommit = &Commit{Hash: pr.GetMergeCommitSHA()}
|
releaseCommit = &git.Commit{Hash: pr.GetMergeCommitSHA()}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ReleasePullRequest{
|
return &releasepr.ReleasePullRequest{
|
||||||
ID: pr.GetNumber(),
|
ID: pr.GetNumber(),
|
||||||
Title: pr.GetTitle(),
|
Title: pr.GetTitle(),
|
||||||
Description: pr.GetBody(),
|
Description: pr.GetBody(),
|
||||||
|
|
@ -584,16 +536,16 @@ func gitHubPRToReleasePullRequest(pr *github.PullRequest) *ReleasePullRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GitHubOptions) autodiscover() {
|
func (g *Options) autodiscover() {
|
||||||
if apiToken := os.Getenv(GitHubEnvAPIToken); apiToken != "" {
|
if apiToken := os.Getenv(EnvAPIToken); apiToken != "" {
|
||||||
g.APIToken = apiToken
|
g.APIToken = apiToken
|
||||||
}
|
}
|
||||||
// TODO: Check if there is a better solution for cloning/pushing locally
|
// 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
|
g.Username = username
|
||||||
}
|
}
|
||||||
|
|
||||||
if envRepository := os.Getenv(GitHubEnvRepository); envRepository != "" {
|
if envRepository := os.Getenv(EnvRepository); envRepository != "" {
|
||||||
// GITHUB_REPOSITORY=apricote/releaser-pleaser
|
// GITHUB_REPOSITORY=apricote/releaser-pleaser
|
||||||
parts := strings.Split(envRepository, "/")
|
parts := strings.Split(envRepository, "/")
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
|
|
@ -604,8 +556,8 @@ func (g *GitHubOptions) autodiscover() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitHubOptions struct {
|
type Options struct {
|
||||||
ForgeOptions
|
forge.Options
|
||||||
|
|
||||||
Owner string
|
Owner string
|
||||||
Repo string
|
Repo string
|
||||||
|
|
@ -614,7 +566,7 @@ type GitHubOptions struct {
|
||||||
Username string
|
Username string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGitHub(log *slog.Logger, options *GitHubOptions) *GitHub {
|
func New(log *slog.Logger, options *Options) *GitHub {
|
||||||
options.autodiscover()
|
options.autodiscover()
|
||||||
|
|
||||||
client := github.NewClient(nil)
|
client := github.NewClient(nil)
|
||||||
|
|
@ -631,29 +583,3 @@ func NewGitHub(log *slog.Logger, options *GitHubOptions) *GitHub {
|
||||||
|
|
||||||
return gh
|
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
|
|
||||||
}
|
|
||||||
31
internal/forge/gitlab/gitlab.go
Normal file
31
internal/forge/gitlab/gitlab.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
227
internal/git/git.go
Normal file
227
internal/git/git.go
Normal file
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
5
internal/pointer/pointer.go
Normal file
5
internal/pointer/pointer.go
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package pointer
|
||||||
|
|
||||||
|
func Pointer[T any](value T) *T {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
1
internal/releasepr/label.go
Normal file
1
internal/releasepr/label.go
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
package releasepr
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package rp
|
package releasepr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -12,8 +12,10 @@ import (
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
"github.com/yuin/goldmark/text"
|
"github.com/yuin/goldmark/text"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/git"
|
||||||
"github.com/apricote/releaser-pleaser/internal/markdown"
|
"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 (
|
var (
|
||||||
|
|
@ -33,7 +35,7 @@ func init() {
|
||||||
|
|
||||||
// ReleasePullRequest
|
// ReleasePullRequest
|
||||||
//
|
//
|
||||||
// TODO: Reuse [PullRequest]
|
// TODO: Reuse [git.PullRequest]
|
||||||
type ReleasePullRequest struct {
|
type ReleasePullRequest struct {
|
||||||
ID int
|
ID int
|
||||||
Title string
|
Title string
|
||||||
|
|
@ -41,9 +43,12 @@ type ReleasePullRequest struct {
|
||||||
Labels []Label
|
Labels []Label
|
||||||
|
|
||||||
Head string
|
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) {
|
func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) {
|
||||||
rp := &ReleasePullRequest{
|
rp := &ReleasePullRequest{
|
||||||
Head: head,
|
Head: head,
|
||||||
|
|
@ -61,50 +66,9 @@ func NewReleasePullRequest(head, branch, version, changelogEntry string) (*Relea
|
||||||
type ReleaseOverrides struct {
|
type ReleaseOverrides struct {
|
||||||
Prefix string
|
Prefix string
|
||||||
Suffix 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 (
|
const (
|
||||||
LabelNextVersionTypeNormal Label = "rp-next-version::normal"
|
LabelNextVersionTypeNormal Label = "rp-next-version::normal"
|
||||||
LabelNextVersionTypeRC Label = "rp-next-version::rc"
|
LabelNextVersionTypeRC Label = "rp-next-version::rc"
|
||||||
|
|
@ -158,13 +122,13 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R
|
||||||
switch label {
|
switch label {
|
||||||
// Versioning
|
// Versioning
|
||||||
case LabelNextVersionTypeNormal:
|
case LabelNextVersionTypeNormal:
|
||||||
overrides.NextVersionType = NextVersionTypeNormal
|
overrides.NextVersionType = versioning.NextVersionTypeNormal
|
||||||
case LabelNextVersionTypeRC:
|
case LabelNextVersionTypeRC:
|
||||||
overrides.NextVersionType = NextVersionTypeRC
|
overrides.NextVersionType = versioning.NextVersionTypeRC
|
||||||
case LabelNextVersionTypeBeta:
|
case LabelNextVersionTypeBeta:
|
||||||
overrides.NextVersionType = NextVersionTypeBeta
|
overrides.NextVersionType = versioning.NextVersionTypeBeta
|
||||||
case LabelNextVersionTypeAlpha:
|
case LabelNextVersionTypeAlpha:
|
||||||
overrides.NextVersionType = NextVersionTypeAlpha
|
overrides.NextVersionType = versioning.NextVersionTypeAlpha
|
||||||
case LabelReleasePending, LabelReleaseTagged:
|
case LabelReleasePending, LabelReleaseTagged:
|
||||||
// These labels have no effect on the versioning.
|
// These labels have no effect on the versioning.
|
||||||
break
|
break
|
||||||
|
|
@ -213,18 +177,18 @@ func (pr *ReleasePullRequest) ChangelogText() (string, error) {
|
||||||
gm := markdown.New()
|
gm := markdown.New()
|
||||||
descriptionAST := gm.Parser().Parse(text.NewReader(source))
|
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) {
|
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
if !entering {
|
if !entering {
|
||||||
return ast.WalkContinue, nil
|
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
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
anySection, ok := n.(*east.Section)
|
anySection, ok := n.(*ast2.Section)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
|
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package rp
|
package releasepr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
internal/updater/changelog.go
Normal file
32
internal/updater/changelog.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
60
internal/updater/changelog_test.go
Normal file
60
internal/updater/changelog_test.go
Normal file
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
17
internal/updater/generic.go
Normal file
17
internal/updater/generic.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
53
internal/updater/generic_test.go
Normal file
53
internal/updater/generic_test.go
Normal file
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
19
internal/updater/updater.go
Normal file
19
internal/updater/updater.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
26
internal/updater/updater_test.go
Normal file
26
internal/updater/updater_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
package rp
|
package versioning
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/blang/semver/v4"
|
"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 {
|
var _ Strategy = SemVerNextVersion
|
||||||
Latest *Tag
|
|
||||||
Stable *Tag
|
|
||||||
}
|
|
||||||
|
|
||||||
type VersioningStrategy = func(Releases, conventionalcommits.VersionBump, NextVersionType) (string, error)
|
func SemVerNextVersion(r git.Releases, versionBump VersionBump, nextVersionType NextVersionType) (string, error) {
|
||||||
|
|
||||||
var _ VersioningStrategy = SemVerNextVersion
|
|
||||||
|
|
||||||
func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) {
|
|
||||||
latest, err := parseSemverWithDefault(r.Latest)
|
latest, err := parseSemverWithDefault(r.Latest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to parse latest version: %w", err)
|
return "", fmt.Errorf("failed to parse latest version: %w", err)
|
||||||
|
|
@ -36,13 +31,13 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch versionBump {
|
switch versionBump {
|
||||||
case conventionalcommits.UnknownVersion:
|
case UnknownVersion:
|
||||||
return "", fmt.Errorf("invalid latest bump (unknown)")
|
return "", fmt.Errorf("invalid latest bump (unknown)")
|
||||||
case conventionalcommits.PatchVersion:
|
case PatchVersion:
|
||||||
err = next.IncrementPatch()
|
err = next.IncrementPatch()
|
||||||
case conventionalcommits.MinorVersion:
|
case MinorVersion:
|
||||||
err = next.IncrementMinor()
|
err = next.IncrementMinor()
|
||||||
case conventionalcommits.MajorVersion:
|
case MajorVersion:
|
||||||
err = next.IncrementMajor()
|
err = next.IncrementMajor()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -68,18 +63,18 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump,
|
||||||
return "v" + next.String(), nil
|
return "v" + next.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func VersionBumpFromCommits(commits []AnalyzedCommit) conventionalcommits.VersionBump {
|
func BumpFromCommits(commits []commitparser.AnalyzedCommit) VersionBump {
|
||||||
bump := conventionalcommits.UnknownVersion
|
bump := UnknownVersion
|
||||||
|
|
||||||
for _, commit := range commits {
|
for _, commit := range commits {
|
||||||
entryBump := conventionalcommits.UnknownVersion
|
entryBump := UnknownVersion
|
||||||
switch {
|
switch {
|
||||||
case commit.BreakingChange:
|
case commit.BreakingChange:
|
||||||
entryBump = conventionalcommits.MajorVersion
|
entryBump = MajorVersion
|
||||||
case commit.Type == "feat":
|
case commit.Type == "feat":
|
||||||
entryBump = conventionalcommits.MinorVersion
|
entryBump = MinorVersion
|
||||||
case commit.Type == "fix":
|
case commit.Type == "fix":
|
||||||
entryBump = conventionalcommits.PatchVersion
|
entryBump = PatchVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
if entryBump > bump {
|
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"
|
version := "v0.0.0"
|
||||||
if tag != nil {
|
if tag != nil {
|
||||||
version = tag.Name
|
version = tag.Name
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
package rp
|
package versioning
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/leodido/go-conventionalcommits"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestReleases_NextVersion(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
releases Releases
|
releases git.Releases
|
||||||
versionBump conventionalcommits.VersionBump
|
versionBump VersionBump
|
||||||
nextVersionType NextVersionType
|
nextVersionType NextVersionType
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|
@ -23,11 +25,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "simple bump (major)",
|
name: "simple bump (major)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1"},
|
Latest: &git.Tag{Name: "v1.1.1"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MajorVersion,
|
versionBump: MajorVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v2.0.0",
|
want: "v2.0.0",
|
||||||
|
|
@ -36,11 +38,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "simple bump (minor)",
|
name: "simple bump (minor)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1"},
|
Latest: &git.Tag{Name: "v1.1.1"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MinorVersion,
|
versionBump: MinorVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v1.2.0",
|
want: "v1.2.0",
|
||||||
|
|
@ -49,11 +51,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "simple bump (patch)",
|
name: "simple bump (patch)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1"},
|
Latest: &git.Tag{Name: "v1.1.1"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v1.1.2",
|
want: "v1.1.2",
|
||||||
|
|
@ -62,11 +64,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "normal to prerelease (major)",
|
name: "normal to prerelease (major)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1"},
|
Latest: &git.Tag{Name: "v1.1.1"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MajorVersion,
|
versionBump: MajorVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v2.0.0-rc.0",
|
want: "v2.0.0-rc.0",
|
||||||
|
|
@ -75,11 +77,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "normal to prerelease (minor)",
|
name: "normal to prerelease (minor)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1"},
|
Latest: &git.Tag{Name: "v1.1.1"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MinorVersion,
|
versionBump: MinorVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v1.2.0-rc.0",
|
want: "v1.2.0-rc.0",
|
||||||
|
|
@ -88,11 +90,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "normal to prerelease (patch)",
|
name: "normal to prerelease (patch)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1"},
|
Latest: &git.Tag{Name: "v1.1.1"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v1.1.2-rc.0",
|
want: "v1.1.2-rc.0",
|
||||||
|
|
@ -101,11 +103,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease bump (major)",
|
name: "prerelease bump (major)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v2.0.0-rc.0"},
|
Latest: &git.Tag{Name: "v2.0.0-rc.0"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MajorVersion,
|
versionBump: MajorVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v2.0.0-rc.1",
|
want: "v2.0.0-rc.1",
|
||||||
|
|
@ -114,11 +116,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease bump (minor)",
|
name: "prerelease bump (minor)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.2.0-rc.0"},
|
Latest: &git.Tag{Name: "v1.2.0-rc.0"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MinorVersion,
|
versionBump: MinorVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v1.2.0-rc.1",
|
want: "v1.2.0-rc.1",
|
||||||
|
|
@ -127,11 +129,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease bump (patch)",
|
name: "prerelease bump (patch)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.2-rc.0"},
|
Latest: &git.Tag{Name: "v1.1.2-rc.0"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v1.1.2-rc.1",
|
want: "v1.1.2-rc.1",
|
||||||
|
|
@ -140,11 +142,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease different bump (major)",
|
name: "prerelease different bump (major)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.2.0-rc.0"},
|
Latest: &git.Tag{Name: "v1.2.0-rc.0"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MajorVersion,
|
versionBump: MajorVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v2.0.0-rc.1",
|
want: "v2.0.0-rc.1",
|
||||||
|
|
@ -153,11 +155,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease different bump (minor)",
|
name: "prerelease different bump (minor)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.2-rc.0"},
|
Latest: &git.Tag{Name: "v1.1.2-rc.0"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MinorVersion,
|
versionBump: MinorVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v1.2.0-rc.1",
|
want: "v1.2.0-rc.1",
|
||||||
|
|
@ -166,11 +168,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease to prerelease",
|
name: "prerelease to prerelease",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1-alpha.2"},
|
Latest: &git.Tag{Name: "v1.1.1-alpha.2"},
|
||||||
Stable: &Tag{Name: "v1.1.0"},
|
Stable: &git.Tag{Name: "v1.1.0"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "v1.1.1-rc.0",
|
want: "v1.1.1-rc.0",
|
||||||
|
|
@ -179,11 +181,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease to normal (explicit)",
|
name: "prerelease to normal (explicit)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1-alpha.2"},
|
Latest: &git.Tag{Name: "v1.1.1-alpha.2"},
|
||||||
Stable: &Tag{Name: "v1.1.0"},
|
Stable: &git.Tag{Name: "v1.1.0"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeNormal,
|
nextVersionType: NextVersionTypeNormal,
|
||||||
},
|
},
|
||||||
want: "v1.1.1",
|
want: "v1.1.1",
|
||||||
|
|
@ -192,11 +194,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "prerelease to normal (implicit)",
|
name: "prerelease to normal (implicit)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1-alpha.2"},
|
Latest: &git.Tag{Name: "v1.1.1-alpha.2"},
|
||||||
Stable: &Tag{Name: "v1.1.0"},
|
Stable: &git.Tag{Name: "v1.1.0"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v1.1.1",
|
want: "v1.1.1",
|
||||||
|
|
@ -205,11 +207,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "nil tag (major)",
|
name: "nil tag (major)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: nil,
|
Latest: nil,
|
||||||
Stable: nil,
|
Stable: nil,
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MajorVersion,
|
versionBump: MajorVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v1.0.0",
|
want: "v1.0.0",
|
||||||
|
|
@ -218,11 +220,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "nil tag (minor)",
|
name: "nil tag (minor)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: nil,
|
Latest: nil,
|
||||||
Stable: nil,
|
Stable: nil,
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MinorVersion,
|
versionBump: MinorVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v0.1.0",
|
want: "v0.1.0",
|
||||||
|
|
@ -231,11 +233,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "nil tag (patch)",
|
name: "nil tag (patch)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: nil,
|
Latest: nil,
|
||||||
Stable: nil,
|
Stable: nil,
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v0.0.1",
|
want: "v0.0.1",
|
||||||
|
|
@ -244,11 +246,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "nil stable release (major)",
|
name: "nil stable release (major)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1-rc.0"},
|
Latest: &git.Tag{Name: "v1.1.1-rc.0"},
|
||||||
Stable: nil,
|
Stable: nil,
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MajorVersion,
|
versionBump: MajorVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v2.0.0",
|
want: "v2.0.0",
|
||||||
|
|
@ -257,11 +259,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "nil stable release (minor)",
|
name: "nil stable release (minor)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1-rc.0"},
|
Latest: &git.Tag{Name: "v1.1.1-rc.0"},
|
||||||
Stable: nil,
|
Stable: nil,
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.MinorVersion,
|
versionBump: MinorVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "v1.2.0",
|
want: "v1.2.0",
|
||||||
|
|
@ -270,11 +272,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "nil stable release (patch)",
|
name: "nil stable release (patch)",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1-rc.0"},
|
Latest: &git.Tag{Name: "v1.1.1-rc.0"},
|
||||||
Stable: nil,
|
Stable: nil,
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
// TODO: Is this actually correct our should it be v1.1.1?
|
// 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",
|
name: "error on invalid tag semver",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "foodazzle"},
|
Latest: &git.Tag{Name: "foodazzle"},
|
||||||
Stable: &Tag{Name: "foodazzle"},
|
Stable: &git.Tag{Name: "foodazzle"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "",
|
want: "",
|
||||||
|
|
@ -297,11 +299,11 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "error on invalid tag prerelease",
|
name: "error on invalid tag prerelease",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1-rc.foo"},
|
Latest: &git.Tag{Name: "v1.1.1-rc.foo"},
|
||||||
Stable: &Tag{Name: "v1.1.1-rc.foo"},
|
Stable: &git.Tag{Name: "v1.1.1-rc.foo"},
|
||||||
},
|
},
|
||||||
versionBump: conventionalcommits.PatchVersion,
|
versionBump: PatchVersion,
|
||||||
nextVersionType: NextVersionTypeRC,
|
nextVersionType: NextVersionTypeRC,
|
||||||
},
|
},
|
||||||
want: "",
|
want: "",
|
||||||
|
|
@ -310,12 +312,12 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "error on invalid bump",
|
name: "error on invalid bump",
|
||||||
args: args{
|
args: args{
|
||||||
releases: Releases{
|
releases: git.Releases{
|
||||||
Latest: &Tag{Name: "v1.1.1"},
|
Latest: &git.Tag{Name: "v1.1.1"},
|
||||||
Stable: &Tag{Name: "v1.1.1"},
|
Stable: &git.Tag{Name: "v1.1.1"},
|
||||||
},
|
},
|
||||||
|
|
||||||
versionBump: conventionalcommits.UnknownVersion,
|
versionBump: UnknownVersion,
|
||||||
nextVersionType: NextVersionTypeUndefined,
|
nextVersionType: NextVersionTypeUndefined,
|
||||||
},
|
},
|
||||||
want: "",
|
want: "",
|
||||||
|
|
@ -336,53 +338,53 @@ func TestReleases_NextVersion(t *testing.T) {
|
||||||
func TestVersionBumpFromCommits(t *testing.T) {
|
func TestVersionBumpFromCommits(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
analyzedCommits []AnalyzedCommit
|
analyzedCommits []commitparser.AnalyzedCommit
|
||||||
want conventionalcommits.VersionBump
|
want VersionBump
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no entries (unknown)",
|
name: "no entries (unknown)",
|
||||||
analyzedCommits: []AnalyzedCommit{},
|
analyzedCommits: []commitparser.AnalyzedCommit{},
|
||||||
want: conventionalcommits.UnknownVersion,
|
want: UnknownVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-release type (unknown)",
|
name: "non-release type (unknown)",
|
||||||
analyzedCommits: []AnalyzedCommit{{Type: "docs"}},
|
analyzedCommits: []commitparser.AnalyzedCommit{{Type: "docs"}},
|
||||||
want: conventionalcommits.UnknownVersion,
|
want: UnknownVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single breaking (major)",
|
name: "single breaking (major)",
|
||||||
analyzedCommits: []AnalyzedCommit{{BreakingChange: true}},
|
analyzedCommits: []commitparser.AnalyzedCommit{{BreakingChange: true}},
|
||||||
want: conventionalcommits.MajorVersion,
|
want: MajorVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single feat (minor)",
|
name: "single feat (minor)",
|
||||||
analyzedCommits: []AnalyzedCommit{{Type: "feat"}},
|
analyzedCommits: []commitparser.AnalyzedCommit{{Type: "feat"}},
|
||||||
want: conventionalcommits.MinorVersion,
|
want: MinorVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single fix (patch)",
|
name: "single fix (patch)",
|
||||||
analyzedCommits: []AnalyzedCommit{{Type: "fix"}},
|
analyzedCommits: []commitparser.AnalyzedCommit{{Type: "fix"}},
|
||||||
want: conventionalcommits.PatchVersion,
|
want: PatchVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple entries (major)",
|
name: "multiple entries (major)",
|
||||||
analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}},
|
analyzedCommits: []commitparser.AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}},
|
||||||
want: conventionalcommits.MajorVersion,
|
want: MajorVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple entries (minor)",
|
name: "multiple entries (minor)",
|
||||||
analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}},
|
analyzedCommits: []commitparser.AnalyzedCommit{{Type: "fix"}, {Type: "feat"}},
|
||||||
want: conventionalcommits.MinorVersion,
|
want: MinorVersion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple entries (patch)",
|
name: "multiple entries (patch)",
|
||||||
analyzedCommits: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}},
|
analyzedCommits: []commitparser.AnalyzedCommit{{Type: "docs"}, {Type: "fix"}},
|
||||||
want: conventionalcommits.PatchVersion,
|
want: PatchVersion,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
56
internal/versioning/versioning.go
Normal file
56
internal/versioning/versioning.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,13 +3,15 @@ package rp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/apricote/releaser-pleaser/internal/changelog"
|
||||||
"github.com/go-git/go-git/v5/config"
|
"github.com/apricote/releaser-pleaser/internal/commitparser"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"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 (
|
const (
|
||||||
|
|
@ -17,16 +19,16 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReleaserPleaser struct {
|
type ReleaserPleaser struct {
|
||||||
forge Forge
|
forge forge.Forge
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
targetBranch string
|
targetBranch string
|
||||||
commitParser CommitParser
|
commitParser commitparser.CommitParser
|
||||||
nextVersion VersioningStrategy
|
nextVersion versioning.Strategy
|
||||||
extraFiles []string
|
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{
|
return &ReleaserPleaser{
|
||||||
forge: forge,
|
forge: forge,
|
||||||
logger: logger,
|
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 {
|
func (rp *ReleaserPleaser) EnsureLabels(ctx context.Context) error {
|
||||||
// TODO: Wrap 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 {
|
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 := rp.logger.With("method", "runCreatePendingReleases")
|
||||||
|
|
||||||
logger.InfoContext(ctx, "checking for pending releases")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +100,7 @@ func (rp *ReleaserPleaser) runCreatePendingReleases(ctx context.Context) error {
|
||||||
return nil
|
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(
|
logger := rp.logger.With(
|
||||||
"method", "createPendingRelease",
|
"method", "createPendingRelease",
|
||||||
"pr.id", pr.ID,
|
"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, "created release", "release.title", version, "release.url", rp.forge.ReleaseURL(version))
|
||||||
|
|
||||||
logger.DebugContext(ctx, "updating pr labels")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -144,14 +147,13 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
|
||||||
logger := rp.logger.With("method", "runReconcileReleasePR")
|
logger := rp.logger.With("method", "runReconcileReleasePR")
|
||||||
|
|
||||||
rpBranch := fmt.Sprintf(PullRequestBranchFormat, rp.targetBranch)
|
rpBranch := fmt.Sprintf(PullRequestBranchFormat, rp.targetBranch)
|
||||||
rpBranchRef := plumbing.NewBranchReferenceName(rpBranch)
|
|
||||||
|
|
||||||
pr, err := rp.forge.PullRequestForBranch(ctx, rpBranch)
|
pr, err := rp.forge.PullRequestForBranch(ctx, rpBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var releaseOverrides ReleaseOverrides
|
var releaseOverrides releasepr.ReleaseOverrides
|
||||||
|
|
||||||
if pr != nil {
|
if pr != nil {
|
||||||
logger = logger.With("pr.id", pr.ID, "pr.title", pr.Title)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
versionBump := VersionBumpFromCommits(analyzedCommits)
|
versionBump := versioning.BumpFromCommits(analyzedCommits)
|
||||||
// TODO: Set version in release pr
|
// TODO: Set version in release pr
|
||||||
nextVersion, err := rp.nextVersion(releases, versionBump, releaseOverrides.NextVersionType)
|
nextVersion, err := rp.nextVersion(releases, versionBump, releaseOverrides.NextVersionType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -224,161 +226,68 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
|
||||||
logger.InfoContext(ctx, "next version", "version", nextVersion)
|
logger.InfoContext(ctx, "next version", "version", nextVersion)
|
||||||
|
|
||||||
logger.DebugContext(ctx, "cloning repository", "clone.url", rp.forge.CloneURL())
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to clone repository: %w", err)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if branch, _ := repo.Branch(rpBranch); branch != nil {
|
if err = repo.Checkout(ctx, rpBranch); err != nil {
|
||||||
logger.DebugContext(ctx, "deleting previous releaser-pleaser branch locally", "branch.name", rpBranch)
|
return err
|
||||||
if err = repo.DeleteBranch(rpBranch); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = worktree.Checkout(&git.CheckoutOptions{
|
changelogEntry, err := changelog.NewChangelogEntry(analyzedCommits, nextVersion, rp.forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to build changelog entry: %w", err)
|
return fmt.Errorf("failed to build changelog entry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info for updaters
|
// Info for updaters
|
||||||
info := ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry}
|
info := updater.ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry}
|
||||||
|
|
||||||
updateFile := func(path string, updaters []Updater) error {
|
err = repo.UpdateFile(ctx, updater.ChangelogFile, updater.WithInfo(info, updater.Changelog))
|
||||||
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{}})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update changelog file: %w", err)
|
return fmt.Errorf("failed to update changelog file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range rp.extraFiles {
|
for _, path := range rp.extraFiles {
|
||||||
_, err = worktree.Filesystem.Stat(path)
|
// TODO: Check for missing files
|
||||||
if err != nil {
|
err = repo.UpdateFile(ctx, path, updater.WithInfo(info, rp.updaters...))
|
||||||
// TODO: Check for non existing file or dirs
|
|
||||||
return fmt.Errorf("failed to run file updater because the file %s does not exist: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = updateFile(path, rp.updaters)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run file updater: %w", err)
|
return fmt.Errorf("failed to run file updater: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion)
|
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion)
|
||||||
releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{
|
releaseCommit, err := repo.Commit(ctx, releaseCommitMessage)
|
||||||
Author: GitSignature(),
|
|
||||||
Committer: GitSignature(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to commit changes: %w", err)
|
return fmt.Errorf("failed to commit changes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommitHash.String(), "commit.message", releaseCommitMessage)
|
logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommit.Hash, "commit.message", releaseCommit.Message)
|
||||||
|
|
||||||
newReleasePRChanges := true
|
|
||||||
|
|
||||||
// Check if anything changed in comparison to the remote branch (if exists)
|
// Check if anything changed in comparison to the remote branch (if exists)
|
||||||
if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName(GitRemoteName, rpBranch), false); err != nil {
|
newReleasePRChanges, err := repo.HasChangesWithRemote(ctx, rpBranch)
|
||||||
if err.Error() != "reference not found" {
|
if err != nil {
|
||||||
// "reference not found" is expected and we should always push
|
return err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if newReleasePRChanges {
|
if newReleasePRChanges {
|
||||||
pushRefSpec := config.RefSpec(fmt.Sprintf(
|
err = repo.ForcePush(ctx, rpBranch)
|
||||||
"+%s:%s",
|
if err != nil {
|
||||||
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 {
|
|
||||||
return fmt.Errorf("failed to push branch: %w", err)
|
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 {
|
} else {
|
||||||
logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push")
|
logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open/Update PR
|
// Open/Update PR
|
||||||
if pr == nil {
|
if pr == nil {
|
||||||
pr, err = NewReleasePullRequest(rpBranch, rp.targetBranch, nextVersion, changelogEntry)
|
pr, err = releasepr.NewReleasePullRequest(rpBranch, rp.targetBranch, nextVersion, changelogEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
updater.go
47
updater.go
|
|
@ -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
|
|
||||||
}
|
|
||||||
129
updater_test.go
129
updater_test.go
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue