Skip to content

Commit 88e0a83

Browse files
committed
fix(repo): Package json generating
1 parent 1514732 commit 88e0a83

File tree

15 files changed

+468
-173
lines changed

15 files changed

+468
-173
lines changed

cmd/container.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package cmd
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67

78
"github.com/compozy/releasepr/internal/config"
9+
"github.com/compozy/releasepr/internal/logger"
810
"github.com/compozy/releasepr/internal/orchestrator"
911
"github.com/compozy/releasepr/internal/repository"
1012
"github.com/compozy/releasepr/internal/service"
1113
"github.com/spf13/afero"
14+
"github.com/spf13/cobra"
15+
"go.uber.org/zap"
1216
)
1317

1418
// container holds all the dependencies for the application.
@@ -65,11 +69,25 @@ func InitCommands() error {
6569
if err != nil {
6670
return err
6771
}
72+
ctx := rootCmd.Context()
73+
if ctx == nil {
74+
return fmt.Errorf("root command context not initialized")
75+
}
76+
ctx = config.IntoContext(ctx, c.cfg)
77+
appLogger, err := logger.New(c.cfg.LoggerConfig())
78+
if err != nil {
79+
return fmt.Errorf("failed to initialize logger: %w", err)
80+
}
81+
ctx = logger.IntoContext(ctx, appLogger)
82+
rootCmd.SetContext(ctx)
83+
rootCmd.PersistentPostRunE = func(cmd *cobra.Command, _ []string) error {
84+
return logger.Sync(logger.FromContext(cmd.Context()))
85+
}
6886

6987
// Individual commands have been replaced by orchestrator commands
7088

7189
// Add orchestrator-based commands
72-
if err := addOrchestratorCommands(c); err != nil {
90+
if err := addOrchestratorCommands(ctx, c); err != nil {
7391
return err
7492
}
7593

@@ -79,7 +97,8 @@ func InitCommands() error {
7997
}
8098

8199
// addOrchestratorCommands adds the new consolidated commands
82-
func addOrchestratorCommands(c *container) error {
100+
func addOrchestratorCommands(ctx context.Context, c *container) error {
101+
log := logger.FromContext(ctx).Named("cmd.container")
83102
// Initialize extended repositories for orchestrators
84103
gitExtRepo, err := repository.NewGitExtendedRepository()
85104
if err != nil {
@@ -98,23 +117,28 @@ func addOrchestratorCommands(c *container) error {
98117
}
99118
owner := c.cfg.GithubOwner
100119
repo := c.cfg.GithubRepo
101-
fmt.Printf("GitHub configuration: owner=%s, repo=%s, token_source=%s, token_present=%t, token_length=%d\n",
102-
owner, repo, tokenSource, token != "", len(token))
120+
log.Info("GitHub configuration",
121+
zap.String("owner", owner),
122+
zap.String("repo", repo),
123+
zap.String("token_source", tokenSource),
124+
zap.Bool("token_present", token != ""),
125+
zap.Int("token_length", len(token)),
126+
)
103127
if owner == "" || repo == "" {
104128
return fmt.Errorf("github owner/repo not configured; set GITHUB_REPOSITORY or config values")
105129
}
106130
var githubExtRepo repository.GithubExtendedRepository
107131
if token == "" {
108-
fmt.Fprintln(os.Stderr, "GitHub token not provided; GitHub operations will be skipped")
132+
log.Warn("GitHub token not provided; GitHub operations will be skipped")
109133
githubExtRepo = repository.NewGithubNoopExtendedRepository(owner, repo)
110134
} else {
111-
fmt.Printf("Initializing GitHub extended repository with token (length=%d)\n", len(token))
135+
log.Info("Initializing GitHub extended repository", zap.Int("token_length", len(token)))
112136
var err error
113137
githubExtRepo, err = repository.NewGithubExtendedRepository(token, owner, repo)
114138
if err != nil {
115139
return fmt.Errorf("failed to initialize GitHub extended repository: %w", err)
116140
}
117-
fmt.Printf("Successfully initialized GitHub extended repository for %s/%s\n", owner, repo)
141+
log.Info("Initialized GitHub extended repository", zap.String("owner", owner), zap.String("repo", repo))
118142
}
119143

120144
// Create PR Release orchestrator

cmd/root.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package cmd
22

33
import (
4+
"errors"
5+
6+
"github.com/compozy/releasepr/internal/logger"
47
"github.com/spf13/cobra"
58
)
69

@@ -11,5 +14,13 @@ var rootCmd = &cobra.Command{
1114
}
1215

1316
func Execute() error {
14-
return rootCmd.Execute()
17+
execErr := rootCmd.Execute()
18+
syncErr := logger.Sync(logger.FromContext(rootCmd.Context()))
19+
if execErr != nil {
20+
if syncErr != nil {
21+
return errors.Join(execErr, syncErr)
22+
}
23+
return execErr
24+
}
25+
return syncErr
1526
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/spf13/cobra v1.10.1
1414
github.com/spf13/viper v1.21.0
1515
github.com/stretchr/testify v1.11.1
16+
go.uber.org/zap v1.27.0
1617
golang.org/x/oauth2 v0.31.0
1718
)
1819

@@ -45,6 +46,7 @@ require (
4546
github.com/stretchr/objx v0.5.2 // indirect
4647
github.com/subosito/gotenv v1.6.0 // indirect
4748
github.com/xanzy/ssh-agent v0.3.3 // indirect
49+
go.uber.org/multierr v1.10.0 // indirect
4850
go.yaml.in/yaml/v3 v3.0.4 // indirect
4951
golang.org/x/crypto v0.37.0 // indirect
5052
golang.org/x/net v0.39.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
111111
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
112112
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
113113
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
114+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
115+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
116+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
117+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
118+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
119+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
114120
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
115121
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
116122
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=

internal/config/config.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"regexp"
88
"strings"
99

10+
"github.com/compozy/releasepr/internal/logger"
1011
"github.com/go-git/go-git/v5"
1112
"github.com/spf13/viper"
1213
)
@@ -17,13 +18,15 @@ type Config struct {
1718
GithubRepo string `mapstructure:"github_repo"`
1819
ToolsDir string `mapstructure:"tools_dir"`
1920
NpmToken string `mapstructure:"npm_token"`
21+
LogLevel string `mapstructure:"log_level"`
22+
LogFormat string `mapstructure:"log_format"`
2023
}
2124

2225
var configFileCandidates = []string{".pr-release", ".compozy-release"}
2326

2427
// DefaultConfig returns a Config with default values.
2528
func DefaultConfig() *Config {
26-
return &Config{ToolsDir: "tools"}
29+
return &Config{ToolsDir: "tools", LogLevel: "info", LogFormat: "json"}
2730
}
2831

2932
// Validate validates the configuration.
@@ -42,9 +45,35 @@ func (c *Config) Validate() error {
4245
if strings.Contains(c.ToolsDir, "..") {
4346
return fmt.Errorf("tools_dir contains invalid path traversal")
4447
}
48+
if err := validateLogLevel(c.LogLevel); err != nil {
49+
return err
50+
}
51+
if err := validateLogFormat(c.LogFormat); err != nil {
52+
return err
53+
}
4554
return nil
4655
}
4756

57+
func (c *Config) LoggerConfig() logger.Config {
58+
return logger.Config{Level: c.LogLevel, Format: c.LogFormat}
59+
}
60+
61+
func validateLogLevel(level string) error {
62+
switch strings.ToLower(strings.TrimSpace(level)) {
63+
case "debug", "info", "warn", "error":
64+
return nil
65+
}
66+
return fmt.Errorf("invalid log_level: %s", level)
67+
}
68+
69+
func validateLogFormat(format string) error {
70+
switch strings.ToLower(strings.TrimSpace(format)) {
71+
case "json", "console":
72+
return nil
73+
}
74+
return fmt.Errorf("invalid log_format: %s", format)
75+
}
76+
4877
// ValidateForGitHubOperations validates that GitHub token is present for operations that require it.
4978
func (c *Config) ValidateForGitHubOperations() error {
5079
if c.GithubToken == "" {
@@ -97,6 +126,8 @@ func LoadConfig() (*Config, error) {
97126
v := viper.New()
98127
v.SetConfigType("yaml")
99128
v.AddConfigPath(".")
129+
v.SetDefault("log_level", "info")
130+
v.SetDefault("log_format", "json")
100131
v.AutomaticEnv()
101132
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
102133
if err := v.BindEnv(
@@ -127,6 +158,22 @@ func LoadConfig() (*Config, error) {
127158
if err := v.BindEnv("tools_dir", "TOOLS_DIR", "PR_RELEASE_TOOLS_DIR", "COMPOZY_RELEASE_TOOLS_DIR"); err != nil {
128159
return nil, fmt.Errorf("failed to bind tools_dir env: %w", err)
129160
}
161+
if err := v.BindEnv(
162+
"log_level",
163+
"LOG_LEVEL",
164+
"PR_RELEASE_LOG_LEVEL",
165+
"COMPOZY_RELEASE_LOG_LEVEL",
166+
); err != nil {
167+
return nil, fmt.Errorf("failed to bind log_level env: %w", err)
168+
}
169+
if err := v.BindEnv(
170+
"log_format",
171+
"LOG_FORMAT",
172+
"PR_RELEASE_LOG_FORMAT",
173+
"COMPOZY_RELEASE_LOG_FORMAT",
174+
); err != nil {
175+
return nil, fmt.Errorf("failed to bind log_format env: %w", err)
176+
}
130177
if err := v.BindEnv("npm_token", "NPM_TOKEN", "PR_RELEASE_NPM_TOKEN", "COMPOZY_RELEASE_NPM_TOKEN"); err != nil {
131178
return nil, fmt.Errorf("failed to bind npm_token env: %w", err)
132179
}

internal/config/context.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package config
2+
3+
import "context"
4+
5+
type contextKey struct{}
6+
7+
func IntoContext(ctx context.Context, cfg *Config) context.Context {
8+
return context.WithValue(ctx, contextKey{}, cfg)
9+
}
10+
11+
func FromContext(ctx context.Context) *Config {
12+
if ctx == nil {
13+
panic("config: nil context")
14+
}
15+
cfg, ok := ctx.Value(contextKey{}).(*Config)
16+
if !ok || cfg == nil {
17+
panic("config: configuration missing from context")
18+
}
19+
return cfg
20+
}

internal/logger/logger.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package logger
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
"syscall"
9+
10+
"go.uber.org/zap"
11+
"go.uber.org/zap/zapcore"
12+
)
13+
14+
type Config struct {
15+
Level string
16+
Format string
17+
}
18+
19+
type contextKey struct{}
20+
21+
func New(cfg Config) (*zap.Logger, error) {
22+
zapCfg, err := buildZapConfig(cfg)
23+
if err != nil {
24+
return nil, err
25+
}
26+
return zapCfg.Build()
27+
}
28+
29+
func buildZapConfig(cfg Config) (zap.Config, error) {
30+
format := strings.ToLower(strings.TrimSpace(cfg.Format))
31+
var zapCfg zap.Config
32+
switch format {
33+
case "json":
34+
zapCfg = zap.NewProductionConfig()
35+
case "console":
36+
zapCfg = zap.NewDevelopmentConfig()
37+
default:
38+
return zap.Config{}, fmt.Errorf("logger: unsupported format %s", cfg.Format)
39+
}
40+
level, err := parseLevel(cfg.Level)
41+
if err != nil {
42+
return zap.Config{}, err
43+
}
44+
zapCfg.Level = zap.NewAtomicLevelAt(level)
45+
zapCfg.Encoding = format
46+
encoder := zapcore.EncoderConfig{
47+
TimeKey: "ts",
48+
LevelKey: "level",
49+
NameKey: "logger",
50+
CallerKey: "caller",
51+
MessageKey: "msg",
52+
StacktraceKey: "stacktrace",
53+
LineEnding: zapcore.DefaultLineEnding,
54+
EncodeLevel: zapcore.LowercaseLevelEncoder,
55+
EncodeTime: zapcore.ISO8601TimeEncoder,
56+
EncodeDuration: zapcore.SecondsDurationEncoder,
57+
EncodeCaller: zapcore.ShortCallerEncoder,
58+
}
59+
if format == "console" {
60+
encoder.EncodeLevel = zapcore.CapitalColorLevelEncoder
61+
}
62+
zapCfg.EncoderConfig = encoder
63+
zapCfg.OutputPaths = []string{"stdout"}
64+
zapCfg.ErrorOutputPaths = []string{"stderr"}
65+
return zapCfg, nil
66+
}
67+
68+
func parseLevel(level string) (zapcore.Level, error) {
69+
switch strings.ToLower(strings.TrimSpace(level)) {
70+
case "debug":
71+
return zapcore.DebugLevel, nil
72+
case "info":
73+
return zapcore.InfoLevel, nil
74+
case "warn":
75+
return zapcore.WarnLevel, nil
76+
case "error":
77+
return zapcore.ErrorLevel, nil
78+
}
79+
return zapcore.InfoLevel, fmt.Errorf("logger: unsupported level %s", level)
80+
}
81+
82+
func IntoContext(ctx context.Context, log *zap.Logger) context.Context {
83+
return context.WithValue(ctx, contextKey{}, log)
84+
}
85+
86+
func FromContext(ctx context.Context) *zap.Logger {
87+
if ctx == nil {
88+
return zap.NewNop()
89+
}
90+
log, ok := ctx.Value(contextKey{}).(*zap.Logger)
91+
if !ok || log == nil {
92+
return zap.NewNop()
93+
}
94+
return log
95+
}
96+
97+
func With(ctx context.Context, fields ...zap.Field) *zap.Logger {
98+
return FromContext(ctx).With(fields...)
99+
}
100+
101+
func Sync(log *zap.Logger) error {
102+
if log == nil {
103+
return nil
104+
}
105+
err := log.Sync()
106+
if err == nil {
107+
return nil
108+
}
109+
if errors.Is(err, syscall.ENOTSUP) {
110+
return nil
111+
}
112+
if errors.Is(err, syscall.EINVAL) {
113+
return nil
114+
}
115+
return err
116+
}

0 commit comments

Comments
 (0)