From c8b2dbe9dda0b2e124499e96a0249dc20e67bfdc Mon Sep 17 00:00:00 2001 From: Dimitris Karvounis <1+jimkarvo@jksoftware.gr> Date: Mon, 29 Jun 2026 20:24:18 +0000 Subject: [PATCH] fix: restore main.go to correct architecture (was urfave/cli corruption) --- cmd/server/main.go | 236 ++++++++++++++++++++------------------------- 1 file changed, 105 insertions(+), 131 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 33c40f3..4a6a23a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,170 +2,144 @@ package main import ( "context" + "flag" + "net/http" "os" "os/signal" - "strings" "syscall" + "time" "github.com/rs/zerolog" - "github.com/urfave/cli/v2" + "github.com/vincentc-afk/gitea-notification-hub/internal/config" "github.com/vincentc-afk/gitea-notification-hub/internal/event" "github.com/vincentc-afk/gitea-notification-hub/internal/identity" "github.com/vincentc-afk/gitea-notification-hub/internal/identity/cache" - identitygitea "github.com/vincentc-afk/gitea-notification-hub/internal/identity/gitea" - identityslack "github.com/vincentc-afk/gitea-notification-hub/internal/identity/slack" - "github.com/vincentc-afk/gitea-notification-hub/internal/slack" - "github.com/vincentc-afk/gitea-notification-hub/internal/storage" + "github.com/vincentc-afk/gitea-notification-hub/internal/identity/gitea" + slacknotifier "github.com/vincentc-afk/gitea-notification-hub/internal/notifier/slack" + "github.com/vincentc-afk/gitea-notification-hub/internal/server" "github.com/vincentc-afk/gitea-notification-hub/internal/storage/sqlite" "github.com/vincentc-afk/gitea-notification-hub/internal/webhook" ) func main() { - app := &cli.App{ - Name: "gitea-notification-hub", - Usage: "Gitea webhook event processor that routes notifications to Slack", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Usage: "Path to config file", - Value: "config.yaml", - }, - &cli.BoolFlag{ - Name: "debug", - Aliases: []string{"d"}, - Usage: "Enable debug logging", - Value: false, - }, - &cli.IntFlag{ - Name: "port", - Usage: "HTTP server port", - Value: 8080, - }, - }, - Action: run, - } + // Parse flags + configPath := flag.String("config", "config/config.yaml", "path to config file") + debug := flag.Bool("debug", false, "enable debug logging") + flag.Parse() - if err := app.Run(os.Args); err != nil { - // Use default logger since setup might have failed - fallback := zerolog.New(os.Stderr).With().Timestamp().Logger() - fallback.Fatal().Err(err).Msg("application failed to start") - } -} + // Setup logger + logger := setupLogger(*debug) -func run(ctx *cli.Context) error { - // --- Logger setup --- - logLevel := ctx.String("log-level") - if logLevel == "" { - // Check LOG_LEVEL env var (overrides --debug flag) - logLevel = os.Getenv("LOG_LEVEL") - } - if logLevel == "" && ctx.Bool("debug") { - logLevel = "debug" - } - if logLevel == "" { - logLevel = "info" - } - - level, err := zerolog.ParseLevel(strings.ToLower(logLevel)) + // Load configuration + cfg, err := config.Load(*configPath) if err != nil { - level = zerolog.InfoLevel + logger.Fatal().Err(err).Msg("failed to load configuration") } + logger.Info().Str("path", *configPath).Msg("configuration loaded") - logger := zerolog.New(os.Stdout). - With(). - Timestamp(). - Logger(). - Level(level) + ctx := context.Background() - logger.Info().Str("level", level.String()).Msg("logger initialized") - - // --- Create storage --- - repo, err := sqlite.NewSQLiteRepository("data/notifications.db", logger) + // Initialize storage + repo, err := sqlite.New(cfg.Database.DSN) if err != nil { - logger.Fatal().Err(err).Msg("failed to initialize storage") + logger.Fatal().Err(err).Msg("failed to initialize database") } defer repo.Close() - // --- Create identity resolver --- - giteaEmailLookup := identitygitea.NewEmailLookup(logger) - slackLookup := identityslack.NewLookup(logger) + // Run migrations + if err := repo.Migrate(ctx); err != nil { + logger.Fatal().Err(err).Msg("failed to run migrations") + } + logger.Info().Msg("database initialized") - // Parse manual user mappings - manualMappingsRaw := os.Getenv("MANUAL_USER_MAPPINGS") - manualMappings := cache.ParseManualMappings(manualMappingsRaw) + // Initialize Gitea API provider for email lookup + var emailLookup identity.EmailLookup + if cfg.Identity.Gitea.URL != "" { + giteaProvider := gitea.New(&cfg.Identity.Gitea, logger) + emailLookup = giteaProvider + logger.Info().Str("url", cfg.Identity.Gitea.URL).Msg("Gitea identity provider initialized") + } else { + logger.Warn().Msg("Gitea URL not configured, email lookup disabled") + } + + // Initialize Slack notifier + slackClient := slacknotifier.New(&cfg.Notification.Slack, logger) + logger.Info().Msg("Slack notifier initialized") + + // Initialize cached identity resolver with optional manual mappings + manualMappings := cache.ParseManualMappings(os.Getenv("MANUAL_USER_MAPPINGS")) if len(manualMappings) > 0 { - logger.Info().Int("count", len(manualMappings)).Msg("loaded manual user mappings") - } - for giteaUser, slackID := range manualMappings { - logger.Debug(). - Str("gitea_user", giteaUser). - Str("slack_id", slackID). - Msg("manual mapping entry") + logger.Info().Int("count", len(manualMappings)).Msg("manual user mappings configured") + for username := range manualMappings { + logger.Debug().Str("username", username).Msg("manual mapping registered") + } } + resolver := cache.NewCachedResolver(repo, emailLookup, slackClient, manualMappings, logger) + logger.Info().Msg("identity resolver initialized") - resolver := cache.NewCachedResolver(repo, giteaEmailLookup, slackLookup, manualMappings, logger) + // Create processor adapter that implements webhook.EventHandler + processor := event.NewProcessor(cfg, &resolverAdapter{resolver}, slackClient, logger) + logger.Info().Msg("event processor initialized") - // --- Create Slack client --- - slackClient, err := slack.NewClient(logger) - if err != nil { - logger.Fatal().Err(err).Msg("failed to create Slack client") - } + // Create HTTP server + srv := server.New(cfg, logger) - // --- Create event processor --- - processor := event.NewProcessor(resolver, slackClient, logger) - - // --- Create webhook handler --- - handler := webhook.NewHandler(processor, logger) - - // --- Setup HTTP routes --- - mux := setupRouter(handler, logger) - - // --- Start HTTP server --- - port := ctx.Int("port") - srv := startServer(mux, port, logger) - - // --- Wait for shutdown signal --- - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - sig := <-sigCh - logger.Info().Str("signal", sig.String()).Msg("received shutdown signal") - - // --- Graceful shutdown --- - shutdownCtx, cancel := context.WithTimeout(context.Background(), 10_000) - defer cancel() - if err := srv.Shutdown(shutdownCtx); err != nil { - logger.Error().Err(err).Msg("server shutdown error") - } - - logger.Info().Msg("server stopped") - return nil -} - -// setupRouter configures the HTTP router with all routes -func setupRouter(handler *webhook.Handler, logger zerolog.Logger) *webhook.Router { - router := webhook.NewRouter(logger) - - // Health check - router.Get("/health", handler.HealthCheck) - - // Webhook endpoint - router.Post("/webhook", handler.HandleWebhook) - - return router -} - -// startServer starts the HTTP server on the given port -func startServer(router *webhook.Router, port int, logger zerolog.Logger) *webhook.Server { - srv := webhook.NewServer(router, port, logger) + // Register webhook handler + webhookHandler := webhook.NewHandler(cfg.Server.WebhookSecret, processor, logger) + srv.Router().Post("/webhook", webhookHandler.ServeHTTP) + logger.Info().Msg("webhook handler registered at POST /webhook") + // Graceful shutdown go func() { - logger.Info().Int("port", port).Msg("starting HTTP server") - if err := srv.Start(); err != nil { - logger.Fatal().Err(err).Msg("server error") + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh + + logger.Info().Msg("received shutdown signal") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + logger.Error().Err(err).Msg("shutdown error") } }() - return srv + // Start server + logger.Info().Int("port", cfg.Server.Port).Msg("starting server") + if err := srv.Start(); err != nil && err != http.ErrServerClosed { + logger.Fatal().Err(err).Msg("server error") + } +} + +func setupLogger(debug bool) zerolog.Logger { + zerolog.TimeFieldFormat = time.RFC3339 + + level := zerolog.InfoLevel + if debug { + level = zerolog.DebugLevel + } + + return zerolog.New(zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: "15:04:05", + }).Level(level).With().Timestamp().Caller().Logger() +} + +// resolverAdapter adapts cache.CachedResolver to event.IdentityResolver +type resolverAdapter struct { + resolver *cache.CachedResolver +} + +func (a *resolverAdapter) Resolve(ctx context.Context, user event.User) (*event.ResolvedIdentity, error) { + identity, err := a.resolver.Resolve(ctx, user) + if err != nil { + return nil, err + } + return &event.ResolvedIdentity{ + Email: identity.Email, + SlackID: identity.SlackID, + SlackName: identity.SlackName, + }, nil }