Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ releaseo

# GoReleaser
dist/

.task/checksum/build
17 changes: 9 additions & 8 deletions internal/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,16 @@ func NewClient(ctx context.Context, token string, opts ...ClientOption) (*Client
}

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

// Validate checks that all required fields are set.
Expand Down
58 changes: 58 additions & 0 deletions internal/github/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ func TestPRRequest_Validate(t *testing.T) {
modify: func(r *PRRequest) { r.Body = "" },
wantErr: "",
},
{
name: "triggered by is optional",
modify: func(r *PRRequest) { r.TriggeredBy = "" },
wantErr: "",
},
{
name: "triggered by with value",
modify: func(r *PRRequest) { r.TriggeredBy = "someuser" },
wantErr: "",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -189,3 +199,51 @@ func TestClient_ImplementsPRCreator(t *testing.T) {
// Runtime assertion that Client implements PRCreator interface.
var _ PRCreator = client
}

// TestCommitMessageFormat tests the commit message format with and without git trailer.
// This tests the format logic used in commitFile().
func TestCommitMessageFormat(t *testing.T) {
t.Parallel()

tests := []struct {
name string
fileName string
triggeredBy string
wantMessage string
}{
{
name: "without triggered by",
fileName: "VERSION",
triggeredBy: "",
wantMessage: "Update VERSION for release",
},
{
name: "with triggered by",
fileName: "VERSION",
triggeredBy: "testuser",
wantMessage: "Update VERSION for release\n\nRelease-Triggered-By: testuser",
},
{
name: "with triggered by on Chart.yaml",
fileName: "Chart.yaml",
triggeredBy: "releasebot",
wantMessage: "Update Chart.yaml for release\n\nRelease-Triggered-By: releasebot",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

// Replicate the message format logic from commitFile()
message := "Update " + tt.fileName + " for release"
if tt.triggeredBy != "" {
message += "\n\nRelease-Triggered-By: " + tt.triggeredBy
}

if message != tt.wantMessage {
t.Errorf("commit message = %q, want %q", message, tt.wantMessage)
}
})
}
}
8 changes: 6 additions & 2 deletions internal/github/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (c *Client) CreateReleasePR(ctx context.Context, req PRRequest) (*PRResult,

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

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

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

opts := &github.RepositoryContentFileOptions{
Message: github.String(message),
Expand Down
17 changes: 10 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Config struct {
RepoOwner string
RepoName string
BaseBranch string
TriggeredBy string
}

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

pr, err := prCreator.CreateReleasePR(ctx, github.PRRequest{
Owner: cfg.RepoOwner,
Repo: cfg.RepoName,
BaseBranch: cfg.BaseBranch,
HeadBranch: branchName,
Title: prTitle,
Body: prBody,
Files: allFiles,
Owner: cfg.RepoOwner,
Repo: cfg.RepoName,
BaseBranch: cfg.BaseBranch,
HeadBranch: branchName,
Title: prTitle,
Body: prBody,
Files: allFiles,
TriggeredBy: cfg.TriggeredBy,
})
if err != nil {
return nil, fmt.Errorf("creating PR: %w", err)
Expand All @@ -243,6 +245,7 @@ func parseFlags() Config {
cfg.VersionFiles = parseVersionFiles(versionFilesJSON)
cfg.Token = resolveToken(cfg.Token)
cfg.RepoOwner, cfg.RepoName = parseRepository()
cfg.TriggeredBy = os.Getenv("GITHUB_ACTOR")

validateConfig(cfg)

Expand Down
58 changes: 46 additions & 12 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ func (m *mockYAMLUpdater) UpdateYAMLFile(_ files.VersionFileConfig, _, _ string)

// mockPRCreator implements github.PRCreator for testing.
type mockPRCreator struct {
result *github.PRResult
err error
result *github.PRResult
err error
lastRequest github.PRRequest // captures the last request for verification
}

func (m *mockPRCreator) CreateReleasePR(_ context.Context, _ github.PRRequest) (*github.PRResult, error) {
func (m *mockPRCreator) CreateReleasePR(_ context.Context, req github.PRRequest) (*github.PRResult, error) {
m.lastRequest = req
return m.result, m.err
}

Expand Down Expand Up @@ -383,15 +385,16 @@ func TestCreateReleasePR(t *testing.T) {
t.Parallel()

tests := []struct {
name string
cfg Config
prCreator *mockPRCreator
newVersion string
helmDocsFiles []string
wantErr bool
errContains string
wantPRNumber int
wantPRURL string
name string
cfg Config
prCreator *mockPRCreator
newVersion string
helmDocsFiles []string
wantErr bool
errContains string
wantPRNumber int
wantPRURL string
wantTriggeredBy string
}{
{
name: "success",
Expand Down Expand Up @@ -454,6 +457,29 @@ func TestCreateReleasePR(t *testing.T) {
wantErr: true,
errContains: "creating PR",
},
{
name: "success with triggered by actor",
cfg: Config{
RepoOwner: "owner",
RepoName: "repo",
BaseBranch: "main",
BumpType: "minor",
VersionFile: "VERSION",
TriggeredBy: "testuser",
},
prCreator: &mockPRCreator{
result: &github.PRResult{
Number: 789,
URL: "https://github.com/owner/repo/pull/789",
},
err: nil,
},
newVersion: "1.1.0",
wantErr: false,
wantPRNumber: 789,
wantPRURL: "https://github.com/owner/repo/pull/789",
wantTriggeredBy: "testuser",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -484,6 +510,14 @@ func TestCreateReleasePR(t *testing.T) {
if result.URL != tt.wantPRURL {
t.Errorf("createReleasePR() PR URL = %q, want %q", result.URL, tt.wantPRURL)
}

// Verify TriggeredBy is passed through to the PRRequest
if tt.wantTriggeredBy != "" {
if tt.prCreator.lastRequest.TriggeredBy != tt.wantTriggeredBy {
t.Errorf("createReleasePR() TriggeredBy = %q, want %q",
tt.prCreator.lastRequest.TriggeredBy, tt.wantTriggeredBy)
}
}
})
}
}
Expand Down