refactor: разделение кода на пакеты и улучшение архитектуры
- Создана новая структура проекта с разделением на пакеты - Добавлены интерфейсы для всех сервисов (Git, Quartz, Files, Build) - Реализован Dependency Injection для сервисов - Добавлены middleware для логирования, Request ID и Response Time - Создан пакет конфигурации с валидацией - Улучшено логирование через интерфейс - Добавлены обработчики HTTP в отдельных пакетах - Создана структура для тестирования - Добавлены конфигурационные файлы и документация
This commit is contained in:
101
internal/config/config.go
Normal file
101
internal/config/config.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Config содержит все настройки приложения
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Paths PathsConfig `yaml:"paths"`
|
||||
Git GitConfig `yaml:"git"`
|
||||
}
|
||||
|
||||
// ServerConfig содержит настройки HTTP сервера
|
||||
type ServerConfig struct {
|
||||
Port string `yaml:"port"`
|
||||
Timeout int `yaml:"timeout"` // в секундах
|
||||
}
|
||||
|
||||
// PathsConfig содержит пути к директориям
|
||||
type PathsConfig struct {
|
||||
Obsidian string `yaml:"obsidian"`
|
||||
Quartz string `yaml:"quartz"`
|
||||
Public string `yaml:"public"`
|
||||
}
|
||||
|
||||
// GitConfig содержит настройки Git
|
||||
type GitConfig struct {
|
||||
Branch string `yaml:"branch"`
|
||||
Remote string `yaml:"remote"`
|
||||
}
|
||||
|
||||
// Load загружает конфигурацию из переменных окружения
|
||||
func Load() *Config {
|
||||
config := &Config{
|
||||
Server: ServerConfig{
|
||||
Port: getEnv("PORT", "3000"),
|
||||
Timeout: getEnvAsInt("SERVER_TIMEOUT", 30),
|
||||
},
|
||||
Paths: PathsConfig{
|
||||
Obsidian: getEnv("OBSIDIAN_PATH", "/obsidian"),
|
||||
Quartz: getEnv("QUARTZ_PATH", "/quartz"),
|
||||
Public: getEnv("PUBLIC_PATH", "/public"),
|
||||
},
|
||||
Git: GitConfig{
|
||||
Branch: getEnv("GIT_BRANCH", "main"),
|
||||
Remote: getEnv("GIT_REMOTE", "origin"),
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Validate проверяет корректность конфигурации
|
||||
func (c *Config) Validate() error {
|
||||
if c.Server.Port == "" {
|
||||
return errors.New("server port is required")
|
||||
}
|
||||
|
||||
if c.Paths.Obsidian == "" {
|
||||
return errors.New("obsidian path is required")
|
||||
}
|
||||
|
||||
if c.Paths.Quartz == "" {
|
||||
return errors.New("quartz path is required")
|
||||
}
|
||||
|
||||
if c.Paths.Public == "" {
|
||||
return errors.New("public path is required")
|
||||
}
|
||||
|
||||
if c.Git.Branch == "" {
|
||||
return errors.New("git branch is required")
|
||||
}
|
||||
|
||||
if c.Git.Remote == "" {
|
||||
return errors.New("git remote is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEnv получает значение переменной окружения или возвращает значение по умолчанию
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getEnvAsInt получает значение переменной окружения как int или возвращает значение по умолчанию
|
||||
func getEnvAsInt(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
37
internal/handlers/health.go
Normal file
37
internal/handlers/health.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go-webhook-server/pkg/logger"
|
||||
)
|
||||
|
||||
// HealthHandler обработчик для health check эндпоинта
|
||||
type HealthHandler struct {
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewHealthHandler создает новый экземпляр health обработчика
|
||||
func NewHealthHandler(log logger.Logger) *HealthHandler {
|
||||
return &HealthHandler{
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleHealth обрабатывает health check запрос
|
||||
func (h *HealthHandler) HandleHealth(c *gin.Context) {
|
||||
requestID := c.GetString("request_id")
|
||||
logger := h.logger.WithField("request_id", requestID)
|
||||
|
||||
logger.Debug("Health check request received")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"service": "go-webhook-server",
|
||||
"version": "1.0.0",
|
||||
"request_id": requestID,
|
||||
})
|
||||
}
|
||||
50
internal/handlers/webhook.go
Normal file
50
internal/handlers/webhook.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go-webhook-server/internal/services"
|
||||
"go-webhook-server/pkg/logger"
|
||||
)
|
||||
|
||||
// WebhookHandler обработчик для webhook эндпоинта
|
||||
type WebhookHandler struct {
|
||||
buildService services.BuildService
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewWebhookHandler создает новый экземпляр webhook обработчика
|
||||
func NewWebhookHandler(buildService services.BuildService, log logger.Logger) *WebhookHandler {
|
||||
return &WebhookHandler{
|
||||
buildService: buildService,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWebhook обрабатывает webhook запрос
|
||||
func (h *WebhookHandler) HandleWebhook(c *gin.Context) {
|
||||
requestID := c.GetString("request_id")
|
||||
logger := h.logger.WithField("request_id", requestID)
|
||||
|
||||
logger.Info("Webhook received, starting site rebuild...")
|
||||
|
||||
// Запускаем сборку в горутине для асинхронной обработки
|
||||
go func() {
|
||||
result := h.buildService.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",
|
||||
"request_id": requestID,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
67
internal/middleware/logging.go
Normal file
67
internal/middleware/logging.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go-webhook-server/pkg/logger"
|
||||
)
|
||||
|
||||
// LoggingMiddleware создает middleware для логирования HTTP запросов
|
||||
func LoggingMiddleware(log logger.Logger) gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
// Логируем детали запроса
|
||||
log.WithFields(map[string]interface{}{
|
||||
"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 ""
|
||||
})
|
||||
}
|
||||
|
||||
// RequestIDMiddleware добавляет уникальный ID к каждому запросу
|
||||
func RequestIDMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
|
||||
c.Set("request_id", requestID)
|
||||
c.Header("X-Request-ID", requestID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseTimeMiddleware добавляет время ответа в заголовки
|
||||
func ResponseTimeMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
c.Next()
|
||||
|
||||
duration := time.Since(start)
|
||||
c.Header("X-Response-Time", duration.String())
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID генерирует простой ID запроса
|
||||
func generateRequestID() string {
|
||||
return time.Now().Format("20060102150405") + "-" + randomString(6)
|
||||
}
|
||||
|
||||
// randomString генерирует случайную строку указанной длины
|
||||
func randomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
88
internal/services/build.go
Normal file
88
internal/services/build.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"go-webhook-server/internal/config"
|
||||
"go-webhook-server/pkg/logger"
|
||||
)
|
||||
|
||||
// BuildService интерфейс для сборки сайта
|
||||
type BuildService interface {
|
||||
BuildSite() BuildResult
|
||||
}
|
||||
|
||||
// buildServiceImpl реализация сервиса сборки
|
||||
type buildServiceImpl struct {
|
||||
config *config.Config
|
||||
logger logger.Logger
|
||||
gitService GitService
|
||||
quartzService QuartzService
|
||||
fileService FileService
|
||||
}
|
||||
|
||||
// NewBuildService создает новый экземпляр сервиса сборки
|
||||
func NewBuildService(
|
||||
cfg *config.Config,
|
||||
log logger.Logger,
|
||||
git GitService,
|
||||
quartz QuartzService,
|
||||
files FileService,
|
||||
) BuildService {
|
||||
return &buildServiceImpl{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
gitService: git,
|
||||
quartzService: quartz,
|
||||
fileService: files,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSite выполняет полную сборку сайта
|
||||
func (b *buildServiceImpl) BuildSite() BuildResult {
|
||||
b.logger.Info("Starting site build process...")
|
||||
|
||||
// Проверяем существование репозитория
|
||||
if !b.gitService.IsRepositoryExists() {
|
||||
b.logger.Error("Repository not found")
|
||||
return BuildResult{
|
||||
Success: false,
|
||||
Message: "Repository not found",
|
||||
Error: "Git repository does not exist at specified path",
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем репозиторий
|
||||
if err := b.gitService.UpdateRepository(); err != nil {
|
||||
b.logger.Errorf("Failed to update repository: %v", err)
|
||||
return BuildResult{
|
||||
Success: false,
|
||||
Message: "Failed to update repository",
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Собираем сайт с помощью Quartz
|
||||
if err := b.quartzService.BuildSite(); err != nil {
|
||||
b.logger.Errorf("Failed to build Quartz site: %v", err)
|
||||
return BuildResult{
|
||||
Success: false,
|
||||
Message: "Failed to build Quartz site",
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Копируем собранные файлы в публичную директорию
|
||||
if err := b.fileService.CopyBuiltSite(); err != nil {
|
||||
b.logger.Errorf("Failed to copy built site: %v", err)
|
||||
return BuildResult{
|
||||
Success: false,
|
||||
Message: "Failed to copy built site",
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
b.logger.Info("Site built successfully!")
|
||||
return BuildResult{
|
||||
Success: true,
|
||||
Message: "Site built successfully",
|
||||
}
|
||||
}
|
||||
78
internal/services/files.go
Normal file
78
internal/services/files.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"go-webhook-server/internal/config"
|
||||
"go-webhook-server/pkg/logger"
|
||||
)
|
||||
|
||||
// FileService интерфейс для файловых операций
|
||||
type FileService interface {
|
||||
CopyBuiltSite() error
|
||||
ClearPublicDirectory() error
|
||||
EnsurePublicDirectory() error
|
||||
}
|
||||
|
||||
// fileServiceImpl реализация файлового сервиса
|
||||
type fileServiceImpl struct {
|
||||
config *config.Config
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewFileService создает новый экземпляр файлового сервиса
|
||||
func NewFileService(cfg *config.Config, log logger.Logger) FileService {
|
||||
return &fileServiceImpl{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// CopyBuiltSite копирует собранные файлы в публичную директорию
|
||||
func (f *fileServiceImpl) CopyBuiltSite() error {
|
||||
f.logger.Info("Copying built site to public directory...")
|
||||
|
||||
// Очищаем публичную директорию
|
||||
if err := f.ClearPublicDirectory(); err != nil {
|
||||
return fmt.Errorf("failed to clear public directory: %w", err)
|
||||
}
|
||||
|
||||
// Создаем публичную директорию заново
|
||||
if err := f.EnsurePublicDirectory(); err != nil {
|
||||
return fmt.Errorf("failed to create public directory: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем существование директории с собранными файлами
|
||||
quartzPublicPath := filepath.Join(f.config.Paths.Quartz, "public")
|
||||
if _, err := os.Stat(quartzPublicPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("Quartz public directory not found: %s", quartzPublicPath)
|
||||
}
|
||||
|
||||
// Копируем файлы с помощью cp команды
|
||||
cmd := exec.Command("cp", "-r", quartzPublicPath+"/.", f.config.Paths.Public+"/")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy built site: %w", err)
|
||||
}
|
||||
|
||||
f.logger.Info("Site files copied successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearPublicDirectory очищает публичную директорию
|
||||
func (f *fileServiceImpl) ClearPublicDirectory() error {
|
||||
if err := os.RemoveAll(f.config.Paths.Public); err != nil {
|
||||
return fmt.Errorf("failed to clear public directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsurePublicDirectory создает публичную директорию если она не существует
|
||||
func (f *fileServiceImpl) EnsurePublicDirectory() error {
|
||||
if err := os.MkdirAll(f.config.Paths.Public, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create public directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
93
internal/services/git.go
Normal file
93
internal/services/git.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"go-webhook-server/internal/config"
|
||||
"go-webhook-server/pkg/logger"
|
||||
)
|
||||
|
||||
// GitService интерфейс для Git операций
|
||||
type GitService interface {
|
||||
UpdateRepository() error
|
||||
IsRepositoryExists() bool
|
||||
}
|
||||
|
||||
// gitServiceImpl реализация Git сервиса
|
||||
type gitServiceImpl struct {
|
||||
config *config.Config
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewGitService создает новый экземпляр Git сервиса
|
||||
func NewGitService(cfg *config.Config, log logger.Logger) GitService {
|
||||
return &gitServiceImpl{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateRepository обновляет репозиторий из удаленного источника
|
||||
func (g *gitServiceImpl) UpdateRepository() error {
|
||||
g.logger.Info("Updating repository...")
|
||||
|
||||
// Открываем репозиторий
|
||||
repo, err := git.PlainOpen(g.config.Paths.Obsidian)
|
||||
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)
|
||||
}
|
||||
|
||||
// Проверяем текущую ветку
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get HEAD: %w", err)
|
||||
}
|
||||
|
||||
g.logger.Infof("Current branch: %s", head.Name().Short())
|
||||
|
||||
// Выполняем git pull
|
||||
err = worktree.Pull(&git.PullOptions{
|
||||
RemoteName: g.config.Git.Remote,
|
||||
ReferenceName: plumbing.NewBranchReferenceName(g.config.Git.Branch),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if err == git.NoErrAlreadyUpToDate {
|
||||
g.logger.Info("Repository is already up to date")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to pull from remote: %w", err)
|
||||
}
|
||||
|
||||
g.logger.Info("Repository updated successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRepositoryExists проверяет существование Git репозитория
|
||||
func (g *gitServiceImpl) IsRepositoryExists() bool {
|
||||
gitPath := filepath.Join(g.config.Paths.Obsidian, ".git")
|
||||
|
||||
// Проверяем существование .git директории
|
||||
if _, err := filepath.Abs(gitPath); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Пытаемся открыть репозиторий
|
||||
repo, err := git.PlainOpen(g.config.Paths.Obsidian)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем что это действительно Git репозиторий
|
||||
_, err = repo.Head()
|
||||
return err == nil
|
||||
}
|
||||
85
internal/services/quartz.go
Normal file
85
internal/services/quartz.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"go-webhook-server/internal/config"
|
||||
"go-webhook-server/pkg/logger"
|
||||
)
|
||||
|
||||
// QuartzService интерфейс для сборки Quartz сайта
|
||||
type QuartzService interface {
|
||||
BuildSite() error
|
||||
InstallDependencies() error
|
||||
}
|
||||
|
||||
// quartzServiceImpl реализация Quartz сервиса
|
||||
type quartzServiceImpl struct {
|
||||
config *config.Config
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewQuartzService создает новый экземпляр Quartz сервиса
|
||||
func NewQuartzService(cfg *config.Config, log logger.Logger) QuartzService {
|
||||
return &quartzServiceImpl{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSite собирает сайт с помощью Quartz
|
||||
func (q *quartzServiceImpl) BuildSite() error {
|
||||
q.logger.Info("Building site with Quartz...")
|
||||
|
||||
// Проверяем существование package.json в директории Quartz
|
||||
packageJSONPath := filepath.Join(q.config.Paths.Quartz, "package.json")
|
||||
if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("package.json not found in Quartz directory: %s", q.config.Paths.Quartz)
|
||||
}
|
||||
|
||||
// Устанавливаем зависимости если необходимо
|
||||
if err := q.InstallDependencies(); err != nil {
|
||||
return fmt.Errorf("failed to install dependencies: %w", err)
|
||||
}
|
||||
|
||||
// Выполняем сборку Quartz
|
||||
cmd := exec.Command("npm", "run", "quartz", "build", "--", "-d", q.config.Paths.Obsidian)
|
||||
cmd.Dir = q.config.Paths.Quartz
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
q.logger.Info("Executing Quartz build command...")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to build Quartz site: %w", err)
|
||||
}
|
||||
|
||||
q.logger.Info("Quartz build completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallDependencies устанавливает npm зависимости
|
||||
func (q *quartzServiceImpl) InstallDependencies() error {
|
||||
// Проверяем существование node_modules
|
||||
nodeModulesPath := filepath.Join(q.config.Paths.Quartz, "node_modules")
|
||||
if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) {
|
||||
q.logger.Info("Installing npm dependencies...")
|
||||
|
||||
cmd := exec.Command("npm", "install")
|
||||
cmd.Dir = q.config.Paths.Quartz
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install npm dependencies: %w", err)
|
||||
}
|
||||
|
||||
q.logger.Info("npm dependencies installed successfully")
|
||||
} else {
|
||||
q.logger.Debug("node_modules already exists, skipping npm install")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
28
internal/services/types.go
Normal file
28
internal/services/types.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package services
|
||||
|
||||
// BuildResult результат сборки сайта
|
||||
type BuildResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// BuildStatus статус сборки
|
||||
type BuildStatus string
|
||||
|
||||
const (
|
||||
BuildStatusPending BuildStatus = "pending"
|
||||
BuildStatusRunning BuildStatus = "running"
|
||||
BuildStatusCompleted BuildStatus = "completed"
|
||||
BuildStatusFailed BuildStatus = "failed"
|
||||
)
|
||||
|
||||
// BuildInfo детальная информация о сборке
|
||||
type BuildInfo struct {
|
||||
ID string `json:"id"`
|
||||
Status BuildStatus `json:"status"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time,omitempty"`
|
||||
Duration string `json:"duration,omitempty"`
|
||||
Result BuildResult `json:"result,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user