Commit fc37682

Eric Bower  ·  2026-08-11 11:33:42 -0400 EDT
parent 1569539
fix: pici runner should not create a published.json artifact
2 files changed,  +88, -27
+37, -27
 1@@ -1343,21 +1343,9 @@ func monitorTick(cfg *Cfg, log *slog.Logger, output io.Writer, jobStates map[str
 2 					log.Error("publish final status", "err", err)
 3 				}
 4 			}
 5-			// Write published.json, then sync to include it in the rsync.
 6-			// This is the only sync for completed jobs. In-progress jobs are
 7-			// synced every tick in the else branch below.
 8-			published := map[string]interface{}{
 9-				"status":      status,
10-				"exit_code":   exitCode,
11-				"job_id":      jobID,
12-				"finished_at": time.Now().UTC().Format(time.RFC3339),
13-			}
14-			publishedJSON, _ := json.Marshal(published)
15-			log.Info("writing sentinel", "path", sentinel)
16-			if err := os.MkdirAll(filepath.Dir(sentinel), 0755); err == nil {
17-				if err := os.WriteFile(sentinel, publishedJSON, 0644); err != nil {
18-					log.Error("write published sentinel", "err", err)
19-				}
20+			log.Info("writing sentinel", "repo", name, "job_id", jobID)
21+			if err := writePublishedSentinel(cfg.ArtifactDir, name, jobID, status, exitCode); err != nil {
22+				log.Error("write published sentinel", "err", err)
23 			}
24 			// Regenerate index.html now that published.json exists, so the artifact list includes it.
25 			indexHTML, indexTXT := generateJobIndex(cfg.ArtifactDir, name, jobID, group)
26@@ -2403,9 +2391,43 @@ func runLocal(cfg *Cfg, dest string) error {
27 		fmt.Fprintf(os.Stdout, "✅ artifacts rsynced to %s\n", eventData.ArtifactDest) //nolint:errcheck
28 	}
29 
30+	// Write published sentinel for local run (`pici`)
31+	domain := getDomain(eventData.Type)
32+	prefix := fmt.Sprintf("%s.%s.%s.", domain, eventData.Name, jobID)
33+	if listOutput, err := exec.Command("zmx", "list").CombinedOutput(); err == nil {
34+		sessions := parseZMXList(string(listOutput))
35+		var jobSessions []SessionInfo
36+		for _, s := range sessions {
37+			if strings.HasPrefix(s.Name, prefix) {
38+				s.Short = cleanSessionShort(s.Name, prefix, eventData.Name, jobID)
39+				jobSessions = append(jobSessions, s)
40+			}
41+		}
42+		exitCode, status := resolveJobExitCode(jobSessions)
43+		_ = writePublishedSentinel(cfg.ArtifactDir, eventData.Name, jobID, status, exitCode)
44+	}
45+
46 	return nil
47 }
48 
49+func writePublishedSentinel(artifactDir, repoName, jobID, status string, exitCode int) error {
50+	sentinel := filepath.Join(artifactDir, repoName, jobID, "artifacts", "published.json")
51+	published := map[string]interface{}{
52+		"status":      status,
53+		"exit_code":   exitCode,
54+		"job_id":      jobID,
55+		"finished_at": time.Now().UTC().Format(time.RFC3339),
56+	}
57+	publishedJSON, err := json.Marshal(published)
58+	if err != nil {
59+		return err
60+	}
61+	if err := os.MkdirAll(filepath.Dir(sentinel), 0755); err != nil {
62+		return err
63+	}
64+	return os.WriteFile(sentinel, publishedJSON, 0644)
65+}
66+
67 func stageLocalArtifacts(cfg *Cfg, log *slog.Logger, repoName, jobID, eventType string) error {
68 	domain := getDomain(eventType)
69 	prefix := fmt.Sprintf("%s.%s.%s.", domain, repoName, jobID)
70@@ -2449,18 +2471,6 @@ func stageLocalArtifacts(cfg *Cfg, log *slog.Logger, repoName, jobID, eventType
71 		}
72 	}
73 
74-	// Write published sentinel
75-	exitCode, status := resolveJobExitCode(jobSessions)
76-	sentinel := filepath.Join(cfg.ArtifactDir, repoName, jobID, "artifacts", "published.json")
77-	published := map[string]interface{}{
78-		"status":      status,
79-		"exit_code":   exitCode,
80-		"job_id":      jobID,
81-		"finished_at": time.Now().UTC().Format(time.RFC3339),
82-	}
83-	publishedJSON, _ := json.Marshal(published)
84-	_ = os.WriteFile(sentinel, publishedJSON, 0644)
85-
86 	indexHTML, indexTXT := generateJobIndex(cfg.ArtifactDir, repoName, jobID, jobSessions)
87 	_ = stageArtifact(cfg.ArtifactDir, repoName, jobID, "index", indexHTML, ".html")
88 	_ = stageArtifact(cfg.ArtifactDir, repoName, jobID, "index", indexTXT, ".txt")
+51, -0
 1@@ -86,6 +86,12 @@ zmx run step2 echo "hello from step2"
 2 		t.Fatal("timeout waiting for runner to complete")
 3 	}
 4 
 5+	// Verify that RunRunner does NOT create published.json (monitor's job)
 6+	sentinel := filepath.Join(artifactDir, "test-repo", testJobID, "artifacts", "published.json")
 7+	if _, err := os.Stat(sentinel); err == nil {
 8+		t.Fatalf("expected published.json NOT to exist after RunRunner, but it exists at %s", sentinel)
 9+	}
