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 }