fix: restore main.go to correct architecture (was urfave/cli corruption)

This commit is contained in:
2026-06-29 20:24:18 +00:00
parent d33b1abd7e
commit c8b2dbe9dd
+100 -126
View File
@@ -2,170 +2,144 @@ package main
import ( import (
"context" "context"
"flag"
"net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time"
"github.com/rs/zerolog" "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/event"
"github.com/vincentc-afk/gitea-notification-hub/internal/identity" "github.com/vincentc-afk/gitea-notification-hub/internal/identity"
"github.com/vincentc-afk/gitea-notification-hub/internal/identity/cache" "github.com/vincentc-afk/gitea-notification-hub/internal/identity/cache"
identitygitea "github.com/vincentc-afk/gitea-notification-hub/internal/identity/gitea" "github.com/vincentc-afk/gitea-notification-hub/internal/identity/gitea"
identityslack "github.com/vincentc-afk/gitea-notification-hub/internal/identity/slack" slacknotifier "github.com/vincentc-afk/gitea-notification-hub/internal/notifier/slack"
"github.com/vincentc-afk/gitea-notification-hub/internal/slack" "github.com/vincentc-afk/gitea-notification-hub/internal/server"
"github.com/vincentc-afk/gitea-notification-hub/internal/storage"
"github.com/vincentc-afk/gitea-notification-hub/internal/storage/sqlite" "github.com/vincentc-afk/gitea-notification-hub/internal/storage/sqlite"
"github.com/vincentc-afk/gitea-notification-hub/internal/webhook" "github.com/vincentc-afk/gitea-notification-hub/internal/webhook"
) )
func main() { func main() {
app := &cli.App{ // Parse flags
Name: "gitea-notification-hub", configPath := flag.String("config", "config/config.yaml", "path to config file")
Usage: "Gitea webhook event processor that routes notifications to Slack", debug := flag.Bool("debug", false, "enable debug logging")
Flags: []cli.Flag{ flag.Parse()
&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,
}
if err := app.Run(os.Args); err != nil { // Setup logger
// Use default logger since setup might have failed logger := setupLogger(*debug)
fallback := zerolog.New(os.Stderr).With().Timestamp().Logger()
fallback.Fatal().Err(err).Msg("application failed to start")
}
}
func run(ctx *cli.Context) error { // Load configuration
// --- Logger setup --- cfg, err := config.Load(*configPath)
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))
if err != nil { 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). ctx := context.Background()
With().
Timestamp().
Logger().
Level(level)
logger.Info().Str("level", level.String()).Msg("logger initialized") // Initialize storage
repo, err := sqlite.New(cfg.Database.DSN)
// --- Create storage ---
repo, err := sqlite.NewSQLiteRepository("data/notifications.db", logger)
if err != nil { if err != nil {
logger.Fatal().Err(err).Msg("failed to initialize storage") logger.Fatal().Err(err).Msg("failed to initialize database")
} }
defer repo.Close() defer repo.Close()
// --- Create identity resolver --- // Run migrations
giteaEmailLookup := identitygitea.NewEmailLookup(logger) if err := repo.Migrate(ctx); err != nil {
slackLookup := identityslack.NewLookup(logger) logger.Fatal().Err(err).Msg("failed to run migrations")
}
logger.Info().Msg("database initialized")
// Parse manual user mappings // Initialize Gitea API provider for email lookup
manualMappingsRaw := os.Getenv("MANUAL_USER_MAPPINGS") var emailLookup identity.EmailLookup
manualMappings := cache.ParseManualMappings(manualMappingsRaw) 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 { if len(manualMappings) > 0 {
logger.Info().Int("count", len(manualMappings)).Msg("loaded manual user mappings") logger.Info().Int("count", len(manualMappings)).Msg("manual user mappings configured")
for username := range manualMappings {
logger.Debug().Str("username", username).Msg("manual mapping registered")
} }
for giteaUser, slackID := range manualMappings {
logger.Debug().
Str("gitea_user", giteaUser).
Str("slack_id", slackID).
Msg("manual mapping entry")
} }
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 --- // Create HTTP server
slackClient, err := slack.NewClient(logger) srv := server.New(cfg, logger)
if err != nil {
logger.Fatal().Err(err).Msg("failed to create Slack client")
}
// --- Create event processor --- // Register webhook handler
processor := event.NewProcessor(resolver, slackClient, logger) webhookHandler := webhook.NewHandler(cfg.Server.WebhookSecret, processor, logger)
srv.Router().Post("/webhook", webhookHandler.ServeHTTP)
logger.Info().Msg("webhook handler registered at POST /webhook")
// --- Create webhook handler --- // Graceful shutdown
handler := webhook.NewHandler(processor, logger) go func() {
// --- 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) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh <-sigCh
logger.Info().Str("signal", sig.String()).Msg("received shutdown signal")
// --- Graceful shutdown --- logger.Info().Msg("received shutdown signal")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10_000)
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error().Err(err).Msg("server shutdown error") logger.Error().Err(err).Msg("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)
go func() {
logger.Info().Int("port", port).Msg("starting HTTP server")
if err := srv.Start(); err != nil {
logger.Fatal().Err(err).Msg("server 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
} }