feat(releasepr): release PRs can be updated

- PR Description
- Read prefix+suffix from PR description and put into changelog
- Keep those overrides on PR description changes
- Add pending level to new PRs
This commit is contained in:
Julian Tölle 2024-08-03 09:26:51 +02:00
parent cb529f4760
commit fe871a0213
17 changed files with 1442 additions and 22 deletions

View file

@ -90,7 +90,7 @@ func UpdateChangelogFile(wt *git.Worktree, newEntry string) error {
return nil return nil
} }
func NewChangelogEntry(changesets []Changeset, version, link string) (string, error) { func NewChangelogEntry(changesets []Changeset, version, link, prefix, suffix string) (string, error) {
features := make([]AnalyzedCommit, 0) features := make([]AnalyzedCommit, 0)
fixes := make([]AnalyzedCommit, 0) fixes := make([]AnalyzedCommit, 0)
@ -111,6 +111,8 @@ func NewChangelogEntry(changesets []Changeset, version, link string) (string, er
"Fixes": fixes, "Fixes": fixes,
"Version": version, "Version": version,
"VersionLink": link, "VersionLink": link,
"Prefix": prefix,
"Suffix": suffix,
}) })
if err != nil { if err != nil {
return "", err return "", err

View file

@ -1,4 +1,7 @@
## [{{.Version}}]({{.VersionLink}}) ## [{{.Version}}]({{.VersionLink}})
{{- if .Prefix }}
{{ .Prefix }}
{{ end -}}
{{- if (gt (len .Features) 0) }} {{- if (gt (len .Features) 0) }}
### Features ### Features
@ -13,3 +16,7 @@
- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}} - {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}}
{{ end -}} {{ end -}}
{{- end -}} {{- end -}}
{{- if .Suffix }}
{{ .Suffix }}
{{ end -}}

View file

@ -99,6 +99,8 @@ func Test_NewChangelogEntry(t *testing.T) {
changesets []Changeset changesets []Changeset
version string version string
link string link string
prefix string
suffix string
} }
tests := []struct { tests := []struct {
name string name string
@ -188,6 +190,54 @@ func Test_NewChangelogEntry(t *testing.T) {
- Foobar! - Foobar!
- **sad**: So sad! - **sad**: So sad!
`,
wantErr: assert.NoError,
},
{
name: "prefix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
}}},
version: "1.0.0",
link: "https://example.com/1.0.0",
prefix: "### Breaking Changes",
},
want: `## [1.0.0](https://example.com/1.0.0)
### Breaking Changes
### Bug Fixes
- Foobar!
`,
wantErr: assert.NoError,
},
{
name: "suffix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
}}},
version: "1.0.0",
link: "https://example.com/1.0.0",
suffix: "### Compatibility\n\nThis version is compatible with flux-compensator v2.2 - v2.9.",
},
want: `## [1.0.0](https://example.com/1.0.0)
### Bug Fixes
- Foobar!
### Compatibility
This version is compatible with flux-compensator v2.2 - v2.9.
`, `,
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
@ -195,7 +245,7 @@ func Test_NewChangelogEntry(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) {
got, err := NewChangelogEntry(tt.args.changesets, tt.args.version, tt.args.link) got, err := NewChangelogEntry(tt.args.changesets, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
if !tt.wantErr(t, err) { if !tt.wantErr(t, err) {
return return
} }

View file

@ -167,7 +167,7 @@ func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Cha
return err return err
} }
changelogEntry, err := rp.NewChangelogEntry(changesets, nextVersion, forge.ReleaseURL(nextVersion)) changelogEntry, err := rp.NewChangelogEntry(changesets, nextVersion, forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix)
if err != nil { if err != nil {
return err return err
} }
@ -237,18 +237,28 @@ func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Cha
// Open/Update PR // Open/Update PR
if pr == nil { if pr == nil {
pr = &rp.ReleasePullRequest{ pr, err = rp.NewReleasePullRequest(rpBranch, flagBranch, nextVersion, changelogEntry)
Title: releaseCommitMessage, if err != nil {
Description: "TODO", return err
Labels: nil,
Head: rpBranch,
} }
pr, err = forge.CreatePullRequest(ctx, pr) err = forge.CreatePullRequest(ctx, pr)
if err != nil { if err != nil {
return err return err
} }
logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID) logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID)
} else {
pr.SetTitle(flagBranch, nextVersion)
err = pr.SetDescription(changelogEntry)
if err != nil {
return err
}
err = forge.UpdatePullRequest(ctx, pr)
if err != nil {
return err
}
logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID)
} }
return nil return nil

