Compare commits

..

No commits in common. "0a199e693f648e3a8d7c6962d8b23e8473be2d92" and "36a0b90bcde26d3eab1b446acb4023ded470460d" have entirely different histories.

15 changed files with 43 additions and 447 deletions

View file

@ -1,24 +1,5 @@
# Changelog # Changelog
## [v0.3.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.3.0)
### :sparkles: Highlights
#### Cleaner pre-release Release Notes
From now on if you create multiple pre-releases in a row, the release notes will only include changes since the last pre-release. Once you decide to create a stable release, the release notes will be in comparison to the last stable release.
#### Edit pull request after merging.
You can now edit the message for a pull request after merging by adding a \```rp-commits code block into the pull request body. Learn more in the [Release Notes Guide](https://apricote.github.io/releaser-pleaser/guides/release-notes.html#editing-the-release-notes).
### Features
- less repetitive entries for prerelease changelogs #37
- format markdown in changelog entry (#41)
- edit commit message after merging through PR (#43)
- **cli**: show release PR url in log messages (#44)
## [v0.2.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.2.0) ## [v0.2.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.2.0)
### Features ### Features

View file

@ -21,7 +21,7 @@ inputs:
outputs: {} outputs: {}
runs: runs:
using: 'docker' using: 'docker'
image: ghcr.io/apricote/releaser-pleaser:v0.3.0 # x-releaser-pleaser-version image: ghcr.io/apricote/releaser-pleaser:v0.2.0 # x-releaser-pleaser-version
args: args:
- run - run
- --forge=github - --forge=github

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c28226eaa769033a45ca801f1e0655178faf86e7ddd764f470ae79d72c4b3c2
size 62031

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:04ca48b3250862d282dd54e14c08f9273ada0a49d2300364601799c56b1f6d11
size 72105

View file

@ -4,34 +4,7 @@ You can customize the generated Release Notes in two ways:
## For a single commit / pull request ## For a single commit / pull request
### Editing the Release Notes This feature is still being worked on. Check out [#5](https://github.com/apricote/releaser-pleaser/issues/5) for the current status.
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:
![Screenshot of a pull request page on GitHub. Currently editing the description of the pull request and adding the rp-commits snippet from above.](release-notes-rp-commits.png)
In turn, `releaser-pleaser` updates the release pull request like this:
![Screenshot of a release pull request on GitHub. It shows the release notes with the three commits from the rp-commits example.](release-notes-rp-commits-release-pr.png)
### 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 ## For the release

View file

@ -28,20 +28,6 @@ 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. 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 ### Status
**Labels**: **Labels**:
@ -57,19 +43,4 @@ Users should not set these labels themselves.
Not created by `releaser-pleaser`. Not created by `releaser-pleaser`.
### Release Notes Normal pull requests do not support any options right now.
**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
```

View file