10+
11 	// 4. Run the monitor and poll for incremental artifacts
12 	monitorDone := make(chan error, 1)
13 	go func() {
14@@ -972,3 +978,48 @@ func TestWorkspaceRsync_ExcludesGitAndJJ(t *testing.T) {
15 		t.Errorf("expected checksum to start with 'sha256:', got %q", checksum)
16 	}
17 }
18+
19+func TestRunnerDoesNotCreatePublishedSentinel(t *testing.T) {
20+	tmpDir := t.TempDir()
21+	artifactDir := filepath.Join(tmpDir, "artifacts")
22+	wkDir := filepath.Join(tmpDir, "workspace")
23+
24+	if err := os.MkdirAll(wkDir, 0755); err != nil {
25+		t.Fatalf("failed to create workspace dir: %v", err)
26+	}
27+
28+	picoScript := `#!/usr/bin/env bash
29+set -euo pipefail
30+zmx run step1 -d echo "step1"
31+zmx wait "*"
32+`
33+	if err := os.WriteFile(filepath.Join(wkDir, "pico.sh"), []byte(picoScript), 0755); err != nil {
34+		t.Fatalf("failed to write pico.sh: %v", err)
35+	}
36+
37+	jobID := "j-no-published-test"
38+	event := Event{
39+		Type:      "git.push",
40+		Name:      "no-published-repo",
41+		JobID:     jobID,
42+		Workspace: wkDir,
43+	}
44+	eventJSON, _ := json.Marshal(event)
45+
46+	cfg := &Cfg{
47+		Logger:        slog.New(slog.NewTextHandler(io.Discard, nil)),
48+		ArtifactDir:   artifactDir,
49+		EventSource:   io.NopCloser(bytes.NewReader(append(eventJSON, '\n'))),
50+		NewWorkspace:  defaultWorkspaceFactory,
51+		SessionPrefix: "test.",
52+	}
53+
54+	if err := RunRunner(cfg); err != nil {
55+		t.Fatalf("RunRunner failed: %v", err)
56+	}
57+
58+	sentinel := filepath.Join(artifactDir, "no-published-repo", jobID, "artifacts", "published.json")
59+	if _, err := os.Stat(sentinel); err == nil {
60+		t.Errorf("expected published.json NOT to exist after RunRunner, but it exists at %s", sentinel)
61+	}
62+}