View file

@ -49,7 +49,8 @@ type Forge interface {
// exists, it returns nil. // exists, it returns nil.
PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error) PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error)
CreatePullRequest(context.Context, *ReleasePullRequest) (*ReleasePullRequest, error) CreatePullRequest(context.Context, *ReleasePullRequest) error
UpdatePullRequest(context.Context, *ReleasePullRequest) error
} }
type ForgeOptions struct { type ForgeOptions struct {
@ -332,7 +333,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele
} }
for _, pr := range prs { for _, pr := range prs {
if pr.GetBase().GetLabel() == g.options.BaseBranch && pr.GetHead().GetLabel() == branch && pr.GetState() == GitHubPRStateOpen { if pr.GetBase().GetRef() == g.options.BaseBranch && pr.GetHead().GetRef() == branch && pr.GetState() == GitHubPRStateOpen {
labels := make([]string, 0, len(pr.Labels)) labels := make([]string, 0, len(pr.Labels))
for _, label := range pr.Labels { for _, label := range pr.Labels {
labels = append(labels, label.GetName()) labels = append(labels, label.GetName())
@ -343,7 +344,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele
Title: pr.GetTitle(), Title: pr.GetTitle(),
Description: pr.GetBody(), Description: pr.GetBody(),
Labels: labels, Labels: labels,
Head: pr.GetHead().GetLabel(), Head: pr.GetHead().GetRef(),
}, nil }, nil
} }
} }
@ -357,7 +358,8 @@ 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) (*ReleasePullRequest, error) { func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest) error {
// TODO: Labels
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{
@ -368,12 +370,36 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest)
}, },
) )
if err != nil { if err != nil {
return nil, err return err
} }
pr.ID = int(*ghPR.ID) // TODO: String ID? _, _, err = g.client.Issues.AddLabelsToIssue(
ctx, g.options.Owner, g.options.Repo,
ghPR.GetNumber(), pr.Labels,
)
if err != nil {
return err
}
return pr, nil // TODO: String ID?
pr.ID = ghPR.GetNumber()
return nil
}
func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest) error {
_, _, err := g.client.PullRequests.Edit(
ctx, g.options.Owner, g.options.Repo, pr.ID,
&github.PullRequest{
Title: &pr.Title,
Body: &pr.Description,
},
)
if err != nil {
return err
}
return nil
} }
func (g *GitHubOptions) autodiscover() { func (g *GitHubOptions) autodiscover() {

2
go.mod
View file

@ -10,7 +10,7 @@ require (
github.com/leodido/go-conventionalcommits v0.12.0 github.com/leodido/go-conventionalcommits v0.12.0
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/yuin/goldmark v1.7.3 github.com/yuin/goldmark v1.7.4
) )
require ( require (

4
go.sum
View file

@ -90,8 +90,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.3 h1:fdk0a/y60GsS4NbEd13GSIP+d8OjtTkmluY32Dy1Z/A= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.3/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=

View file

@ -0,0 +1,32 @@
package ast
import (
gast "github.com/yuin/goldmark/ast"
)
// A Section struct represents a section of elements.
type Section struct {
gast.BaseBlock
Name string
}
// Dump implements Node.Dump.
func (n *Section) Dump(source []byte, level int) {
m := map[string]string{
"Name": n.Name,
}
gast.DumpHelper(n, source, level, m, nil)
}
// KindSection is a NodeKind of the Section node.
var KindSection = gast.NewNodeKind("Section")
// Kind implements Node.Kind.
func (n *Section) Kind() gast.NodeKind {
return KindSection
}
// NewSection returns a new Section node.
func NewSection(name string) *Section {
return &Section{Name: name}
}

View file

@ -0,0 +1,88 @@
package extensions
import (
"regexp"
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
)
var sectionStartRegex = regexp.MustCompile(`^<!-- section-start (.+) -->`)
var sectionEndRegex = regexp.MustCompile(`^<!-- section-end (.+) -->`)
const (
sectionTrigger = "<!--"
SectionStartFormat = "<!-- section-start %s -->"
SectionEndFormat = "<!-- section-end %s -->"
)
type sectionParser struct {
}
func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) {
line, _ := reader.PeekLine()
if result := sectionStartRegex.FindSubmatch(line); result != nil {
reader.AdvanceLine()
return ast.NewSection(string(result[1])), parser.HasChildren
}
return nil, parser.NoChildren
}
func (s *sectionParser) Continue(node gast.Node, reader text.Reader, _ parser.Context) parser.State {
n := node.(*ast.Section)
line, _ := reader.PeekLine()
if result := sectionEndRegex.FindSubmatch(line); result != nil {
if string(result[1]) == n.Name {
reader.AdvanceLine()
return parser.Close
}
}
return parser.Continue | parser.HasChildren
}
func (s *sectionParser) Close(_ gast.Node, _ text.Reader, _ parser.Context) {
// Nothing to do
}
func (s *sectionParser) CanInterruptParagraph() bool {
return true
}
func (s *sectionParser) CanAcceptIndentedLine() bool {
return false
}
var defaultSectionParser = &sectionParser{}
// NewSectionParser returns a new BlockParser that can parse
// a section block. Section blocks can be used to group various nodes under a parent ast node.
// This parser must take precedence over the parser.HTMLParser.
func NewSectionParser() parser.BlockParser {
return defaultSectionParser
}
func (s *sectionParser) Trigger() []byte {
return []byte(sectionTrigger)
}
type section struct {
}
// Section is an extension that allow you to use group content under a shared parent ast node.
var Section = &section{}
func (e *section) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithBlockParsers(
util.Prioritized(NewSectionParser(), 0),
))
}

