Compare commits

..

17 commits

Author SHA1 Message Date
releaser-pleaser
6810ace9b8 chore(main): release v0.2.0 2024-08-23 23:46:52 +02:00
d5fd606577
chore: enable some more linters (#16) 2024-08-23 22:35:06 +02:00
f0eed8cc56
refactor: move changelog file to updater interface (#15) 2024-08-23 22:17:38 +02:00
47de2f97bc
feat: update version references in any files (#14) 2024-08-23 22:02:58 +02:00
589fdde401 chore: update golangci-lint 2024-08-20 17:37:46 +02:00
6f616f9923 chore: upgrade to go 1.23 2024-08-20 17:37:46 +02:00
ffcc3b60cb chore: update dependencies 2024-08-20 17:37:46 +02:00
08e7b31eff ci: output test coverage file 2024-08-20 17:37:46 +02:00
69796325c7 ci: codecov coverage 2024-08-20 17:37:46 +02:00
0c93645b21 refactor: smaller fixes 2024-08-20 17:37:46 +02:00
fbd47fac62 refactor: remove unused code 2024-08-20 17:37:46 +02:00
5882a6bf2c refactor: move commit analyzing out of forge 2024-08-20 17:37:46 +02:00
f2a982d7a0 refactor: cleanup label handling 2024-08-20 17:37:46 +02:00
32734d9aa1 refactor: move run logic outside of cli code 2024-08-20 17:37:46 +02:00
d8daad7623
refactor: Various cleanup (#11)
* refactor: interface for commit message analyzer

* refactor: interface for versioning strategy

* refactor(releasepr): rebuild pr description

Build PR description from scratch and parsed values instead of copying some of the AST to next description.
2024-08-08 19:01:44 +02:00
8f106e4028 ci: use custom PAT for github api so tag workflow is started 2024-08-06 00:17:43 +02:00
github-actions[bot]
d5bc18b904
chore(main): release v0.1.0 (#2)
Co-authored-by: releaser-pleaser <>
2024-08-06 00:11:38 +02:00
25 changed files with 1095 additions and 1006 deletions

View file

@ -21,7 +21,7 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.59.1 # renovate: datasource=github-releases depName=golangci/golangci-lint
version: v1.60.1 # renovate: datasource=github-releases depName=golangci/golangci-lint
args: --timeout 5m
@ -37,8 +37,12 @@ jobs:
go-version-file: go.mod
- name: Run tests
run: go test -v -race -coverpkg=./... ./...
run: go test -v -race -coverpkg=./... -coverprofile=coverage.txt ./...
- name: Upload results to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
go-mod-tidy:
runs-on: ubuntu-latest

View file

@ -9,10 +9,7 @@ on:
- labeled
- unlabeled
permissions:
contents: write
issues: write
pull-requests: write
permissions: {}
jobs:
releaser-pleaser:
@ -21,5 +18,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- run: env
- uses: ./
- name: releaser-pleaser
uses: ./
with:
token: ${{ secrets.RELEASER_PLEASER_TOKEN }}

27
.golangci.yaml Normal file
View file

@ -0,0 +1,27 @@
linters:
presets:
- bugs
- error
- import
- metalinter
- module
- unused
enable:
- testifylint
disable:
# preset error
# These should probably be cleaned up at some point if we want to publish part of this as a library.
- err113 # Very annoying to define static errors everywhere
- wrapcheck # Very annoying to wrap errors everywhere
# preset import
- depguard
linters-settings:
gci:
sections:
- standard
- default
- localmodule

View file

@ -1,5 +1,10 @@
# Changelog
## [v0.2.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.2.0)
### Features
- update version references in any files (#14)
## [v0.1.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.1.0)
### This is the first release ever, so it also includes a lot of other functionality.

View file

@ -12,14 +12,19 @@ inputs:
description: 'GitHub token for creating and grooming release PRs, defaults to using secrets.GITHUB_TOKEN'
required: false
default: ${{ github.token }}
extra-files:
description: 'List of files that are scanned for version references.'
required: false
default: ""
outputs: {}
runs:
using: 'docker'
image: ghcr.io/apricote/releaser-pleaser:v0.1.0
image: ghcr.io/apricote/releaser-pleaser:v0.2.0 # x-releaser-pleaser-version
args:
- run
- --forge=github
- --branch=${{ inputs.branch }}
- --extra-files="${{ inputs.extra-files }}"
env:
GITHUB_TOKEN: ${{ inputs.token }}
GITHUB_USER: "oauth2"

View file

@ -3,26 +3,17 @@ package rp
import (
"bytes"
_ "embed"
"fmt"
"html/template"
"io"
"log"
"os"
"regexp"
"github.com/go-git/go-git/v5"
)
const (
ChangelogFile = "CHANGELOG.md"
ChangelogFileBuffer = "CHANGELOG.md.tmp"
ChangelogHeader = "# Changelog"
ChangelogFile = "CHANGELOG.md"
ChangelogHeader = "# Changelog"
)
var (
changelogTemplate *template.Template
headerRegex = regexp.MustCompile(`^# Changelog\n`)
)
//go:embed changelog.md.tpl
@ -36,72 +27,16 @@ func init() {
}
}
func UpdateChangelogFile(wt *git.Worktree, newEntry string) error {
file, err := wt.Filesystem.OpenFile(ChangelogFile, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return err
}
headerIndex := headerRegex.FindIndex(content)
if headerIndex == nil && len(content) != 0 {
return fmt.Errorf("unexpected format of CHANGELOG.md, header does not match")
}
if headerIndex != nil {
// Remove the header from the content
content = content[headerIndex[1]:]
}
err = file.Truncate(0)
if err != nil {
return err
}
_, err = file.Seek(0, io.SeekStart)
if err != nil {
return err
}
_, err = file.Write([]byte(ChangelogHeader + "\n\n" + newEntry))
if err != nil {
return err
}
_, err = file.Write(content)
if err != nil {
return err
}
// Close file to make sure it is written to disk.
err = file.Close()
if err != nil {
return err
}
_, err = wt.Add(ChangelogFile)
if err != nil {
return err
}
return nil
}
func NewChangelogEntry(changesets []Changeset, version, link, prefix, suffix string) (string, error) {
func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) {
features := make([]AnalyzedCommit, 0)
fixes := make([]AnalyzedCommit, 0)
for _, changeset := range changesets {
for _, commit := range changeset.ChangelogEntries {
switch commit.Type {
case "feat":
features = append(features, commit)
case "fix":
fixes = append(fixes, commit)
}
for _, commit := range commits {
switch commit.Type {
case "feat":
features = append(features, commit)
case "fix":
fixes = append(fixes, commit)
}
}

View file

@ -1,106 +1,22 @@
package rp
import (
"io"
"testing"
"github.com/go-git/go-git/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/apricote/releaser-pleaser/internal/testutils"
)
func ptr[T any](input T) *T {
return &input
}
func TestUpdateChangelogFile(t *testing.T) {
tests := []struct {
name string
repoFn testutils.Repo
entry string
expectedContent string
wantErr assert.ErrorAssertionFunc
}{
{
name: "empty repo",
repoFn: testutils.WithTestRepo(),
entry: "## v1.0.0\n",
expectedContent: "# Changelog\n\n## v1.0.0\n",
wantErr: assert.NoError,
},
{
name: "repo with well-formatted changelog",
repoFn: testutils.WithTestRepo(testutils.WithCommit("feat: add changelog", testutils.WithFile(ChangelogFile, `# Changelog
## v0.0.1
- Bazzle
## v0.1.0
### Bazuuum
`))),
entry: "## v1.0.0\n\n- Version 1, juhu.\n",
expectedContent: `# Changelog
## v1.0.0
- Version 1, juhu.
## v0.0.1
- Bazzle
## v0.1.0
### Bazuuum
`,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := tt.repoFn(t)
wt, err := repo.Worktree()
require.NoError(t, err, "failed to get worktree")
err = UpdateChangelogFile(wt, tt.entry)
if !tt.wantErr(t, err) {
return
}
wtStatus, err := wt.Status()
require.NoError(t, err, "failed to get worktree status")
assert.Len(t, wtStatus, 1, "worktree status does not have the expected entry number")
changelogFileStatus := wtStatus.File(ChangelogFile)
assert.Equal(t, git.Unmodified, changelogFileStatus.Worktree, "unexpected file status in worktree")
assert.Equal(t, git.Added, changelogFileStatus.Staging, "unexpected file status in staging")
changelogFile, err := wt.Filesystem.Open(ChangelogFile)
require.NoError(t, err)
defer changelogFile.Close()
changelogFileContent, err := io.ReadAll(changelogFile)
require.NoError(t, err)
assert.Equal(t, tt.expectedContent, string(changelogFileContent))
})
}
}
func Test_NewChangelogEntry(t *testing.T) {
type args struct {
changesets []Changeset
version string
link string
prefix string
suffix string
analyzedCommits []AnalyzedCommit
version string
link string
prefix string
suffix string
}
tests := []struct {
name string
@ -111,9 +27,9 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "empty",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{}}},
version: "1.0.0",
link: "https://example.com/1.0.0",
analyzedCommits: []AnalyzedCommit{},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
want: "## [1.0.0](https://example.com/1.0.0)",
wantErr: assert.NoError,
@ -121,13 +37,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "single feature",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "feat",
Description: "Foobar!",
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@ -137,13 +53,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "single fix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@ -153,7 +69,7 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "multiple commits with scopes",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "feat",
@ -176,7 +92,7 @@ func Test_NewChangelogEntry(t *testing.T) {
Description: "So sad!",
Scope: ptr("sad"),
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@ -196,13 +112,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "prefix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
prefix: "### Breaking Changes",
@ -219,13 +135,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "suffix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []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.",
@ -245,7 +161,7 @@ This version is compatible with flux-compensator v2.2 - v2.9.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewChangelogEntry(tt.args.changesets, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
got, err := NewChangelogEntry(tt.args.analyzedCommits, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
if !tt.wantErr(t, err) {
return
}

View file

@ -1,21 +1,13 @@
package cmd
import (
"context"
"fmt"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/spf13/cobra"
rp "github.com/apricote/releaser-pleaser"
)
const (
RELEASER_PLEASER_BRANCH = "releaser-pleaser--branches--%s"
)
// runCmd represents the run command
var runCmd = &cobra.Command{
Use: "run",
@ -23,10 +15,11 @@ var runCmd = &cobra.Command{
}
var (
flagForge string
flagBranch string
flagOwner string
flagRepo string
flagForge string
flagBranch string
flagOwner string
flagRepo string
flagExtraFiles string
)
func init() {
@ -38,6 +31,7 @@ func init() {
runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "")
runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "")
runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "")
runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
}
func run(cmd *cobra.Command, _ []string) error {
@ -50,319 +44,50 @@ func run(cmd *cobra.Command, _ []string) error {
"repo", flagRepo,
)
var f rp.Forge
var forge rp.Forge
forgeOptions := rp.ForgeOptions{
Repository: flagRepo,
BaseBranch: flagBranch,
}
switch flagForge {
//case "gitlab":
//f = rp.NewGitLab(forgeOptions)
switch flagForge { // nolint:gocritic // Will become a proper switch once gitlab is added
// case "gitlab":
// f = rp.NewGitLab(forgeOptions)
case "github":
logger.DebugContext(ctx, "using forge GitHub")
f = rp.NewGitHub(logger, &rp.GitHubOptions{
forge = rp.NewGitHub(logger, &rp.GitHubOptions{
ForgeOptions: forgeOptions,
Owner: flagOwner,
Repo: flagRepo,
})
}
err := ensureLabels(ctx, f)
if err != nil {
return fmt.Errorf("failed to ensure all labels exist: %w", err)
}
extraFiles := parseExtraFiles(flagExtraFiles)
err = createPendingReleases(ctx, f)
if err != nil {
return fmt.Errorf("failed to create pending releases: %w", err)
}
releaserPleaser := rp.New(
forge,
logger,
flagBranch,
rp.NewConventionalCommitsParser(),
rp.SemVerNextVersion,
extraFiles,
[]rp.Updater{&rp.GenericUpdater{}},
)
changesets, releases, err := getChangesetsFromForge(ctx, f)
if err != nil {
return fmt.Errorf("failed to get changesets: %w", err)
}
err = reconcileReleasePR(ctx, f, changesets, releases)
if err != nil {
return fmt.Errorf("failed to reconcile release pr: %w", err)
}
return nil
return releaserPleaser.Run(ctx)
}
func ensureLabels(ctx context.Context, forge rp.Forge) error {
return forge.EnsureLabelsExist(ctx, rp.Labels)
}
func createPendingReleases(ctx context.Context, forge rp.Forge) error {
logger.InfoContext(ctx, "checking for pending releases")
prs, err := forge.PendingReleases(ctx)
if err != nil {
return err
}
if len(prs) == 0 {
logger.InfoContext(ctx, "No pending releases found")
return nil
}
logger.InfoContext(ctx, "Found pending releases", "length", len(prs))
for _, pr := range prs {
err = createPendingRelease(ctx, forge, pr)
if err != nil {
return err
}
}
return nil
}
func createPendingRelease(ctx context.Context, forge rp.Forge, pr *rp.ReleasePullRequest) error {
logger := logger.With("pr.id", pr.ID, "pr.title", pr.Title)
if pr.ReleaseCommit == nil {
return fmt.Errorf("pull request is missing the merge commit")
}
logger.Info("Creating release", "commit.hash", pr.ReleaseCommit.Hash)
version, err := pr.Version()
if err != nil {
return err
}
changelog, err := pr.ChangelogText()
if err != nil {
return err
}
// TODO: pre-release & latest
logger.DebugContext(ctx, "Creating release on forge")
err = forge.CreateRelease(ctx, *pr.ReleaseCommit, version, changelog, false, true)
if err != nil {
return fmt.Errorf("failed to create release on forge: %w", err)
}
logger.DebugContext(ctx, "created release", "release.title", version, "release.url", forge.ReleaseURL(version))
logger.DebugContext(ctx, "updating pr labels")
err = forge.SetPullRequestLabels(ctx, pr, []string{rp.LabelReleasePending}, []string{rp.LabelReleaseTagged})
if err != nil {
return err
}
logger.DebugContext(ctx, "updated pr labels")
logger.InfoContext(ctx, "Created release", "release.title", version, "release.url", forge.ReleaseURL(version))
return nil
}
func getChangesetsFromForge(ctx context.Context, forge rp.Forge) ([]rp.Changeset, rp.Releases, error) {
releases, err := forge.LatestTags(ctx)
if err != nil {
return nil, rp.Releases{}, err
}
if releases.Latest != nil {
logger.InfoContext(ctx, "found latest tag", "tag.hash", releases.Latest.Hash, "tag.name", releases.Latest.Name)
if releases.Stable != nil && releases.Latest.Hash != releases.Stable.Hash {
logger.InfoContext(ctx, "found stable tag", "tag.hash", releases.Stable.Hash, "tag.name", releases.Stable.Name)
}
} else {
logger.InfoContext(ctx, "no latest tag found")
}
releasableCommits, err := forge.CommitsSince(ctx, releases.Stable)
if err != nil {
return nil, rp.Releases{}, err
}
logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits))
changesets, err := forge.Changesets(ctx, releasableCommits)
if err != nil {
return nil, rp.Releases{}, err
}
logger.InfoContext(ctx, "Found changesets", "length", len(changesets))
return changesets, releases, nil
}
func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Changeset, releases rp.Releases) error {
rpBranch := fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch)
rpBranchRef := plumbing.NewBranchReferenceName(rpBranch)
// Check Forge for open PR
// Get any modifications from open PR
// Clone Repo
// Run Updaters + Changelog
// Upsert PR
pr, err := forge.PullRequestForBranch(ctx, fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch))
if err != nil {
return err
}
if pr != nil {
logger.InfoContext(ctx, "found existing release pull request", "pr.id", pr.ID, "pr.title", pr.Title)
}
if len(changesets) == 0 {
if pr != nil {
logger.InfoContext(ctx, "closing existing pull requests, no changesets available", "pr.id", pr.ID, "pr.title", pr.Title)
err = forge.ClosePullRequest(ctx, pr)
if err != nil {
return err
}
} else {
logger.InfoContext(ctx, "No changesets available for release")
}
return nil
}
var releaseOverrides rp.ReleaseOverrides
if pr != nil {
releaseOverrides, err = pr.GetOverrides()
if err != nil {
return err
}
}
versionBump := rp.VersionBumpFromChangesets(changesets)
nextVersion, err := releases.NextVersion(versionBump, releaseOverrides.NextVersionType)
if err != nil {
return err
}
logger.InfoContext(ctx, "next version", "version", nextVersion)
logger.DebugContext(ctx, "cloning repository", "clone.url", forge.CloneURL())
repo, err := rp.CloneRepo(ctx, forge.CloneURL(), flagBranch, forge.GitAuth())
if err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
if branch, _ := repo.Branch(rpBranch); branch != nil {
logger.DebugContext(ctx, "deleting previous releaser-pleaser branch locally", "branch.name", rpBranch)
if err = repo.DeleteBranch(rpBranch); err != nil {
return err
}
}
if err = worktree.Checkout(&git.CheckoutOptions{
Branch: rpBranchRef,
Create: true,
}); err != nil {
return fmt.Errorf("failed to check out branch: %w", err)
}
err = rp.RunUpdater(ctx, nextVersion, worktree)
if err != nil {
return fmt.Errorf("failed to update files with new version: %w", err)
}
changelogEntry, err := rp.NewChangelogEntry(changesets, nextVersion, forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix)
if err != nil {
return fmt.Errorf("failed to build changelog entry: %w", err)
}
err = rp.UpdateChangelogFile(worktree, changelogEntry)
if err != nil {
return fmt.Errorf("failed to update changelog file: %w", err)
}
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", flagBranch, nextVersion)
releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{
Author: rp.GitSignature(),
Committer: rp.GitSignature(),
})
if err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommitHash.String(), "commit.message", releaseCommitMessage)
newReleasePRChanges := true
// Check if anything changed in comparison to the remote branch (if exists)
if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName(rp.GitRemoteName, rpBranch), false); err != nil {
if err.Error() != "reference not found" {
// "reference not found" is expected and we should always push
return err
}
} else {
remoteCommit, err := repo.CommitObject(remoteRef.Hash())
if err != nil {
return err
}
localCommit, err := repo.CommitObject(releaseCommitHash)
if err != nil {
return err
}
diff, err := localCommit.PatchContext(ctx, remoteCommit)
if err != nil {
return err
}
newReleasePRChanges = len(diff.FilePatches()) > 0
}
if newReleasePRChanges {
pushRefSpec := config.RefSpec(fmt.Sprintf(
"+%s:%s",
rpBranchRef,
// This needs to be the local branch name, not the remotes/origin ref
// See https://stackoverflow.com/a/75727620
rpBranchRef,
))
logger.DebugContext(ctx, "pushing branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String())
if err = repo.PushContext(ctx, &git.PushOptions{
RemoteName: rp.GitRemoteName,
RefSpecs: []config.RefSpec{pushRefSpec},
Force: true,
Auth: forge.GitAuth(),
}); err != nil {
return fmt.Errorf("failed to push branch: %w", err)
}
logger.InfoContext(ctx, "pushed branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String())
} else {
logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push")
}
// Open/Update PR
if pr == nil {
pr, err = rp.NewReleasePullRequest(rpBranch, flagBranch, nextVersion, changelogEntry)
if err != nil {
return err
}
err = forge.CreatePullRequest(ctx, pr)
if err != nil {
return err
}
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
func parseExtraFiles(input string) []string {
lines := strings.Split(input, "\n")
extraFiles := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) > 0 {
extraFiles = append(extraFiles, line)
}
}
return extraFiles
}

View file

@ -7,6 +7,19 @@ import (
"github.com/leodido/go-conventionalcommits/parser"
)
type Commit struct {
Hash string
Message string
PullRequest *PullRequest
}
type PullRequest struct {
ID int
Title string
Description string
}
type AnalyzedCommit struct {
Commit
Type string
@ -15,24 +28,36 @@ type AnalyzedCommit struct {
BreakingChange bool
}
func AnalyzeCommits(commits []Commit) ([]AnalyzedCommit, conventionalcommits.VersionBump, error) {
type CommitParser interface {
Analyze(commits []Commit) ([]AnalyzedCommit, error)
}
type ConventionalCommitsParser struct {
machine conventionalcommits.Machine
}
func NewConventionalCommitsParser() *ConventionalCommitsParser {
parserMachine := parser.NewMachine(
parser.WithBestEffort(),
parser.WithTypes(conventionalcommits.TypesConventional),
)
return &ConventionalCommitsParser{
machine: parserMachine,
}
}
func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit, error) {
analyzedCommits := make([]AnalyzedCommit, 0, len(commits))
highestVersionBump := conventionalcommits.UnknownVersion
for _, commit := range commits {
msg, err := parserMachine.Parse([]byte(commit.Message))
msg, err := c.machine.Parse([]byte(commit.Message))
if err != nil {
return nil, conventionalcommits.UnknownVersion, fmt.Errorf("failed to parse message of commit %q: %w", commit.Hash, err)
return nil, fmt.Errorf("failed to parse message of commit %q: %w", commit.Hash, err)
}
conventionalCommit, ok := msg.(*conventionalcommits.ConventionalCommit)
if !ok {
return nil, conventionalcommits.UnknownVersion, fmt.Errorf("unable to get ConventionalCommit from parser result: %T", msg)
return nil, fmt.Errorf("unable to get ConventionalCommit from parser result: %T", msg)
}
commitVersionBump := conventionalCommit.VersionBump(conventionalcommits.DefaultStrategy)
@ -47,11 +72,7 @@ func AnalyzeCommits(commits []Commit) ([]AnalyzedCommit, conventionalcommits.Ver
})
}
if commitVersionBump > highestVersionBump {
// Get max version bump from all releasable commits
highestVersionBump = commitVersionBump
}
}
return analyzedCommits, highestVersionBump, nil
return analyzedCommits, nil
}

View file

@ -3,7 +3,6 @@ package rp
import (
"testing"
"github.com/leodido/go-conventionalcommits"
"github.com/stretchr/testify/assert"
)
@ -12,14 +11,12 @@ func TestAnalyzeCommits(t *testing.T) {
name string
commits []Commit
expectedCommits []AnalyzedCommit
expectedBump conventionalcommits.VersionBump
wantErr assert.ErrorAssertionFunc
}{
{
name: "empty commits",
commits: []Commit{},
expectedCommits: []AnalyzedCommit{},
expectedBump: conventionalcommits.UnknownVersion,
wantErr: assert.NoError,
},
{
@ -30,7 +27,6 @@ func TestAnalyzeCommits(t *testing.T) {
},
},
expectedCommits: nil,
expectedBump: conventionalcommits.UnknownVersion,
wantErr: assert.Error,
},
{
@ -41,7 +37,6 @@ func TestAnalyzeCommits(t *testing.T) {
},
},
expectedCommits: []AnalyzedCommit{},
expectedBump: conventionalcommits.UnknownVersion,
wantErr: assert.NoError,
},
{
@ -61,8 +56,7 @@ func TestAnalyzeCommits(t *testing.T) {
Description: "blabla",
},
},
expectedBump: conventionalcommits.PatchVersion,
wantErr: assert.NoError,
wantErr: assert.NoError,
},
{
name: "highest bump (minor)",
@ -86,8 +80,7 @@ func TestAnalyzeCommits(t *testing.T) {
Description: "foobar",
},
},
expectedBump: conventionalcommits.MinorVersion,
wantErr: assert.NoError,
wantErr: assert.NoError,
},
{
@ -113,19 +106,17 @@ func TestAnalyzeCommits(t *testing.T) {
BreakingChange: true,
},
},
expectedBump: conventionalcommits.MajorVersion,
wantErr: assert.NoError,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzedCommits, versionBump, err := AnalyzeCommits(tt.commits)
analyzedCommits, err := NewConventionalCommitsParser().Analyze(tt.commits)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.expectedCommits, analyzedCommits)
assert.Equal(t, tt.expectedBump, versionBump)
})
}
}

