diff --git a/internal/forge/forge.go b/internal/forge/forge.go index 0bd119a..3e4c25e 100644 --- a/internal/forge/forge.go +++ b/internal/forge/forge.go @@ -17,6 +17,9 @@ type Forge interface { GitAuth() transport.AuthMethod + // CommitAuthor returns the git author used for the release commit. It should be the user whose token is used to talk to the API. + CommitAuthor(context.Context) (git.Author, error) + // LatestTags returns the last stable tag created on the main branch. If there is a more recent pre-release tag, // that is also returned. If no tag is found, it returns nil. LatestTags(context.Context) (git.Releases, error) diff --git a/internal/forge/github/github.go b/internal/forge/github/github.go index a296c2a..3bff3e6 100644 --- a/internal/forge/github/github.go +++ b/internal/forge/github/github.go @@ -29,6 +29,13 @@ const ( EnvRepository = "GITHUB_REPOSITORY" ) +var ( + gitHubActionsBotAuthor = git.Author{ + Name: "github-actions[bot]", + Email: "41898282+github-actions[bot]@users.noreply.github.com", + } +) + var _ forge.Forge = &GitHub{} type GitHub struct { @@ -61,6 +68,22 @@ func (g *GitHub) GitAuth() transport.AuthMethod { } } +func (g *GitHub) CommitAuthor(ctx context.Context) (git.Author, error) { + g.log.DebugContext(ctx, "getting commit author from current token user") + + user, _, err := g.client.Users.Get(ctx, "") + if err != nil { + g.log.WarnContext(ctx, "failed to get commit author from API, using default github-actions[bot] user", "error", err) + + return gitHubActionsBotAuthor, nil + } + + return git.Author{ + Name: user.GetName(), + Email: user.GetEmail(), + }, nil +} + func (g *GitHub) LatestTags(ctx context.Context) (git.Releases, error) { g.log.DebugContext(ctx, "listing all tags in github repository") diff --git a/internal/forge/gitlab/gitlab.go b/internal/forge/gitlab/gitlab.go index f53777a..06de7fd 100644 --- a/internal/forge/gitlab/gitlab.go +++ b/internal/forge/gitlab/gitlab.go @@ -69,6 +69,22 @@ func (g *GitLab) GitAuth() transport.AuthMethod { } } +func (g *GitLab) CommitAuthor(ctx context.Context) (git.Author, error) { + g.log.DebugContext(ctx, "getting commit author from current token user") + + user, _, err := g.client.Users.CurrentUser(gitlab.WithContext(ctx)) + if err != nil { + return git.Author{}, err + } + + // TODO: Return bot when nothing is returned? + + return git.Author{ + Name: user.Name, + Email: user.Email, + }, nil +} + func (g *GitLab) LatestTags(ctx context.Context) (git.Releases, error) { g.log.DebugContext(ctx, "listing all tags in gitlab repository") diff --git a/internal/git/git.go b/internal/git/git.go index 87d94d9..95f05e0 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -45,6 +45,27 @@ type Releases struct { Stable *Tag } +type Author struct { + Name string + Email string +} + +func (a Author) signature(when time.Time) *object.Signature { + return &object.Signature{ + Name: a.Name, + Email: a.Email, + When: when, + } +} + +func (a Author) String() string { + return fmt.Sprintf("%s <%s>", a.Name, a.Email) +} + +var ( + committer = Author{Name: "releaser-pleaser", Email: ""} +) + func CloneRepo(ctx context.Context, logger *slog.Logger, cloneURL, branch string, auth transport.AuthMethod) (*Repository, error) { dir, err := os.MkdirTemp("", "releaser-pleaser.*") if err != nil { @@ -150,15 +171,17 @@ func (r *Repository) UpdateFile(_ context.Context, path string, create bool, upd return nil } -func (r *Repository) Commit(_ context.Context, message string) (Commit, error) { +func (r *Repository) Commit(_ context.Context, message string, author Author) (Commit, error) { worktree, err := r.r.Worktree() if err != nil { return Commit{}, err } + now := time.Now() + releaseCommitHash, err := worktree.Commit(message, &git.CommitOptions{ - Author: signature(), - Committer: signature(), + Author: author.signature(now), + Committer: committer.signature(now), }) if err != nil { return Commit{}, fmt.Errorf("failed to commit changes: %w", err) @@ -223,11 +246,3 @@ func (r *Repository) ForcePush(ctx context.Context, branch string) error { Auth: r.auth, }) } - -func signature() *object.Signature { - return &object.Signature{ - Name: "releaser-pleaser", - Email: "", - When: time.Now(), - } -} diff --git a/internal/git/git_test.go b/internal/git/git_test.go new file mode 100644 index 0000000..399c2ba --- /dev/null +++ b/internal/git/git_test.go @@ -0,0 +1,46 @@ +package git + +import ( + "reflect" + "strconv" + "testing" + "time" + + "github.com/go-git/go-git/v5/plumbing/object" +) + +func TestAuthor_signature(t *testing.T) { + now := time.Now() + + tests := []struct { + author Author + want *object.Signature + }{ + {author: Author{Name: "foo", Email: "bar@example.com"}, want: &object.Signature{Name: "foo", Email: "bar@example.com", When: now}}, + {author: Author{Name: "bar", Email: "foo@example.com"}, want: &object.Signature{Name: "bar", Email: "foo@example.com", When: now}}, + } + for i, tt := range tests { + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { + if got := tt.author.signature(now); !reflect.DeepEqual(got, tt.want) { + t.Errorf("signature() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthor_String(t *testing.T) { + tests := []struct { + author Author + want string + }{ + {author: Author{Name: "foo", Email: "bar@example.com"}, want: "foo "}, + {author: Author{Name: "bar", Email: "foo@example.com"}, want: "bar "}, + } + for i, tt := range tests { + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { + if got := tt.author.String(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/releaserpleaser.go b/releaserpleaser.go index f2f4064..13e2381 100644 --- a/releaserpleaser.go +++ b/releaserpleaser.go @@ -255,13 +255,18 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error { } } + releaseCommitAuthor, err := rp.forge.CommitAuthor(ctx) + if err != nil { + return fmt.Errorf("failed to get commit author: %w", err) + } + releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion) - releaseCommit, err := repo.Commit(ctx, releaseCommitMessage) + releaseCommit, err := repo.Commit(ctx, releaseCommitMessage, releaseCommitAuthor) if err != nil { return fmt.Errorf("failed to commit changes: %w", err) } - logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommit.Hash, "commit.message", releaseCommit.Message) + logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommit.Hash, "commit.message", releaseCommit.Message, "commit.author", releaseCommitAuthor) // Check if anything changed in comparison to the remote branch (if exists) newReleasePRChanges, err := repo.HasChangesWithRemote(ctx, rpBranch)