View file

@ -0,0 +1,17 @@
package markdown
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
"github.com/apricote/releaser-pleaser/internal/markdown/renderer/markdown"
)
func New() goldmark.Markdown {
return goldmark.New(
goldmark.WithExtensions(extensions.Section),
goldmark.WithRenderer(renderer.NewRenderer(renderer.WithNodeRenderers(util.Prioritized(markdown.NewRenderer(), 1)))),
)
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Rolf Lewis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,4 @@
This directory is a vendored copy of https://github.com/RolfLewis/goldmark-down/blob/main/markdown.go.
The original repository is set to a `main` package which can not be imported.
It is under the MIT license.

View file

@ -0,0 +1,836 @@
package markdown
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"github.com/yuin/goldmark/ast"
exast "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
rpexast "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
)
type blockState struct {
node ast.Node
fresh bool
}
type listState struct {
marker byte
ordered bool
index int
}
type Renderer struct {
listStack []listState
openBlocks []blockState
prefixStack []string
prefix []byte
atNewline bool
}
// NewRenderer returns a new Renderer with given options.
func NewRenderer() renderer.NodeRenderer {
r := &Renderer{}
return r
}
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// default registrations
// blocks
reg.Register(ast.KindDocument, r.renderDocument)
reg.Register(ast.KindHeading, r.renderHeading)
reg.Register(ast.KindBlockquote, r.renderBlockquote)
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
reg.Register(ast.KindList, r.renderList)
reg.Register(ast.KindListItem, r.renderListItem)
reg.Register(ast.KindParagraph, r.renderParagraph)
reg.Register(ast.KindTextBlock, r.renderTextBlock)
reg.Register(ast.KindThematicBreak, r.renderThematicBreak)
// inlines
reg.Register(ast.KindAutoLink, r.renderAutoLink)
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
reg.Register(ast.KindEmphasis, r.renderEmphasis)
reg.Register(ast.KindImage, r.renderImage)
reg.Register(ast.KindLink, r.renderLink)
reg.Register(ast.KindRawHTML, r.renderRawHTML)
reg.Register(ast.KindText, r.renderText)
reg.Register(ast.KindString, r.renderString)
// GFM Extensions
// Tables
reg.Register(exast.KindTable, r.renderTable)
reg.Register(exast.KindTableHeader, r.renderTableHeader)
reg.Register(exast.KindTableRow, r.renderTableRow)
reg.Register(exast.KindTableCell, r.renderTableCell)
// Strikethrough
reg.Register(exast.KindStrikethrough, r.renderStrikethrough)
// Checkbox
reg.Register(exast.KindTaskCheckBox, r.renderTaskCheckBox)
// releaser-pleaser Extensions
// Section
reg.Register(rpexast.KindSection, r.renderSection)
}
func (r *Renderer) write(w io.Writer, buf []byte) (int, error) {
written := 0
for len(buf) > 0 {
if r.atNewline {
if err := r.beginLine(w); err != nil {
return 0, fmt.Errorf(": %w", err)
}
}
atNewline := false
newline := bytes.IndexByte(buf, '\n')
if newline == -1 {
newline = len(buf) - 1
} else {
atNewline = true
}
n, err := w.Write(buf[:newline+1])
written += n
r.atNewline = n > 0 && atNewline && n == newline+1
if len(r.openBlocks) != 0 {
r.openBlocks[len(r.openBlocks)-1].fresh = false
}
if err != nil {
return written, fmt.Errorf(": %w", err)
}
buf = buf[n:]
}
return written, nil
}
func (r *Renderer) beginLine(w io.Writer) error {
if len(r.openBlocks) != 0 {
current := r.openBlocks[len(r.openBlocks)-1]
if current.node.Kind() == ast.KindParagraph && !current.fresh {
return nil
}
}
n, err := w.Write(r.prefix)
if n != 0 {
r.atNewline = r.prefix[len(r.prefix)-1] == '\n'
}
if err != nil {
return fmt.Errorf(": %w", err)
}
return nil
}
func (r *Renderer) writeLines(w util.BufWriter, source []byte, lines *text.Segments) error {
for i := 0; i < lines.Len(); i++ {
line := lines.At(i)
if _, err := r.write(w, line.Value(source)); err != nil {
return fmt.Errorf(": %w", err)
}
}
return nil
}
func (r *Renderer) writeByte(w io.Writer, c byte) error {
if _, err := r.write(w, []byte{c}); err != nil {
return fmt.Errorf(": %w", err)
}
return nil
}
// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefices are added at the
// beginning of each line.
func (r *Renderer) writeString(w io.Writer, s string) (int, error) {
n, err := r.write(w, []byte(s))
if err != nil {
return n, fmt.Errorf(": %w", err)
}
return n, nil
}
// PushIndent adds the specified amount of indentation to the current line prefix.
func (r *Renderer) pushIndent(amount int) {
r.pushPrefix(strings.Repeat(" ", amount))
}
// PushPrefix adds the specified string to the current line prefix.
func (r *Renderer) pushPrefix(prefix string) {
r.prefixStack = append(r.prefixStack, prefix)
r.prefix = append(r.prefix, []byte(prefix)...)
}
// PopPrefix removes the last piece added by a call to PushIndent or PushPrefix from the current line prefix.
func (r *Renderer) popPrefix() {
r.prefix = r.prefix[:len(r.prefix)-len(r.prefixStack[len(r.prefixStack)-1])]
r.prefixStack = r.prefixStack[:len(r.prefixStack)-1]
}
// OpenBlock ensures that each block begins on a new line, and that blank lines are inserted before blocks as
// indicated by node.HasPreviousBlankLines.
func (r *Renderer) openBlock(w util.BufWriter, source []byte, node ast.Node) error {
r.openBlocks = append(r.openBlocks, blockState{
node: node,
fresh: true,
})
hasBlankPreviousLines := node.HasBlankPreviousLines()
// FIXME: standard goldmark table parser doesn't recognize Blank Previous Lines so we'll always add one
if node.Kind() == exast.KindTable {
hasBlankPreviousLines = true
}
// Work around the fact that the first child of a node notices the same set of preceding blank lines as its parent.
if p := node.Parent(); p != nil && p.FirstChild() == node {
if p.Kind() == ast.KindDocument || p.Kind() == ast.KindListItem || p.HasBlankPreviousLines() {
hasBlankPreviousLines = false
}
}
if hasBlankPreviousLines {
if err := r.writeByte(w, '\n'); err != nil {
return fmt.Errorf(": %w", err)
}
}
r.openBlocks[len(r.openBlocks)-1].fresh = true
return nil
}
// CloseBlock marks the current block as closed.
func (r *Renderer) closeBlock(w io.Writer) error {
if !r.atNewline {
if err := r.writeByte(w, '\n'); err != nil {
return fmt.Errorf(": %w", err)
}
}
r.openBlocks = r.openBlocks[:len(r.openBlocks)-1]
return nil
}
// RenderDocument renders an *ast.Document node to the given BufWriter.
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
r.listStack, r.prefixStack, r.prefix, r.atNewline = nil, nil, nil, false
return ast.WalkContinue, nil
}
// RenderHeading renders an *ast.Heading node to the given BufWriter.
func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := r.writeString(w, strings.Repeat("#", node.(*ast.Heading).Level)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.writeByte(w, ' '); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
} else {
if err := r.writeByte(w, '\n'); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderBlockquote renders an *ast.Blockquote node to the given BufWriter.
func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := r.writeString(w, "> "); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
r.pushPrefix("> ")
} else {
r.popPrefix()
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderCodeBlock renders an *ast.CodeBlock node to the given BufWriter.
func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
r.popPrefix()
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
// // Each line of a code block needs to be aligned at the same offset, and a code block must start with at least four
// // spaces. To achieve this, we unconditionally add four spaces to the first line of the code block and indent the
// // rest as necessary.
// if _, err := r.writeString(w, " "); err != nil {
// return ast.WalkStop, fmt.Errorf(": %w", err)
// }
r.pushIndent(4)
if err := r.writeLines(w, source, node.Lines()); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
// RenderFencedCodeBlock renders an *ast.FencedCodeBlock node to the given BufWriter.
func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
code := node.(*ast.FencedCodeBlock)
// Write the start of the fenced code block.
fence := []byte("```")
if _, err := r.write(w, fence); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
language := code.Language(source)
if _, err := r.write(w, language); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.writeByte(w, '\n'); err != nil {
return ast.WalkStop, nil
}
// Write the contents of the fenced code block.
if err := r.writeLines(w, source, node.Lines()); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
// Write the end of the fenced code block.
if err := r.beginLine(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := r.write(w, fence); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.writeByte(w, '\n'); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
// RenderHTMLBlock renders an *ast.HTMLBlock node to the given BufWriter.
func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
// Write the contents of the HTML block.
if err := r.writeLines(w, source, node.Lines()); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
// Write the closure line, if any.
html := node.(*ast.HTMLBlock)
if html.HasClosure() {
if _, err := r.write(w, html.ClosureLine.Value(source)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderList renders an *ast.List node to the given BufWriter.
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
list := node.(*ast.List)
r.listStack = append(r.listStack, listState{
marker: list.Marker,
ordered: list.IsOrdered(),
index: list.Start,
})
} else {
r.listStack = r.listStack[:len(r.listStack)-1]
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderListItem renders an *ast.ListItem node to the given BufWriter.
func (r *Renderer) renderListItem(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
markerWidth := 2 // marker + space
state := &r.listStack[len(r.listStack)-1]
if state.ordered {
width, err := r.writeString(w, strconv.FormatInt(int64(state.index), 10))
if err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
state.index++
markerWidth += width // marker, space, and digits
}
if _, err := r.write(w, []byte{state.marker, ' '}); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
r.pushIndent(markerWidth)
} else {
r.popPrefix()
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderParagraph renders an *ast.Paragraph node to the given BufWriter.
func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
// A paragraph that follows another paragraph or a blockquote must be preceded by a blank line.
if !node.HasBlankPreviousLines() {
if prev := node.PreviousSibling(); prev != nil && (prev.Kind() == ast.KindParagraph || prev.Kind() == ast.KindBlockquote) {
if err := r.writeByte(w, '\n'); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
}
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
} else {
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderTextBlock renders an *ast.TextBlock node to the given BufWriter.
func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
} else {
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderThematicBreak renders an *ast.ThematicBreak node to the given BufWriter.
func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
// TODO: this prints an extra no line
if _, err := r.writeString(w, "--------"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
// RenderAutoLink renders an *ast.AutoLink node to the given BufWriter.
func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
if err := r.writeByte(w, '<'); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := r.write(w, node.(*ast.AutoLink).Label(source)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.writeByte(w, '>'); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
func (r *Renderer) shouldPadCodeSpan(source []byte, node *ast.CodeSpan) bool {
c := node.FirstChild()
if c == nil {
return false
}
segment := c.(*ast.Text).Segment
text := segment.Value(source)
var firstChar byte
if len(text) > 0 {
firstChar = text[0]
}
allWhitespace := true
for {
if util.FirstNonSpacePosition(text) != -1 {
allWhitespace = false
break
}
c = c.NextSibling()
if c == nil {
break
}
segment = c.(*ast.Text).Segment
text = segment.Value(source)
}
if allWhitespace {
return false
}
var lastChar byte
if len(text) > 0 {
lastChar = text[len(text)-1]
}
return firstChar == '`' || firstChar == ' ' || lastChar == '`' || lastChar == ' '
}
// RenderCodeSpan renders an *ast.CodeSpan node to the given BufWriter.
func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
code := node.(*ast.CodeSpan)
delimiter := []byte{'`'}
pad := r.shouldPadCodeSpan(source, code)
if _, err := r.write(w, delimiter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if pad {
if err := r.writeByte(w, ' '); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
text := c.(*ast.Text).Segment
if _, err := r.write(w, text.Value(source)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
if pad {
if err := r.writeByte(w, ' '); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
if _, err := r.write(w, delimiter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkSkipChildren, nil
}
// RenderEmphasis renders an *ast.Emphasis node to the given BufWriter.
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
em := node.(*ast.Emphasis)
if _, err := r.writeString(w, strings.Repeat("*", em.Level)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
func (r *Renderer) escapeLinkDest(dest []byte) []byte {
requiresEscaping := false
for _, c := range dest {
if c <= 32 || c == '(' || c == ')' || c == 127 {
requiresEscaping = true
break
}
}
if !requiresEscaping {
return dest
}
escaped := make([]byte, 0, len(dest)+2)
escaped = append(escaped, '<')
for _, c := range dest {
if c == '<' || c == '>' {
escaped = append(escaped, '\\')
}
escaped = append(escaped, c)
}
escaped = append(escaped, '>')
return escaped
}
func (r *Renderer) linkTitleDelimiter(title []byte) byte {
for i, c := range title {
if c == '"' && (i == 0 || title[i-1] != '\\') {
return '\''
}
}
return '"'
}
func (r *Renderer) renderLinkOrImage(w util.BufWriter, open string, dest, title []byte, enter bool) error {
if enter {
if _, err := r.writeString(w, open); err != nil {
return fmt.Errorf(": %w", err)
}
} else {
if _, err := r.writeString(w, "]("); err != nil {
return fmt.Errorf(": %w", err)
}
if _, err := r.write(w, r.escapeLinkDest(dest)); err != nil {
return fmt.Errorf(": %w", err)
}
if len(title) != 0 {
delimiter := r.linkTitleDelimiter(title)
if _, err := fmt.Fprintf(w, ` %c%s%c`, delimiter, string(title), delimiter); err != nil {
return fmt.Errorf(": %w", err)
}
}
if err := r.writeByte(w, ')'); err != nil {
return fmt.Errorf(": %w", err)
}
}
return nil
}
// RenderImage renders an *ast.Image node to the given BufWriter.
func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
img := node.(*ast.Image)
if err := r.renderLinkOrImage(w, "![", img.Destination, img.Title, enter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
// RenderLink renders an *ast.Link node to the given BufWriter.
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
link := node.(*ast.Link)
if err := r.renderLinkOrImage(w, "[", link.Destination, link.Title, enter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
// RenderRawHTML renders an *ast.RawHTML node to the given BufWriter.
func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkSkipChildren, nil
}
raw := node.(*ast.RawHTML)
for i := 0; i < raw.Segments.Len(); i++ {
segment := raw.Segments.At(i)
if _, err := r.write(w, segment.Value(source)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkSkipChildren, nil
}
// RenderText renders an *ast.Text node to the given BufWriter.
func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
text := node.(*ast.Text)
value := text.Segment.Value(source)
if _, err := r.write(w, value); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
switch {
case text.HardLineBreak():
if _, err := r.writeString(w, "\\\n"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
case text.SoftLineBreak():
if err := r.writeByte(w, '\n'); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
// RenderString renders an *ast.String node to the given BufWriter.
func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
str := node.(*ast.String)
if _, err := r.write(w, str.Value); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
func (r *Renderer) renderTable(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
} else {
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
func (r *Renderer) renderTableHeader(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := r.writeString(w, "| "); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
} else {
if _, err := r.writeString(w, " |\n|"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
for x := 0; x < node.ChildCount(); x++ { // use as column count
if _, err := r.writeString(w, " --- |"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
func (r *Renderer) renderTableRow(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := r.writeString(w, "| "); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
} else {
if _, err := r.writeString(w, " |"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}
func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
if node.NextSibling() != nil {
if _, err := r.writeString(w, " | "); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
}
return ast.WalkContinue, nil
}
func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if _, err := r.writeString(w, "~~"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
func (r *Renderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
var fill byte = ' '
if task := node.(*exast.TaskCheckBox); task.IsChecked {
fill = 'x'
}
if _, err := r.write(w, []byte{'[', fill, ']', ' '}); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}

View file

@ -0,0 +1,35 @@
package markdown
import (
"fmt"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/util"
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
rpexast "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
)
func (r *Renderer) renderSection(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
n := node.(*rpexast.Section)
if enter {
if err := r.openBlock(w, source, node); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := r.writeString(w, fmt.Sprintf(extensions.SectionStartFormat, n.Name)+"\n"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
} else {
if _, err := r.writeString(w, "\n"+fmt.Sprintf(extensions.SectionEndFormat, n.Name)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.closeBlock(w); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
}
return ast.WalkContinue, nil
}

View file

@ -1,13 +1,34 @@
package rp package rp
import ( import (
"bytes"
_ "embed"
"fmt" "fmt"
"log"
"text/template"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text" "github.com/yuin/goldmark/text"
"github.com/apricote/releaser-pleaser/internal/markdown"
east "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
) )
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)
}
}
type ReleasePullRequest struct { type ReleasePullRequest struct {
ID int ID int
Title string Title string
@ -17,6 +38,20 @@ type ReleasePullRequest struct {
Head string Head string
} }
func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) {
rp := &ReleasePullRequest{
Head: head,
Labels: []string{LabelReleasePending},
}
rp.SetTitle(branch, version)
if err := rp.SetDescription(changelogEntry); err != nil {
return nil, err
}
return rp, nil
}
type ReleaseOverrides struct { type ReleaseOverrides struct {
Prefix string Prefix string
Suffix string Suffix string
@ -57,6 +92,9 @@ const (
LabelNextVersionTypeRC = "rp-next-version::rc" LabelNextVersionTypeRC = "rp-next-version::rc"
LabelNextVersionTypeBeta = "rp-next-version::beta" LabelNextVersionTypeBeta = "rp-next-version::beta"
LabelNextVersionTypeAlpha = "rp-next-version::alpha" LabelNextVersionTypeAlpha = "rp-next-version::alpha"
LabelReleasePending = "rp-release::pending"
LabelReleaseTagged = "rp-release::tagged"
) )
const ( const (
@ -64,6 +102,10 @@ const (
DescriptionLanguageSuffix = "rp-suffix" DescriptionLanguageSuffix = "rp-suffix"
) )
const (
MarkdownSectionOverrides = "overrides"
)
func (pr *ReleasePullRequest) GetOverrides() (ReleaseOverrides, error) { func (pr *ReleasePullRequest) GetOverrides() (ReleaseOverrides, error) {
overrides := ReleaseOverrides{} overrides := ReleaseOverrides{}
overrides = pr.parseVersioningFlags(overrides) overrides = pr.parseVersioningFlags(overrides)
@ -95,7 +137,7 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R
func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (ReleaseOverrides, error) { func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (ReleaseOverrides, error) {
source := []byte(pr.Description) source := []byte(pr.Description)
descriptionAST := parser.NewParser().Parse(text.NewReader(source)) descriptionAST := markdown.New().Parser().Parse(text.NewReader(source))
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 {
@ -127,6 +169,51 @@ func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (Rele
return overrides, nil return overrides, nil
} }
func (pr *ReleasePullRequest) getCurrentOverridesText() (string, error) {
source := []byte(pr.Description)
gm := markdown.New()
descriptionAST := gm.Parser().Parse(text.NewReader(source))
var section *east.Section
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if n.Type() != ast.TypeBlock || n.Kind() != east.KindSection {
return ast.WalkContinue, nil
}
anySection, ok := n.(*east.Section)
if !ok {
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
}
if anySection.Name != MarkdownSectionOverrides {
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 { func textFromLines(source []byte, n ast.Node) string {
content := make([]byte, 0) content := make([]byte, 0)
@ -137,5 +224,28 @@ func textFromLines(source []byte, n ast.Node) string {
} }
return string(content) return string(content)
}
func (pr *ReleasePullRequest) SetTitle(branch, version string) {
pr.Title = fmt.Sprintf("chore(%s): release %s", branch, version)
}
func (pr *ReleasePullRequest) SetDescription(changelogEntry string) error {
overrides, err := pr.getCurrentOverridesText()
if err != nil {
return err
}
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
} }

25
releasepr.md.tpl Normal file
View file

@ -0,0 +1,25 @@
{{ .Changelog }}
---
## releaser-pleaser Instructions
{{ if .Overrides }}
{{- .Overrides -}}
{{- else }}
<!-- section-start overrides -->
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
### Prefix
```rp-prefix
```
### Suffix
```rp-suffix
```
<!-- section-end overrides -->
{{ end }}
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)

157
releasepr_test.go Normal file
View file

@ -0,0 +1,157 @@
package rp
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
pr *ReleasePullRequest
changelogEntry string
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "empty description",
pr: &ReleasePullRequest{},
changelogEntry: `## v1.0.0`,
want: `## v1.0.0
---
## releaser-pleaser Instructions
<!-- section-start overrides -->
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
### Prefix
` + "```" + `rp-prefix
` + "```" + `
### Suffix
` + "```" + `rp-suffix
` + "```" + `
<!-- section-end overrides -->
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
`,
wantErr: assert.NoError,
},
{
name: "existing overrides",
pr: &ReleasePullRequest{
Description: `## v0.1.0
### Features
- bedazzle
---
## releaser-pleaser Instructions
<!-- section-start overrides -->
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
### Prefix
` + "```" + `rp-prefix
This release is awesome!
` + "```" + `
### Suffix
` + "```" + `rp-suffix
` + "```" + `
<!-- section-end overrides -->
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
`,
},
changelogEntry: `## v1.0.0`,
want: `## v1.0.0
---
## releaser-pleaser Instructions
<!-- section-start overrides -->
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
### Prefix
` + "```" + `rp-prefix
This release is awesome!
` + "```" + `
### Suffix
` + "```" + `rp-suffix
` + "```" + `
<!-- section-end overrides -->
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
`,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.pr.SetDescription(tt.changelogEntry)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, tt.pr.Description)
})
}
}