172
forge.go
View file

@ -19,18 +19,12 @@ const (
GitHubPerPageMax = 100
GitHubPRStateOpen = "open"
GitHubPRStateClosed = "closed"
GitHubEnvAPIToken = "GITHUB_TOKEN"
GitHubEnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential
GitHubEnvUsername = "GITHUB_USER"
GitHubEnvRepository = "GITHUB_REPOSITORY"
GitHubLabelColor = "dedede"
)
type Changeset struct {
URL string
Identifier string
ChangelogEntries []AnalyzedCommit
}
type Forge interface {
RepoURL() string
CloneURL() string
@ -46,23 +40,35 @@ type Forge interface {
// function should return all commits.
CommitsSince(context.Context, *Tag) ([]Commit, error)
// Changesets looks up the Pull/Merge Requests for each commit, returning its parsed metadata.
Changesets(context.Context, []Commit) ([]Changeset, error)
EnsureLabelsExist(context.Context, []string) error
// EnsureLabelsExist verifies that all desired labels are available on the repository. If labels are missing, they
// are created them.
EnsureLabelsExist(context.Context, []Label) error
// PullRequestForBranch returns the open pull request between the branch and ForgeOptions.BaseBranch. If no open PR
// exists, it returns nil.
PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error)
// CreatePullRequest opens a new pull/merge request for the ReleasePullRequest.
CreatePullRequest(context.Context, *ReleasePullRequest) error
// UpdatePullRequest updates the pull/merge request identified through the ID of
// the ReleasePullRequest to the current description and title.
UpdatePullRequest(context.Context, *ReleasePullRequest) error
SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []string) error
// SetPullRequestLabels updates the pull/merge request identified through the ID of
// the ReleasePullRequest to the current labels.
SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []Label) error
// ClosePullRequest closes the pull/merge request identified through the ID of
// the ReleasePullRequest, as it is no longer required.
ClosePullRequest(context.Context, *ReleasePullRequest) error
PendingReleases(context.Context) ([]*ReleasePullRequest, error)
// PendingReleases returns a list of ReleasePullRequest. The list should contain all pull/merge requests that are
// merged and have the matching label.
PendingReleases(context.Context, Label) ([]*ReleasePullRequest, error)
CreateRelease(ctx context.Context, commit Commit, title, changelog string, prelease, latest bool) error
// CreateRelease creates a release on the Forge, pointing at the commit with the passed in details.
CreateRelease(ctx context.Context, commit Commit, title, changelog string, prerelease, latest bool) error
}
type ForgeOptions struct {
@ -169,10 +175,16 @@ func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) {
var commits = make([]Commit, 0, len(repositoryCommits))
for _, ghCommit := range repositoryCommits {
commits = append(commits, Commit{
commit := Commit{
Hash: ghCommit.GetSHA(),
Message: ghCommit.GetCommit().GetMessage(),
})
}
commit.PullRequest, err = g.prForCommit(ctx, commit)
if err != nil {
return nil, fmt.Errorf("failed to check for commit pull request: %w", err)
}
commits = append(commits, commit)
}
return commits, nil
@ -257,76 +269,52 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm
return repositoryCommits, nil
}
func (g *GitHub) Changesets(ctx context.Context, commits []Commit) ([]Changeset, error) {
func (g *GitHub) prForCommit(ctx context.Context, commit Commit) (*PullRequest, error) {
// We naively look up the associated PR for each commit through the "List pull requests associated with a commit"
// endpoint. This requires len(commits) requests.
// Using the "List pull requests" endpoint might be faster, as it allows us to fetch 100 arbitrary PRs per request,
// but worst case we need to look up all PRs made in the repository ever.
changesets := make([]Changeset, 0, len(commits))
log := g.log.With("commit.hash", commit.Hash)
page := 1
var associatedPRs []*github.PullRequest
for _, commit := range commits {
log := g.log.With("commit.hash", commit.Hash)
page := 1
var associatedPRs []*github.PullRequest
for {
log.Debug("fetching pull requests associated with commit", "page", page)
prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit(
ctx, g.options.Owner, g.options.Repo,
commit.Hash, &github.ListOptions{
Page: page,
PerPage: GitHubPerPageMax,
})
if err != nil {
return nil, err
}
associatedPRs = append(associatedPRs, prs...)
if page == resp.LastPage || resp.LastPage == 0 {
break
}
page = resp.NextPage
}
var pullrequest *github.PullRequest
for _, pr := range associatedPRs {
// We only look for the PR that has this commit set as the "merge commit" => The result of squashing this branch onto main
if pr.GetMergeCommitSHA() == commit.Hash {
pullrequest = pr
break
}
}
if pullrequest == nil {
log.Warn("did not find associated pull request, not considering it for changesets")
// No pull request was found for this commit, nothing to do here
// TODO: We could also return the minimal changeset for this commit, so at least it would come up in the changelog.
continue
}
log = log.With("pullrequest.id", pullrequest.GetID())
// TODO: Parse PR description for overrides
changelogEntries, _, err := AnalyzeCommits([]Commit{commit})
if err != nil {
log.Warn("unable to parse changelog entries", "error", err)
continue
}
if len(changelogEntries) > 0 {
changesets = append(changesets, Changeset{
URL: pullrequest.GetHTMLURL(),
Identifier: fmt.Sprintf("#%d", pullrequest.GetNumber()),
ChangelogEntries: changelogEntries,
for {
log.Debug("fetching pull requests associated with commit", "page", page)
prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit(
ctx, g.options.Owner, g.options.Repo,
commit.Hash, &github.ListOptions{
Page: page,
PerPage: GitHubPerPageMax,
})
if err != nil {
return nil, err
}
associatedPRs = append(associatedPRs, prs...)
if page == resp.LastPage || resp.LastPage == 0 {
break
}
page = resp.NextPage
}
return changesets, nil
var pullrequest *github.PullRequest
for _, pr := range associatedPRs {
// We only look for the PR that has this commit set as the "merge commit" => The result of squashing this branch onto main
if pr.GetMergeCommitSHA() == commit.Hash {
pullrequest = pr
break
}
}
if pullrequest == nil {
return nil, nil
}
return gitHubPRToPullRequest(pullrequest), nil
}
func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []string) error {
func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error {
existingLabels := make([]string, 0, len(labels))
page := 1
@ -354,12 +342,12 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []string) error {
}
for _, label := range labels {
if !slices.Contains(existingLabels, label) {
if !slices.Contains(existingLabels, string(label)) {
g.log.Info("creating label in repository", "label.name", label)
_, _, err := g.client.Issues.CreateLabel(
ctx, g.options.Owner, g.options.Repo,
&github.Label{
Name: &label,
Name: Pointer(string(label)),
Color: Pointer(GitHubLabelColor),
},
)
@ -422,7 +410,7 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest)
// TODO: String ID?
pr.ID = ghPR.GetNumber()
err = g.SetPullRequestLabels(ctx, pr, []string{}, pr.Labels)
err = g.SetPullRequestLabels(ctx, pr, []Label{}, pr.Labels)
if err != nil {
return err
}
@ -445,20 +433,25 @@ func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest)
return nil
}
func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []string) error {
func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []Label) error {
for _, label := range remove {
_, err := g.client.Issues.RemoveLabelForIssue(
ctx, g.options.Owner, g.options.Repo,
pr.ID, label,
pr.ID, string(label),
)
if err != nil {
return err
}
}
addString := make([]string, 0, len(add))
for _, label := range add {
addString = append(addString, string(label))
}
_, _, err := g.client.Issues.AddLabelsToIssue(
ctx, g.options.Owner, g.options.Repo,
pr.ID, add,
pr.ID, addString,
)
if err != nil {
return err
@ -481,7 +474,7 @@ func (g *GitHub) ClosePullRequest(ctx context.Context, pr *ReleasePullRequest) e
return nil
}
func (g *GitHub) PendingReleases(ctx context.Context) ([]*ReleasePullRequest, error) {
func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*ReleasePullRequest, error) {
page := 1
var prs []*ReleasePullRequest
@ -509,7 +502,7 @@ func (g *GitHub) PendingReleases(ctx context.Context) ([]*ReleasePullRequest, er
for _, pr := range ghPRs {
pending := slices.ContainsFunc(pr.Labels, func(l *github.Label) bool {
return l.GetName() == LabelReleasePending
return l.GetName() == string(pendingLabel)
})
if !pending {
continue
@ -558,10 +551,21 @@ func (g *GitHub) CreateRelease(ctx context.Context, commit Commit, title, change
return nil
}
func gitHubPRToPullRequest(pr *github.PullRequest) *PullRequest {
return &PullRequest{
ID: pr.GetNumber(),
Title: pr.GetTitle(),
Description: pr.GetBody(),
}
}
func gitHubPRToReleasePullRequest(pr *github.PullRequest) *ReleasePullRequest {
labels := make([]string, 0, len(pr.Labels))
labels := make([]Label, 0, len(pr.Labels))
for _, label := range pr.Labels {
labels = append(labels, label.GetName())
labelName := Label(label.GetName())
if slices.Contains(KnownLabels, Label(label.GetName())) {
labels = append(labels, labelName)
}
}
var releaseCommit *Commit

8
git.go
View file

@ -13,15 +13,9 @@ import (
)
const (
CommitSearchDepth = 50 // TODO: Increase
GitRemoteName = "origin"
GitRemoteName = "origin"
)
type Commit struct {
Hash string
Message string
}
type Tag struct {
Hash string
Name string

20
go.mod
View file

@ -1,6 +1,6 @@
module github.com/apricote/releaser-pleaser
go 1.22.4
go 1.23.0
require (
github.com/blang/semver/v4 v4.0.0
@ -14,11 +14,11 @@ require (
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/cyphar/filepath-securejoin v0.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
@ -31,14 +31,12 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/tools v0.13.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

46
go.sum
View file

@ -1,8 +1,8 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@ -13,11 +13,11 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE=
github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -75,8 +75,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -97,12 +97,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -110,13 +108,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -130,15 +126,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -146,14 +142,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -12,8 +12,10 @@ import (
"github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
)
var sectionStartRegex = regexp.MustCompile(`^<!-- section-start (.+) -->`)
var sectionEndRegex = regexp.MustCompile(`^<!-- section-end (.+) -->`)
var (
sectionStartRegex = regexp.MustCompile(`^<!-- section-start (.+) -->`)
sectionEndRegex = regexp.MustCompile(`^<!-- section-end (.+) -->`)
)
const (
sectionTrigger = "<!--"
@ -21,8 +23,7 @@ const (
SectionEndFormat = "<!-- section-end %s -->"
)
type sectionParser struct {
}
type sectionParser struct{}
func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) {
line, _ := reader.PeekLine()
@ -75,8 +76,7 @@ func (s *sectionParser) Trigger() []byte {
return []byte(sectionTrigger)
}
type section struct {
}
type section struct{}
// Section is an extension that allow you to use group content under a shared parent ast node.
var Section = &section{}

View file

@ -149,7 +149,7 @@ func (r *Renderer) writeByte(w io.Writer, c byte) error {
return nil
}
// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefices are added at the
// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefixes 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))
@ -178,7 +178,7 @@ func (r *Renderer) popPrefix() {
// 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 {
func (r *Renderer) openBlock(w util.BufWriter, _ []byte, node ast.Node) error {
r.openBlocks = append(r.openBlocks, blockState{
node: node,
fresh: true,
@ -222,7 +222,7 @@ func (r *Renderer) closeBlock(w io.Writer) error {
}
// 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) {
func (r *Renderer) renderDocument(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
r.listStack, r.prefixStack, r.prefix, r.atNewline = nil, nil, nil, false
return ast.WalkContinue, nil
}
@ -331,7 +331,7 @@ func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node a
return ast.WalkStop, fmt.Errorf(": %w", err)
}
if err := r.writeByte(w, '\n'); err != nil {
return ast.WalkStop, nil
return ast.WalkStop, fmt.Errorf(": %w", err)
}
// Write the contents of the fenced code block.
@ -594,7 +594,7 @@ func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node
}
// 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) {
func (r *Renderer) renderEmphasis(w util.BufWriter, _ []byte, node ast.Node, _ 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)
@ -663,7 +663,7 @@ func (r *Renderer) renderLinkOrImage(w util.BufWriter, open string, dest, title
}
// 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) {
func (r *Renderer) renderImage(w util.BufWriter, _ []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)
@ -672,7 +672,7 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, e
}
// 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) {
func (r *Renderer) renderLink(w util.BufWriter, _ []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)
@ -724,7 +724,7 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, en
}
// 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) {
func (r *Renderer) renderString(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
@ -801,7 +801,7 @@ func (r *Renderer) renderTableRow(w util.BufWriter, source []byte, node ast.Node
return ast.WalkContinue, nil
}
func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderTableCell(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
if node.NextSibling() != nil {
if _, err := r.writeString(w, " | "); err != nil {
@ -813,14 +813,14 @@ func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Nod
return ast.WalkContinue, nil
}
func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderStrikethrough(w util.BufWriter, _ []byte, _ ast.Node, _ 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) {
func (r *Renderer) renderTaskCheckBox(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
var fill byte = ' '
if task := node.(*exast.TaskCheckBox); task.IsChecked {

View file

@ -11,25 +11,26 @@ import (
"github.com/stretchr/testify/require"
)
var (
author = &object.Signature{
Name: "releaser-pleaser",
When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC),
}
)
var author = &object.Signature{
Name: "releaser-pleaser",
When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC),
}
type CommitOption func(*commitOptions)
type commitOptions struct {
cleanFiles bool
files []commitFile
tags []string
}
type commitFile struct {
path string
content string
}
type Commit func(*testing.T, *git.Repository) error
type Repo func(*testing.T) *git.Repository
func WithCommit(message string, options ...CommitOption) Commit {
@ -61,9 +62,9 @@ func WithCommit(message string, options ...CommitOption) Commit {
for _, fileInfo := range opts.files {
file, err := wt.Filesystem.Create(fileInfo.path)
require.NoError(t, err, "failed to create file %q", fileInfo.path)
defer file.Close()
_, err = file.Write([]byte(fileInfo.content))
file.Close()
require.NoError(t, err, "failed to write content to file %q", fileInfo.path)
}
@ -83,7 +84,6 @@ func WithCommit(message string, options ...CommitOption) Commit {
}
return nil
}
}

View file

@ -6,6 +6,7 @@ import (
"fmt"
"log"
"regexp"
"strings"
"text/template"
"github.com/yuin/goldmark/ast"
@ -30,11 +31,14 @@ func init() {
}
}
// ReleasePullRequest
//
// TODO: Reuse [PullRequest]
type ReleasePullRequest struct {
ID int
Title string
Description string
Labels []string
Labels []Label
Head string
ReleaseCommit *Commit
@ -43,11 +47,11 @@ type ReleasePullRequest struct {
func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) {
rp := &ReleasePullRequest{
Head: head,
Labels: []string{LabelReleasePending},
Labels: []Label{LabelReleasePending},
}
rp.SetTitle(branch, version)
if err := rp.SetDescription(changelogEntry); err != nil {
if err := rp.SetDescription(changelogEntry, ReleaseOverrides{}); err != nil {
return nil, err
}
@ -57,7 +61,7 @@ func NewReleasePullRequest(head, branch, version, changelogEntry string) (*Relea
type ReleaseOverrides struct {
Prefix string
Suffix string
// TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the changesets
// TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the commits
NextVersionType NextVersionType
}
@ -88,18 +92,20 @@ func (n NextVersionType) String() string {
}
}
// PR Labels
const (
LabelNextVersionTypeNormal = "rp-next-version::normal"
LabelNextVersionTypeRC = "rp-next-version::rc"
LabelNextVersionTypeBeta = "rp-next-version::beta"
LabelNextVersionTypeAlpha = "rp-next-version::alpha"
// Label is the string identifier of a pull/merge request label on the forge.
type Label string
LabelReleasePending = "rp-release::pending"
LabelReleaseTagged = "rp-release::tagged"
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 Labels = []string{
var KnownLabels = []Label{
LabelNextVersionTypeNormal,
LabelNextVersionTypeRC,
LabelNextVersionTypeBeta,
@ -115,7 +121,6 @@ const (
)
const (
MarkdownSectionOverrides = "overrides"
MarkdownSectionChangelog = "changelog"
)
@ -150,6 +155,9 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R
overrides.NextVersionType = NextVersionTypeBeta
case LabelNextVersionTypeAlpha:
overrides.NextVersionType = NextVersionTypeAlpha
case LabelReleasePending, LabelReleaseTagged:
// These labels have no effect on the versioning.
break
}
}
@ -190,51 +198,6 @@ func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (Rele
return overrides, nil
}
func (pr *ReleasePullRequest) overridesText() (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 (pr *ReleasePullRequest) ChangelogText() (string, error) {
source := []byte(pr.Description)
gm := markdown.New()
@ -289,11 +252,11 @@ func textFromLines(source []byte, n ast.Node) string {
content = append(content, line.Value(source)...)
}
return string(content)
return strings.TrimSpace(string(content))
}
func (pr *ReleasePullRequest) SetTitle(branch, version string) {
pr.Title = fmt.Sprintf("chore(%s): release %s", branch, version)
pr.Title = fmt.Sprintf(TitleFormat, branch, version)
}
func (pr *ReleasePullRequest) Version() (string, error) {
@ -305,14 +268,9 @@ func (pr *ReleasePullRequest) Version() (string, error) {
return matches[2], nil
}
func (pr *ReleasePullRequest) SetDescription(changelogEntry string) error {
overrides, err := pr.overridesText()
if err != nil {
return err
}
func (pr *ReleasePullRequest) SetDescription(changelogEntry string, overrides ReleaseOverrides) error {
var description bytes.Buffer
err = releasePRTemplate.Execute(&description, map[string]any{
err := releasePRTemplate.Execute(&description, map[string]any{
"Changelog": changelogEntry,
"Overrides": overrides,
})

View file

@ -1,29 +1,32 @@
---
<!-- section-start changelog -->
{{ .Changelog }}
<!-- section-end 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.
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
### Prefix
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
### Suffix / End
This will be added to the end of the release notes.
```rp-suffix
{{- if .Overrides.Suffix }}
{{ .Overrides.Suffix }}{{ end }}
```
<!-- section-end overrides -->
{{ end }}
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
</details>

View file

@ -49,121 +49,96 @@ func TestReleasePullRequest_SetDescription(t *testing.T) {
tests := []struct {
name string
pr *ReleasePullRequest
changelogEntry string
overrides ReleaseOverrides
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "empty description",
pr: &ReleasePullRequest{},
name: "no overrides",
changelogEntry: `## v1.0.0`,
want: `---
<!-- section-start changelog -->
overrides: ReleaseOverrides{},
want: `<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
## releaser-pleaser Instructions
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
<!-- 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.
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
### Prefix
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
` + "```" + `rp-prefix
` + "```" + `
### Suffix
### Suffix / End
This will be added to the end of the release notes.
` + "```" + `rp-suffix
` + "```" + `
<!-- section-end overrides -->
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
</details>
`,
wantErr: assert.NoError,
},
{
name: "existing overrides",
pr: &ReleasePullRequest{
Description: `---
<!-- section-start changelog -->
## v0.1.0
### Features
- bedazzle
<!-- section-end changelog -->
---
## 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)
`,
},
name: "existing overrides",
changelogEntry: `## v1.0.0`,
want: `---
<!-- section-start changelog -->
overrides: ReleaseOverrides{
Prefix: "This release is awesome!",
Suffix: "Fooo",
},
want: `<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
## releaser-pleaser Instructions
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
<!-- 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.
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
### Prefix
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
` + "```" + `rp-prefix
This release is awesome!
` + "```" + `
### Suffix
### Suffix / End
This will be added to the end of the release notes.
` + "```" + `rp-suffix
Fooo
` + "```" + `
<!-- section-end overrides -->
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
</details>
`,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.pr.SetDescription(tt.changelogEntry)
pr := &ReleasePullRequest{}
err := pr.SetDescription(tt.changelogEntry, tt.overrides)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, tt.pr.Description)
assert.Equal(t, tt.want, pr.Description)
})
}
}

406
releaserpleaser.go Normal file
View file

@ -0,0 +1,406 @@
package rp
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
)
const (
PullRequestBranchFormat = "releaser-pleaser--branches--%s"
)
type ReleaserPleaser struct {
forge Forge
logger *slog.Logger
targetBranch string
commitParser CommitParser
nextVersion VersioningStrategy
extraFiles []string
updaters []Updater
}
func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy, extraFiles []string, updaters []Updater) *ReleaserPleaser {
return &ReleaserPleaser{
forge: forge,
logger: logger,
targetBranch: targetBranch,
commitParser: commitParser,
nextVersion: versioningStrategy,
extraFiles: extraFiles,
updaters: updaters,
}
}
func (rp *ReleaserPleaser) EnsureLabels(ctx context.Context) error {
// TODO: Wrap Error
return rp.forge.EnsureLabelsExist(ctx, KnownLabels)
}
func (rp *ReleaserPleaser) Run(ctx context.Context) error {
err := rp.runOnboarding(ctx)
if err != nil {
return fmt.Errorf("failed to onboard repository: %w", err)
}
err = rp.runCreatePendingReleases(ctx)
if err != nil {
return fmt.Errorf("failed to create pending releases: %w", err)
}
err = rp.runReconcileReleasePR(ctx)
if err != nil {
return fmt.Errorf("failed to reconcile release pull request: %w", err)
}
return nil
}
func (rp *ReleaserPleaser) runOnboarding(ctx context.Context) error {
err := rp.EnsureLabels(ctx)
if err != nil {
return fmt.Errorf("failed to ensure all labels exist: %w", err)
}
return nil
}
func (rp *ReleaserPleaser) runCreatePendingReleases(ctx context.Context) error {
logger := rp.logger.With("method", "runCreatePendingReleases")
logger.InfoContext(ctx, "checking for pending releases")
prs, err := rp.forge.PendingReleases(ctx, LabelReleasePending)
if err != nil {
return err
}
if len(prs) == 0 {
logger.InfoContext(ctx, "No pending releases found")
return nil
}
logger.InfoContext(ctx, "Found pending releases", "length", len(prs))
for _, pr := range prs {
err = rp.createPendingRelease(ctx, pr)
if err != nil {
return err
}
}
return nil
}
func (rp *ReleaserPleaser) createPendingRelease(ctx context.Context, pr *ReleasePullRequest) error {
logger := rp.logger.With(
"method", "createPendingRelease",
"pr.id", pr.ID,
"pr.title", pr.Title)
if pr.ReleaseCommit == nil {
return fmt.Errorf("pull request is missing the merge commit")
}
logger.Info("Creating release", "commit.hash", pr.ReleaseCommit.Hash)
version, err := pr.Version()
if err != nil {
return err
}
changelog, err := pr.ChangelogText()
if err != nil {
return err
}
// TODO: pre-release & latest
logger.DebugContext(ctx, "Creating release on forge")
err = rp.forge.CreateRelease(ctx, *pr.ReleaseCommit, version, changelog, false, true)
if err != nil {
return fmt.Errorf("failed to create release on forge: %w", err)
}
logger.DebugContext(ctx, "created release", "release.title", version, "release.url", rp.forge.ReleaseURL(version))
logger.DebugContext(ctx, "updating pr labels")
err = rp.forge.SetPullRequestLabels(ctx, pr, []Label{LabelReleasePending}, []Label{LabelReleaseTagged})
if err != nil {
return err
}
logger.DebugContext(ctx, "updated pr labels")
logger.InfoContext(ctx, "Created release", "release.title", version, "release.url", rp.forge.ReleaseURL(version))
return nil
}
func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
logger := rp.logger.With("method", "runReconcileReleasePR")
releases, err := rp.forge.LatestTags(ctx)
if err != nil {
return err
}
if releases.Latest != nil {
logger.InfoContext(ctx, "found latest tag", "tag.hash", releases.Latest.Hash, "tag.name", releases.Latest.Name)
if releases.Stable != nil && releases.Latest.Hash != releases.Stable.Hash {
logger.InfoContext(ctx, "found stable tag", "tag.hash", releases.Stable.Hash, "tag.name", releases.Stable.Name)
}
} else {
logger.InfoContext(ctx, "no latest tag found")
}
releasableCommits, err := rp.forge.CommitsSince(ctx, releases.Stable)
if err != nil {
return err
}
logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits))
// TODO: Handle commit overrides
analyzedCommits, err := rp.commitParser.Analyze(releasableCommits)
if err != nil {
return err
}
logger.InfoContext(ctx, "Analyzed commits", "length", len(analyzedCommits))
rpBranch := fmt.Sprintf(PullRequestBranchFormat, rp.targetBranch)
rpBranchRef := plumbing.NewBranchReferenceName(rpBranch)
// Check Forge for open PR
// Get any modifications from open PR
// Clone Repo
// Run Updaters + Changelog
// Upsert PR
pr, err := rp.forge.PullRequestForBranch(ctx, rpBranch)
if err != nil {
return err
}
if pr != nil {
logger.InfoContext(ctx, "found existing release pull request", "pr.id", pr.ID, "pr.title", pr.Title)
}
if len(analyzedCommits) == 0 {
if pr != nil {
logger.InfoContext(ctx, "closing existing pull requests, no commits available", "pr.id", pr.ID, "pr.title", pr.Title)
err = rp.forge.ClosePullRequest(ctx, pr)
if err != nil {
return err
}
} else {
logger.InfoContext(ctx, "No commits available for release")
}
return nil
}
var releaseOverrides ReleaseOverrides
if pr != nil {
releaseOverrides, err = pr.GetOverrides()
if err != nil {
return err
}
}
versionBump := VersionBumpFromCommits(analyzedCommits)
// TODO: Set version in release pr
nextVersion, err := rp.nextVersion(releases, versionBump, releaseOverrides.NextVersionType)
if err != nil {
return err
}
logger.InfoContext(ctx, "next version", "version", nextVersion)
logger.DebugContext(ctx, "cloning repository", "clone.url", rp.forge.CloneURL())
repo, err := CloneRepo(ctx, rp.forge.CloneURL(), rp.targetBranch, rp.forge.GitAuth())
if err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
if branch, _ := repo.Branch(rpBranch); branch != nil {
logger.DebugContext(ctx, "deleting previous releaser-pleaser branch locally", "branch.name", rpBranch)
if err = repo.DeleteBranch(rpBranch); err != nil {
return err
}
}
if err = worktree.Checkout(&git.CheckoutOptions{
Branch: rpBranchRef,
Create: true,
}); err != nil {
return fmt.Errorf("failed to check out branch: %w", err)
}
changelogEntry, err := NewChangelogEntry(analyzedCommits, nextVersion, rp.forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix)
if err != nil {
return fmt.Errorf("failed to build changelog entry: %w", err)
}
// Info for updaters
info := ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry}
updateFile := func(path string, updaters []Updater) error {
file, err := worktree.Filesystem.OpenFile(path, os.O_RDWR, 0)
if err != nil {
return err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return err
}
updatedContent := string(content)
for _, updater := range updaters {
updatedContent, err = updater.UpdateContent(updatedContent, info)
if err != nil {
return fmt.Errorf("failed to run updater %T on file %s", updater, path)
}
}
err = file.Truncate(0)
if err != nil {
return fmt.Errorf("failed to replace file content: %w", err)
}
_, err = file.Seek(0, 0)
if err != nil {
return fmt.Errorf("failed to replace file content: %w", err)
}
_, err = file.Write([]byte(updatedContent))
if err != nil {
return fmt.Errorf("failed to replace file content: %w", err)
}
_, err = worktree.Add(path)
if err != nil {
return fmt.Errorf("failed to add updated file to git worktree: %w", err)
}
return nil
}
err = updateFile(ChangelogFile, []Updater{&ChangelogUpdater{}})
if err != nil {
return fmt.Errorf("failed to update changelog file: %w", err)
}
for _, path := range rp.extraFiles {
_, err = worktree.Filesystem.Stat(path)
if err != nil {
// TODO: Check for non existing file or dirs
return fmt.Errorf("failed to run file updater because the file %s does not exist: %w", path, err)
}
err = updateFile(path, rp.updaters)
if err != nil {
return fmt.Errorf("failed to run file updater: %w", err)
}
}
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion)
releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{
Author: GitSignature(),
Committer: GitSignature(),
})
if err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommitHash.String(), "commit.message", releaseCommitMessage)
newReleasePRChanges := true
// Check if anything changed in comparison to the remote branch (if exists)
if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName(GitRemoteName, rpBranch), false); err != nil {
if err.Error() != "reference not found" {
// "reference not found" is expected and we should always push
return err
}
} else {
remoteCommit, err := repo.CommitObject(remoteRef.Hash())
if err != nil {
return err
}
localCommit, err := repo.CommitObject(releaseCommitHash)
if err != nil {
return err
}
diff, err := localCommit.PatchContext(ctx, remoteCommit)
if err != nil {
return err
}
newReleasePRChanges = len(diff.FilePatches()) > 0
}
if newReleasePRChanges {
pushRefSpec := config.RefSpec(fmt.Sprintf(
"+%s:%s",
rpBranchRef,
// This needs to be the local branch name, not the remotes/origin ref
// See https://stackoverflow.com/a/75727620
rpBranchRef,
))
logger.DebugContext(ctx, "pushing branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String())
if err = repo.PushContext(ctx, &git.PushOptions{
RemoteName: GitRemoteName,
RefSpecs: []config.RefSpec{pushRefSpec},
Force: true,
Auth: rp.forge.GitAuth(),
}); err != nil {
return fmt.Errorf("failed to push branch: %w", err)
}
logger.InfoContext(ctx, "pushed branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String())
} else {
logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push")
}
// Open/Update PR
if pr == nil {
pr, err = NewReleasePullRequest(rpBranch, rp.targetBranch, nextVersion, changelogEntry)
if err != nil {
return err
}
err = rp.forge.CreatePullRequest(ctx, pr)
if err != nil {
return err
}
logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID)
} else {
pr.SetTitle(rp.targetBranch, nextVersion)
overrides, err := pr.GetOverrides()
if err != nil {
return err
}
err = pr.SetDescription(changelogEntry, overrides)
if err != nil {
return err
}
err = rp.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
}

View file

@ -1,12 +1,47 @@
package rp
import (
"context"
"github.com/go-git/go-git/v5"
"fmt"
"regexp"
"strings"
)
func RunUpdater(ctx context.Context, version string, worktree *git.Worktree) error {
// TODO: Implement updater for Go,Python,ExtraFilesMarkers
return nil
var (
GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`)
ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`)
)
type ReleaseInfo struct {
Version string
ChangelogEntry string
}
type Updater interface {
UpdateContent(content string, info ReleaseInfo) (string, error)
}
type GenericUpdater struct{}
func (u *GenericUpdater) UpdateContent(content string, info ReleaseInfo) (string, error) {
// We strip the "v" prefix to avoid adding/removing it from the users input.
version := strings.TrimPrefix(info.Version, "v")
return GenericUpdaterSemVerRegex.ReplaceAllString(content, version+"${2}"), nil
}
type ChangelogUpdater struct{}
func (u *ChangelogUpdater) UpdateContent(content string, info ReleaseInfo) (string, error) {
headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content)
if headerIndex == nil && len(content) != 0 {
return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match")
}
if headerIndex != nil {
// Remove the header from the content
content = content[headerIndex[1]:]
}
content = ChangelogHeader + "\n\n" + info.ChangelogEntry + content
return content, nil
}

129
updater_test.go Normal file
View file

@ -0,0 +1,129 @@
package rp
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type updaterTestCase struct {
name string
content string
info ReleaseInfo
want string
wantErr assert.ErrorAssertionFunc
}
func runUpdaterTest(t *testing.T, updater Updater, tt updaterTestCase) {
t.Helper()
got, err := updater.UpdateContent(tt.content, tt.info)
if !tt.wantErr(t, err, fmt.Sprintf("UpdateContent(%v, %v)", tt.content, tt.info)) {
return
}
assert.Equalf(t, tt.want, got, "UpdateContent(%v, %v)", tt.content, tt.info)
}
func TestGenericUpdater_UpdateContent(t *testing.T) {
updater := &GenericUpdater{}
tests := []updaterTestCase{
{
name: "single line",
content: "v1.0.0 // x-releaser-pleaser-version",
info: ReleaseInfo{
Version: "v1.2.0",
},
want: "v1.2.0 // x-releaser-pleaser-version",
wantErr: assert.NoError,
},
{
name: "multiline line",
content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n",
info: ReleaseInfo{
Version: "v1.2.0",
},
want: "Foooo\n\v1.2.0\nv1.2.0 // x-releaser-pleaser-version\n",
wantErr: assert.NoError,
},
{
name: "invalid existing version",
content: "1.0 // x-releaser-pleaser-version",
info: ReleaseInfo{
Version: "v1.2.0",
},
want: "1.0 // x-releaser-pleaser-version",
wantErr: assert.NoError,
},
{
name: "complicated line",
content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar",
info: ReleaseInfo{
Version: "v1.2.0",
},
want: "version: v1.2.0 => Awesome, isnt it? x-releaser-pleaser-version foobar",
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runUpdaterTest(t, updater, tt)
})
}
}
func TestChangelogUpdater_UpdateContent(t *testing.T) {
updater := &ChangelogUpdater{}
tests := []updaterTestCase{
{
name: "empty file",
content: "",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"},
want: "# Changelog\n\n## v1.0.0\n",
wantErr: assert.NoError,
},
{
name: "well-formatted changelog",
content: `# Changelog
## v0.0.1
- Bazzle
## v0.1.0
### Bazuuum
`,
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"},
want: `# Changelog
## v1.0.0
- Version 1, juhu.
## v0.0.1
- Bazzle
## v0.1.0
### Bazuuum
`,
wantErr: assert.NoError,
},
{
name: "error on invalid header",
content: "What even is this file?",
info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"},
want: "",
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runUpdaterTest(t, updater, tt)
})
}
}

View file

@ -13,7 +13,11 @@ type Releases struct {
Stable *Tag
}
func (r Releases) NextVersion(versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) {
type VersioningStrategy = func(Releases, conventionalcommits.VersionBump, NextVersionType) (string, error)
var _ VersioningStrategy = SemVerNextVersion
func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) {
latest, err := parseSemverWithDefault(r.Latest)
if err != nil {
return "", fmt.Errorf("failed to parse latest version: %w", err)
@ -65,24 +69,22 @@ func (r Releases) NextVersion(versionBump conventionalcommits.VersionBump, nextV
return "v" + next.String(), nil
}
func VersionBumpFromChangesets(changesets []Changeset) conventionalcommits.VersionBump {
func VersionBumpFromCommits(commits []AnalyzedCommit) conventionalcommits.VersionBump {
bump := conventionalcommits.UnknownVersion
for _, changeset := range changesets {
for _, entry := range changeset.ChangelogEntries {
entryBump := conventionalcommits.UnknownVersion
switch {
case entry.BreakingChange:
entryBump = conventionalcommits.MajorVersion
case entry.Type == "feat":
entryBump = conventionalcommits.MinorVersion
case entry.Type == "fix":
entryBump = conventionalcommits.PatchVersion
}
for _, commit := range commits {
entryBump := conventionalcommits.UnknownVersion
switch {
case commit.BreakingChange:
entryBump = conventionalcommits.MajorVersion
case commit.Type == "feat":
entryBump = conventionalcommits.MinorVersion
case commit.Type == "fix":
entryBump = conventionalcommits.PatchVersion
}
if entryBump > bump {
bump = entryBump
}
if entryBump > bump {
bump = entryBump
}
}

View file

@ -10,23 +10,23 @@ import (
func TestReleases_NextVersion(t *testing.T) {
type args struct {
releases Releases
versionBump conventionalcommits.VersionBump
nextVersionType NextVersionType
}
tests := []struct {
name string
releases Releases
args args
want string
wantErr assert.ErrorAssertionFunc
name string
args args
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "simple bump (major)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MajorVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -35,12 +35,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "simple bump (minor)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MinorVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -49,12 +48,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "simple bump (patch)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -63,12 +61,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "normal to prerelease (major)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MajorVersion,
nextVersionType: NextVersionTypeRC,
},
@ -77,12 +74,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "normal to prerelease (minor)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MinorVersion,
nextVersionType: NextVersionTypeRC,
},
@ -91,12 +87,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "normal to prerelease (patch)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeRC,
},
@ -105,11 +100,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease bump (major)",
releases: Releases{
Latest: &Tag{Name: "v2.0.0-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v2.0.0-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MajorVersion,
nextVersionType: NextVersionTypeRC,
},
@ -118,11 +113,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease bump (minor)",
releases: Releases{
Latest: &Tag{Name: "v1.2.0-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.2.0-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MinorVersion,
nextVersionType: NextVersionTypeRC,
},
@ -131,11 +126,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease bump (patch)",
releases: Releases{
Latest: &Tag{Name: "v1.1.2-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.2-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeRC,
},
@ -144,11 +139,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease different bump (major)",
releases: Releases{
Latest: &Tag{Name: "v1.2.0-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.2.0-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MajorVersion,
nextVersionType: NextVersionTypeRC,
},
@ -157,11 +152,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease different bump (minor)",
releases: Releases{
Latest: &Tag{Name: "v1.1.2-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.2-rc.0"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.MinorVersion,
nextVersionType: NextVersionTypeRC,
},
@ -170,11 +165,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease to prerelease",
releases: Releases{
Latest: &Tag{Name: "v1.1.1-alpha.2"},
Stable: &Tag{Name: "v1.1.0"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1-alpha.2"},
Stable: &Tag{Name: "v1.1.0"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeRC,
},
@ -183,11 +178,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease to normal (explicit)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1-alpha.2"},
Stable: &Tag{Name: "v1.1.0"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1-alpha.2"},
Stable: &Tag{Name: "v1.1.0"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeNormal,
},
@ -196,11 +191,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "prerelease to normal (implicit)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1-alpha.2"},
Stable: &Tag{Name: "v1.1.0"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1-alpha.2"},
Stable: &Tag{Name: "v1.1.0"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -209,11 +204,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "nil tag (major)",
releases: Releases{
Latest: nil,
Stable: nil,
},
args: args{
releases: Releases{
Latest: nil,
Stable: nil,
},
versionBump: conventionalcommits.MajorVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -222,11 +217,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "nil tag (minor)",
releases: Releases{
Latest: nil,
Stable: nil,
},
args: args{
releases: Releases{
Latest: nil,
Stable: nil,
},
versionBump: conventionalcommits.MinorVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -235,11 +230,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "nil tag (patch)",
releases: Releases{
Latest: nil,
Stable: nil,
},
args: args{
releases: Releases{
Latest: nil,
Stable: nil,
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -248,11 +243,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "nil stable release (major)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.0"},
Stable: nil,
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.0"},
Stable: nil,
},
versionBump: conventionalcommits.MajorVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -261,12 +256,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "nil stable release (minor)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.0"},
Stable: nil,
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.0"},
Stable: nil,
},
versionBump: conventionalcommits.MinorVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -275,12 +269,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "nil stable release (patch)",
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.0"},
Stable: nil,
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.0"},
Stable: nil,
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeUndefined,
},
@ -290,11 +283,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "error on invalid tag semver",
releases: Releases{
Latest: &Tag{Name: "foodazzle"},
Stable: &Tag{Name: "foodazzle"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "foodazzle"},
Stable: &Tag{Name: "foodazzle"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeRC,
},
@ -303,11 +296,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "error on invalid tag prerelease",
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.foo"},
Stable: &Tag{Name: "v1.1.1-rc.foo"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1-rc.foo"},
Stable: &Tag{Name: "v1.1.1-rc.foo"},
},
versionBump: conventionalcommits.PatchVersion,
nextVersionType: NextVersionTypeRC,
},
@ -316,11 +309,11 @@ func TestReleases_NextVersion(t *testing.T) {
},
{
name: "error on invalid bump",
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
args: args{
releases: Releases{
Latest: &Tag{Name: "v1.1.1"},
Stable: &Tag{Name: "v1.1.1"},
},
versionBump: conventionalcommits.UnknownVersion,
nextVersionType: NextVersionTypeUndefined,
@ -331,95 +324,65 @@ func TestReleases_NextVersion(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.releases.NextVersion(tt.args.versionBump, tt.args.nextVersionType)
if !tt.wantErr(t, err, fmt.Sprintf("Releases(%v, %v).NextVersion(%v, %v)", tt.releases.Latest, tt.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)) {
got, err := SemVerNextVersion(tt.args.releases, tt.args.versionBump, tt.args.nextVersionType)
if !tt.wantErr(t, err, fmt.Sprintf("SemVerNextVersion(Releases(%v, %v), %v, %v)", tt.args.releases.Latest, tt.args.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)) {
return
}
assert.Equalf(t, tt.want, got, "Releases(%v, %v).NextVersion(%v, %v)", tt.releases.Latest, tt.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)
assert.Equalf(t, tt.want, got, "SemVerNextVersion(Releases(%v, %v), %v, %v)", tt.args.releases.Latest, tt.args.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)
})
}
}
func TestVersionBumpFromChangesets(t *testing.T) {
func TestVersionBumpFromCommits(t *testing.T) {
tests := []struct {
name string
changesets []Changeset
want conventionalcommits.VersionBump
name string
analyzedCommits []AnalyzedCommit
want conventionalcommits.VersionBump
}{
{
name: "no entries (unknown)",
changesets: []Changeset{},
want: conventionalcommits.UnknownVersion,
name: "no entries (unknown)",
analyzedCommits: []AnalyzedCommit{},
want: conventionalcommits.UnknownVersion,
},
{
name: "non-release type (unknown)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}}}},
want: conventionalcommits.UnknownVersion,
name: "non-release type (unknown)",
analyzedCommits: []AnalyzedCommit{{Type: "docs"}},
want: conventionalcommits.UnknownVersion,
},
{
name: "single breaking (major)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}}},
want: conventionalcommits.MajorVersion,
name: "single breaking (major)",
analyzedCommits: []AnalyzedCommit{{BreakingChange: true}},
want: conventionalcommits.MajorVersion,
},
{
name: "single feat (minor)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}}},
want: conventionalcommits.MinorVersion,
name: "single feat (minor)",
analyzedCommits: []AnalyzedCommit{{Type: "feat"}},
want: conventionalcommits.MinorVersion,
},
{
name: "single fix (patch)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}}},
want: conventionalcommits.PatchVersion,
name: "single fix (patch)",
analyzedCommits: []AnalyzedCommit{{Type: "fix"}},
want: conventionalcommits.PatchVersion,
},
{
name: "multiple changesets (major)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
{ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}},
},
want: conventionalcommits.MajorVersion,
name: "multiple entries (major)",
analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}},
want: conventionalcommits.MajorVersion,
},
{
name: "multiple changesets (minor)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
{ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}},
},
want: conventionalcommits.MinorVersion,
name: "multiple entries (minor)",
analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}},
want: conventionalcommits.MinorVersion,
},
{
name: "multiple changesets (patch)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}}},
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
},
want: conventionalcommits.PatchVersion,
},
{
name: "multiple entries in one changeset (major)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}}},
},
want: conventionalcommits.MajorVersion,
},
{
name: "multiple entries in one changeset (minor)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}}},
},
want: conventionalcommits.MinorVersion,
},
{
name: "multiple entries in one changeset (patch)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}}},
},
want: conventionalcommits.PatchVersion,
name: "multiple entries (patch)",
analyzedCommits: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}},
want: conventionalcommits.PatchVersion,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, VersionBumpFromChangesets(tt.changesets), "VersionBumpFromChangesets(%v)", tt.changesets)
assert.Equalf(t, tt.want, VersionBumpFromCommits(tt.analyzedCommits), "VersionBumpFromCommits(%v)", tt.analyzedCommits)
})
}
}