Commit 9aad8f0

Eric Bower  ·  2026-05-06 21:02:52 -0400 EDT
parent 643168c
style: use emoji based logs
1 files changed,  +61, -36
M main.go
+61, -36
  1@@ -42,6 +42,7 @@ type Cfg struct {
  2 	NewWorkspace        WorkspaceFactory
  3 	StatusOutput        io.Writer // where status JSONL is written (default: os.Stdout)
  4 	StatusFilter        string    // "terminal" (default) or "all"
  5+	HumanOutput         bool      // human-readable output instead of JSONL / slog
  6 }
  7 
  8 type Event struct {
  9@@ -65,7 +66,9 @@ func NewCfg() *Cfg {
 10 	flag.StringVar(&logLevel, "log-level", "info", "log level: debug, info, warn, error")
 11 	flag.BoolVar(&structured, "structured", false, "use structured key=value log output")
 12 	var statusFilter string
 13+	var human bool
 14 	flag.StringVar(&statusFilter, "status-filter", "terminal", "status output filter: terminal (default) or all")
 15+	flag.BoolVar(&human, "human", false, "human-readable output (default: JSONL / slog)")
 16 	flag.Parse()
 17 
 18 	logger := newLogger("ci", logLevel, structured)
 19@@ -81,6 +84,7 @@ func NewCfg() *Cfg {
 20 		Event:               event,
 21 		MonitorInterval:     monitorInterval,
 22 		StatusFilter:        statusFilter,
 23+		HumanOutput:         human,
 24 	}
 25 }
 26 
 27@@ -182,7 +186,8 @@ FLAGS
 28   -monitor-interval <dur>  Interval for monitoring zmx sessions (default: 5s)
 29   -log-level <level>   Log level: debug, info, warn, error (default: info)
 30   -structured          Use structured key=value log output
 31-  -status-filter <filter>  Status output filter: terminal (default) or all`)
 32+  -status-filter <filter>  Status output filter: terminal (default) or all
 33+  -human                   Human-readable output instead of JSONL / slog`)
 34 }
 35 
 36 func RunRunner(cfg *Cfg) error {
 37@@ -219,8 +224,6 @@ func RunRunner(cfg *Cfg) error {
 38 		return fmt.Errorf("event missing required field: workspace")
 39 	}
 40 
 41-	cfg.Logger.Info("received event", "type", eventData.Type, "repo", eventData.Name, "workspace", eventData.Workspace)
 42-
 43 	return eventHandler(cfg, &eventData)
 44 }
 45 
 46@@ -348,14 +351,13 @@ func (eng *JobEngine) getManifest() (string, error) {
 47 
 48 func eventHandler(cfg *Cfg, eventData *Event) error {
 49 	log := cfg.Logger.With("repo", eventData.Name, "type", eventData.Type)
 50-	log.Info("processing event", "workspace", eventData.Workspace)
 51 
 52 	// Cancel any existing job for this repo before starting a new one
 53 	cancelRunningJobs(cfg, log, eventData.Name)
 54 
 55 	jobID := generateJobID(eventData.Name, eventData.Workspace)
 56 	log = log.With("job_id", jobID)
 57-	log.Info("starting job", "session_prefix", fmt.Sprintf("ci.%s.%s", eventData.Name, jobID))
 58+	fmt.Fprintf(os.Stdout, "🚀 starting job ci.%s.%s\n", eventData.Name, jobID)
 59 
 60 	wk := cfg.NewWorkspace(cfg, log, eventData.Workspace)
 61 	eng := &JobEngine{
 62@@ -371,11 +373,11 @@ func eventHandler(cfg *Cfg, eventData *Event) error {
 63 		}
 64 	}()
 65 
 66-	log.Info("cloning workspace", "source", eventData.Workspace)
 67+	fmt.Fprint(os.Stdout, "📦 syncing workspace...\n")
 68 	if err := eng.Setup(); err != nil {
 69 		return fmt.Errorf("setup: %w", err)
 70 	}
 71-	log.Info("workspace ready", "dir", eng.Wk.GetDir())
 72+	fmt.Fprintln(os.Stdout, "✅ workspace ready")
 73 
 74 	// Store the event in the artifact directory so the monitor can access it
 75 	eventBytes, _ := json.Marshal(eventData)
 76@@ -388,13 +390,13 @@ func eventHandler(cfg *Cfg, eventData *Event) error {
 77 		}
 78 	}
 79 
 80-	log.Info("launching job sessions")
 81+	fmt.Fprint(os.Stdout, "🏃 launching sessions...\n")
 82 	if err := eng.Run(); err != nil {
 83 		return fmt.Errorf("run: %w", err)
 84 	}
 85 
 86-	log.Info("job launched successfully — monitor will track progress", "artifact_dir", cfg.ArtifactDir, "artifact_dest", eventData.ArtifactDest)
 87-	log.Info("follow the runner live", "command", fmt.Sprintf("zmx tail ci.%s.%s.runner", eventData.Name, jobID))
 88+	fmt.Fprintln(os.Stdout, "✅ job launched")
 89+	fmt.Fprintf(os.Stdout, "   follow: zmx tail ci.%s.%s.runner\n", eventData.Name, jobID)
 90 	return nil
 91 }
 92 
 93@@ -517,42 +519,55 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer) error {
 94 			log.Debug("job completed, publishing final status", "sessions", len(group))
 95 			exitCode, status := resolveJobExitCode(group)
 96 			log.Info("job finished", "status", status, "exit_code", exitCode)
 97-			payload := StatusPayload{
 98-				Name:         name,
 99-				JobID:        jobID,
100-				Status:       status,
101-				ExitCode:     &exitCode,
102-				Duration:     duration,
103-				StartedAt:    startedAt,
104-				EndedAt:      endedAt,
105-				ArtifactURL:  artifactURL,
106-				SessionCount: len(group),
107-				Sessions:     group,
108-			}
109-			if err := publishStatus(output, payload); err != nil {
110-				log.Error("publish final status", "err", err)
111-			}
112-			// Write sentinel so we don't re-publish on subsequent ticks
113-			if err := os.WriteFile(sentinel, []byte(status), 0644); err != nil {
114-				log.Error("write published sentinel", "err", err)
115-			}
116-		} else {
117-			log.Debug("job still running", "sessions", len(group))
118-			// Publish running status only when filter is "all"
119-			if cfg.StatusFilter != "terminal" {
120+
121+			if cfg.HumanOutput {
122+				icon := map[string]string{"success": "✅", "failed": "❌"}[status]
123+				fmt.Fprintf(output, "[%d/%d] %s %s: %s (%s)\n",
124+					countCompleted(group), len(group), icon, status, name, duration)
125+			} else {
126 				payload := StatusPayload{
127 					Name:         name,
128 					JobID:        jobID,
129-					Status:       "running",
130-					ExitCode:     nil,
131+					Status:       status,
132+					ExitCode:     &exitCode,
133 					Duration:     duration,
134 					StartedAt:    startedAt,
135+					EndedAt:      endedAt,
136 					ArtifactURL:  artifactURL,
137 					SessionCount: len(group),
138 					Sessions:     group,
139 				}
140 				if err := publishStatus(output, payload); err != nil {
141-					log.Error("publish status", "err", err)
142+					log.Error("publish final status", "err", err)
143+				}
144+			}
145+			// Write sentinel so we don't re-publish on subsequent ticks
146+			if err := os.WriteFile(sentinel, []byte(status), 0644); err != nil {
147+				log.Error("write published sentinel", "err", err)
148+			}
149+		} else {
150+			log.Debug("job still running", "sessions", len(group))
151+			// Publish running status only when filter is "all"
152+			if cfg.StatusFilter != "terminal" {
153+				if cfg.HumanOutput {
154+					completed := countCompleted(group)
155+					fmt.Fprintf(output, "[%d/%d] 🚀 running: %s (%s)\n",
156+						completed, len(group), name, duration)
157+				} else {
158+					payload := StatusPayload{
159+						Name:         name,
160+						JobID:        jobID,
161+						Status:       "running",
162+						ExitCode:     nil,
163+						Duration:     duration,
164+						StartedAt:    startedAt,
165+						ArtifactURL:  artifactURL,
166+						SessionCount: len(group),
167+						Sessions:     group,
168+					}
169+					if err := publishStatus(output, payload); err != nil {
170+						log.Error("publish status", "err", err)
171+					}
172 				}
173 			}
174 		}
175@@ -867,6 +882,16 @@ func allCompleted(sessions []SessionInfo) bool {
176 	return true
177 }
178 
179+func countCompleted(sessions []SessionInfo) int {
180+	count := 0
181+	for _, s := range sessions {
182+		if s.Ended != "" {
183+			count++
184+		}
185+	}
186+	return count
187+}
188+
189 func runCmd(cmd *exec.Cmd, log *slog.Logger) error {
190 	stdout, err := cmd.StdoutPipe()
191 	if err != nil {