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
M
main.go
+61,
-36
| ... | ... | @@ -42,6 +42,7 @@ type Cfg struct { | |
| 42 | 42 | NewWorkspace WorkspaceFactory | |
| 43 | 43 | StatusOutput io.Writer // where status JSONL is written (default: os.Stdout) | |
| 44 | 44 | StatusFilter string // "terminal" (default) or "all" | |
| 45 | + | HumanOutput bool // human-readable output instead of JSONL / slog | |
| 45 | 46 | } | |
| 46 | 47 | ||
| 47 | 48 | type Event struct { |
| ... | ... | @@ -65,7 +66,9 @@ func NewCfg() *Cfg { | |
| 65 | 66 | flag.StringVar(&logLevel, "log-level", "info", "log level: debug, info, warn, error") | |
| 66 | 67 | flag.BoolVar(&structured, "structured", false, "use structured key=value log output") | |
| 67 | 68 | var statusFilter string | |
| 69 | + | var human bool | |
| 68 | 70 | flag.StringVar(&statusFilter, "status-filter", "terminal", "status output filter: terminal (default) or all") | |
| 71 | + | flag.BoolVar(&human, "human", false, "human-readable output (default: JSONL / slog)") | |
| 69 | 72 | flag.Parse() | |
| 70 | 73 | ||
| 71 | 74 | logger := newLogger("ci", logLevel, structured) |
| ... | ... | @@ -81,6 +84,7 @@ func NewCfg() *Cfg { | |
| 81 | 84 | Event: event, | |
| 82 | 85 | MonitorInterval: monitorInterval, | |
| 83 | 86 | StatusFilter: statusFilter, | |
| 87 | + | HumanOutput: human, | |
| 84 | 88 | } | |
| 85 | 89 | } | |
| 86 | 90 |
| ... | ... | @@ -182,7 +186,8 @@ FLAGS | |
| 182 | 186 | -monitor-interval <dur> Interval for monitoring zmx sessions (default: 5s) | |
| 183 | 187 | -log-level <level> Log level: debug, info, warn, error (default: info) | |
| 184 | 188 | -structured Use structured key=value log output | |
| 185 | - | -status-filter <filter> Status output filter: terminal (default) or all`) | |
| 189 | + | -status-filter <filter> Status output filter: terminal (default) or all | |
| 190 | + | -human Human-readable output instead of JSONL / slog`) | |
| 186 | 191 | } | |
| 187 | 192 | ||
| 188 | 193 | func RunRunner(cfg *Cfg) error { |
| ... | ... | @@ -219,8 +224,6 @@ func RunRunner(cfg *Cfg) error { | |
| 219 | 224 | return fmt.Errorf("event missing required field: workspace") | |
| 220 | 225 | } | |
| 221 | 226 | ||
| 222 | - | cfg.Logger.Info("received event", "type", eventData.Type, "repo", eventData.Name, "workspace", eventData.Workspace) | |
| 223 | - | ||
| 224 | 227 | return eventHandler(cfg, &eventData) | |
| 225 | 228 | } | |
| 226 | 229 |
| ... | ... | @@ -348,14 +351,13 @@ func (eng *JobEngine) getManifest() (string, error) { | |
| 348 | 351 | ||
| 349 | 352 | func eventHandler(cfg *Cfg, eventData *Event) error { | |
| 350 | 353 | log := cfg.Logger.With("repo", eventData.Name, "type", eventData.Type) | |
| 351 | - | log.Info("processing event", "workspace", eventData.Workspace) | |
| 352 | 354 | ||
| 353 | 355 | // Cancel any existing job for this repo before starting a new one | |
| 354 | 356 | cancelRunningJobs(cfg, log, eventData.Name) | |
| 355 | 357 | ||
| 356 | 358 | jobID := generateJobID(eventData.Name, eventData.Workspace) | |
| 357 | 359 | log = log.With("job_id", jobID) | |
| 358 | - | log.Info("starting job", "session_prefix", fmt.Sprintf("ci.%s.%s", eventData.Name, jobID)) | |
| 360 | + | fmt.Fprintf(os.Stdout, "🚀 starting job ci.%s.%s\n", eventData.Name, jobID) | |
| 359 | 361 | ||
| 360 | 362 | wk := cfg.NewWorkspace(cfg, log, eventData.Workspace) | |
| 361 | 363 | eng := &JobEngine{ |
| ... | ... | @@ -371,11 +373,11 @@ func eventHandler(cfg *Cfg, eventData *Event) error { | |
| 371 | 373 | } | |
| 372 | 374 | }() | |
| 373 | 375 | ||
| 374 | - | log.Info("cloning workspace", "source", eventData.Workspace) | |
| 376 | + | fmt.Fprint(os.Stdout, "📦 syncing workspace...\n") | |
| 375 | 377 | if err := eng.Setup(); err != nil { | |
| 376 | 378 | return fmt.Errorf("setup: %w", err) | |
| 377 | 379 | } | |
| 378 | - | log.Info("workspace ready", "dir", eng.Wk.GetDir()) | |
| 380 | + | fmt.Fprintln(os.Stdout, "✅ workspace ready") | |
| 379 | 381 | ||
| 380 | 382 | // Store the event in the artifact directory so the monitor can access it | |
| 381 | 383 | eventBytes, _ := json.Marshal(eventData) |
| ... | ... | @@ -388,13 +390,13 @@ func eventHandler(cfg *Cfg, eventData *Event) error { | |
| 388 | 390 | } | |
| 389 | 391 | } | |
| 390 | 392 | ||
| 391 | - | log.Info("launching job sessions") | |
| 393 | + | fmt.Fprint(os.Stdout, "🏃 launching sessions...\n") | |
| 392 | 394 | if err := eng.Run(); err != nil { | |
| 393 | 395 | return fmt.Errorf("run: %w", err) | |
| 394 | 396 | } | |
| 395 | 397 | ||
| 396 | - | log.Info("job launched successfully — monitor will track progress", "artifact_dir", cfg.ArtifactDir, "artifact_dest", eventData.ArtifactDest) | |
| 397 | - | log.Info("follow the runner live", "command", fmt.Sprintf("zmx tail ci.%s.%s.runner", eventData.Name, jobID)) | |
| 398 | + | fmt.Fprintln(os.Stdout, "✅ job launched") | |
| 399 | + | fmt.Fprintf(os.Stdout, " follow: zmx tail ci.%s.%s.runner\n", eventData.Name, jobID) | |
| 398 | 400 | return nil | |
| 399 | 401 | } | |
| 400 | 402 |
| ... | ... | @@ -517,42 +519,55 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer) error { | |
| 517 | 519 | log.Debug("job completed, publishing final status", "sessions", len(group)) | |
| 518 | 520 | exitCode, status := resolveJobExitCode(group) | |
| 519 | 521 | log.Info("job finished", "status", status, "exit_code", exitCode) | |
| 520 | - | payload := StatusPayload{ | |
| 521 | - | Name: name, | |
| 522 | - | JobID: jobID, | |
| 523 | - | Status: status, | |
| 524 | - | ExitCode: &exitCode, | |
| 525 | - | Duration: duration, | |
| 526 | - | StartedAt: startedAt, | |
| 527 | - | EndedAt: endedAt, | |
| 528 | - | ArtifactURL: artifactURL, | |
| 529 | - | SessionCount: len(group), | |
| 530 | - | Sessions: group, | |
| 531 | - | } | |
| 532 | - | if err := publishStatus(output, payload); err != nil { | |
| 533 | - | log.Error("publish final status", "err", err) | |
| 534 | - | } | |
| 535 | - | // Write sentinel so we don't re-publish on subsequent ticks | |
| 536 | - | if err := os.WriteFile(sentinel, []byte(status), 0644); err != nil { | |
| 537 | - | log.Error("write published sentinel", "err", err) | |
| 538 | - | } | |
| 539 | - | } else { | |
| 540 | - | log.Debug("job still running", "sessions", len(group)) | |
| 541 | - | // Publish running status only when filter is "all" | |
| 542 | - | if cfg.StatusFilter != "terminal" { | |
| 522 | + | ||
| 523 | + | if cfg.HumanOutput { | |
| 524 | + | icon := map[string]string{"success": "✅", "failed": "❌"}[status] | |
| 525 | + | fmt.Fprintf(output, "[%d/%d] %s %s: %s (%s)\n", | |
| 526 | + | countCompleted(group), len(group), icon, status, name, duration) | |
| 527 | + | } else { | |
| 543 | 528 | payload := StatusPayload{ | |
| 544 | 529 | Name: name, | |
| 545 | 530 | JobID: jobID, | |
| 546 | - | Status: "running", | |
| 547 | - | ExitCode: nil, | |
| 531 | + | Status: status, | |
| 532 | + | ExitCode: &exitCode, | |
| 548 | 533 | Duration: duration, | |
| 549 | 534 | StartedAt: startedAt, | |
| 535 | + | EndedAt: endedAt, | |
| 550 | 536 | ArtifactURL: artifactURL, | |
| 551 | 537 | SessionCount: len(group), | |
| 552 | 538 | Sessions: group, | |
| 553 | 539 | } | |
| 554 | 540 | if err := publishStatus(output, payload); err != nil { | |
| 555 | - | log.Error("publish status", "err", err) | |
| 541 | + | log.Error("publish final status", "err", err) | |
| 542 | + | } | |
| 543 | + | } | |
| 544 | + | // Write sentinel so we don't re-publish on subsequent ticks | |
| 545 | + | if err := os.WriteFile(sentinel, []byte(status), 0644); err != nil { | |
| 546 | + | log.Error("write published sentinel", "err", err) | |
| 547 | + | } | |
| 548 | + | } else { | |
| 549 | + | log.Debug("job still running", "sessions", len(group)) | |
| 550 | + | // Publish running status only when filter is "all" | |
| 551 | + | if cfg.StatusFilter != "terminal" { | |
| 552 | + | if cfg.HumanOutput { | |
| 553 | + | completed := countCompleted(group) | |
| 554 | + | fmt.Fprintf(output, "[%d/%d] 🚀 running: %s (%s)\n", | |
| 555 | + | completed, len(group), name, duration) | |
| 556 | + | } else { | |
| 557 | + | payload := StatusPayload{ | |
| 558 | + | Name: name, | |
| 559 | + | JobID: jobID, | |
| 560 | + | Status: "running", | |
| 561 | + | ExitCode: nil, | |
| 562 | + | Duration: duration, | |
| 563 | + | StartedAt: startedAt, | |
| 564 | + | ArtifactURL: artifactURL, | |
| 565 | + | SessionCount: len(group), | |
| 566 | + | Sessions: group, | |
| 567 | + | } | |
| 568 | + | if err := publishStatus(output, payload); err != nil { | |
| 569 | + | log.Error("publish status", "err", err) | |
| 570 | + | } | |
| 556 | 571 | } | |
| 557 | 572 | } | |
| 558 | 573 | } |
| ... | ... | @@ -867,6 +882,16 @@ func allCompleted(sessions []SessionInfo) bool { | |
| 867 | 882 | return true | |
| 868 | 883 | } | |
| 869 | 884 | ||
| 885 | + | func countCompleted(sessions []SessionInfo) int { | |
| 886 | + | count := 0 | |
| 887 | + | for _, s := range sessions { | |
| 888 | + | if s.Ended != "" { | |
| 889 | + | count++ | |
| 890 | + | } | |
| 891 | + | } | |
| 892 | + | return count | |
| 893 | + | } | |
| 894 | + | ||
| 870 | 895 | func runCmd(cmd *exec.Cmd, log *slog.Logger) error { | |
| 871 | 896 | stdout, err := cmd.StdoutPipe() | |
| 872 | 897 | if err != nil { |