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 '<': "<", 169 '>': ">", 170 '&': "&", 171 '"': """, 172 '\'': "'", 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 }