Commit 8759922
Eric Bower
·
2026-08-09 10:53:11 -0400 EDT
parent a9e9d52
fix: random monitoring issues
2 files changed,
+41,
-18
M
main.go
M
main.go
+23,
-12
1@@ -73,6 +73,7 @@ type Cfg struct {
2 HumanOutput bool // human-readable output instead of JSONL / slog
3 Wait bool // block until job completes, print history and summary
4 EnvVars envList // custom environment variables passed via -e / -env
5+ SessionPrefix string // session prefix filter for monitor (default: "ci.")
6 }
7
8 type Event struct {
9@@ -93,10 +94,12 @@ func NewCfg() (*Cfg, string, bool) {
10 var gcInterval time.Duration
11 var logLevel string
12 var envVars envList
13+ var sessionPrefix string
14 flag.StringVar(&keyLoc, "pk", "", "ssh private key used to authenticate with pico services")
15 flag.StringVar(&certLoc, "ck", "", "ssh certificate public key used to authenticate with pico services (only required if using ssh certificates)")
16 flag.StringVar(&artifactDir, "artifact-dir", "/tmp/pici-artifacts", "local directory to stage artifacts")
17 flag.StringVar(&event, "event", "", "event JSON to run (alternative to reading from stdin)")
18+ flag.StringVar(&sessionPrefix, "prefix", "ci.", "session prefix filter for monitor (default: ci.)")
19 flag.DurationVar(&monitorInterval, "monitor-interval", 5*time.Second, "interval for monitoring zmx sessions")
20 flag.DurationVar(&gcInterval, "gc-interval", 10*time.Minute, "interval for garbage collection in monitor (0 to disable)")
21 flag.StringVar(&logLevel, "log-level", "info", "log level: debug, info, warn, error")
22@@ -134,6 +137,7 @@ func NewCfg() (*Cfg, string, bool) {
23 HumanOutput: human,
24 Wait: wait,
25 EnvVars: envVars,
26+ SessionPrefix: sessionPrefix,
27 }, cmd, wantHelp
28 }
29
30@@ -614,8 +618,8 @@ func (eng *JobEngine) Setup() error {
31
32 func (eng *JobEngine) Run(manifest string) error {
33 domain := "ci"
34- if eng.Ev != nil && eng.Ev.Type == "local" {
35- domain = "local"
36+ if eng.Ev != nil && eng.Ev.Type != "" {
37+ domain = eng.Ev.Type
38 }
39 prefix := fmt.Sprintf("%s.%s.%s.", domain, eng.Ev.Name, eng.JobID)
40 // Child sessions use ".step." sub-prefix so zmx wait "*" inside pico.sh
41@@ -693,8 +697,8 @@ func eventHandler(cfg *Cfg, eventData *Event) error {
42
43 eventBytes, _ := json.Marshal(eventData)
44 domain := "ci"
45- if eventData.Type == "local" {
46- domain = "local"
47+ if eventData.Type != "" {
48+ domain = eventData.Type
49 }
50 fmt.Fprintf(os.Stdout, "🚀 starting job %s.%s.%s\n", domain, eventData.Name, jobID) //nolint:errcheck
51 fmt.Fprintf(os.Stdout, " event: type=%s name=%s workspace=%s\n", eventData.Type, eventData.Name, eventData.Workspace) //nolint:errcheck
52@@ -804,8 +808,8 @@ See: https://github.com/picosh/pici
53 }
54
55 domain = "ci"
56- if eventData.Type == "local" {
57- domain = "local"
58+ if eventData.Type != "" {
59+ domain = eventData.Type
60 }
61 session := fmt.Sprintf("%s.%s.%s.runner", domain, eventData.Name, jobID)
62 fmt.Fprintf(os.Stdout, " zmx tail %s\n", session) //nolint:errcheck
63@@ -818,8 +822,8 @@ See: https://github.com/picosh/pici
64 // progress to stdout, then dumps session history and a final summary.
65 func waitAndReport(cfg *Cfg, log *slog.Logger, name, jobID, eventType string) error {
66 domain := "ci"
67- if eventType == "local" {
68- domain = "local"
69+ if eventType != "" {
70+ domain = eventType
71 }
72 prefix := domain + "." + name + "." + jobID + "."
73 interval := cfg.MonitorInterval
74@@ -1180,7 +1184,7 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
75 return fmt.Errorf("zmx list: %w", err)
76 }
77 sessions := parseZMXList(string(listOutput))
78- ciSessions := filterCISessions(sessions)
79+ ciSessions := filterCISessions(cfg, sessions)
80
81 if len(ciSessions) == 0 {
82 log.Debug("no ci.* sessions found")
83@@ -1420,11 +1424,18 @@ func fmtDurationTs(started, ended int64) string {
84 return fmt.Sprintf("%.1fs", secs)
85 }
86
87-// filterCISessions returns only sessions with ci. prefix.
88-func filterCISessions(sessions []SessionInfo) []SessionInfo {
89+// filterCISessions returns only sessions matching cfg.SessionPrefix (default "ci.").
90+func filterCISessions(cfg *Cfg, sessions []SessionInfo) []SessionInfo {
91+ prefix := "ci."
92+ if cfg != nil && cfg.SessionPrefix != "" {
93+ prefix = cfg.SessionPrefix
94+ }
95+ if !strings.HasSuffix(prefix, ".") {
96+ prefix = prefix + "."
97+ }
98 var filtered []SessionInfo
99 for _, s := range sessions {
100- if strings.HasPrefix(s.Name, "ci.") {
101+ if strings.HasPrefix(s.Name, prefix) {
102 filtered = append(filtered, s)
103 }
104 }
+18,
-6
1@@ -48,10 +48,10 @@ zmx run step2 echo "hello from step2"
2 ctx, cancel := context.WithCancel(context.Background())
3 testJobID := fmt.Sprintf("j-%d", time.Now().UnixNano()%100000000)
4 t.Cleanup(func() {
5- _ = exec.Command("zmx", "kill", "-f", fmt.Sprintf("ci.test-repo.%s", testJobID)).Run()
6+ _ = exec.Command("zmx", "kill", "-f", fmt.Sprintf("test-ci.test-repo.%s", testJobID)).Run()
7 })
8 event := Event{
9- Type: "ci",
10+ Type: "test-ci",
11 Name: "test-repo",
12 JobID: testJobID,
13 Workspace: workspaceDir,
14@@ -68,6 +68,7 @@ zmx run step2 echo "hello from step2"
15 NewWorkspace: defaultWorkspaceFactory,
16 StatusOutput: statusBuf,
17 IncludeRunning: true,
18+ SessionPrefix: "test-ci.",
19 }
20
21 // 3. Run the runner (fire-and-forget, exits quickly)
22@@ -184,8 +185,8 @@ zmx run step2 echo "hello from step2"
23 if finalPayload == nil {
24 t.Fatal("no final payload")
25 }
26- // Filter to only sessions with our job prefix to avoid picking up unrelated ci.* sessions
27- expectedPrefix := fmt.Sprintf("ci.%s.%s.", finalPayload.Name, finalPayload.JobID)
28+ // Filter to only sessions with our job prefix to avoid picking up unrelated test-ci.* sessions
29+ expectedPrefix := fmt.Sprintf("test-ci.%s.%s.", finalPayload.Name, finalPayload.JobID)
30 sessions := make([]SessionInfo, 0, len(finalPayload.Sessions))
31 for _, s := range finalPayload.Sessions {
32 if strings.HasPrefix(s.Name, expectedPrefix) {
33@@ -756,6 +757,13 @@ func TestDetectGitCommit(t *testing.T) {
34 }
35
36 func TestWaitAndReport_Cancellation(t *testing.T) {
37+ if testing.Short() {
38+ t.Skip("skip integration test")
39+ }
40+ if _, err := exec.LookPath("zmx"); err != nil {
41+ t.Skip("zmx not found, skipping integration test")
42+ }
43+
44 ctx, cancel := context.WithCancel(context.Background())
45 cfg := &Cfg{
46 Ctx: ctx,
47@@ -764,9 +772,13 @@ func TestWaitAndReport_Cancellation(t *testing.T) {
48 }
49
50 jobID := fmt.Sprintf("canceltest-%d", time.Now().UnixNano())
51- prefix := "local.testrepo." + jobID + "."
52+ prefix := "test-local.testrepo." + jobID + "."
53 runnerSession := prefix + "runner"
54
55+ t.Cleanup(func() {
56+ _ = exec.Command("zmx", "kill", "-f", runnerSession).Run()
57+ })
58+
59 cmd := exec.Command("zmx", "run", runnerSession, "-d", "sleep", "30")
60 if err := cmd.Run(); err != nil {
61 t.Fatalf("failed to start zmx session: %v", err)
62@@ -777,7 +789,7 @@ func TestWaitAndReport_Cancellation(t *testing.T) {
63 cancel()
64 }()
65
66- err := waitAndReport(cfg, nil, "testrepo", jobID, "local")
67+ err := waitAndReport(cfg, nil, "testrepo", jobID, "test-local")
68 if err == nil {
69 t.Error("expected error when waitAndReport is cancelled, got nil")
70 }