mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-01-13 13:21:00 +00:00
parent
36a0b90bcd
commit
2effe5e72d
12 changed files with 420 additions and 40 deletions
3
docs/guides/release-notes-rp-commits-release-pr.png
Normal file
3
docs/guides/release-notes-rp-commits-release-pr.png
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c28226eaa769033a45ca801f1e0655178faf86e7ddd764f470ae79d72c4b3c2
|
||||
size 62031
|
||||
3
docs/guides/release-notes-rp-commits.png
Normal file
3
docs/guides/release-notes-rp-commits.png
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04ca48b3250862d282dd54e14c08f9273ada0a49d2300364601799c56b1f6d11
|
||||
size 72105
|
||||
|
|
@ -4,7 +4,34 @@ You can customize the generated Release Notes in two ways:
|
|||
|
||||
## For a single commit / pull request
|
||||
|
||||
This feature is still being worked on. Check out [#5](https://github.com/apricote/releaser-pleaser/issues/5) for the current status.
|
||||
### Editing the Release Notes
|
||||
|
||||
After merging a non-release pull request, you can still modify how it appears in the Release Notes.
|
||||
|
||||
To do this, add a code block named `rp-commits` in the pull request description. When this block is present, `releaser-pleaser` will use its content for generating Release Notes instead of the commit message. If the code block contains multiple lines, each line will be treated as if it came from separate pull requests. This is useful for pull requests that introduce multiple features or fix several bugs.
|
||||
|
||||
You can update the description at any time after merging the pull request but before merging the release pull request. `releaser-pleaser` will then re-run and update the suggested Release Notes accordingly.
|
||||
|
||||
> ```rp-commits
|
||||
> feat(api): add movie endpoints
|
||||
> feat(api): add cinema endpoints
|
||||
> fix(db): invalid schema for actor model
|
||||
> ```
|
||||
|
||||
Using GitHub as an example, the pull request you are trying to change the Release Notes for should look like this:
|
||||
|
||||

|
||||
|
||||
In turn, `releaser-pleaser` updates the release pull request like this:
|
||||
|
||||

|
||||
|
||||
### Removing the pull request from the Release Notes
|
||||
|
||||
If you add an empty code block, the pull request will be removed from the Release Notes.
|
||||
|
||||
> ```rp-commits
|
||||
> ```
|
||||
|
||||
## For the release
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,20 @@ Adding more than one of these labels is not allowed and the behaviour if multipl
|
|||
|
||||
Any text in code blocks with these languages is being added to the start or end of the Release Notes and Changelog. Learn more in the [Release Notes](../guides/release-notes.md) guide.
|
||||
|
||||
**Examples**:
|
||||
|
||||
```rp-prefix
|
||||
#### Awesome new feature!
|
||||
|
||||
This text is at the start of the release notes.
|
||||
```
|
||||
|
||||
```rp-suffix
|
||||
#### Version Compatibility
|
||||
|
||||
And this at the end.
|
||||
```
|
||||
|
||||
### Status
|
||||
|
||||
**Labels**:
|
||||
|
|
@ -43,4 +57,19 @@ Users should not set these labels themselves.
|
|||
|
||||
Not created by `releaser-pleaser`.
|
||||
|
||||
Normal pull requests do not support any options right now.
|
||||
### Release Notes
|
||||
|
||||
**Code Blocks**:
|
||||
|
||||
- `rp-commits`
|
||||
|
||||
If specified, `releaser-pleaser` will consider each line in the code block as a commit message and add all of them to the Release Notes. Learn more in the [Release Notes](../guides/release-notes.md) guide.
|
||||
|
||||
The types of commits (`feat`, `fix`, ...) are also considered for the next version.
|
||||
|
||||
**Examples**:
|
||||
|
||||
```rp-commits
|
||||
feat(api): add movie endpoints
|
||||
fix(db): invalid schema for actor model
|
||||
```
|
||||
|
|
|
|||
|
|
@ -526,9 +526,7 @@ func gitHubPRToReleasePullRequest(pr *github.PullRequest) *releasepr.ReleasePull
|
|||
}
|
||||
|
||||
return &releasepr.ReleasePullRequest{
|
||||
ID: pr.GetNumber(),
|
||||
Title: pr.GetTitle(),
|
||||
Description: pr.GetBody(),
|
||||
PullRequest: *gitHubPRToPullRequest(pr),
|
||||
Labels: labels,
|
||||
|
||||
Head: pr.GetHead().GetRef(),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func Format(input string) (string, error) {
|
|||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func GetCodeBlockText(source []byte, language string, output *string) gast.Walker {
|
||||
func GetCodeBlockText(source []byte, language string, output *string, found *bool) gast.Walker {
|
||||
return func(n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return gast.WalkContinue, nil
|
||||
|
|
@ -56,6 +56,9 @@ func GetCodeBlockText(source []byte, language string, output *string) gast.Walke
|
|||
}
|
||||
|
||||
*output = textFromLines(source, codeBlock)
|
||||
if found != nil {
|
||||
*found = true
|
||||
}
|
||||
// Stop looking after we find the first result
|
||||
return gast.WalkStop, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ func TestGetCodeBlockText(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantText string
|
||||
wantFound bool
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
|
|
@ -62,7 +63,8 @@ func TestGetCodeBlockText(t *testing.T) {
|
|||
source: []byte("# Foo"),
|
||||
language: "missing",
|
||||
},
|
||||
want: "",
|
||||
wantText: "",
|
||||
wantFound: false,
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
|
|
@ -71,7 +73,8 @@ func TestGetCodeBlockText(t *testing.T) {
|
|||
source: []byte("```test\nContent\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "Content",
|
||||
wantText: "Content",
|
||||
wantFound: true,
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
|
|
@ -80,7 +83,8 @@ func TestGetCodeBlockText(t *testing.T) {
|
|||
source: []byte("```unknown\nContent\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "",
|
||||
wantText: "",
|
||||
wantFound: false,
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
|
|
@ -89,7 +93,8 @@ func TestGetCodeBlockText(t *testing.T) {
|
|||
source: []byte("```unknown\nContent\n```\n\n```test\n1337\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "1337",
|
||||
wantText: "1337",
|
||||
wantFound: true,
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
|
|
@ -98,22 +103,25 @@ func TestGetCodeBlockText(t *testing.T) {
|
|||
source: []byte("```test\nContent\n```\n\n```test\n1337\n```"),
|
||||
language: "test",
|
||||
},
|
||||
want: "Content",
|
||||
wantText: "Content",
|
||||
wantFound: true,
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
var gotText string
|
||||
var gotFound bool
|
||||
|
||||
err := WalkAST(tt.args.source,
|
||||
GetCodeBlockText(tt.args.source, tt.args.language, &got),
|
||||
GetCodeBlockText(tt.args.source, tt.args.language, &gotText, &gotFound),
|
||||
)
|
||||
if !tt.wantErr(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
assert.Equal(t, tt.wantText, gotText)
|
||||
assert.Equal(t, tt.wantFound, gotFound)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,13 +28,8 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
// ReleasePullRequest
|
||||
//
|
||||
// TODO: Reuse [git.PullRequest]
|
||||
type ReleasePullRequest struct {
|
||||
ID int
|
||||
Title string
|
||||
Description string
|
||||
git.PullRequest
|
||||
Labels []Label
|
||||
|
||||
Head string
|
||||
|
|
@ -137,8 +132,8 @@ func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (Rele
|
|||
source := []byte(pr.Description)
|
||||
|
||||
err := markdown.WalkAST(source,
|
||||
markdown.GetCodeBlockText(source, DescriptionLanguagePrefix, &overrides.Prefix),
|
||||
markdown.GetCodeBlockText(source, DescriptionLanguageSuffix, &overrides.Suffix),
|
||||
markdown.GetCodeBlockText(source, DescriptionLanguagePrefix, &overrides.Prefix, nil),
|
||||
markdown.GetCodeBlockText(source, DescriptionLanguageSuffix, &overrides.Suffix, nil),
|
||||
)
|
||||
if err != nil {
|
||||
return ReleaseOverrides{}, err
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/apricote/releaser-pleaser/internal/git"
|
||||
"github.com/apricote/releaser-pleaser/internal/versioning"
|
||||
)
|
||||
|
||||
|
|
@ -36,16 +37,20 @@ func TestReleasePullRequest_GetOverrides(t *testing.T) {
|
|||
{
|
||||
name: "prefix in description",
|
||||
pr: ReleasePullRequest{
|
||||
PullRequest: git.PullRequest{
|
||||
Description: "```rp-prefix\n## Foo\n\n- Cool thing\n```",
|
||||
},
|
||||
},
|
||||
want: ReleaseOverrides{Prefix: "## Foo\n\n- Cool thing"},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "suffix in description",
|
||||
pr: ReleasePullRequest{
|
||||
PullRequest: git.PullRequest{
|
||||
Description: "```rp-suffix\n## Compatibility\n\nNo compatibility guarantees.\n```",
|
||||
},
|
||||
},
|
||||
want: ReleaseOverrides{Suffix: "## Compatibility\n\nNo compatibility guarantees."},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
|
|
@ -105,7 +110,9 @@ Suffix Things
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pr := &ReleasePullRequest{
|
||||
PullRequest: git.PullRequest{
|
||||
Description: tt.description,
|
||||
},
|
||||
}
|
||||
got, err := pr.ChangelogText()
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("ChangelogText()")) {
|
||||
|
|
@ -129,7 +136,11 @@ func TestReleasePullRequest_SetTitle(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "simple update",
|
||||
pr: &ReleasePullRequest{Title: "foo: bar"},
|
||||
pr: &ReleasePullRequest{
|
||||
PullRequest: git.PullRequest{
|
||||
Title: "foo: bar",
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
branch: "main",
|
||||
version: "v1.0.0",
|
||||
|
|
|
|||
57
prbody.go
Normal file
57
prbody.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package rp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/apricote/releaser-pleaser/internal/git"
|
||||
"github.com/apricote/releaser-pleaser/internal/markdown"
|
||||
)
|
||||
|
||||
func parsePRBodyForCommitOverrides(commits []git.Commit) ([]git.Commit, error) {
|
||||
result := make([]git.Commit, 0, len(commits))
|
||||
|
||||
for _, commit := range commits {
|
||||
singleResult, err := parseSinglePRBodyForCommitOverrides(commit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, singleResult...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseSinglePRBodyForCommitOverrides(commit git.Commit) ([]git.Commit, error) {
|
||||
if commit.PullRequest == nil {
|
||||
return []git.Commit{commit}, nil
|
||||
}
|
||||
|
||||
source := []byte(commit.PullRequest.Description)
|
||||
var overridesText string
|
||||
var found bool
|
||||
err := markdown.WalkAST(source, markdown.GetCodeBlockText(source, "rp-commits", &overridesText, &found))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !found {
|
||||
return []git.Commit{commit}, nil
|
||||
}
|
||||
|
||||
lines := strings.Split(overridesText, "\n")
|
||||
result := make([]git.Commit, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
// Only consider lines with text
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
newCommit := commit
|
||||
newCommit.Message = line
|
||||
result = append(result, newCommit)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
242
prbody_test.go
Normal file
242
prbody_test.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package rp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/apricote/releaser-pleaser/internal/git"
|
||||
)
|
||||
|
||||
func Test_parsePRBodyForCommitOverrides(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commits []git.Commit
|
||||
want []git.Commit
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "no commits",
|
||||
commits: []git.Commit{},
|
||||
want: []git.Commit{},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "single commit",
|
||||
commits: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "multiple commits",
|
||||
commits: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\nfeat: shiny\nfix: boom\n```\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
Hash: "456",
|
||||
Message: "654",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 2,
|
||||
Title: "Bar",
|
||||
Description: "# Foobazzle\n\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "feat: shiny",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\nfeat: shiny\nfix: boom\n```\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "fix: boom",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\nfeat: shiny\nfix: boom\n```\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
Hash: "456",
|
||||
Message: "654",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 2,
|
||||
Title: "Bar",
|
||||
Description: "# Foobazzle\n\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parsePRBodyForCommitOverrides(tt.commits)
|
||||
if !tt.wantErr(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseSinglePRBodyForCommitOverrides(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commit git.Commit
|
||||
want []git.Commit
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "same commit if no PR is available",
|
||||
commit: git.Commit{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: nil,
|
||||
},
|
||||
want: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "same commit if no overrides are defined",
|
||||
commit: git.Commit{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n",
|
||||
},
|
||||
},
|
||||
want: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "no commit if override is defined but empty",
|
||||
commit: git.Commit{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "```rp-commits\n```\n",
|
||||
},
|
||||
},
|
||||
want: []git.Commit{},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "commit messages from override",
|
||||
commit: git.Commit{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\nfeat: shiny\nfix: boom\n```\n",
|
||||
},
|
||||
},
|
||||
want: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "feat: shiny",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\nfeat: shiny\nfix: boom\n```\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "fix: boom",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\nfeat: shiny\nfix: boom\n```\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "ignore empty lines",
|
||||
commit: git.Commit{
|
||||
Hash: "123",
|
||||
Message: "321",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\n\n \nfeat: shiny\n\n```\n",
|
||||
},
|
||||
},
|
||||
want: []git.Commit{
|
||||
{
|
||||
Hash: "123",
|
||||
Message: "feat: shiny",
|
||||
PullRequest: &git.PullRequest{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Description: "# Cool new thingy\n\n```rp-commits\n\n \nfeat: shiny\n\n```\n",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseSinglePRBodyForCommitOverrides(tt.commit)
|
||||
if !tt.wantErr(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -188,15 +188,19 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
|
|||
lastReleaseCommit = releases.Latest
|
||||
}
|
||||
|
||||
releasableCommits, err := rp.forge.CommitsSince(ctx, lastReleaseCommit)
|
||||
commits, err := rp.forge.CommitsSince(ctx, lastReleaseCommit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits))
|
||||
commits, err = parsePRBodyForCommitOverrides(commits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Handle commit overrides
|
||||
analyzedCommits, err := rp.commitParser.Analyze(releasableCommits)
|
||||
logger.InfoContext(ctx, "Found releasable commits", "length", len(commits))
|
||||
|
||||
analyzedCommits, err := rp.commitParser.Analyze(commits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue