mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-02-11 20:27:02 +00:00
Compare commits
No commits in common. "6a7d9203591177011bf059a44f8aaa9f8cd94720" and "589fdde401443612e199dab020c2717a3983ee61" have entirely different histories.
6a7d920359
...
589fdde401
16 changed files with 189 additions and 330 deletions
|
|
@ -1,27 +0,0 @@
|
||||||
linters:
|
|
||||||
presets:
|
|
||||||
- bugs
|
|
||||||
- error
|
|
||||||
- import
|
|
||||||
- metalinter
|
|
||||||
- module
|
|
||||||
- unused
|
|
||||||
|
|
||||||
enable:
|
|
||||||
- testifylint
|
|
||||||
|
|
||||||
disable:
|
|
||||||
# preset error
|
|
||||||
# These should probably be cleaned up at some point if we want to publish part of this as a library.
|
|
||||||
- err113 # Very annoying to define static errors everywhere
|
|
||||||
- wrapcheck # Very annoying to wrap errors everywhere
|
|
||||||
# preset import
|
|
||||||
- depguard
|
|
||||||
|
|
||||||
linters-settings:
|
|
||||||
gci:
|
|
||||||
sections:
|
|
||||||
- standard
|
|
||||||
- default
|
|
||||||
- localmodule
|
|
||||||
|
|
||||||
|
|
@ -12,19 +12,14 @@ inputs:
|
||||||
description: 'GitHub token for creating and grooming release PRs, defaults to using secrets.GITHUB_TOKEN'
|
description: 'GitHub token for creating and grooming release PRs, defaults to using secrets.GITHUB_TOKEN'
|
||||||
required: false
|
required: false
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
extra-files:
|
|
||||||
description: 'List of files that are scanned for version references.'
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
outputs: {}
|
outputs: {}
|
||||||
runs:
|
runs:
|
||||||
using: 'docker'
|
using: 'docker'
|
||||||
image: ghcr.io/apricote/releaser-pleaser:v0.1.0 # x-releaser-pleaser-version
|
image: ghcr.io/apricote/releaser-pleaser:v0.1.0
|
||||||
args:
|
args:
|
||||||
- run
|
- run
|
||||||
- --forge=github
|
- --forge=github
|
||||||
- --branch=${{ inputs.branch }}
|
- --branch=${{ inputs.branch }}
|
||||||
- --extra-files="${{ inputs.extra-files }}"
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ inputs.token }}
|
GITHUB_TOKEN: ${{ inputs.token }}
|
||||||
GITHUB_USER: "oauth2"
|
GITHUB_USER: "oauth2"
|
||||||
|
|
|
||||||
62
changelog.go
62
changelog.go
|
|
@ -3,8 +3,14 @@ package rp
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -14,6 +20,8 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
changelogTemplate *template.Template
|
changelogTemplate *template.Template
|
||||||
|
|
||||||
|
headerRegex = regexp.MustCompile(`^# Changelog\n`)
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed changelog.md.tpl
|
//go:embed changelog.md.tpl
|
||||||
|
|
@ -27,6 +35,60 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateChangelogFile(wt *git.Worktree, newEntry string) error {
|
||||||
|
file, err := wt.Filesystem.OpenFile(ChangelogFile, os.O_RDWR|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
content, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headerIndex := headerRegex.FindIndex(content)
|
||||||
|
if headerIndex == nil && len(content) != 0 {
|
||||||
|
return fmt.Errorf("unexpected format of CHANGELOG.md, header does not match")
|
||||||
|
}
|
||||||
|
if headerIndex != nil {
|
||||||
|
// Remove the header from the content
|
||||||
|
content = content[headerIndex[1]:]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = file.Write([]byte(ChangelogHeader + "\n\n" + newEntry))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = file.Write(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close file to make sure it is written to disk.
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = wt.Add(ChangelogFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) {
|
func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) {
|
||||||
features := make([]AnalyzedCommit, 0)
|
features := make([]AnalyzedCommit, 0)
|
||||||
fixes := make([]AnalyzedCommit, 0)
|
fixes := make([]AnalyzedCommit, 0)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,99 @@
|
||||||
package rp
|
package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ptr[T any](input T) *T {
|
func ptr[T any](input T) *T {
|
||||||
return &input
|
return &input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateChangelogFile(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
repoFn testutils.Repo
|
||||||
|
entry string
|
||||||
|
expectedContent string
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty repo",
|
||||||
|
repoFn: testutils.WithTestRepo(),
|
||||||
|
entry: "## v1.0.0\n",
|
||||||
|
expectedContent: "# Changelog\n\n## v1.0.0\n",
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repo with well-formatted changelog",
|
||||||
|
repoFn: testutils.WithTestRepo(testutils.WithCommit("feat: add changelog", testutils.WithFile(ChangelogFile, `# Changelog
|
||||||
|
|
||||||
|
## v0.0.1
|
||||||
|
|
||||||
|
- Bazzle
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
### Bazuuum
|
||||||
|
`))),
|
||||||
|
entry: "## v1.0.0\n\n- Version 1, juhu.\n",
|
||||||
|
expectedContent: `# Changelog
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
|
||||||
|
- Version 1, juhu.
|
||||||
|
|
||||||
|
## v0.0.1
|
||||||
|
|
||||||
|
- Bazzle
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
### Bazuuum
|
||||||
|
`,
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
repo := tt.repoFn(t)
|
||||||
|
wt, err := repo.Worktree()
|
||||||
|
require.NoError(t, err, "failed to get worktree")
|
||||||
|
|
||||||
|
err = UpdateChangelogFile(wt, tt.entry)
|
||||||
|
if !tt.wantErr(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wtStatus, err := wt.Status()
|
||||||
|
require.NoError(t, err, "failed to get worktree status")
|
||||||
|
|
||||||
|
assert.Len(t, wtStatus, 1, "worktree status does not have the expected entry number")
|
||||||
|
|
||||||
|
changelogFileStatus := wtStatus.File(ChangelogFile)
|
||||||
|
|
||||||
|
assert.Equal(t, git.Unmodified, changelogFileStatus.Worktree, "unexpected file status in worktree")
|
||||||
|
assert.Equal(t, git.Added, changelogFileStatus.Staging, "unexpected file status in staging")
|
||||||
|
|
||||||
|
changelogFile, err := wt.Filesystem.Open(ChangelogFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer changelogFile.Close()
|
||||||
|
|
||||||
|
changelogFileContent, err := io.ReadAll(changelogFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedContent, string(changelogFileContent))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_NewChangelogEntry(t *testing.T) {
|
func Test_NewChangelogEntry(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
analyzedCommits []AnalyzedCommit
|
analyzedCommits []AnalyzedCommit
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
rp "github.com/apricote/releaser-pleaser"
|
rp "github.com/apricote/releaser-pleaser"
|
||||||
|
|
@ -19,7 +17,6 @@ var (
|
||||||
flagBranch string
|
flagBranch string
|
||||||
flagOwner string
|
flagOwner string
|
||||||
flagRepo string
|
flagRepo string
|
||||||
flagExtraFiles string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -31,7 +28,6 @@ func init() {
|
||||||
runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "")
|
runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "")
|
||||||
runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "")
|
runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "")
|
||||||
runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "")
|
runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "")
|
||||||
runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(cmd *cobra.Command, _ []string) error {
|
func run(cmd *cobra.Command, _ []string) error {
|
||||||
|
|
@ -51,9 +47,9 @@ func run(cmd *cobra.Command, _ []string) error {
|
||||||
BaseBranch: flagBranch,
|
BaseBranch: flagBranch,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch flagForge { // nolint:gocritic // Will become a proper switch once gitlab is added
|
switch flagForge {
|
||||||
// case "gitlab":
|
//case "gitlab":
|
||||||
// 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{
|
forge = rp.NewGitHub(logger, &rp.GitHubOptions{
|
||||||
|
|
@ -63,31 +59,7 @@ func run(cmd *cobra.Command, _ []string) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
extraFiles := parseExtraFiles(flagExtraFiles)
|
releaserPleaser := rp.New(forge, logger, flagBranch, rp.NewConventionalCommitsParser(), rp.SemVerNextVersion)
|
||||||
|
|
||||||
releaserPleaser := rp.New(
|
|
||||||
forge,
|
|
||||||
logger,
|
|
||||||
flagBranch,
|
|
||||||
rp.NewConventionalCommitsParser(),
|
|
||||||
rp.SemVerNextVersion,
|
|
||||||
extraFiles,
|
|
||||||
[]rp.Updater{&rp.GenericUpdater{}},
|
|
||||||
)
|
|
||||||
|
|
||||||
return releaserPleaser.Run(ctx)
|
return releaserPleaser.Run(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseExtraFiles(input string) []string {
|
|
||||||
lines := strings.Split(input, "\n")
|
|
||||||
|
|
||||||
extraFiles := make([]string, 0, len(lines))
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if len(line) > 0 {
|
|
||||||
extraFiles = append(extraFiles, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return extraFiles
|
|
||||||
}
|
|
||||||
|
|
|
||||||
2
forge.go
2
forge.go
|
|
@ -19,7 +19,7 @@ const (
|
||||||
GitHubPerPageMax = 100
|
GitHubPerPageMax = 100
|
||||||
GitHubPRStateOpen = "open"
|
GitHubPRStateOpen = "open"
|
||||||
GitHubPRStateClosed = "closed"
|
GitHubPRStateClosed = "closed"
|
||||||
GitHubEnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential
|
GitHubEnvAPIToken = "GITHUB_TOKEN"
|
||||||
GitHubEnvUsername = "GITHUB_USER"
|
GitHubEnvUsername = "GITHUB_USER"
|
||||||
GitHubEnvRepository = "GITHUB_REPOSITORY"
|
GitHubEnvRepository = "GITHUB_REPOSITORY"
|
||||||
GitHubLabelColor = "dedede"
|
GitHubLabelColor = "dedede"
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -4,6 +4,7 @@ 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
|
||||||
|
|
@ -16,12 +17,11 @@ require (
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||||
github.com/cloudflare/circl v1.4.0 // indirect
|
github.com/cloudflare/circl v1.3.9 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.3.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.3.1 // indirect
|
||||||
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
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -15,8 +15,6 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
|
||||||
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
|
||||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE=
|
github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE=
|
||||||
github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc=
|
github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc=
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,8 @@ import (
|
||||||
"github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
"github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var sectionStartRegex = regexp.MustCompile(`^<!-- section-start (.+) -->`)
|
||||||
sectionStartRegex = regexp.MustCompile(`^<!-- section-start (.+) -->`)
|
var sectionEndRegex = regexp.MustCompile(`^<!-- section-end (.+) -->`)
|
||||||
sectionEndRegex = regexp.MustCompile(`^<!-- section-end (.+) -->`)
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sectionTrigger = "<!--"
|
sectionTrigger = "<!--"
|
||||||
|
|
@ -23,7 +21,8 @@ const (
|
||||||
SectionEndFormat = "<!-- section-end %s -->"
|
SectionEndFormat = "<!-- section-end %s -->"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sectionParser struct{}
|
type sectionParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) {
|
func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) {
|
||||||
line, _ := reader.PeekLine()
|
line, _ := reader.PeekLine()
|
||||||
|
|
@ -76,7 +75,8 @@ func (s *sectionParser) Trigger() []byte {
|
||||||
return []byte(sectionTrigger)
|
return []byte(sectionTrigger)
|
||||||
}
|
}
|
||||||
|
|
||||||
type section struct{}
|
type section struct {
|
||||||
|
}
|
||||||
|
|
||||||
// Section is an extension that allow you to use group content under a shared parent ast node.
|
// Section is an extension that allow you to use group content under a shared parent ast node.
|
||||||
var Section = §ion{}
|
var Section = §ion{}
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,7 @@ func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node a
|
||||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
}
|
}
|
||||||
if err := r.writeByte(w, '\n'); err != nil {
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
return ast.WalkStop, fmt.Errorf(": %w", err)
|
return ast.WalkStop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the contents of the fenced code block.
|
// Write the contents of the fenced code block.
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,25 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var author = &object.Signature{
|
var (
|
||||||
|
author = &object.Signature{
|
||||||
Name: "releaser-pleaser",
|
Name: "releaser-pleaser",
|
||||||
When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC),
|
When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC),
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type CommitOption func(*commitOptions)
|
type CommitOption func(*commitOptions)
|
||||||
|
|
||||||
type commitOptions struct {
|
type commitOptions struct {
|
||||||
cleanFiles bool
|
cleanFiles bool
|
||||||
files []commitFile
|
files []commitFile
|
||||||
tags []string
|
tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type commitFile struct {
|
type commitFile struct {
|
||||||
path string
|
path string
|
||||||
content string
|
content string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Commit func(*testing.T, *git.Repository) error
|
type Commit func(*testing.T, *git.Repository) error
|
||||||
|
|
||||||
type Repo func(*testing.T) *git.Repository
|
type Repo func(*testing.T) *git.Repository
|
||||||
|
|
||||||
func WithCommit(message string, options ...CommitOption) Commit {
|
func WithCommit(message string, options ...CommitOption) Commit {
|
||||||
|
|
@ -84,6 +83,7 @@ func WithCommit(message string, options ...CommitOption) Commit {
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,9 +155,6 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R
|
||||||
overrides.NextVersionType = NextVersionTypeBeta
|
overrides.NextVersionType = NextVersionTypeBeta
|
||||||
case LabelNextVersionTypeAlpha:
|
case LabelNextVersionTypeAlpha:
|
||||||
overrides.NextVersionType = NextVersionTypeAlpha
|
overrides.NextVersionType = NextVersionTypeAlpha
|
||||||
case LabelReleasePending, LabelReleaseTagged:
|
|
||||||
// These labels have no effect on the versioning.
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ package rp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/config"
|
"github.com/go-git/go-git/v5/config"
|
||||||
|
|
@ -22,19 +20,15 @@ type ReleaserPleaser struct {
|
||||||
targetBranch string
|
targetBranch string
|
||||||
commitParser CommitParser
|
commitParser CommitParser
|
||||||
nextVersion VersioningStrategy
|
nextVersion VersioningStrategy
|
||||||
extraFiles []string
|
|
||||||
updaters []Updater
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy, extraFiles []string, updaters []Updater) *ReleaserPleaser {
|
func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy) *ReleaserPleaser {
|
||||||
return &ReleaserPleaser{
|
return &ReleaserPleaser{
|
||||||
forge: forge,
|
forge: forge,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
targetBranch: targetBranch,
|
targetBranch: targetBranch,
|
||||||
commitParser: commitParser,
|
commitParser: commitParser,
|
||||||
nextVersion: versioningStrategy,
|
nextVersion: versioningStrategy,
|
||||||
extraFiles: extraFiles,
|
|
||||||
updaters: updaters,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,74 +236,21 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
|
||||||
return fmt.Errorf("failed to check out branch: %w", err)
|
return fmt.Errorf("failed to check out branch: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = RunUpdater(ctx, nextVersion, worktree)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update files with new version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
changelogEntry, err := NewChangelogEntry(analyzedCommits, nextVersion, rp.forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix)
|
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
|
err = UpdateChangelogFile(worktree, changelogEntry)
|
||||||
info := ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry}
|
|
||||||
|
|
||||||
updateFile := func(path string, updaters []Updater) error {
|
|
||||||
file, err := worktree.Filesystem.OpenFile(path, os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
content, err := io.ReadAll(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedContent := string(content)
|
|
||||||
|
|
||||||
for _, updater := range updaters {
|
|
||||||
updatedContent, err = updater.UpdateContent(updatedContent, info)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to run updater %T on file %s", updater, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = file.Truncate(0)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to replace file content: %w", err)
|
|
||||||
}
|
|
||||||
_, err = file.Seek(0, 0)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to replace file content: %w", err)
|
|
||||||
}
|
|
||||||
_, err = file.Write([]byte(updatedContent))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to replace file content: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = worktree.Add(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add updated file to git worktree: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = updateFile(ChangelogFile, []Updater{&ChangelogUpdater{}})
|
|
||||||
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 {
|
|
||||||
_, err = worktree.Filesystem.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: Check for non existing file or dirs
|
|
||||||
return fmt.Errorf("failed to run file updater because the file %s does not exist: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = updateFile(path, rp.updaters)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to run file updater: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion)
|
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion)
|
||||||
releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{
|
releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{
|
||||||
Author: GitSignature(),
|
Author: GitSignature(),
|
||||||
|
|
|
||||||
47
updater.go
47
updater.go
|
|
@ -1,47 +1,12 @@
|
||||||
package rp
|
package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"github.com/go-git/go-git/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func RunUpdater(ctx context.Context, version string, worktree *git.Worktree) error {
|
||||||
GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`)
|
// TODO: Implement updater for Go,Python,ExtraFilesMarkers
|
||||||
ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`)
|
return nil
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -45,9 +45,6 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump,
|
||||||
case conventionalcommits.MajorVersion:
|
case conventionalcommits.MajorVersion:
|
||||||
err = next.IncrementMajor()
|
err = next.IncrementMajor()
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch nextVersionType {
|
switch nextVersionType {
|
||||||
case NextVersionTypeUndefined, NextVersionTypeNormal:
|
case NextVersionTypeUndefined, NextVersionTypeNormal:
|
||||||
|
|
@ -65,6 +62,10 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump,
|
||||||
setPRVersion(&next, nextVersionType.String(), id)
|
setPRVersion(&next, nextVersionType.String(), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return "v" + next.String(), nil
|
return "v" + next.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue