From 328e29a70bf0890b5d31da104b0e819a0f16dfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sun, 8 Jun 2025 12:37:32 +0200 Subject: [PATCH] feat: real user as commit author Previously all commits were authored and committed by releaser-pleaser <> This looked weird when looking at the commit. We now check with the Forge API for details on the currently authenticated user, and use that name and email as the commit author. The commit committer stays the same for now. In GitHub, the default `$GITHUB_TOKEN` does not allow access to the required endpoint, so for github the user `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` is hardcoded when the request fails. --- internal/forge/forge.go | 3 +++ internal/forge/github/github.go | 23 +++++++++++++++++ internal/forge/gitlab/gitlab.go | 16 ++++++++++++ internal/git/git.go | 37 ++++++++++++++++++-------- internal/git/git_test.go | 46 +++++++++++++++++++++++++++++++++ releaserpleaser.go | 9 +++++-- 6 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 internal/git/git_test.go 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)