Skip to content

Commit 387c987

Browse files
committed
refactor(repo): General improvements
1 parent c9b7f69 commit 387c987

File tree

8 files changed

+326
-138
lines changed

8 files changed

+326
-138
lines changed

cmd/container.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func InitCommands() error {
9797
func addOrchestratorCommands(ctx context.Context, c *container) error {
9898
log := logger.FromContext(ctx).Named("cmd.container")
9999
// Initialize extended repositories for orchestrators
100-
gitExtRepo, err := repository.NewGitExtendedRepository()
100+
gitExtRepo, err := repository.NewGitExtendedRepositoryWithTimeout(c.cfg.GitPushTimeoutMinutes)
101101
if err != nil {
102102
return fmt.Errorf("failed to initialize git extended repository: %w", err)
103103
}

internal/config/config.go

Lines changed: 56 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ import (
1313
)
1414

1515
type Config struct {
16-
GithubToken string `mapstructure:"github_token"`
17-
GithubOwner string `mapstructure:"github_owner"`
18-
GithubRepo string `mapstructure:"github_repo"`
19-
ToolsDir string `mapstructure:"tools_dir"`
20-
NpmToken string `mapstructure:"npm_token"`
21-
LogLevel string `mapstructure:"log_level"`
22-
LogFormat string `mapstructure:"log_format"`
16+
GithubToken string `mapstructure:"github_token"`
17+
GithubOwner string `mapstructure:"github_owner"`
18+
GithubRepo string `mapstructure:"github_repo"`
19+
ToolsDir string `mapstructure:"tools_dir"`
20+
NpmToken string `mapstructure:"npm_token"`
21+
LogLevel string `mapstructure:"log_level"`
22+
LogFormat string `mapstructure:"log_format"`
23+
GitPushTimeoutMinutes int `mapstructure:"git_push_timeout_minutes"`
2324
}
2425

2526
var configFileCandidates = []string{".pr-release", ".compozy-release"}
@@ -29,7 +30,12 @@ func DefaultConfig() *Config {
2930
if isCI() {
3031
logFormat = "console"
3132
}
32-
return &Config{ToolsDir: "tools", LogLevel: "info", LogFormat: logFormat}
33+
return &Config{
34+
ToolsDir: "tools",
35+
LogLevel: "info",
36+
LogFormat: logFormat,
37+
GitPushTimeoutMinutes: 2,
38+
}
3339
}
3440

3541
func isCI() bool {
@@ -75,6 +81,9 @@ func (c *Config) Validate() error {
7581
if err := validateLogFormat(c.LogFormat); err != nil {
7682
return err
7783
}
84+
if c.GitPushTimeoutMinutes < 1 || c.GitPushTimeoutMinutes > 30 {
85+
return fmt.Errorf("git_push_timeout_minutes must be between 1 and 30, got %d", c.GitPushTimeoutMinutes)
86+
}
7887
return nil
7988
}
8089

@@ -146,63 +155,52 @@ func ValidateGitHubOwnerRepo(owner, repo string) error {
146155
return nil
147156
}
148157

158+
func bindEnvironmentVariables(v *viper.Viper) error {
159+
bindings := map[string][]string{
160+
"github_token": {
161+
"GITHUB_TOKEN",
162+
"PR_RELEASE_GITHUB_TOKEN",
163+
"COMPOZY_RELEASE_GITHUB_TOKEN",
164+
"RELEASE_TOKEN",
165+
},
166+
"github_owner": {"GITHUB_OWNER", "PR_RELEASE_GITHUB_OWNER", "COMPOZY_RELEASE_GITHUB_OWNER"},
167+
"github_repo": {"GITHUB_REPO", "PR_RELEASE_GITHUB_REPO", "COMPOZY_RELEASE_GITHUB_REPO"},
168+
"tools_dir": {"TOOLS_DIR", "PR_RELEASE_TOOLS_DIR", "COMPOZY_RELEASE_TOOLS_DIR"},
169+
"log_level": {"LOG_LEVEL", "PR_RELEASE_LOG_LEVEL", "COMPOZY_RELEASE_LOG_LEVEL"},
170+
"log_format": {"LOG_FORMAT", "PR_RELEASE_LOG_FORMAT", "COMPOZY_RELEASE_LOG_FORMAT"},
171+
"npm_token": {"NPM_TOKEN", "PR_RELEASE_NPM_TOKEN", "COMPOZY_RELEASE_NPM_TOKEN"},
172+
"git_push_timeout_minutes": {
173+
"GIT_PUSH_TIMEOUT_MINUTES",
174+
"PR_RELEASE_GIT_PUSH_TIMEOUT_MINUTES",
175+
"COMPOZY_RELEASE_GIT_PUSH_TIMEOUT_MINUTES",
176+
},
177+
}
178+
for key, envs := range bindings {
179+
if err := v.BindEnv(append([]string{key}, envs...)...); err != nil {
180+
return fmt.Errorf("failed to bind %s env: %w", key, err)
181+
}
182+
}
183+
return nil
184+
}
185+
186+
func setConfigDefaults(v *viper.Viper) {
187+
defaults := DefaultConfig()
188+
v.SetDefault("tools_dir", defaults.ToolsDir)
189+
v.SetDefault("log_level", defaults.LogLevel)
190+
v.SetDefault("log_format", defaults.LogFormat)
191+
v.SetDefault("git_push_timeout_minutes", defaults.GitPushTimeoutMinutes)
192+
}
193+
149194
func LoadConfig() (*Config, error) {
150195
v := viper.New()
151196
v.SetConfigType("yaml")
152197
v.AddConfigPath(".")
153198
v.AutomaticEnv()
154199
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
155-
if err := v.BindEnv(
156-
"github_token",
157-
"GITHUB_TOKEN",
158-
"PR_RELEASE_GITHUB_TOKEN",
159-
"COMPOZY_RELEASE_GITHUB_TOKEN",
160-
"RELEASE_TOKEN",
161-
); err != nil {
162-
return nil, fmt.Errorf("failed to bind github_token env: %w", err)
163-
}
164-
if err := v.BindEnv(
165-
"github_owner",
166-
"GITHUB_OWNER",
167-
"PR_RELEASE_GITHUB_OWNER",
168-
"COMPOZY_RELEASE_GITHUB_OWNER",
169-
); err != nil {
170-
return nil, fmt.Errorf("failed to bind github_owner env: %w", err)
171-
}
172-
if err := v.BindEnv(
173-
"github_repo",
174-
"GITHUB_REPO",
175-
"PR_RELEASE_GITHUB_REPO",
176-
"COMPOZY_RELEASE_GITHUB_REPO",
177-
); err != nil {
178-
return nil, fmt.Errorf("failed to bind github_repo env: %w", err)
179-
}
180-
if err := v.BindEnv("tools_dir", "TOOLS_DIR", "PR_RELEASE_TOOLS_DIR", "COMPOZY_RELEASE_TOOLS_DIR"); err != nil {
181-
return nil, fmt.Errorf("failed to bind tools_dir env: %w", err)
182-
}
183-
if err := v.BindEnv(
184-
"log_level",
185-
"LOG_LEVEL",
186-
"PR_RELEASE_LOG_LEVEL",
187-
"COMPOZY_RELEASE_LOG_LEVEL",
188-
); err != nil {
189-
return nil, fmt.Errorf("failed to bind log_level env: %w", err)
190-
}
191-
if err := v.BindEnv(
192-
"log_format",
193-
"LOG_FORMAT",
194-
"PR_RELEASE_LOG_FORMAT",
195-
"COMPOZY_RELEASE_LOG_FORMAT",
196-
); err != nil {
197-
return nil, fmt.Errorf("failed to bind log_format env: %w", err)
198-
}
199-
if err := v.BindEnv("npm_token", "NPM_TOKEN", "PR_RELEASE_NPM_TOKEN", "COMPOZY_RELEASE_NPM_TOKEN"); err != nil {
200-
return nil, fmt.Errorf("failed to bind npm_token env: %w", err)
200+
if err := bindEnvironmentVariables(v); err != nil {
201+
return nil, err
201202
}
202-
defaults := DefaultConfig()
203-
v.SetDefault("tools_dir", defaults.ToolsDir)
204-
v.SetDefault("log_level", defaults.LogLevel)
205-
v.SetDefault("log_format", defaults.LogFormat)
203+
setConfigDefaults(v)
206204
for _, name := range configFileCandidates {
207205
v.SetConfigName(name)
208206
if err := v.ReadInConfig(); err != nil {

internal/orchestrator/compensating_acts.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -261,16 +261,11 @@ func (ca *CompensatingActions) branchExistsLocally(ctx context.Context, branchNa
261261
}
262262

263263
func (ca *CompensatingActions) branchExistsRemotely(ctx context.Context, branchName string) bool {
264-
branches, err := ca.gitRepo.ListRemoteBranches(ctx)
264+
exists, err := ca.gitRepo.RemoteBranchExists(ctx, branchName)
265265
if err != nil {
266266
return false
267267
}
268-
for _, branch := range branches {
269-
if strings.HasSuffix(branch, "/"+branchName) {
270-
return true
271-
}
272-
}
273-
return false
268+
return exists
274269
}
275270

276271
func (ca *CompensatingActions) fileHasChanges(ctx context.Context, file string) bool {

internal/orchestrator/mocks_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ func (m *mockGitExtendedRepository) ListRemoteBranches(ctx context.Context) ([]s
9999
}
100100
return nil, args.Error(1)
101101
}
102+
func (m *mockGitExtendedRepository) RemoteBranchExists(ctx context.Context, branchName string) (bool, error) {
103+
args := m.Called(ctx, branchName)
104+
return args.Bool(0), args.Error(1)
105+
}
102106
func (m *mockGitExtendedRepository) GetFileStatus(ctx context.Context, path string) (string, error) {
103107
args := m.Called(ctx, path)
104108
return args.String(0), args.Error(1)

internal/orchestrator/pr_release.go

Lines changed: 116 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"slices"
88
"strings"
9+
"time"
910

1011
"github.com/compozy/releasepr/internal/domain"
1112
"github.com/compozy/releasepr/internal/logger"
@@ -372,7 +373,7 @@ func (o *PRReleaseOrchestrator) buildAndExecuteWorkflow(
372373
// Add all workflow steps
373374
o.addCheckChangesStep(saga, cfg, compensator, wctx)
374375
o.addCalculateVersionStep(saga, cfg, compensator, wctx)
375-
o.addCreateBranchStep(saga, compensator, wctx, originalBranch)
376+
o.addCreateBranchStep(saga, cfg, compensator, wctx, originalBranch)
376377
o.addUpdatePackagesStep(saga, compensator, wctx)
377378
o.addGenerateChangelogStep(saga, compensator, wctx)
378379
o.addCommitChangesStep(saga, compensator, wctx)
@@ -461,6 +462,7 @@ func (o *PRReleaseOrchestrator) addCalculateVersionStep(
461462

462463
func (o *PRReleaseOrchestrator) addCreateBranchStep(
463464
saga *SagaExecutor,
465+
cfg PRReleaseConfig,
464466
compensator *CompensatingActions,
465467
wctx *workflowContext,
466468
originalBranch string,
@@ -472,49 +474,39 @@ func (o *PRReleaseOrchestrator) addCreateBranchStep(
472474
if wctx.version == "" {
473475
return map[string]any{"skip": true}, nil
474476
}
475-
wctx.branchName = fmt.Sprintf("release/%s", wctx.version)
476-
o.logger(ctx).Info("Determined release branch", zap.String("branch", wctx.branchName))
477-
if err := ValidateBranchName(wctx.branchName); err != nil {
478-
return nil, fmt.Errorf("invalid branch name: %w", err)
479-
}
480-
saga.SetBranchName(wctx.branchName)
481-
branches, err := o.gitRepo.ListLocalBranches(ctx)
477+
branchName, err := o.prepareBranchName(ctx, saga, wctx)
482478
if err != nil {
483-
return nil, fmt.Errorf("failed to list local branches: %w", err)
479+
return nil, err
484480
}
485-
branchExists := slices.Contains(branches, wctx.branchName)
486-
remoteBranches, err := o.gitRepo.ListRemoteBranches(ctx)
481+
branchExists, remoteExists, err := o.checkBranchExistence(ctx, branchName)
487482
if err != nil {
488-
return nil, fmt.Errorf("failed to list remote branches: %w", err)
483+
return nil, err
489484
}
490-
remoteExists := slices.Contains(remoteBranches, fmt.Sprintf("origin/%s", wctx.branchName))
491-
switch {
492-
case branchExists && remoteExists:
493-
o.logger(ctx).Info(
494-
"Reusing existing branch",
495-
zap.String("branch", wctx.branchName),
496-
zap.Bool("local", true),
497-
zap.Bool("remote", true),
485+
if cfg.ForceRelease {
486+
branchExists, remoteExists, err = o.forceDeleteBranch(
487+
ctx,
488+
branchName,
489+
branchExists,
490+
remoteExists,
491+
originalBranch,
498492
)
499-
case branchExists:
500-
o.logger(ctx).Info("Checking out existing local branch", zap.String("branch", wctx.branchName))
501-
case remoteExists:
502-
o.logger(ctx).Info("Checking out remote branch", zap.String("branch", wctx.branchName))
503-
default:
504-
o.logger(ctx).Info("Creating new branch", zap.String("branch", wctx.branchName))
493+
if err != nil {
494+
return nil, err
495+
}
505496
}
497+
o.logBranchStatus(ctx, branchName, branchExists, remoteExists)
506498
wctx.createdInSession = !branchExists
507499
if wctx.createdInSession {
508-
if err := o.createReleaseBranch(ctx, wctx.branchName); err != nil {
509-
return nil, fmt.Errorf("failed to create release branch %s: %w", wctx.branchName, err)
500+
if err := o.createReleaseBranch(ctx, branchName); err != nil {
501+
return nil, fmt.Errorf("failed to create release branch %s: %w", branchName, err)
510502
}
511503
}
512-
if err := o.gitRepo.CheckoutBranch(ctx, wctx.branchName); err != nil {
513-
return nil, fmt.Errorf("failed to checkout release branch %s: %w", wctx.branchName, err)
504+
if err := o.gitRepo.CheckoutBranch(ctx, branchName); err != nil {
505+
return nil, fmt.Errorf("failed to checkout release branch %s: %w", branchName, err)
514506
}
515-
o.logger(ctx).Info("Checked out release branch", zap.String("branch", wctx.branchName))
507+
o.logger(ctx).Info("Checked out release branch", zap.String("branch", branchName))
516508
return map[string]any{
517-
"branch_name": wctx.branchName,
509+
"branch_name": branchName,
518510
"original_branch": originalBranch,
519511
"created_in_session": wctx.createdInSession,
520512
"remote_exists": remoteExists,
@@ -524,6 +516,98 @@ func (o *PRReleaseOrchestrator) addCreateBranchStep(
524516
})
525517
}
526518

519+
func (o *PRReleaseOrchestrator) prepareBranchName(
520+
ctx context.Context,
521+
saga *SagaExecutor,
522+
wctx *workflowContext,
523+
) (string, error) {
524+
wctx.branchName = fmt.Sprintf("release/%s", wctx.version)
525+
o.logger(ctx).Info("Determined release branch", zap.String("branch", wctx.branchName))
526+
if err := ValidateBranchName(wctx.branchName); err != nil {
527+
return "", fmt.Errorf("invalid branch name: %w", err)
528+
}
529+
saga.SetBranchName(wctx.branchName)
530+
return wctx.branchName, nil
531+
}
532+
533+
func (o *PRReleaseOrchestrator) checkBranchExistence(ctx context.Context, branchName string) (bool, bool, error) {
534+
branches, err := o.gitRepo.ListLocalBranches(ctx)
535+
if err != nil {
536+
return false, false, fmt.Errorf("failed to list local branches: %w", err)
537+
}
538+
branchExists := slices.Contains(branches, branchName)
539+
checkCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
540+
defer cancel()
541+
remoteExists, err := o.gitRepo.RemoteBranchExists(checkCtx, branchName)
542+
if err != nil {
543+
o.logger(ctx).Warn("Failed to check remote branch, assuming it doesn't exist",
544+
zap.String("branch", branchName),
545+
zap.Error(err))
546+
remoteExists = false
547+
}
548+
return branchExists, remoteExists, nil
549+
}
550+
551+
func (o *PRReleaseOrchestrator) forceDeleteBranch(
552+
ctx context.Context,
553+
branchName string,
554+
branchExists, remoteExists bool,
555+
originalBranch string,
556+
) (bool, bool, error) {
557+
if !branchExists && !remoteExists {
558+
return branchExists, remoteExists, nil
559+
}
560+
o.logger(ctx).Info("Force flag set, deleting existing branch",
561+
zap.String("branch", branchName),
562+
zap.Bool("local_exists", branchExists),
563+
zap.Bool("remote_exists", remoteExists))
564+
if branchExists {
565+
if err := o.gitRepo.CheckoutBranch(ctx, originalBranch); err != nil {
566+
return branchExists, remoteExists, fmt.Errorf(
567+
"failed to checkout original branch %s: %w",
568+
originalBranch,
569+
err,
570+
)
571+
}
572+
if err := o.gitRepo.DeleteBranch(ctx, branchName); err != nil {
573+
return branchExists, remoteExists, fmt.Errorf("failed to delete local branch %s: %w", branchName, err)
574+
}
575+
o.logger(ctx).Info("Deleted local branch", zap.String("branch", branchName))
576+
}
577+
if remoteExists {
578+
if err := o.gitRepo.DeleteRemoteBranch(ctx, branchName); err != nil {
579+
o.logger(ctx).Warn("Failed to delete remote branch, will force push",
580+
zap.String("branch", branchName),
581+
zap.Error(err))
582+
} else {
583+
o.logger(ctx).Info("Deleted remote branch", zap.String("branch", branchName))
584+
}
585+
}
586+
return false, false, nil
587+
}
588+
589+
func (o *PRReleaseOrchestrator) logBranchStatus(
590+
ctx context.Context,
591+
branchName string,
592+
branchExists, remoteExists bool,
593+
) {
594+
switch {
595+
case branchExists && remoteExists:
596+
o.logger(ctx).Info(
597+
"Reusing existing branch",
598+
zap.String("branch", branchName),
599+
zap.Bool("local", true),
600+
zap.Bool("remote", true),
601+
)
602+
case branchExists:
603+
o.logger(ctx).Info("Checking out existing local branch", zap.String("branch", branchName))
604+
case remoteExists:
605+
o.logger(ctx).Info("Checking out remote branch", zap.String("branch", branchName))
606+
default:
607+
o.logger(ctx).Info("Creating new branch", zap.String("branch", branchName))
608+
}
609+
}
610+
527611
func (o *PRReleaseOrchestrator) addUpdatePackagesStep(
528612
saga *SagaExecutor,
529613
compensator *CompensatingActions,

0 commit comments

Comments
 (0)