@ -13,7 +13,6 @@ type Forge interface {
RepoURL() string RepoURL() string
CloneURL() string CloneURL() string
ReleaseURL(version string) string ReleaseURL(version string) string
PullRequestURL(id int) string
GitAuth() transport.AuthMethod GitAuth() transport.AuthMethod

View file

@ -51,10 +51,6 @@ func (g *GitHub) ReleaseURL(version string) string {
return fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s", g.options.Owner, g.options.Repo, version) return fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s", g.options.Owner, g.options.Repo, version)
} }
func (g *GitHub) PullRequestURL(id int) string {
return fmt.Sprintf("https://github.com/%s/%s/pull/%d", g.options.Owner, g.options.Repo, id)
}
func (g *GitHub) GitAuth() transport.AuthMethod { func (g *GitHub) GitAuth() transport.AuthMethod {
return &http.BasicAuth{ return &http.BasicAuth{
Username: g.options.Username, Username: g.options.Username,
@ -530,7 +526,9 @@ func gitHubPRToReleasePullRequest(pr *github.PullRequest) *releasepr.ReleasePull
} }
return &releasepr.ReleasePullRequest{ return &releasepr.ReleasePullRequest{
PullRequest: *gitHubPRToPullRequest(pr), ID: pr.GetNumber(),
Title: pr.GetTitle(),
Description: pr.GetBody(),
Labels: labels, Labels: labels,
Head: pr.GetHead().GetRef(), Head: pr.GetHead().GetRef(),

View file

@ -39,7 +39,7 @@ func Format(input string) (string, error) {
return buf.String(), nil return buf.String(), nil
} }
func GetCodeBlockText(source []byte, language string, output *string, found *bool) gast.Walker { func GetCodeBlockText(source []byte, language string, output *string) gast.Walker {
return func(n gast.Node, entering bool) (gast.WalkStatus, error) { return func(n gast.Node, entering bool) (gast.WalkStatus, error) {
if !entering { if !entering {
return gast.WalkContinue, nil return gast.WalkContinue, nil
@ -56,9 +56,6 @@ func GetCodeBlockText(source []byte, language string, output *string, found *boo
} }
*output = textFromLines(source, codeBlock) *output = textFromLines(source, codeBlock)
if found != nil {
*found = true
}
// Stop looking after we find the first result // Stop looking after we find the first result
return gast.WalkStop, nil return gast.WalkStop, nil
} }

View file

@ -53,8 +53,7 @@ func TestGetCodeBlockText(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args args args
wantText string want string
wantFound bool
wantErr assert.ErrorAssertionFunc wantErr assert.ErrorAssertionFunc
}{ }{
{ {
@ -63,8 +62,7 @@ func TestGetCodeBlockText(t *testing.T) {
source: []byte("# Foo"), source: []byte("# Foo"),
language: "missing", language: "missing",
}, },
wantText: "", want: "",
wantFound: false,
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
@ -73,8 +71,7 @@ func TestGetCodeBlockText(t *testing.T) {
source: []byte("```test\nContent\n```"), source: []byte("```test\nContent\n```"),
language: "test", language: "test",
}, },
wantText: "Content", want: "Content",
wantFound: true,
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
@ -83,8 +80,7 @@ func TestGetCodeBlockText(t *testing.T) {
source: []byte("```unknown\nContent\n```"), source: []byte("```unknown\nContent\n```"),
language: "test", language: "test",
}, },
wantText: "", want: "",
wantFound: false,
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
@ -93,8 +89,7 @@ func TestGetCodeBlockText(t *testing.T) {
source: []byte("```unknown\nContent\n```\n\n```test\n1337\n```"), source: []byte("```unknown\nContent\n```\n\n```test\n1337\n```"),
language: "test", language: "test",
}, },
wantText: "1337", want: "1337",
wantFound: true,
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
@ -103,25 +98,22 @@ func TestGetCodeBlockText(t *testing.T) {
source: []byte("```test\nContent\n```\n\n```test\n1337\n```"), source: []byte("```test\nContent\n```\n\n```test\n1337\n```"),
language: "test", language: "test",
}, },
wantText: "Content", want: "Content",
wantFound: true,
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var gotText string var got string
var gotFound bool
err := WalkAST(tt.args.source, err := WalkAST(tt.args.source,
GetCodeBlockText(tt.args.source, tt.args.language, &gotText, &gotFound), GetCodeBlockText(tt.args.source, tt.args.language, &got),
) )
if !tt.wantErr(t, err) { if !tt.wantErr(t, err) {
return return
} }
assert.Equal(t, tt.wantText, gotText) assert.Equal(t, tt.want, got)
assert.Equal(t, tt.wantFound, gotFound)
}) })
} }
} }

View file

