179 lines
4.3 KiB
Go
179 lines
4.3 KiB
Go
package builder
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"ci/internal/model"
|
|
)
|
|
|
|
// LineCallback is called for each line of build output.
|
|
type LineCallback func(line string)
|
|
|
|
// BuildResult holds the outcome of a build.
|
|
type BuildResult struct {
|
|
Success bool
|
|
LogPath string
|
|
CommitHash string
|
|
Output string
|
|
}
|
|
|
|
// Service handles build operations.
|
|
type Service struct{}
|
|
|
|
// New creates a new builder Service.
|
|
func New() *Service {
|
|
return &Service{}
|
|
}
|
|
|
|
// Execute runs a build for the given project, streaming output via callback.
|
|
func (s *Service) Execute(ctx context.Context, project *model.Project, workDir, logsDir, commitHash string, onLine LineCallback) (*BuildResult, error) {
|
|
// Create log file
|
|
logFileName := fmt.Sprintf("build-%d.log", time.Now().UnixMilli())
|
|
logPath := filepath.Join(logsDir, logFileName)
|
|
logFile, err := os.Create(logPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create log file: %w", err)
|
|
}
|
|
defer logFile.Close()
|
|
|
|
writer := io.MultiWriter(logFile, &callbackWriter{fn: onLine})
|
|
|
|
// Resolve the build script
|
|
script := strings.TrimSpace(project.BuildScript)
|
|
if script == "" {
|
|
// Default: try go build
|
|
script = "go build -o app ."
|
|
}
|
|
|
|
// Determine shell for the build script
|
|
var cmd *exec.Cmd
|
|
if isShellScript(script) {
|
|
// Multi-line script or complex — write to temp file and execute
|
|
scriptPath := filepath.Join(workDir, ".build-script.sh")
|
|
if err := os.WriteFile(scriptPath, []byte("#!/bin/sh\nset -e\n"+script), 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to write build script: %w", err)
|
|
}
|
|
cmd = exec.CommandContext(ctx, "sh", scriptPath)
|
|
} else {
|
|
cmd = exec.CommandContext(ctx, "sh", "-c", script)
|
|
}
|
|
|
|
cmd.Dir = workDir
|
|
cmd.Env = os.Environ()
|
|
cmd.Stdout = writer
|
|
cmd.Stderr = writer
|
|
|
|
fmt.Fprintf(writer, "=== Build started at %s ===\n", time.Now().Format(time.RFC3339))
|
|
fmt.Fprintf(writer, "Working directory: %s\n", workDir)
|
|
fmt.Fprintf(writer, "Build script: %s\n", script)
|
|
fmt.Fprintf(writer, "Commit: %s\n\n", commitHash)
|
|
|
|
startTime := time.Now()
|
|
err = cmd.Run()
|
|
duration := time.Since(startTime)
|
|
|
|
result := &BuildResult{
|
|
LogPath: logPath,
|
|
CommitHash: commitHash,
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(writer, "\n=== Build FAILED after %s ===\n", duration.Round(time.Second))
|
|
fmt.Fprintf(writer, "Error: %v\n", err)
|
|
result.Success = false
|
|
result.Output = fmt.Sprintf("Build failed: %v", err)
|
|
} else {
|
|
fmt.Fprintf(writer, "\n=== Build SUCCESS after %s ===\n", duration.Round(time.Second))
|
|
result.Success = true
|
|
result.Output = fmt.Sprintf("Build succeeded in %s", duration.Round(time.Second))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ReadLog reads a build log file and returns its contents.
|
|
func (s *Service) ReadLog(logPath string) (string, error) {
|
|
data, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// ReadLogTail reads the last n lines from a log file.
|
|
func (s *Service) ReadLogTail(logPath string, lines int) ([]string, error) {
|
|
file, err := os.Open(logPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var result []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
result = append(result, scanner.Text())
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(result) > lines {
|
|
result = result[len(result)-lines:]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// isShellScript checks if the script looks multi-line or complex enough to need a temp file.
|
|
func isShellScript(script string) bool {
|
|
return strings.Contains(script, "\n") || len(script) > 200
|
|
}
|
|
|
|
// callbackWriter implements io.Writer, calling a function for each line written.
|
|
type callbackWriter struct {
|
|
fn LineCallback
|
|
buf []byte
|
|
}
|
|
|
|
func (w *callbackWriter) Write(p []byte) (n int, err error) {
|
|
if w.fn != nil {
|
|
w.buf = append(w.buf, p...)
|
|
for {
|
|
idx := indexOfByte(w.buf, '\n')
|
|
if idx < 0 {
|
|
break
|
|
}
|
|
line := string(w.buf[:idx])
|
|
if strings.HasSuffix(line, "\r") {
|
|
line = line[:len(line)-1]
|
|
}
|
|
w.fn(line)
|
|
w.buf = w.buf[idx+1:]
|
|
}
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (w *callbackWriter) flush() {
|
|
if w.fn != nil && len(w.buf) > 0 {
|
|
w.fn(string(w.buf))
|
|
w.buf = nil
|
|
}
|
|
}
|
|
|
|
func indexOfByte(data []byte, b byte) int {
|
|
for i, c := range data {
|
|
if c == b {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|