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
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 {