refactor: move things to packages (#39)

This commit is contained in:
Julian Tölle 2024-08-31 15:23:21 +02:00 committed by GitHub
parent 44184a77f9
commit a0a064d387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 923 additions and 892 deletions

View file

@ -0,0 +1 @@
package releasepr

View file

@ -0,0 +1,258 @@
package releasepr
import (
"bytes"
_ "embed"
"fmt"
"log"
"regexp"
"strings"
"text/template"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
"github.com/apricote/releaser-pleaser/internal/git"
"github.com/apricote/releaser-pleaser/internal/markdown"
ast2 "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
"github.com/apricote/releaser-pleaser/internal/versioning"
)
var (
releasePRTemplate *template.Template
)
//go:embed releasepr.md.tpl
var rawReleasePRTemplate string
func init() {
var err error
releasePRTemplate, err = template.New("releasepr").Parse(rawReleasePRTemplate)
if err != nil {
log.Fatalf("failed to parse release pr template: %v", err)
}
}
// ReleasePullRequest
//
// TODO: Reuse [git.PullRequest]
type ReleasePullRequest struct {
ID int
Title string
Description string
Labels []Label
Head string
ReleaseCommit *git.Commit
}
// Label is the string identifier of a pull/merge request label on the forge.
type Label string
func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) {
rp := &ReleasePullRequest{
Head: head,
Labels: []Label{LabelReleasePending},
}
rp.SetTitle(branch, version)
if err := rp.SetDescription(changelogEntry, ReleaseOverrides{}); err != nil {
return nil, err
}
return rp, nil
}
type ReleaseOverrides struct {
Prefix string
Suffix string
NextVersionType versioning.NextVersionType
}
const (
LabelNextVersionTypeNormal Label = "rp-next-version::normal"
LabelNextVersionTypeRC Label = "rp-next-version::rc"
LabelNextVersionTypeBeta Label = "rp-next-version::beta"
LabelNextVersionTypeAlpha Label = "rp-next-version::alpha"
LabelReleasePending Label = "rp-release::pending"
LabelReleaseTagged Label = "rp-release::tagged"
)
var KnownLabels = []Label{
LabelNextVersionTypeNormal,
LabelNextVersionTypeRC,
LabelNextVersionTypeBeta,
LabelNextVersionTypeAlpha,
LabelReleasePending,
LabelReleaseTagged,
}
const (
DescriptionLanguagePrefix = "rp-prefix"
DescriptionLanguageSuffix = "rp-suffix"
)
const (
MarkdownSectionChangelog = "changelog"
)
const (
TitleFormat = "chore(%s): release %s"
)
var (
TitleRegex = regexp.MustCompile("chore(.*): release (.*)")
)
func (pr *ReleasePullRequest) GetOverrides() (ReleaseOverrides, error) {
overrides := ReleaseOverrides{}
overrides = pr.parseVersioningFlags(overrides)
overrides, err := pr.parseDescription(overrides)
if err != nil {
return ReleaseOverrides{}, err
}
return overrides, nil
}
func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) ReleaseOverrides {
for _, label := range pr.Labels {
switch label {
// Versioning
case LabelNextVersionTypeNormal:
overrides.NextVersionType = versioning.NextVersionTypeNormal
case LabelNextVersionTypeRC:
overrides.NextVersionType = versioning.NextVersionTypeRC
case LabelNextVersionTypeBeta:
overrides.NextVersionType = versioning.NextVersionTypeBeta
case LabelNextVersionTypeAlpha:
overrides.NextVersionType = versioning.NextVersionTypeAlpha
case LabelReleasePending, LabelReleaseTagged:
// These labels have no effect on the versioning.
break
}
}
return overrides
}
func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (ReleaseOverrides, error) {
source := []byte(pr.Description)
descriptionAST := markdown.New().Parser().Parse(text.NewReader(source))
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if n.Type() != ast.TypeBlock || n.Kind() != ast.KindFencedCodeBlock {
return ast.WalkContinue, nil
}
codeBlock, ok := n.(*ast.FencedCodeBlock)
if !ok {
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
}
switch string(codeBlock.Language(source)) {
case DescriptionLanguagePrefix:
overrides.Prefix = textFromLines(source, codeBlock)
case DescriptionLanguageSuffix:
overrides.Suffix = textFromLines(source, codeBlock)
}
return ast.WalkContinue, nil
})
if err != nil {
return ReleaseOverrides{}, err
}
return overrides, nil
}
func (pr *ReleasePullRequest) ChangelogText() (string, error) {
source := []byte(pr.Description)
gm := markdown.New()
descriptionAST := gm.Parser().Parse(text.NewReader(source))
var section *ast2.Section
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if n.Type() != ast.TypeBlock || n.Kind() != ast2.KindSection {
return ast.WalkContinue, nil
}
anySection, ok := n.(*ast2.Section)
if !ok {
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
}
if anySection.Name != MarkdownSectionChangelog {
return ast.WalkContinue, nil
}
section = anySection
return ast.WalkStop, nil
})
if err != nil {
return "", err
}
if section == nil {
return "", nil
}
outputBuffer := new(bytes.Buffer)
err = gm.Renderer().Render(outputBuffer, source, section)
if err != nil {
return "", err
}
return outputBuffer.String(), nil
}
func textFromLines(source []byte, n ast.Node) string {
content := make([]byte, 0)
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
content = append(content, line.Value(source)...)
}
return strings.TrimSpace(string(content))
}
func (pr *ReleasePullRequest) SetTitle(branch, version string) {
pr.Title = fmt.Sprintf(TitleFormat, branch, version)
}
func (pr *ReleasePullRequest) Version() (string, error) {
matches := TitleRegex.FindStringSubmatch(pr.Title)
if len(matches) != 3 {
return "", fmt.Errorf("title has unexpected format")
}
return matches[2], nil
}
func (pr *ReleasePullRequest) SetDescription(changelogEntry string, overrides ReleaseOverrides) error {
var description bytes.Buffer
err := releasePRTemplate.Execute(&description, map[string]any{
"Changelog": changelogEntry,
"Overrides": overrides,
})
if err != nil {
return err
}
pr.Description = description.String()
return nil
}

