diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 38a1f4c..8fbca23 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -21,7 +21,7 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
- version: v1.60.1 # renovate: datasource=github-releases depName=golangci/golangci-lint
+ version: v1.59.1 # renovate: datasource=github-releases depName=golangci/golangci-lint
args: --timeout 5m
@@ -37,12 +37,8 @@ jobs:
go-version-file: go.mod
- name: Run tests
- run: go test -v -race -coverpkg=./... -coverprofile=coverage.txt ./...
+ run: go test -v -race -coverpkg=./... ./...
- - name: Upload results to Codecov
- uses: codecov/codecov-action@v4
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
go-mod-tidy:
runs-on: ubuntu-latest
diff --git a/.github/workflows/releaser-pleaser.yaml b/.github/workflows/releaser-pleaser.yaml
index e0ce818..44690a0 100644
--- a/.github/workflows/releaser-pleaser.yaml
+++ b/.github/workflows/releaser-pleaser.yaml
@@ -9,7 +9,10 @@ on:
- labeled
- unlabeled
-permissions: {}
+permissions:
+ contents: write
+ issues: write
+ pull-requests: write
jobs:
releaser-pleaser:
@@ -18,7 +21,5 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: releaser-pleaser
- uses: ./
- with:
- token: ${{ secrets.RELEASER_PLEASER_TOKEN }}
+ - run: env
+ - uses: ./
diff --git a/.golangci.yaml b/.golangci.yaml
deleted file mode 100644
index b3e717d..0000000
--- a/.golangci.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-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
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19df7c2..01ed7bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,5 @@
# 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.
diff --git a/action.yml b/action.yml
index 9d68042..c84aa28 100644
--- a/action.yml
+++ b/action.yml
@@ -12,19 +12,14 @@ 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.2.0 # x-releaser-pleaser-version
+ image: ghcr.io/apricote/releaser-pleaser:v0.1.0
args:
- run
- --forge=github
- --branch=${{ inputs.branch }}
- - --extra-files="${{ inputs.extra-files }}"
env:
GITHUB_TOKEN: ${{ inputs.token }}
GITHUB_USER: "oauth2"
diff --git a/changelog.go b/changelog.go
index 286faf4..40c65d4 100644
--- a/changelog.go
+++ b/changelog.go
@@ -3,17 +3,26 @@ package rp
import (
"bytes"
_ "embed"
+ "fmt"
"html/template"
+ "io"
"log"
+ "os"
+ "regexp"
+
+ "github.com/go-git/go-git/v5"
)
const (
- ChangelogFile = "CHANGELOG.md"
- ChangelogHeader = "# Changelog"
+ ChangelogFile = "CHANGELOG.md"
+ ChangelogFileBuffer = "CHANGELOG.md.tmp"
+ ChangelogHeader = "# Changelog"
)
var (
changelogTemplate *template.Template
+
+ headerRegex = regexp.MustCompile(`^# Changelog\n`)
)
//go:embed changelog.md.tpl
@@ -27,16 +36,72 @@ func init() {
}
}
-func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) {
+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) {
features := make([]AnalyzedCommit, 0)
fixes := make([]AnalyzedCommit, 0)
- for _, commit := range commits {
- switch commit.Type {
- case "feat":
- features = append(features, commit)
- case "fix":
- fixes = append(fixes, commit)
+ for _, changeset := range changesets {
+ for _, commit := range changeset.ChangelogEntries {
+ switch commit.Type {
+ case "feat":
+ features = append(features, commit)
+ case "fix":
+ fixes = append(fixes, commit)
+ }
}
}
diff --git a/changelog_test.go b/changelog_test.go
index 3fabff8..91ffc85 100644
--- a/changelog_test.go
+++ b/changelog_test.go
@@ -1,22 +1,106 @@
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 {
- analyzedCommits []AnalyzedCommit
- version string
- link string
- prefix string
- suffix string
+ changesets []Changeset
+ version string
+ link string
+ prefix string
+ suffix string
}
tests := []struct {
name string
@@ -27,9 +111,9 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "empty",
args: args{
- analyzedCommits: []AnalyzedCommit{},
- version: "1.0.0",
- link: "https://example.com/1.0.0",
+ changesets: []Changeset{{ChangelogEntries: []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,
@@ -37,13 +121,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "single feature",
args: args{
- analyzedCommits: []AnalyzedCommit{
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
{
Commit: Commit{},
Type: "feat",
Description: "Foobar!",
},
- },
+ }}},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@@ -53,13 +137,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "single fix",
args: args{
- analyzedCommits: []AnalyzedCommit{
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
- },
+ }}},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@@ -69,7 +153,7 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "multiple commits with scopes",
args: args{
- analyzedCommits: []AnalyzedCommit{
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
{
Commit: Commit{},
Type: "feat",
@@ -92,7 +176,7 @@ func Test_NewChangelogEntry(t *testing.T) {
Description: "So sad!",
Scope: ptr("sad"),
},
- },
+ }}},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@@ -112,13 +196,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "prefix",
args: args{
- analyzedCommits: []AnalyzedCommit{
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
- },
+ }}},
version: "1.0.0",
link: "https://example.com/1.0.0",
prefix: "### Breaking Changes",
@@ -135,13 +219,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "suffix",
args: args{
- analyzedCommits: []AnalyzedCommit{
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
- },
+ }}},
version: "1.0.0",
link: "https://example.com/1.0.0",
suffix: "### Compatibility\n\nThis version is compatible with flux-compensator v2.2 - v2.9.",
@@ -161,7 +245,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.analyzedCommits, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
+ got, err := NewChangelogEntry(tt.args.changesets, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
if !tt.wantErr(t, err) {
return
}
diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go
index 7661af5..4b7c9e2 100644
--- a/cmd/rp/cmd/run.go
+++ b/cmd/rp/cmd/run.go
@@ -1,13 +1,21 @@
package cmd
import (
- "strings"
+ "context"
+ "fmt"
+ "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",
@@ -15,11 +23,10 @@ var runCmd = &cobra.Command{
}
var (
- flagForge string
- flagBranch string
- flagOwner string
- flagRepo string
- flagExtraFiles string
+ flagForge string
+ flagBranch string
+ flagOwner string
+ flagRepo string
)
func init() {
@@ -31,7 +38,6 @@ 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 {
@@ -44,50 +50,319 @@ func run(cmd *cobra.Command, _ []string) error {
"repo", flagRepo,
)
- var forge rp.Forge
+ var f rp.Forge
forgeOptions := rp.ForgeOptions{
Repository: flagRepo,
BaseBranch: flagBranch,
}
- switch flagForge { // nolint:gocritic // Will become a proper switch once gitlab is added
- // case "gitlab":
- // f = rp.NewGitLab(forgeOptions)
+ switch flagForge {
+ //case "gitlab":
+ //f = rp.NewGitLab(forgeOptions)
case "github":
logger.DebugContext(ctx, "using forge GitHub")
- forge = rp.NewGitHub(logger, &rp.GitHubOptions{
+ f = rp.NewGitHub(logger, &rp.GitHubOptions{
ForgeOptions: forgeOptions,
Owner: flagOwner,
Repo: flagRepo,
})
}
- extraFiles := parseExtraFiles(flagExtraFiles)
+ err := ensureLabels(ctx, f)
+ if err != nil {
+ return fmt.Errorf("failed to ensure all labels exist: %w", err)
+ }
- releaserPleaser := rp.New(
- forge,
- logger,
- flagBranch,
- rp.NewConventionalCommitsParser(),
- rp.SemVerNextVersion,
- extraFiles,
- []rp.Updater{&rp.GenericUpdater{}},
- )
+ err = createPendingReleases(ctx, f)
+ if err != nil {
+ return fmt.Errorf("failed to create pending releases: %w", err)
+ }
- return releaserPleaser.Run(ctx)
+ 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
}
-func parseExtraFiles(input string) []string {
- lines := strings.Split(input, "\n")
+func ensureLabels(ctx context.Context, forge rp.Forge) error {
+ return forge.EnsureLabelsExist(ctx, rp.Labels)
+}
- extraFiles := make([]string, 0, len(lines))
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if len(line) > 0 {
- extraFiles = append(extraFiles, line)
+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 extraFiles
+ 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
}
diff --git a/commits.go b/commits.go
index f0c64e9..565deb7 100644
--- a/commits.go
+++ b/commits.go
@@ -7,19 +7,6 @@ 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
@@ -28,36 +15,24 @@ type AnalyzedCommit struct {
BreakingChange bool
}
-type CommitParser interface {
- Analyze(commits []Commit) ([]AnalyzedCommit, error)
-}
-
-type ConventionalCommitsParser struct {
- machine conventionalcommits.Machine
-}
-
-func NewConventionalCommitsParser() *ConventionalCommitsParser {
+func AnalyzeCommits(commits []Commit) ([]AnalyzedCommit, conventionalcommits.VersionBump, error) {
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 := c.machine.Parse([]byte(commit.Message))
+ msg, err := parserMachine.Parse([]byte(commit.Message))
if err != nil {
- return nil, fmt.Errorf("failed to parse message of commit %q: %w", commit.Hash, err)
+ return nil, conventionalcommits.UnknownVersion, fmt.Errorf("failed to parse message of commit %q: %w", commit.Hash, err)
}
conventionalCommit, ok := msg.(*conventionalcommits.ConventionalCommit)
if !ok {
- return nil, fmt.Errorf("unable to get ConventionalCommit from parser result: %T", msg)
+ return nil, conventionalcommits.UnknownVersion, fmt.Errorf("unable to get ConventionalCommit from parser result: %T", msg)
}
commitVersionBump := conventionalCommit.VersionBump(conventionalcommits.DefaultStrategy)
@@ -72,7 +47,11 @@ func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit,
})
}
+ if commitVersionBump > highestVersionBump {
+ // Get max version bump from all releasable commits
+ highestVersionBump = commitVersionBump
+ }
}
- return analyzedCommits, nil
+ return analyzedCommits, highestVersionBump, nil
}
diff --git a/commits_test.go b/commits_test.go
index e58a718..0e686e6 100644
--- a/commits_test.go
+++ b/commits_test.go
@@ -3,6 +3,7 @@ package rp
import (
"testing"
+ "github.com/leodido/go-conventionalcommits"
"github.com/stretchr/testify/assert"
)
@@ -11,12 +12,14 @@ 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,
},
{
@@ -27,6 +30,7 @@ func TestAnalyzeCommits(t *testing.T) {
},
},
expectedCommits: nil,
+ expectedBump: conventionalcommits.UnknownVersion,
wantErr: assert.Error,
},
{
@@ -37,6 +41,7 @@ func TestAnalyzeCommits(t *testing.T) {
},
},
expectedCommits: []AnalyzedCommit{},
+ expectedBump: conventionalcommits.UnknownVersion,
wantErr: assert.NoError,
},
{
@@ -56,7 +61,8 @@ func TestAnalyzeCommits(t *testing.T) {
Description: "blabla",
},
},
- wantErr: assert.NoError,
+ expectedBump: conventionalcommits.PatchVersion,
+ wantErr: assert.NoError,
},
{
name: "highest bump (minor)",
@@ -80,7 +86,8 @@ func TestAnalyzeCommits(t *testing.T) {
Description: "foobar",
},
},
- wantErr: assert.NoError,
+ expectedBump: conventionalcommits.MinorVersion,
+ wantErr: assert.NoError,
},
{
@@ -106,17 +113,19 @@ func TestAnalyzeCommits(t *testing.T) {
BreakingChange: true,
},
},
- wantErr: assert.NoError,
+ expectedBump: conventionalcommits.MajorVersion,
+ wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- analyzedCommits, err := NewConventionalCommitsParser().Analyze(tt.commits)
+ analyzedCommits, versionBump, err := AnalyzeCommits(tt.commits)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.expectedCommits, analyzedCommits)
+ assert.Equal(t, tt.expectedBump, versionBump)
})
}
}
diff --git a/forge.go b/forge.go
index 1086564..8a3c9bb 100644
--- a/forge.go
+++ b/forge.go
@@ -19,12 +19,18 @@ const (
GitHubPerPageMax = 100
GitHubPRStateOpen = "open"
GitHubPRStateClosed = "closed"
- GitHubEnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential
+ GitHubEnvAPIToken = "GITHUB_TOKEN"
GitHubEnvUsername = "GITHUB_USER"
GitHubEnvRepository = "GITHUB_REPOSITORY"
GitHubLabelColor = "dedede"
)
+type Changeset struct {
+ URL string
+ Identifier string
+ ChangelogEntries []AnalyzedCommit
+}
+
type Forge interface {
RepoURL() string
CloneURL() string
@@ -40,35 +46,23 @@ type Forge interface {
// function should return all commits.
CommitsSince(context.Context, *Tag) ([]Commit, 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
+ // 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
// 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 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.
+ SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []string) error
ClosePullRequest(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)
+ PendingReleases(context.Context) ([]*ReleasePullRequest, 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
+ CreateRelease(ctx context.Context, commit Commit, title, changelog string, prelease, latest bool) error
}
type ForgeOptions struct {
@@ -175,16 +169,10 @@ func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) {
var commits = make([]Commit, 0, len(repositoryCommits))
for _, ghCommit := range repositoryCommits {
- commit := Commit{
+ commits = append(commits, 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
@@ -269,52 +257,76 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm
return repositoryCommits, nil
}
-func (g *GitHub) prForCommit(ctx context.Context, commit Commit) (*PullRequest, error) {
+func (g *GitHub) Changesets(ctx context.Context, commits []Commit) ([]Changeset, 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.
- log := g.log.With("commit.hash", commit.Hash)
- page := 1
- var associatedPRs []*github.PullRequest
+ changesets := make([]Changeset, 0, len(commits))
- 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,
- })
+ 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 {
- return nil, err
+ log.Warn("unable to parse changelog entries", "error", err)
+ continue
}
- 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 len(changelogEntries) > 0 {
+ changesets = append(changesets, Changeset{
+ URL: pullrequest.GetHTMLURL(),
+ Identifier: fmt.Sprintf("#%d", pullrequest.GetNumber()),
+ ChangelogEntries: changelogEntries,
+ })
}
}
- if pullrequest == nil {
- return nil, nil
- }
- return gitHubPRToPullRequest(pullrequest), nil
+ return changesets, nil
}
-func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error {
+func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []string) error {
existingLabels := make([]string, 0, len(labels))
page := 1
@@ -342,12 +354,12 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error {
}
for _, label := range labels {
- if !slices.Contains(existingLabels, string(label)) {
+ if !slices.Contains(existingLabels, 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: Pointer(string(label)),
+ Name: &label,
Color: Pointer(GitHubLabelColor),
},
)
@@ -410,7 +422,7 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest)
// TODO: String ID?
pr.ID = ghPR.GetNumber()
- err = g.SetPullRequestLabels(ctx, pr, []Label{}, pr.Labels)
+ err = g.SetPullRequestLabels(ctx, pr, []string{}, pr.Labels)
if err != nil {
return err
}
@@ -433,25 +445,20 @@ func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest)
return nil
}
-func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []Label) error {
+func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []string) error {
for _, label := range remove {
_, err := g.client.Issues.RemoveLabelForIssue(
ctx, g.options.Owner, g.options.Repo,
- pr.ID, string(label),
+ pr.ID, 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, addString,
+ pr.ID, add,
)
if err != nil {
return err
@@ -474,7 +481,7 @@ func (g *GitHub) ClosePullRequest(ctx context.Context, pr *ReleasePullRequest) e
return nil
}
-func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*ReleasePullRequest, error) {
+func (g *GitHub) PendingReleases(ctx context.Context) ([]*ReleasePullRequest, error) {
page := 1
var prs []*ReleasePullRequest
@@ -502,7 +509,7 @@ func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*Re
for _, pr := range ghPRs {
pending := slices.ContainsFunc(pr.Labels, func(l *github.Label) bool {
- return l.GetName() == string(pendingLabel)
+ return l.GetName() == LabelReleasePending
})
if !pending {
continue
@@ -551,21 +558,10 @@ 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([]Label, 0, len(pr.Labels))
+ labels := make([]string, 0, len(pr.Labels))
for _, label := range pr.Labels {
- labelName := Label(label.GetName())
- if slices.Contains(KnownLabels, Label(label.GetName())) {
- labels = append(labels, labelName)
- }
+ labels = append(labels, label.GetName())
}
var releaseCommit *Commit
diff --git a/git.go b/git.go
index 9131570..7df742c 100644
--- a/git.go
+++ b/git.go
@@ -13,9 +13,15 @@ import (
)
const (
- GitRemoteName = "origin"
+ CommitSearchDepth = 50 // TODO: Increase
+ GitRemoteName = "origin"
)
+type Commit struct {
+ Hash string
+ Message string
+}
+
type Tag struct {
Hash string
Name string
diff --git a/go.mod b/go.mod
index 3be3b4e..e55cfa9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/apricote/releaser-pleaser
-go 1.23.0
+go 1.22.4
require (
github.com/blang/semver/v4 v4.0.0
@@ -14,11 +14,11 @@ require (
)
require (
- dario.cat/mergo v1.0.1 // indirect
- github.com/Microsoft/go-winio v0.6.2 // indirect
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
- github.com/cloudflare/circl v1.3.9 // indirect
- github.com/cyphar/filepath-securejoin v0.3.1 // indirect
+ github.com/cloudflare/circl v1.3.7 // indirect
+ github.com/cyphar/filepath-securejoin v0.2.4 // 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,12 +31,14 @@ 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.3.0 // indirect
+ github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // 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
+ 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
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index c43a8f2..385a5a2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
-dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
-dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
-github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
-github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+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/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.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
-github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
+github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
+github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-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/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
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.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
-github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
+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/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,10 +97,12 @@ 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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
-golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+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/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=
@@ -108,11 +110,13 @@ 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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+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/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=
@@ -126,15 +130,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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
-golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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/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.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
-golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+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/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=
@@ -142,12 +146,14 @@ 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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
-golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+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/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=
diff --git a/internal/markdown/extensions/section.go b/internal/markdown/extensions/section.go
index dcca37d..e85808a 100644
--- a/internal/markdown/extensions/section.go
+++ b/internal/markdown/extensions/section.go
@@ -12,10 +12,8 @@ import (
"github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
)
-var (
- sectionStartRegex = regexp.MustCompile(`^`)
- sectionEndRegex = regexp.MustCompile(`^`)
-)
+var sectionStartRegex = regexp.MustCompile(`^`)
+var sectionEndRegex = regexp.MustCompile(`^`)
const (
sectionTrigger = ""
)
-type sectionParser struct{}
+type sectionParser struct {
+}
func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) {
line, _ := reader.PeekLine()
@@ -76,7 +75,8 @@ 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 = §ion{}
diff --git a/internal/markdown/renderer/markdown/renderer.go b/internal/markdown/renderer/markdown/renderer.go
index 69b2883..7a369e7 100644
--- a/internal/markdown/renderer/markdown/renderer.go
+++ b/internal/markdown/renderer/markdown/renderer.go
@@ -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 prefixes are added at the
+// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefices are added at the
// beginning of each line.
func (r *Renderer) writeString(w io.Writer, s string) (int, error) {
n, err := r.write(w, []byte(s))
@@ -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, _ []byte, node ast.Node) error {
+func (r *Renderer) openBlock(w util.BufWriter, source []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(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
r.listStack, r.prefixStack, r.prefix, r.atNewline = nil, nil, nil, false
return ast.WalkContinue, nil
}
@@ -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, fmt.Errorf(": %w", err)
+ return ast.WalkStop, nil
}
// 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, _ []byte, node ast.Node, _ bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
em := node.(*ast.Emphasis)
if _, err := r.writeString(w, strings.Repeat("*", em.Level)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
@@ -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, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
img := node.(*ast.Image)
if err := r.renderLinkOrImage(w, "![", img.Destination, img.Title, enter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
@@ -672,7 +672,7 @@ func (r *Renderer) renderImage(w util.BufWriter, _ []byte, node ast.Node, enter
}
// RenderLink renders an *ast.Link node to the given BufWriter.
-func (r *Renderer) renderLink(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
link := node.(*ast.Link)
if err := r.renderLinkOrImage(w, "[", link.Destination, link.Title, enter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
@@ -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, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderString(w util.BufWriter, source []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, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
if node.NextSibling() != nil {
if _, err := r.writeString(w, " | "); err != nil {
@@ -813,14 +813,14 @@ func (r *Renderer) renderTableCell(w util.BufWriter, _ []byte, node ast.Node, en
return ast.WalkContinue, nil
}
-func (r *Renderer) renderStrikethrough(w util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if _, err := r.writeString(w, "~~"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
-func (r *Renderer) renderTaskCheckBox(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
+func (r *Renderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
var fill byte = ' '
if task := node.(*exast.TaskCheckBox); task.IsChecked {
diff --git a/internal/testutils/git.go b/internal/testutils/git.go
index f5721b6..3bfd053 100644
--- a/internal/testutils/git.go
+++ b/internal/testutils/git.go
@@ -11,26 +11,25 @@ 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 {
@@ -62,9 +61,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)
}
@@ -84,6 +83,7 @@ func WithCommit(message string, options ...CommitOption) Commit {
}
return nil
+
}
}
diff --git a/releasepr.go b/releasepr.go
index a6744c4..eadb3b0 100644
--- a/releasepr.go
+++ b/releasepr.go
@@ -6,7 +6,6 @@ import (
"fmt"
"log"
"regexp"
- "strings"
"text/template"
"github.com/yuin/goldmark/ast"
@@ -31,14 +30,11 @@ func init() {
}
}
-// ReleasePullRequest
-//
-// TODO: Reuse [PullRequest]
type ReleasePullRequest struct {
ID int
Title string
Description string
- Labels []Label
+ Labels []string
Head string
ReleaseCommit *Commit
@@ -47,11 +43,11 @@ type ReleasePullRequest struct {
func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) {
rp := &ReleasePullRequest{
Head: head,
- Labels: []Label{LabelReleasePending},
+ Labels: []string{LabelReleasePending},
}
rp.SetTitle(branch, version)
- if err := rp.SetDescription(changelogEntry, ReleaseOverrides{}); err != nil {
+ if err := rp.SetDescription(changelogEntry); err != nil {
return nil, err
}
@@ -61,7 +57,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 commits
+ // TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the changesets
NextVersionType NextVersionType
}
@@ -92,20 +88,18 @@ func (n NextVersionType) String() string {
}
}
-// Label is the string identifier of a pull/merge request label on the forge.
-type Label string
-
+// PR Labels
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"
+ LabelNextVersionTypeNormal = "rp-next-version::normal"
+ LabelNextVersionTypeRC = "rp-next-version::rc"
+ LabelNextVersionTypeBeta = "rp-next-version::beta"
+ LabelNextVersionTypeAlpha = "rp-next-version::alpha"
- LabelReleasePending Label = "rp-release::pending"
- LabelReleaseTagged Label = "rp-release::tagged"
+ LabelReleasePending = "rp-release::pending"
+ LabelReleaseTagged = "rp-release::tagged"
)
-var KnownLabels = []Label{
+var Labels = []string{
LabelNextVersionTypeNormal,
LabelNextVersionTypeRC,
LabelNextVersionTypeBeta,
@@ -121,6 +115,7 @@ const (
)
const (
+ MarkdownSectionOverrides = "overrides"
MarkdownSectionChangelog = "changelog"
)
@@ -155,9 +150,6 @@ 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
}
}
@@ -198,6 +190,51 @@ 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()
@@ -252,11 +289,11 @@ func textFromLines(source []byte, n ast.Node) string {
content = append(content, line.Value(source)...)
}
- return strings.TrimSpace(string(content))
+ return string(content)
}
func (pr *ReleasePullRequest) SetTitle(branch, version string) {
- pr.Title = fmt.Sprintf(TitleFormat, branch, version)
+ pr.Title = fmt.Sprintf("chore(%s): release %s", branch, version)
}
func (pr *ReleasePullRequest) Version() (string, error) {
@@ -268,9 +305,14 @@ func (pr *ReleasePullRequest) Version() (string, error) {
return matches[2], nil
}
-func (pr *ReleasePullRequest) SetDescription(changelogEntry string, overrides ReleaseOverrides) error {
+func (pr *ReleasePullRequest) SetDescription(changelogEntry string) error {
+ overrides, err := pr.overridesText()
+ if err != nil {
+ return err
+ }
+
var description bytes.Buffer
- err := releasePRTemplate.Execute(&description, map[string]any{
+ err = releasePRTemplate.Execute(&description, map[string]any{
"Changelog": changelogEntry,
"Overrides": overrides,
})
diff --git a/releasepr.md.tpl b/releasepr.md.tpl
index 6f74aa0..e48872b 100644
--- a/releasepr.md.tpl
+++ b/releasepr.md.tpl
@@ -1,32 +1,29 @@
+---
+
{{ .Changelog }}
---
-
-
+## releaser-pleaser Instructions
+{{ if .Overrides }}
+{{- .Overrides -}}
+{{- else }}
+
+> 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.
-
-## Release Notes
-
-### Prefix / Start
-
-This will be added to the start of the release notes.
+### Prefix
```rp-prefix
-{{- if .Overrides.Prefix }}
-{{ .Overrides.Prefix }}{{ end }}
```
-### Suffix / End
-
-This will be added to the end of the release notes.
+### Suffix
```rp-suffix
-{{- if .Overrides.Suffix }}
-{{ .Overrides.Suffix }}{{ end }}
```
-
+
+
+{{ end }}
+#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
diff --git a/releasepr_test.go b/releasepr_test.go
index b1ffbb2..124b063 100644
--- a/releasepr_test.go
+++ b/releasepr_test.go
@@ -49,96 +49,121 @@ func TestReleasePullRequest_SetDescription(t *testing.T) {
tests := []struct {
name string
+ pr *ReleasePullRequest
changelogEntry string
- overrides ReleaseOverrides
want string
wantErr assert.ErrorAssertionFunc
}{
{
- name: "no overrides",
+ name: "empty description",
+ pr: &ReleasePullRequest{},
changelogEntry: `## v1.0.0`,
- overrides: ReleaseOverrides{},
- want: `
+ want: `---
+
+
## v1.0.0
---
-
-
+## releaser-pleaser Instructions
-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.
-## Release Notes
-
-### Prefix / Start
-
-This will be added to the start of the release notes.
+### Prefix
` + "```" + `rp-prefix
` + "```" + `
-### Suffix / End
-
-This will be added to the end of the release notes.
+### Suffix
` + "```" + `rp-suffix
` + "```" + `
-
+
+
+
+#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
`,
wantErr: assert.NoError,
},
{
- name: "existing overrides",
- changelogEntry: `## v1.0.0`,
- overrides: ReleaseOverrides{
- Prefix: "This release is awesome!",
- Suffix: "Fooo",
- },
- want: `
-## v1.0.0
+ name: "existing overrides",
+ pr: &ReleasePullRequest{
+ Description: `---
+
+
+## v0.1.0
+
+### Features
+
+- bedazzle
---
-
-
+## releaser-pleaser Instructions
-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.
-## Release Notes
-
-### Prefix / Start
-
-This will be added to the start of the release notes.
+### Prefix
` + "```" + `rp-prefix
This release is awesome!
` + "```" + `
-### Suffix / End
-
-This will be added to the end of the release notes.
+### Suffix
` + "```" + `rp-suffix
-Fooo
` + "```" + `
-
+
+
+#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
+`,
+ },
+ changelogEntry: `## v1.0.0`,
+ want: `---
+
+
+## v1.0.0
+
+
+---
+
+## releaser-pleaser Instructions
+
+
+> 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
+` + "```" + `
+
+
+
+#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
`,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- pr := &ReleasePullRequest{}
- err := pr.SetDescription(tt.changelogEntry, tt.overrides)
+ err := tt.pr.SetDescription(tt.changelogEntry)
if !tt.wantErr(t, err) {
return
}
- assert.Equal(t, tt.want, pr.Description)
+ assert.Equal(t, tt.want, tt.pr.Description)
})
}
}
diff --git a/releaserpleaser.go b/releaserpleaser.go
deleted file mode 100644
index 5dfcf12..0000000
--- a/releaserpleaser.go
+++ /dev/null
@@ -1,406 +0,0 @@
-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
-}
diff --git a/updater.go b/updater.go
index 3aaedae..ce01d21 100644
--- a/updater.go
+++ b/updater.go
@@ -1,47 +1,12 @@
package rp
import (
- "fmt"
- "regexp"
- "strings"
+ "context"
+
+ "github.com/go-git/go-git/v5"
)
-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
+func RunUpdater(ctx context.Context, version string, worktree *git.Worktree) error {
+ // TODO: Implement updater for Go,Python,ExtraFilesMarkers
+ return nil
}
diff --git a/updater_test.go b/updater_test.go
deleted file mode 100644
index c0e1419..0000000
--- a/updater_test.go
+++ /dev/null
@@ -1,129 +0,0 @@
-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)
- })
- }
-}
diff --git a/versioning.go b/versioning.go
index 176a28d..126e06f 100644
--- a/versioning.go
+++ b/versioning.go
@@ -13,11 +13,7 @@ type Releases struct {
Stable *Tag
}
-type VersioningStrategy = func(Releases, conventionalcommits.VersionBump, NextVersionType) (string, error)
-
-var _ VersioningStrategy = SemVerNextVersion
-
-func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) {
+func (r Releases) NextVersion(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)
@@ -69,22 +65,24 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump,
return "v" + next.String(), nil
}
-func VersionBumpFromCommits(commits []AnalyzedCommit) conventionalcommits.VersionBump {
+func VersionBumpFromChangesets(changesets []Changeset) conventionalcommits.VersionBump {
bump := conventionalcommits.UnknownVersion
- 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
- }
+ 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
+ }
- if entryBump > bump {
- bump = entryBump
+ if entryBump > bump {
+ bump = entryBump
+ }
}
}
diff --git a/versioning_test.go b/versioning_test.go
index b6a0995..9319c63 100644
--- a/versioning_test.go
+++ b/versioning_test.go
@@ -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
- args args
- want string
- wantErr assert.ErrorAssertionFunc
+ name string
+ releases Releases
+ 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,11 +35,12 @@ 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,
},
@@ -48,11 +49,12 @@ 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,
},
@@ -61,11 +63,12 @@ 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,
},
@@ -74,11 +77,12 @@ 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,
},
@@ -87,11 +91,12 @@ 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,
},
@@ -100,11 +105,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,
},
@@ -113,11 +118,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,
},
@@ -126,11 +131,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,
},
@@ -139,11 +144,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,
},
@@ -152,11 +157,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,
},
@@ -165,11 +170,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,
},
@@ -178,11 +183,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,
},
@@ -191,11 +196,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,
},
@@ -204,11 +209,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,
},
@@ -217,11 +222,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,
},
@@ -230,11 +235,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,
},
@@ -243,11 +248,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,
},
@@ -256,11 +261,12 @@ 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,
},
@@ -269,11 +275,12 @@ 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,
},
@@ -283,11 +290,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,
},
@@ -296,11 +303,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,
},
@@ -309,11 +316,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,
@@ -324,65 +331,95 @@ func TestReleases_NextVersion(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- 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)) {
+ 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)) {
return
}
- 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)
+ assert.Equalf(t, tt.want, got, "Releases(%v, %v).NextVersion(%v, %v)", tt.releases.Latest, tt.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)
})
}
}
-func TestVersionBumpFromCommits(t *testing.T) {
+func TestVersionBumpFromChangesets(t *testing.T) {
tests := []struct {
- name string
- analyzedCommits []AnalyzedCommit
- want conventionalcommits.VersionBump
+ name string
+ changesets []Changeset
+ want conventionalcommits.VersionBump
}{
{
- name: "no entries (unknown)",
- analyzedCommits: []AnalyzedCommit{},
- want: conventionalcommits.UnknownVersion,
+ name: "no entries (unknown)",
+ changesets: []Changeset{},
+ want: conventionalcommits.UnknownVersion,
},
{
- name: "non-release type (unknown)",
- analyzedCommits: []AnalyzedCommit{{Type: "docs"}},
- want: conventionalcommits.UnknownVersion,
+ name: "non-release type (unknown)",
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}}}},
+ want: conventionalcommits.UnknownVersion,
},
{
- name: "single breaking (major)",
- analyzedCommits: []AnalyzedCommit{{BreakingChange: true}},
- want: conventionalcommits.MajorVersion,
+ name: "single breaking (major)",
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}}},
+ want: conventionalcommits.MajorVersion,
},
{
- name: "single feat (minor)",
- analyzedCommits: []AnalyzedCommit{{Type: "feat"}},
- want: conventionalcommits.MinorVersion,
+ name: "single feat (minor)",
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}}},
+ want: conventionalcommits.MinorVersion,
},
{
- name: "single fix (patch)",
- analyzedCommits: []AnalyzedCommit{{Type: "fix"}},
- want: conventionalcommits.PatchVersion,
+ name: "single fix (patch)",
+ changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}}},
+ want: conventionalcommits.PatchVersion,
},
{
- name: "multiple entries (major)",
- analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}},
- want: conventionalcommits.MajorVersion,
+ name: "multiple changesets (major)",
+ changesets: []Changeset{
+ {ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
+ {ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}},
+ },
+ want: conventionalcommits.MajorVersion,
},
{
- name: "multiple entries (minor)",
- analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}},
- want: conventionalcommits.MinorVersion,
+ name: "multiple changesets (minor)",
+ changesets: []Changeset{
+ {ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
+ {ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}},
+ },
+ want: conventionalcommits.MinorVersion,
},
{
- name: "multiple entries (patch)",
- analyzedCommits: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}},
- want: conventionalcommits.PatchVersion,
+ 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,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- assert.Equalf(t, tt.want, VersionBumpFromCommits(tt.analyzedCommits), "VersionBumpFromCommits(%v)", tt.analyzedCommits)
+ assert.Equalf(t, tt.want, VersionBumpFromChangesets(tt.changesets), "VersionBumpFromChangesets(%v)", tt.changesets)
})
}
}