Skip to content

Commit b702275

Browse files
danbarrclaude
andauthored
Add git trailer to preserve workflow triggering actor (#21)
Co-authored-by: Dan Barr <6922515+danbarr@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent add86e3 commit b702275

File tree

6 files changed

+131
-29
lines changed

6 files changed

+131
-29
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ releaseo
3636

3737
# GoReleaser
3838
dist/
39+
40+
.task/checksum/build

internal/github/client.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,16 @@ func NewClient(ctx context.Context, token string, opts ...ClientOption) (*Client
8383
}
8484

8585
// PRRequest contains the parameters for creating a pull request.
86-
// All fields except Body are required.
86+
// All fields except Body and TriggeredBy are required.
8787
type PRRequest struct {
88-
Owner string // GitHub repository owner (required)
89-
Repo string // GitHub repository name (required)
90-
BaseBranch string // Base branch for the PR (required, e.g., "main")
91-
HeadBranch string // Feature branch to create (required)
92-
Title string // PR title (required)
93-
Body string // PR body/description
94-
Files []string // Files to commit (required, must not be empty)
88+
Owner string // GitHub repository owner (required)
89+
Repo string // GitHub repository name (required)
90+
BaseBranch string // Base branch for the PR (required, e.g., "main")
91+
HeadBranch string // Feature branch to create (required)
92+
Title string // PR title (required)
93+
Body string // PR body/description
94+
Files []string // Files to commit (required, must not be empty)
95+
TriggeredBy string // GitHub actor who triggered the release (optional, added as git trailer)
9596
}
9697

9798
// Validate checks that all required fields are set.

internal/github/client_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ func TestPRRequest_Validate(t *testing.T) {
117117
modify: func(r *PRRequest) { r.Body = "" },
118118
wantErr: "",
119119
},
120+
{
121+
name: "triggered by is optional",
122+
modify: func(r *PRRequest) { r.TriggeredBy = "" },
123+
wantErr: "",
124+
},
125+
{
126+
name: "triggered by with value",
127+
modify: func(r *PRRequest) { r.TriggeredBy = "someuser" },
128+
wantErr: "",
129+
},
120130
}
121131

