Jaypore CI

> Jaypore CI: Minimal, Offline, Local CI system.
Log | Files | Refs | README

run.go (4858B)


      1 package jci
      2 
      3 import (
      4 	"fmt"
      5 	"os"
      6 	"os/exec"
      7 	"path/filepath"
      8 	"time"
      9 )
     10 
     11 // Run executes CI for the current commit
     12 func Run(args []string) error {
     13 	// Get current commit
     14 	commit, err := GetCurrentCommit()
     15 	if err != nil {
     16 		return fmt.Errorf("failed to get current commit: %w", err)
     17 	}
     18 
     19 	fmt.Printf("Running CI for commit %s\n", commit[:12])
     20 
     21 	// Check if CI already ran for this commit
     22 	ref := "refs/jci/" + commit
     23 	if RefExists(ref) {
     24 		fmt.Printf("CI results already exist for %s\n", commit[:12])
     25 		return nil
     26 	}
     27 
     28 	repoRoot, err := GetRepoRoot()
     29 	if err != nil {
     30 		return fmt.Errorf("failed to get repo root: %w", err)
     31 	}
     32 
     33 	// Check if .jci/run.sh exists
     34 	runScript := filepath.Join(repoRoot, ".jci", "run.sh")
     35 	if _, err := os.Stat(runScript); os.IsNotExist(err) {
     36 		return fmt.Errorf(".jci/run.sh not found - create it to define your CI pipeline")
     37 	}
     38 
     39 	// Create output directory .jci/<commit>
     40 	outputDir := filepath.Join(repoRoot, ".jci", commit)
     41 	if err := os.MkdirAll(outputDir, 0755); err != nil {
     42 		return fmt.Errorf("failed to create output dir: %w", err)
     43 	}
     44 
     45 	// Run CI
     46 	err = runCI(repoRoot, outputDir, commit)
     47 	// Continue even if CI fails - we still want to store the results
     48 
     49 	// Generate index.html with results
     50 	if err := generateIndexHTML(outputDir, commit, err); err != nil {
     51 		fmt.Printf("Warning: failed to generate index.html: %v\n", err)
     52 	}
     53 
     54 	// Store results in git
     55 	msg := fmt.Sprintf("CI results for %s", commit[:12])
     56 	if storeErr := StoreTree(outputDir, commit, msg); storeErr != nil {
     57 		return fmt.Errorf("failed to store CI results: %w", storeErr)
     58 	}
     59 
     60 	// Clean up the output directory after storing in git
     61 	os.RemoveAll(outputDir)
     62 
     63 	fmt.Printf("CI results stored at %s\n", ref)
     64 	if err != nil {
     65 		return fmt.Errorf("CI failed (results stored): %w", err)
     66 	}
     67 	return nil
     68 }
     69 
     70 // runCI executes .jci/run.sh and captures output
     71 func runCI(repoRoot string, outputDir string, commit string) error {
     72 	runScript := filepath.Join(repoRoot, ".jci", "run.sh")
     73 	outputFile := filepath.Join(outputDir, "run.output.txt")
     74 
     75 	// Create output file
     76 	f, err := os.Create(outputFile)
     77 	if err != nil {
     78 		return fmt.Errorf("failed to create output file: %w", err)
     79 	}
     80 	defer f.Close()
     81 
     82 	// Write header
     83 	fmt.Fprintf(f, "=== JCI Run Output ===\n")
     84 	fmt.Fprintf(f, "Commit: %s\n", commit)
     85 	fmt.Fprintf(f, "Started: %s\n", time.Now().Format(time.RFC3339))
     86 	fmt.Fprintf(f, "======================\n\n")
     87 
     88 	// Run the script
     89 	cmd := exec.Command("bash", runScript)
     90 	cmd.Dir = outputDir
     91 	cmd.Env = append(os.Environ(),
     92 		"JCI_COMMIT="+commit,
     93 		"JCI_REPO_ROOT="+repoRoot,
     94 		"JCI_OUTPUT_DIR="+outputDir,
     95 	)
     96 
     97 	// Capture both stdout and stderr to the same file
     98 	cmd.Stdout = f
     99 	cmd.Stderr = f
    100 
    101 	fmt.Printf("Executing .jci/run.sh...\n")
    102 	startTime := time.Now()
    103 	runErr := cmd.Run()
    104 	duration := time.Since(startTime)
    105 
    106 	// Write footer
    107 	fmt.Fprintf(f, "\n======================\n")
    108 	fmt.Fprintf(f, "Finished: %s\n", time.Now().Format(time.RFC3339))
    109 	fmt.Fprintf(f, "Duration: %s\n", duration.Round(time.Millisecond))
    110 	if runErr != nil {
    111 		fmt.Fprintf(f, "Exit: FAILED - %v\n", runErr)
    112 	} else {
    113 		fmt.Fprintf(f, "Exit: SUCCESS\n")
    114 	}
    115 
    116 	return runErr
    117 }
    118 
    119 // generateIndexHTML creates a minimal index.html for standalone viewing
    120 // The main UI is served by the web server; this is for direct file access
    121 func generateIndexHTML(outputDir string, commit string, ciErr error) error {
    122 	commitMsg, _ := git("log", "-1", "--format=%s", commit)
    123 
    124 	status := "success"
    125 	statusIcon := "✓ PASSED"
    126 	if ciErr != nil {
    127 		status = "failed"
    128 		statusIcon = "✗ FAILED"
    129 	}
    130 
    131 	// Read output for standalone view
    132 	outputContent := ""
    133 	outputFile := filepath.Join(outputDir, "run.output.txt")
    134 	if data, err := os.ReadFile(outputFile); err == nil {
    135 		outputContent = string(data)
    136 	}
    137 
    138 	html := fmt.Sprintf(`<!DOCTYPE html>
    139 <html>
    140 <head>
    141     <meta charset="utf-8">
    142     <title>%s %s</title>
    143     <style>
    144         body { font-family: monospace; font-size: 12px; background: #1a1a1a; color: #e0e0e0; padding: 8px; }
    145         .header { margin-bottom: 8px; }
    146         .%s { color: %s; font-weight: bold; }
    147         pre { white-space: pre-wrap; }
    148     </style>
    149 </head>
    150 <body>
    151     <div class="header">
    152         <span class="%s">%s</span> %s %s
    153     </div>
    154     <pre>%s</pre>
    155 </body>
    156 </html>
    157 `, commit[:7], escapeHTML(commitMsg),
    158 		status, map[string]string{"success": "#3fb950", "failed": "#f85149"}[status],
    159 		status, statusIcon, commit[:7], escapeHTML(commitMsg),
    160 		escapeHTML(outputContent))
    161 
    162 	indexPath := filepath.Join(outputDir, "index.html")
    163 	return os.WriteFile(indexPath, []byte(html), 0644)
    164 }
    165 
    166 func escapeHTML(s string) string {
    167 	replacer := map[rune]string{
    168 		'<':  "&lt;",
    169 		'>':  "&gt;",
    170 		'&':  "&amp;",
    171 		'"':  "&quot;",
    172 		'\'': "&#39;",
    173 	}
    174 	result := ""
    175 	for _, r := range s {
    176 		if rep, ok := replacer[r]; ok {
    177 			result += rep
    178 		} else {
    179 			result += string(r)
    180 		}
    181 	}
    182 	return result
    183 }