@ -28,8 +28,13 @@ func init() {
} }
} }
// ReleasePullRequest
//
// TODO: Reuse [git.PullRequest]
type ReleasePullRequest struct { type ReleasePullRequest struct {
git.PullRequest ID int
Title string
Description string
Labels []Label Labels []Label
Head string Head string
@ -132,8 +137,8 @@ func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (Rele
source := []byte(pr.Description) source := []byte(pr.Description)
err := markdown.WalkAST(source, err := markdown.WalkAST(source,
markdown.GetCodeBlockText(source, DescriptionLanguagePrefix, &overrides.Prefix, nil), markdown.GetCodeBlockText(source, DescriptionLanguagePrefix, &overrides.Prefix),
markdown.GetCodeBlockText(source, DescriptionLanguageSuffix, &overrides.Suffix, nil), markdown.GetCodeBlockText(source, DescriptionLanguageSuffix, &overrides.Suffix),
) )
if err != nil { if err != nil {
return ReleaseOverrides{}, err return ReleaseOverrides{}, err

View file

@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/apricote/releaser-pleaser/internal/git"
"github.com/apricote/releaser-pleaser/internal/versioning" "github.com/apricote/releaser-pleaser/internal/versioning"
) )
@ -37,20 +36,16 @@ func TestReleasePullRequest_GetOverrides(t *testing.T) {
{ {
name: "prefix in description", name: "prefix in description",
pr: ReleasePullRequest{ pr: ReleasePullRequest{
PullRequest: git.PullRequest{
Description: "```rp-prefix\n## Foo\n\n- Cool thing\n```", Description: "```rp-prefix\n## Foo\n\n- Cool thing\n```",
}, },
},
want: ReleaseOverrides{Prefix: "## Foo\n\n- Cool thing"}, want: ReleaseOverrides{Prefix: "## Foo\n\n- Cool thing"},
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
name: "suffix in description", name: "suffix in description",
pr: ReleasePullRequest{ pr: ReleasePullRequest{
PullRequest: git.PullRequest{
Description: "```rp-suffix\n## Compatibility\n\nNo compatibility guarantees.\n```", Description: "```rp-suffix\n## Compatibility\n\nNo compatibility guarantees.\n```",
}, },
},
want: ReleaseOverrides{Suffix: "## Compatibility\n\nNo compatibility guarantees."}, want: ReleaseOverrides{Suffix: "## Compatibility\n\nNo compatibility guarantees."},
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
@ -110,9 +105,7 @@ Suffix Things
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
pr := &ReleasePullRequest{ pr := &ReleasePullRequest{
PullRequest: git.PullRequest{
Description: tt.description, Description: tt.description,
},
} }
got, err := pr.ChangelogText() got, err := pr.ChangelogText()
if !tt.wantErr(t, err, fmt.Sprintf("ChangelogText()")) { if !tt.wantErr(t, err, fmt.Sprintf("ChangelogText()")) {
@ -136,11 +129,7 @@ func TestReleasePullRequest_SetTitle(t *testing.T) {
}{ }{
{ {
name: "simple update", name: "simple update",
pr: &ReleasePullRequest{ pr: &ReleasePullRequest{Title: "foo: bar"},
PullRequest: git.PullRequest{
Title: "foo: bar",
},
},
args: args{ args: args{
branch: "main", branch: "main",
version: "v1.0.0", version: "v1.0.0",

View file

@ -1,57 +0,0 @@
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
}

View file

@ -1,242 +0,0 @@
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)
})
}
}

View file

@ -188,19 +188,15 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
lastReleaseCommit = releases.Latest lastReleaseCommit = releases.Latest
} }
commits, err := rp.forge.CommitsSince(ctx, lastReleaseCommit) releasableCommits, err := rp.forge.CommitsSince(ctx, lastReleaseCommit)
if err != nil { if err != nil {
return err return err
} }
commits, err = parsePRBodyForCommitOverrides(commits) logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits))
if err != nil {
return err
}
logger.InfoContext(ctx, "Found releasable commits", "length", len(commits)) // TODO: Handle commit overrides
analyzedCommits, err := rp.commitParser.Analyze(releasableCommits)
analyzedCommits, err := rp.commitParser.Analyze(commits)
if err != nil { if err != nil {
return err return err
} }
@ -300,7 +296,7 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID, "pr.url", rp.forge.PullRequestURL(pr.ID)) logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID)
} else { } else {
pr.SetTitle(rp.targetBranch, nextVersion) pr.SetTitle(rp.targetBranch, nextVersion)
@ -317,7 +313,7 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID, "pr.url", rp.forge.PullRequestURL(pr.ID)) logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID)
} }
return nil return nil