122132
for _, tt := range tests {
@@ -189,3 +199,51 @@ func TestClient_ImplementsPRCreator(t *testing.T) {
189199
// Runtime assertion that Client implements PRCreator interface.
190200
var _ PRCreator = client
191201
}
202+
203+
// TestCommitMessageFormat tests the commit message format with and without git trailer.
204+
// This tests the format logic used in commitFile().
205+
func TestCommitMessageFormat(t *testing.T) {
206+
t.Parallel()
207+
208+
tests := []struct {
209+
name string
210+
fileName string
211+
triggeredBy string
212+
wantMessage string
213+
}{
214+
{
215+
name: "without triggered by",
216+
fileName: "VERSION",
217+
triggeredBy: "",
218+
wantMessage: "Update VERSION for release",
219+
},
220+
{
221+
name: "with triggered by",
222+
fileName: "VERSION",
223+
triggeredBy: "testuser",
224+
wantMessage: "Update VERSION for release\n\nRelease-Triggered-By: testuser",
225+
},
226+
{
227+
name: "with triggered by on Chart.yaml",
228+
fileName: "Chart.yaml",
229+
triggeredBy: "releasebot",
230+
wantMessage: "Update Chart.yaml for release\n\nRelease-Triggered-By: releasebot",
231+
},
232+
}
233+
234+
for _, tt := range tests {
235+
t.Run(tt.name, func(t *testing.T) {
236+
t.Parallel()
237+
238+
// Replicate the message format logic from commitFile()
239+
message := "Update " + tt.fileName + " for release"
240+
if tt.triggeredBy != "" {
241+
message += "\n\nRelease-Triggered-By: " + tt.triggeredBy
242+
}
243+
244+
if message != tt.wantMessage {
245+
t.Errorf("commit message = %q, want %q", message, tt.wantMessage)
246+
}
247+
})
248+
}
249+
}

internal/github/pr.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (c *Client) CreateReleasePR(ctx context.Context, req PRRequest) (*PRResult,
4747

4848
// Commit the files to the new branch
4949
for _, filePath := range req.Files {
50-
if err := c.commitFile(ctx, req.Owner, req.Repo, req.HeadBranch, filePath); err != nil {
50+
if err := c.commitFile(ctx, req.Owner, req.Repo, req.HeadBranch, filePath, req.TriggeredBy); err != nil {
5151
return nil, fmt.Errorf("committing file %s: %w", filePath, err)
5252
}
5353
}
@@ -73,7 +73,8 @@ func (c *Client) CreateReleasePR(ctx context.Context, req PRRequest) (*PRResult,
7373
}
7474

7575
// commitFile commits a single file to a branch.
76-
func (c *Client) commitFile(ctx context.Context, owner, repo, branch, filePath string) error {
76+
// If triggeredBy is non-empty, a git trailer is added to the commit message.
77+
func (c *Client) commitFile(ctx context.Context, owner, repo, branch, filePath, triggeredBy string) error {
7778
// Read file content using the fileReader interface
7879
content, err := c.fileReader.ReadFile(filePath)
7980
if err != nil {
@@ -87,6 +88,9 @@ func (c *Client) commitFile(ctx context.Context, owner, repo, branch, filePath s
8788
)
8889

8990
message := fmt.Sprintf("Update %s for release", filepath.Base(filePath))
91+
if triggeredBy != "" {
92+
message += fmt.Sprintf("\n\nRelease-Triggered-By: %s", triggeredBy)
93+
}
9094

9195
opts := &github.RepositoryContentFileOptions{
9296
Message: github.String(message),

main.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Config struct {
4040
RepoOwner string
4141
RepoName string
4242
BaseBranch string
43+
TriggeredBy string
4344
}
4445

4546
// Dependencies holds the external dependencies for the release process.
@@ -212,13 +213,14 @@ func createReleasePR(
212213
allFiles = append(allFiles, helmDocsFiles...)
213214

214215
pr, err := prCreator.CreateReleasePR(ctx, github.PRRequest{
215-
Owner: cfg.RepoOwner,
216-
Repo: cfg.RepoName,
217-
BaseBranch: cfg.BaseBranch,
218-
HeadBranch: branchName,
219-
Title: prTitle,
220-
Body: prBody,
221-
Files: allFiles,
216+
Owner: cfg.RepoOwner,
217+
Repo: cfg.RepoName,
218+
BaseBranch: cfg.BaseBranch,
219+
HeadBranch: branchName,
220+
Title: prTitle,
221+
Body: prBody,
222+
Files: allFiles,
223+
TriggeredBy: cfg.TriggeredBy,
222224
})
223225
if err != nil {
224226
return nil, fmt.Errorf("creating PR: %w", err)
@@ -243,6 +245,7 @@ func parseFlags() Config {
243245
cfg.VersionFiles = parseVersionFiles(versionFilesJSON)
244246
cfg.Token = resolveToken(cfg.Token)
245247
cfg.RepoOwner, cfg.RepoName = parseRepository()
248+
cfg.TriggeredBy = os.Getenv("GITHUB_ACTOR")
246249

247250
validateConfig(cfg)
248251

main_test.go

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ func (m *mockYAMLUpdater) UpdateYAMLFile(_ files.VersionFileConfig, _, _ string)
5454

5555
// mockPRCreator implements github.PRCreator for testing.
5656
type mockPRCreator struct {
57-
result *github.PRResult
58-
err error
57+
result *github.PRResult
58+
err error
59+
lastRequest github.PRRequest // captures the last request for verification
5960
}
6061

61-
func (m *mockPRCreator) CreateReleasePR(_ context.Context, _ github.PRRequest) (*github.PRResult, error) {
62+
func (m *mockPRCreator) CreateReleasePR(_ context.Context, req github.PRRequest) (*github.PRResult, error) {
63+
m.lastRequest = req
6264
return m.result, m.err
6365
}
6466

@@ -383,15 +385,16 @@ func TestCreateReleasePR(t *testing.T) {
383385
t.Parallel()
384386

385387
tests := []struct {
386-
name string
387-
cfg Config
388-
prCreator *mockPRCreator
389-
newVersion string
390-
helmDocsFiles []string
391-
wantErr bool
392-
errContains string
393-
wantPRNumber int
394-
wantPRURL string
388+
name string
389+
cfg Config
390+
prCreator *mockPRCreator
391+
newVersion string
392+
helmDocsFiles []string
393+
wantErr bool
394+
errContains string
395+
wantPRNumber int
396+
wantPRURL string
397+
wantTriggeredBy string
395398
}{
396399
{
397400
name: "success",
@@ -454,6 +457,29 @@ func TestCreateReleasePR(t *testing.T) {
454457
wantErr: true,
455458
errContains: "creating PR",
456459
},
460+
{
461+
name: "success with triggered by actor",
462+
cfg: Config{
463+
RepoOwner: "owner",
464+
RepoName: "repo",
465+
BaseBranch: "main",
466+
BumpType: "minor",
467+
VersionFile: "VERSION",
468+
TriggeredBy: "testuser",
469+
},
470+
prCreator: &mockPRCreator{
471+
result: &github.PRResult{
472+
Number: 789,
473+
URL: "https://github.com/owner/repo/pull/789",
474+
},
475+
err: nil,
476+
},
477+
newVersion: "1.1.0",
478+
wantErr: false,
479+
wantPRNumber: 789,
480+
wantPRURL: "https://github.com/owner/repo/pull/789",
481+
wantTriggeredBy: "testuser",
482+
},
457483
}
458484

459485
for _, tt := range tests {
@@ -484,6 +510,14 @@ func TestCreateReleasePR(t *testing.T) {
484510
if result.URL != tt.wantPRURL {
485511
t.Errorf("createReleasePR() PR URL = %q, want %q", result.URL, tt.wantPRURL)
486512
}
513+
514+
// Verify TriggeredBy is passed through to the PRRequest
515+
if tt.wantTriggeredBy != "" {
516+
if tt.prCreator.lastRequest.TriggeredBy != tt.wantTriggeredBy {
517+
t.Errorf("createReleasePR() TriggeredBy = %q, want %q",
518+
tt.prCreator.lastRequest.TriggeredBy, tt.wantTriggeredBy)
519+
}
520+
}
487521
})
488522
}
489523
}

0 commit comments

Comments
 (0)