View file

@ -0,0 +1,32 @@
<!-- section-start changelog -->
{{ .Changelog }}
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
```rp-prefix
{{- if .Overrides.Prefix }}
{{ .Overrides.Prefix }}{{ end }}
```
### Suffix / End
This will be added to the end of the release notes.
```rp-suffix
{{- if .Overrides.Suffix }}
{{ .Overrides.Suffix }}{{ end }}
```
</details>

View file

@ -0,0 +1,144 @@
package releasepr
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestReleasePullRequest_SetTitle(t *testing.T) {
type args struct {
branch string
version string
}
tests := []struct {
name string
pr *ReleasePullRequest
args args
want string
}{
{
name: "simple update",
pr: &ReleasePullRequest{Title: "foo: bar"},
args: args{
branch: "main",
version: "v1.0.0",
},
want: "chore(main): release v1.0.0",
},
{
name: "no previous title",
pr: &ReleasePullRequest{},
args: args{
branch: "release-1.x",
version: "v1.1.1-rc.0",
},
want: "chore(release-1.x): release v1.1.1-rc.0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pr.SetTitle(tt.args.branch, tt.args.version)
assert.Equal(t, tt.want, tt.pr.Title)
})
}
}
func TestReleasePullRequest_SetDescription(t *testing.T) {
tests := []struct {
name string
changelogEntry string
overrides ReleaseOverrides
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "no overrides",
changelogEntry: `## v1.0.0`,
overrides: ReleaseOverrides{},
want: `<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
` + "```" + `rp-prefix
` + "```" + `
### Suffix / End
This will be added to the end of the release notes.
` + "```" + `rp-suffix
` + "```" + `
</details>
`,
wantErr: assert.NoError,
},
{
name: "existing overrides",
changelogEntry: `## v1.0.0`,
overrides: ReleaseOverrides{
Prefix: "This release is awesome!",
Suffix: "Fooo",
},
want: `<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
` + "```" + `rp-prefix
This release is awesome!
` + "```" + `
### Suffix / End
This will be added to the end of the release notes.
` + "```" + `rp-suffix
Fooo
` + "```" + `
</details>
`,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pr := &ReleasePullRequest{}
err := pr.SetDescription(tt.changelogEntry, tt.overrides)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, pr.Description)
})
}
}