Compare commits

...

10 commits

16 changed files with 587 additions and 571 deletions

View file

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

View file

@ -15,7 +15,6 @@ import (
const (
ChangelogFile = "CHANGELOG.md"
ChangelogFileBuffer = "CHANGELOG.md.tmp"
ChangelogHeader = "# Changelog"
)
@ -90,12 +89,11 @@ func UpdateChangelogFile(wt *git.Worktree, newEntry string) error {
return nil
}
func NewChangelogEntry(changesets []Changeset, version, link, prefix, suffix string) (string, error) {
func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) {
features := make([]AnalyzedCommit, 0)
fixes := make([]AnalyzedCommit, 0)
for _, changeset := range changesets {
for _, commit := range changeset.ChangelogEntries {
for _, commit := range commits {
switch commit.Type {
case "feat":
features = append(features, commit)
@ -103,7 +101,6 @@ func NewChangelogEntry(changesets []Changeset, version, link, prefix, suffix str
fixes = append(fixes, commit)
}
}
}
var changelog bytes.Buffer
err := changelogTemplate.Execute(&changelog, map[string]any{

View file

@ -96,7 +96,7 @@ func TestUpdateChangelogFile(t *testing.T) {
func Test_NewChangelogEntry(t *testing.T) {
type args struct {
changesets []Changeset
analyzedCommits []AnalyzedCommit
version string
link string
prefix string
@ -111,7 +111,7 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "empty",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{}}},
analyzedCommits: []AnalyzedCommit{},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@ -121,13 +121,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "single feature",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "feat",
Description: "Foobar!",
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@ -137,13 +137,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "single fix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
@ -153,7 +153,7 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "multiple commits with scopes",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "feat",
@ -176,7 +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",
},
@ -196,13 +196,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "prefix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
prefix: "### Breaking Changes",
@ -219,13 +219,13 @@ func Test_NewChangelogEntry(t *testing.T) {
{
name: "suffix",
args: args{
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
analyzedCommits: []AnalyzedCommit{
{
Commit: Commit{},
Type: "fix",
Description: "Foobar!",
},
}}},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
suffix: "### Compatibility\n\nThis version is compatible with flux-compensator v2.2 - v2.9.",
@ -245,7 +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.changesets, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
got, err := NewChangelogEntry(tt.args.analyzedCommits, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
if !tt.wantErr(t, err) {
return
}

View file

@ -1,21 +1,11 @@
package cmd
import (
"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",
@ -50,7 +40,7 @@ func run(cmd *cobra.Command, _ []string) error {
"repo", flagRepo,
)
var f rp.Forge
var forge rp.Forge
forgeOptions := rp.ForgeOptions{
Repository: flagRepo,
@ -62,312 +52,14 @@ func run(cmd *cobra.Command, _ []string) error {
//f = rp.NewGitLab(forgeOptions)
case "github":
logger.DebugContext(ctx, "using forge GitHub")
f = rp.NewGitHub(logger, &rp.GitHubOptions{
forge = rp.NewGitHub(logger, &rp.GitHubOptions{
ForgeOptions: forgeOptions,
Owner: flagOwner,
Repo: flagRepo,
})
}
err := ensureLabels(ctx, f)
if err != nil {
return fmt.Errorf("failed to ensure all labels exist: %w", err)
}
releaserPleaser := rp.New(forge, logger, flagBranch, rp.NewConventionalCommitsParser(), rp.SemVerNextVersion)
err = createPendingReleases(ctx, f)
if err != nil {
return fmt.Errorf("failed to create pending releases: %w", err)
}
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 ensureLabels(ctx context.Context, forge rp.Forge) error {
return forge.EnsureLabelsExist(ctx, rp.Labels)
}
func createPendingReleases(ctx context.Context, forge rp.Forge) error {
logger.InfoContext(ctx, "checking for pending releases")
prs, err := forge.PendingReleases(ctx)
if err != nil {
return err
}
if len(prs) == 0 {
logger.InfoContext(ctx, "No pending releases found")
return nil
}
logger.InfoContext(ctx, "Found pending releases", "length", len(prs))
for _, pr := range prs {
err = createPendingRelease(ctx, forge, pr)
if err != nil {
return err
}
}
return nil
}
func createPendingRelease(ctx context.Context, forge rp.Forge, pr *rp.ReleasePullRequest) error {
logger := logger.With("pr.id", pr.ID, "pr.title", pr.Title)
if pr.ReleaseCommit == nil {
return fmt.Errorf("pull request is missing the merge commit")
}
logger.Info("Creating release", "commit.hash", pr.ReleaseCommit.Hash)
version, err := pr.Version()
if err != nil {
return err
}
changelog, err := pr.ChangelogText()
if err != nil {
return err
}
// TODO: pre-release & latest
logger.DebugContext(ctx, "Creating release on forge")
err = forge.CreateRelease(ctx, *pr.ReleaseCommit, version, changelog, false, true)
if err != nil {
return fmt.Errorf("failed to create release on forge: %w", err)
}
logger.DebugContext(ctx, "created release", "release.title", version, "release.url", forge.ReleaseURL(version))
logger.DebugContext(ctx, "updating pr labels")
err = forge.SetPullRequestLabels(ctx, pr, []string{rp.LabelReleasePending}, []string{rp.LabelReleaseTagged})
if err != nil {
return err
}
logger.DebugContext(ctx, "updated pr labels")
logger.InfoContext(ctx, "Created release", "release.title", version, "release.url", forge.ReleaseURL(version))
return nil
}
func getChangesetsFromForge(ctx context.Context, forge rp.Forge) ([]rp.Changeset, rp.Releases, error) {
releases, err := forge.LatestTags(ctx)
if err != nil {
return nil, rp.Releases{}, err
}
if releases.Latest != nil {
logger.InfoContext(ctx, "found latest tag", "tag.hash", releases.Latest.Hash, "tag.name", releases.Latest.Name)
if releases.Stable != nil && releases.Latest.Hash != releases.Stable.Hash {
logger.InfoContext(ctx, "found stable tag", "tag.hash", releases.Stable.Hash, "tag.name", releases.Stable.Name)
}
} else {
logger.InfoContext(ctx, "no latest tag found")
}
releasableCommits, err := forge.CommitsSince(ctx, releases.Stable)
if err != nil {
return nil, rp.Releases{}, err
}
logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits))
changesets, err := forge.Changesets(ctx, releasableCommits)
if err != nil {
return nil, rp.Releases{}, err
}
logger.InfoContext(ctx, "Found changesets", "length", len(changesets))
return changesets, releases, nil
}
func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Changeset, releases rp.Releases) error {
rpBranch := fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch)
rpBranchRef := plumbing.NewBranchReferenceName(rpBranch)
// Check Forge for open PR
// Get any modifications from open PR
// Clone Repo
// Run Updaters + Changelog
// Upsert PR
pr, err := forge.PullRequestForBranch(ctx, fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch))
if err != nil {
return err
}
if pr != nil {
logger.InfoContext(ctx, "found existing release pull request", "pr.id", pr.ID, "pr.title", pr.Title)
}
if len(changesets) == 0 {
if pr != nil {
logger.InfoContext(ctx, "closing existing pull requests, no changesets available", "pr.id", pr.ID, "pr.title", pr.Title)
err = forge.ClosePullRequest(ctx, pr)
if err != nil {
return err
}
} else {
logger.InfoContext(ctx, "No changesets available for release")
}
return nil
}
var releaseOverrides rp.ReleaseOverrides
if pr != nil {
releaseOverrides, err = pr.GetOverrides()
if err != nil {
return err
}
}
versionBump := rp.VersionBumpFromChangesets(changesets)
nextVersion, err := rp.SemVerNextVersion(releases, 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)
overrides, err := pr.GetOverrides()
if err != nil {
return err
}
err = pr.SetDescription(changelogEntry, overrides)
if err != nil {
return err
}
err = forge.UpdatePullRequest(ctx, pr)
if err != nil {
return err
}
logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID)
}
return nil
return releaserPleaser.Run(ctx)
}

View file

@ -7,6 +7,19 @@ import (
"github.com/leodido/go-conventionalcommits/parser"
)
type Commit struct {
Hash string
Message string
PullRequest *PullRequest
}
type PullRequest struct {
ID int
Title string
Description string
}
type AnalyzedCommit struct {
Commit
Type string
@ -34,7 +47,7 @@ func NewConventionalCommitsParser() *ConventionalCommitsParser {
}
}
func (c *ConventionalCommitsParser) AnalyzeCommits(commits []Commit) ([]AnalyzedCommit, error) {
func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit, error) {
analyzedCommits := make([]AnalyzedCommit, 0, len(commits))
for _, commit := range commits {

View file

@ -111,7 +111,7 @@ func TestAnalyzeCommits(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzedCommits, err := NewConventionalCommitsParser().AnalyzeCommits(tt.commits)
analyzedCommits, err := NewConventionalCommitsParser().Analyze(tt.commits)
if !tt.wantErr(t, err) {
return
}

110
forge.go
View file

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

6
git.go
View file

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

20
go.mod
View file

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

46
go.sum
View file

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

View file

@ -149,7 +149,7 @@ func (r *Renderer) writeByte(w io.Writer, c byte) error {
return nil
}
// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefices are added at the
// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefixes are added at the
// beginning of each line.
func (r *Renderer) writeString(w io.Writer, s string) (int, error) {
n, err := r.write(w, []byte(s))
@ -178,7 +178,7 @@ func (r *Renderer) popPrefix() {
// OpenBlock ensures that each block begins on a new line, and that blank lines are inserted before blocks as
// indicated by node.HasPreviousBlankLines.
func (r *Renderer) openBlock(w util.BufWriter, source []byte, node ast.Node) error {
func (r *Renderer) openBlock(w util.BufWriter, _ []byte, node ast.Node) error {
r.openBlocks = append(r.openBlocks, blockState{
node: node,
fresh: true,
@ -222,7 +222,7 @@ func (r *Renderer) closeBlock(w io.Writer) error {
}
// RenderDocument renders an *ast.Document node to the given BufWriter.
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderDocument(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
r.listStack, r.prefixStack, r.prefix, r.atNewline = nil, nil, nil, false
return ast.WalkContinue, nil
}
@ -594,7 +594,7 @@ func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node
}
// RenderEmphasis renders an *ast.Emphasis node to the given BufWriter.
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderEmphasis(w util.BufWriter, _ []byte, node ast.Node, _ bool) (ast.WalkStatus, error) {
em := node.(*ast.Emphasis)
if _, err := r.writeString(w, strings.Repeat("*", em.Level)); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
@ -663,7 +663,7 @@ func (r *Renderer) renderLinkOrImage(w util.BufWriter, open string, dest, title
}
// RenderImage renders an *ast.Image node to the given BufWriter.
func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderImage(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
img := node.(*ast.Image)
if err := r.renderLinkOrImage(w, "![", img.Destination, img.Title, enter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
@ -672,7 +672,7 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, e
}
// RenderLink renders an *ast.Link node to the given BufWriter.
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderLink(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
link := node.(*ast.Link)
if err := r.renderLinkOrImage(w, "[", link.Destination, link.Title, enter); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
@ -724,7 +724,7 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, en
}
// RenderString renders an *ast.String node to the given BufWriter.
func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderString(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
@ -801,7 +801,7 @@ func (r *Renderer) renderTableRow(w util.BufWriter, source []byte, node ast.Node
return ast.WalkContinue, nil
}
func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderTableCell(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
if node.NextSibling() != nil {
if _, err := r.writeString(w, " | "); err != nil {
@ -813,14 +813,14 @@ func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Nod
return ast.WalkContinue, nil
}
func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderStrikethrough(w util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
if _, err := r.writeString(w, "~~"); err != nil {
return ast.WalkStop, fmt.Errorf(": %w", err)
}
return ast.WalkContinue, nil
}
func (r *Renderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
func (r *Renderer) renderTaskCheckBox(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
if enter {
var fill byte = ' '
if task := node.(*exast.TaskCheckBox); task.IsChecked {

View file

@ -61,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)
}

View file

@ -31,11 +31,14 @@ func init() {
}
}
// ReleasePullRequest
//
// TODO: Reuse [PullRequest]
type ReleasePullRequest struct {
ID int
Title string
Description string
Labels []string
Labels []Label
Head string
ReleaseCommit *Commit
@ -44,7 +47,7 @@ type ReleasePullRequest struct {
func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) {
rp := &ReleasePullRequest{
Head: head,
Labels: []string{LabelReleasePending},
Labels: []Label{LabelReleasePending},
}
rp.SetTitle(branch, version)
@ -58,7 +61,7 @@ func NewReleasePullRequest(head, branch, version, changelogEntry string) (*Relea
type ReleaseOverrides struct {
Prefix string
Suffix string
// TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the changesets
// TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the commits
NextVersionType NextVersionType
}
@ -89,18 +92,20 @@ func (n NextVersionType) String() string {
}
}
// PR Labels
const (
LabelNextVersionTypeNormal = "rp-next-version::normal"
LabelNextVersionTypeRC = "rp-next-version::rc"
LabelNextVersionTypeBeta = "rp-next-version::beta"
LabelNextVersionTypeAlpha = "rp-next-version::alpha"
// Label is the string identifier of a pull/merge request label on the forge.
type Label string
LabelReleasePending = "rp-release::pending"
LabelReleaseTagged = "rp-release::tagged"
const (
LabelNextVersionTypeNormal Label = "rp-next-version::normal"
LabelNextVersionTypeRC Label = "rp-next-version::rc"
LabelNextVersionTypeBeta Label = "rp-next-version::beta"
LabelNextVersionTypeAlpha Label = "rp-next-version::alpha"
LabelReleasePending Label = "rp-release::pending"
LabelReleaseTagged Label = "rp-release::tagged"
)
var Labels = []string{
var KnownLabels = []Label{
LabelNextVersionTypeNormal,
LabelNextVersionTypeRC,
LabelNextVersionTypeBeta,

347
releaserpleaser.go Normal file
View file

@ -0,0 +1,347 @@
package rp
import (
"context"
"fmt"
"log/slog"
"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
}
func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy) *ReleaserPleaser {
return &ReleaserPleaser{
forge: forge,
logger: logger,
targetBranch: targetBranch,
commitParser: commitParser,
nextVersion: versioningStrategy,
}
}
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)
}
err = RunUpdater(ctx, nextVersion, worktree)
if err != nil {
return fmt.Errorf("failed to update files with new version: %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)
}
err = UpdateChangelogFile(worktree, changelogEntry)
if err != nil {
return fmt.Errorf("failed to update changelog file: %w", err)
}
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion)
releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{
Author: GitSignature(),
Committer: GitSignature(),
})
if err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommitHash.String(), "commit.message", releaseCommitMessage)
newReleasePRChanges := true
// Check if anything changed in comparison to the remote branch (if exists)
if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName(GitRemoteName, rpBranch), false); err != nil {
if err.Error() != "reference not found" {
// "reference not found" is expected and we should always push
return err
}
} else {
remoteCommit, err := repo.CommitObject(remoteRef.Hash())
if err != nil {
return err
}
localCommit, err := repo.CommitObject(releaseCommitHash)
if err != nil {
return err
}
diff, err := localCommit.PatchContext(ctx, remoteCommit)
if err != nil {
return err
}
newReleasePRChanges = len(diff.FilePatches()) > 0
}
if newReleasePRChanges {
pushRefSpec := config.RefSpec(fmt.Sprintf(
"+%s:%s",
rpBranchRef,
// This needs to be the local branch name, not the remotes/origin ref
// See https://stackoverflow.com/a/75727620
rpBranchRef,
))
logger.DebugContext(ctx, "pushing branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String())
if err = repo.PushContext(ctx, &git.PushOptions{
RemoteName: GitRemoteName,
RefSpecs: []config.RefSpec{pushRefSpec},
Force: true,
Auth: rp.forge.GitAuth(),
}); err != nil {
return fmt.Errorf("failed to push branch: %w", err)
}
logger.InfoContext(ctx, "pushed branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String())
} else {
logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push")
}
// Open/Update PR
if pr == nil {
pr, err = NewReleasePullRequest(rpBranch, rp.targetBranch, nextVersion, changelogEntry)
if err != nil {
return err
}
err = rp.forge.CreatePullRequest(ctx, pr)
if err != nil {
return err
}
logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID)
} else {
pr.SetTitle(rp.targetBranch, nextVersion)
overrides, err := pr.GetOverrides()
if err != nil {
return err
}
err = pr.SetDescription(changelogEntry, overrides)
if err != nil {
return err
}
err = rp.forge.UpdatePullRequest(ctx, pr)
if err != nil {
return err
}
logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID)
}
return nil
}

View file

@ -69,18 +69,17 @@ func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump,
return "v" + next.String(), nil
}
func VersionBumpFromChangesets(changesets []Changeset) conventionalcommits.VersionBump {
func VersionBumpFromCommits(commits []AnalyzedCommit) conventionalcommits.VersionBump {
bump := conventionalcommits.UnknownVersion
for _, changeset := range changesets {
for _, entry := range changeset.ChangelogEntries {
for _, commit := range commits {
entryBump := conventionalcommits.UnknownVersion
switch {
case entry.BreakingChange:
case commit.BreakingChange:
entryBump = conventionalcommits.MajorVersion
case entry.Type == "feat":
case commit.Type == "feat":
entryBump = conventionalcommits.MinorVersion
case entry.Type == "fix":
case commit.Type == "fix":
entryBump = conventionalcommits.PatchVersion
}
@ -88,7 +87,6 @@ func VersionBumpFromChangesets(changesets []Changeset) conventionalcommits.Versi
bump = entryBump
}
}
}
return bump
}

View file

@ -333,86 +333,56 @@ func TestReleases_NextVersion(t *testing.T) {
}
}
func TestVersionBumpFromChangesets(t *testing.T) {
func TestVersionBumpFromCommits(t *testing.T) {
tests := []struct {
name string
changesets []Changeset
analyzedCommits []AnalyzedCommit
want conventionalcommits.VersionBump
}{
{
name: "no entries (unknown)",
changesets: []Changeset{},
analyzedCommits: []AnalyzedCommit{},
want: conventionalcommits.UnknownVersion,
},
{
name: "non-release type (unknown)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}}}},
analyzedCommits: []AnalyzedCommit{{Type: "docs"}},
want: conventionalcommits.UnknownVersion,
},
{
name: "single breaking (major)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}}},
analyzedCommits: []AnalyzedCommit{{BreakingChange: true}},
want: conventionalcommits.MajorVersion,
},
{
name: "single feat (minor)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}}},
analyzedCommits: []AnalyzedCommit{{Type: "feat"}},
want: conventionalcommits.MinorVersion,
},
{
name: "single fix (patch)",
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}}},
analyzedCommits: []AnalyzedCommit{{Type: "fix"}},
want: conventionalcommits.PatchVersion,
},
{
name: "multiple changesets (major)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
{ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}},
},
name: "multiple entries (major)",
analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}},
want: conventionalcommits.MajorVersion,
},
{
name: "multiple changesets (minor)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
{ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}},
},
name: "multiple entries (minor)",
analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}},
want: conventionalcommits.MinorVersion,
},
{
name: "multiple changesets (patch)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}}},
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}},
},
want: conventionalcommits.PatchVersion,
},
{
name: "multiple entries in one changeset (major)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}}},
},
want: conventionalcommits.MajorVersion,
},
{
name: "multiple entries in one changeset (minor)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}}},
},
want: conventionalcommits.MinorVersion,
},
{
name: "multiple entries in one changeset (patch)",
changesets: []Changeset{
{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}}},
},
name: "multiple entries (patch)",
analyzedCommits: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}},
want: conventionalcommits.PatchVersion,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, VersionBumpFromChangesets(tt.changesets), "VersionBumpFromChangesets(%v)", tt.changesets)
assert.Equalf(t, tt.want, VersionBumpFromCommits(tt.analyzedCommits), "VersionBumpFromCommits(%v)", tt.analyzedCommits)
})
}
}