Initial commit: Go webhook server for Quartz site rebuild

This commit is contained in:
Andrey Epifancev
2025-08-11 19:26:57 +04:00
commit 1b340362be
8 changed files with 935 additions and 0 deletions

292
main.go Normal file
View File

@@ -0,0 +1,292 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
type Config struct {
Port string
ObsidianPath string
QuartzPath string
PublicPath string
GitBranch string
GitRemote string
}
type BuildResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
}
var (
config Config
logger *logrus.Logger
)
func init() {
// Инициализация логгера
logger = logrus.New()
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
logger.SetLevel(logrus.InfoLevel)
// Загрузка конфигурации
config = Config{
Port: getEnv("PORT", "3000"),
ObsidianPath: getEnv("OBSIDIAN_PATH", "/obsidian"),
QuartzPath: getEnv("QUARTZ_PATH", "/quartz"),
PublicPath: getEnv("PUBLIC_PATH", "/public"),
GitBranch: getEnv("GIT_BRANCH", "main"),
GitRemote: getEnv("GIT_REMOTE", "origin"),
}
logger.Infof("Configuration loaded: Port=%s, ObsidianPath=%s, QuartzPath=%s, PublicPath=%s, GitBranch=%s, GitRemote=%s",
config.Port, config.ObsidianPath, config.QuartzPath, config.PublicPath, config.GitBranch, config.GitRemote)
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func buildSite() BuildResult {
logger.Info("Starting site build process...")
// Проверяем существование репозитория
if _, err := os.Stat(filepath.Join(config.ObsidianPath, ".git")); os.IsNotExist(err) {
logger.Error("Repository not found")
return BuildResult{
Success: false,
Message: "Repository not found",
Error: "Git repository does not exist at specified path",
}
}
// Обновляем репозиторий
if err := updateRepository(); err != nil {
logger.Errorf("Failed to update repository: %v", err)
return BuildResult{
Success: false,
Message: "Failed to update repository",
Error: err.Error(),
}
}
// Собираем сайт с помощью Quartz
if err := buildQuartzSite(); err != nil {
logger.Errorf("Failed to build Quartz site: %v", err)
return BuildResult{
Success: false,
Message: "Failed to build Quartz site",
Error: err.Error(),
}
}
// Копируем собранные файлы в публичную директорию
if err := copyBuiltSite(); err != nil {
logger.Errorf("Failed to copy built site: %v", err)
return BuildResult{
Success: false,
Message: "Failed to copy built site",
Error: err.Error(),
}
}
logger.Info("Site built successfully!")
return BuildResult{
Success: true,
Message: "Site built successfully",
}
}
func updateRepository() error {
logger.Info("Updating repository...")
// Открываем репозиторий
repo, err := git.PlainOpen(config.ObsidianPath)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}
// Получаем worktree
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
// Выполняем git pull
err = worktree.Pull(&git.PullOptions{
RemoteName: config.GitRemote,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to pull from remote: %w", err)
}
if err == git.NoErrAlreadyUpToDate {
logger.Info("Repository is already up to date")
} else {
logger.Info("Repository updated successfully")
}
return nil
}
func buildQuartzSite() error {
logger.Info("Building site with Quartz...")
// Проверяем существование package.json в директории Quartz
packageJSONPath := filepath.Join(config.QuartzPath, "package.json")
if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) {
return fmt.Errorf("package.json not found in Quartz directory")
}
// Выполняем npm install если node_modules не существует
nodeModulesPath := filepath.Join(config.QuartzPath, "node_modules")
if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) {
logger.Info("Installing npm dependencies...")
cmd := exec.Command("npm", "install")
cmd.Dir = config.QuartzPath
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to install npm dependencies: %w", err)
}
}
// Выполняем сборку Quartz
cmd := exec.Command("npm", "run", "quartz", "build", "--", "-d", config.ObsidianPath)
cmd.Dir = config.QuartzPath
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Quartz site: %w", err)
}
logger.Info("Quartz build completed successfully")
return nil
}
func copyBuiltSite() error {
logger.Info("Copying built site to public directory...")
// Очищаем публичную директорию
if err := os.RemoveAll(config.PublicPath); err != nil {
return fmt.Errorf("failed to clear public directory: %w", err)
}
// Создаем публичную директорию заново
if err := os.MkdirAll(config.PublicPath, 0755); err != nil {
return fmt.Errorf("failed to create public directory: %w", err)
}
// Копируем собранные файлы
quartzPublicPath := filepath.Join(config.QuartzPath, "public")
if _, err := os.Stat(quartzPublicPath); os.IsNotExist(err) {
return fmt.Errorf("Quartz public directory not found")
}
// Используем cp команду для копирования
cmd := exec.Command("cp", "-r", quartzPublicPath+"/.", config.PublicPath+"/")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to copy built site: %w", err)
}
logger.Info("Site files copied successfully")
return nil
}
func webhookHandler(c *gin.Context) {
logger.Info("Webhook received, starting site rebuild...")
// Запускаем сборку в горутине для асинхронной обработки
go func() {
result := buildSite()
if result.Success {
logger.Info("Webhook build completed successfully")
} else {
logger.Errorf("Webhook build failed: %s", result.Error)
}
}()
// Сразу возвращаем ответ
c.JSON(http.StatusAccepted, gin.H{
"status": "accepted",
"message": "Build process started",
})
}
func healthHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"service": "go-webhook-server",
"version": "1.0.0",
})
}
func main() {
// Настраиваем Gin
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// Добавляем middleware для логирования
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
logger.WithFields(logrus.Fields{
"status": param.StatusCode,
"latency": param.Latency,
"client_ip": param.ClientIP,
"method": param.Method,
"path": param.Path,
"user_agent": param.Request.UserAgent(),
}).Info("HTTP Request")
return ""
}))
// Роуты
router.POST("/webhook", webhookHandler)
router.GET("/health", healthHandler)
// Запуск сервера
addr := ":" + config.Port
logger.Infof("Starting webhook server on port %s", config.Port)
server := &http.Server{
Addr: addr,
Handler: router,
}
// Graceful shutdown
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Failed to start server: %v", err)
}
}()
// Ожидаем сигнал для graceful shutdown
<-make(chan os.Signal, 1)
logger.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Errorf("Server forced to shutdown: %v", err)
}
logger.Info("Server exited")
}