Commit a45593d

Eric Bower  ·  2026-08-12 10:05:47 -0400 EDT
parent 07ea8af
feat: inject artifacts dir
2 files changed,  +81, -11
+35, -11
  1@@ -238,6 +238,8 @@ ENVIRONMENT VARIABLES: pici injects these vars into pico.sh:
  2     PICI_BRANCH         Git branch name (auto-detected)
  3     PICI_COMMIT         Git commit SHA (auto-detected)
  4     PICI_TAG            Git tag name (set for tag events)
  5+    PICI_ARTIFACTS_DIR  Path to job artifacts directory for staging assets
  6+    PICI_ARTIFACT_DIR   Alias for PICI_ARTIFACTS_DIR
  7     ZMX_SESSION_PREFIX  Internal session prefix managed by pici
  8 
  9   Pass custom env vars or overrides with -e / -env (e.g. pici -e CGO_ENABLED=0).
 10@@ -415,11 +417,12 @@ EXAMPLE
 11 
 12 ENV VARS IN pico.sh
 13   The runner exports these environment variables for your pico.sh script:
 14-    PICI_JOB      Unique job identifier (e.g. "a3f2b8c1")
 15+    PICI_JOB          Unique job identifier (e.g. "a3f2b8c1")
 16     PICI_REPO        Repository name (from event "name" field)
 17     PICI_EVENT       Event type (e.g. "git.push", "git.tag")
 18     PICI_TAG         Git tag name (set only for git.tag events)
 19     PICI_BRANCH      Git branch name (set only for git.push events)
 20+    PICI_ARTIFACTS_DIR Path to job artifacts directory for staging assets
 21 
 22   Note: pico.sh runs with the workspace as its working directory, so
 23   $(pwd) gives you the workspace path directly.
 24@@ -750,7 +753,12 @@ func (eng *JobEngine) Run(manifest string) error {
 25 	// outer zmx run call — it would be prepended to the runner's own name.
 26 	// The prefix is only for child sessions spawned inside pico.sh (via the
 27 	// exported env var).
 28-	cmdEnv := make([]string, 0, len(os.Environ())+5)
 29+	artifactsDir := filepath.Join(eng.Cfg.ArtifactDir, eng.Ev.Name, eng.JobID, "artifacts")
 30+	if err := os.MkdirAll(artifactsDir, 0755); err != nil {
 31+		log.Error("create artifacts dir", "err", err)
 32+	}
 33+
 34+	cmdEnv := make([]string, 0, len(os.Environ())+7)
 35 	for _, e := range os.Environ() {
 36 		if !strings.HasPrefix(e, "ZMX_SESSION_PREFIX=") {
 37 			cmdEnv = append(cmdEnv, e)
 38@@ -761,6 +769,8 @@ func (eng *JobEngine) Run(manifest string) error {
 39 		fmt.Sprintf("PICI_REPO=%s", eng.Ev.Name),
 40 		fmt.Sprintf("PICI_EVENT=%s", eng.Ev.Type),
 41 		fmt.Sprintf("PICI_BRANCH=%s", eng.Ev.Branch),
 42+		fmt.Sprintf("PICI_ARTIFACTS_DIR=%s", artifactsDir),
 43+		fmt.Sprintf("PICI_ARTIFACT_DIR=%s", artifactsDir),
 44 	)
 45 	if eng.Ev.Tag != "" {
 46 		cmdEnv = append(cmdEnv, fmt.Sprintf("PICI_TAG=%s", eng.Ev.Tag))
 47@@ -2200,21 +2210,29 @@ func generateJobIndex(artifactDir, name, jobID string, sessions []SessionInfo) (
 48 	var artifacts []artifactRow
 49 	artifactsDir := filepath.Join(artifactDir, name, jobID, "artifacts")
 50 
 51-	if entries, err := os.ReadDir(artifactsDir); err == nil {
 52-		for _, entry := range entries {
 53-			if entry.IsDir() {
 54-				continue
 55+	if _, err := os.Stat(artifactsDir); err == nil {
 56+		_ = filepath.WalkDir(artifactsDir, func(path string, d os.DirEntry, err error) error {
 57+			if err != nil || d.IsDir() {
 58+				return nil
 59 			}
 60-			info, err := entry.Info()
 61+			rel, err := filepath.Rel(artifactsDir, path)
 62 			if err != nil {
 63-				continue
 64+				return nil
 65+			}
 66+			if strings.HasPrefix(d.Name(), ".") {
 67+				return nil
 68+			}
 69+			info, err := d.Info()
 70+			if err != nil {
 71+				return nil
 72 			}
 73 			artifacts = append(artifacts, artifactRow{
 74-				Name:    entry.Name(),
 75+				Name:    rel,
 76 				Size:    formatFileSize(info.Size()),
 77 				ModTime: formatTimestamp(fmt.Sprintf("%d", info.ModTime().Unix())),
 78 			})
 79-		}
 80+			return nil
 81+		})
 82 	}
 83 
 84 	// Resolve overall job status
 85@@ -2262,6 +2280,12 @@ func generateJobIndex(artifactDir, name, jobID string, sessions []SessionInfo) (
 86 		fmt.Fprintf(&buf, "  %s %-20s  exit: %-5s  duration: %-8s  ended: %s\n",
 87 			rIcon, r.Short, r.ExitCode, r.Duration, r.Ended)
 88 	}
 89+	if len(artifacts) > 0 {
 90+		fmt.Fprintln(&buf, "Artifacts:")
 91+		for _, a := range artifacts {
 92+			fmt.Fprintf(&buf, "  %-30s  %-10s  %s\n", a.Name, a.Size, a.ModTime)
 93+		}
 94+	}
 95 	fmt.Fprintln(&buf, strings.Repeat("-", 60))
 96 	txtContent = buf.String()
 97 
 98@@ -2387,10 +2411,10 @@ func runLocal(cfg *Cfg, dest string) error {
 99 	}()
100 
101 	fmt.Fprintf(os.Stdout, "🚀 starting local job local.%s.%s\n", eventData.Name, jobID) //nolint:errcheck
102-	fmt.Fprintln(os.Stdout, "📦 syncing workspace to temp directory...")                 //nolint:errcheck
103 	if err := eng.Setup(); err != nil {
104 		return fmt.Errorf("workspace setup: %w", err)
105 	}
106+	fmt.Fprintf(os.Stdout, "📦 syncing workspace to temp directory %s\n", eng.Wk.GetDir()) //nolint:errcheck
107 	log.Debug("workspace directory", "dir", eng.Wk.GetDir())
108 
109 	// Store event.json in artifact dir
+46, -0
 1@@ -1036,3 +1036,49 @@ func (w *mockLocalWorkspace) Setup() error     { return nil }
 2 func (w *mockLocalWorkspace) Cleanup() error   { return nil }
 3 func (w *mockLocalWorkspace) GetDir() string   { return w.dir }
 4 func (w *mockLocalWorkspace) Checksum() string { return "" }
 5+
 6+func TestJobEngine_ExportsArtifactsDir(t *testing.T) {
 7+	tmpDir := t.TempDir()
 8+	eng := &JobEngine{
 9+		Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
10+		Cfg: &Cfg{
11+			ArtifactDir: tmpDir,
12+		},
13+		Ev: &Event{
14+			Name: "myrepo",
15+			Type: "local",
16+		},
17+		JobID: "job123",
18+	}
19+
20+	expectedArtifactsDir := filepath.Join(tmpDir, "myrepo", "job123", "artifacts")
21+
22+	// Verify that artifacts directory path matches expected layout
23+	artifactsDir := filepath.Join(eng.Cfg.ArtifactDir, eng.Ev.Name, eng.JobID, "artifacts")
24+	if artifactsDir != expectedArtifactsDir {
25+		t.Errorf("expected artifactsDir %s, got %s", expectedArtifactsDir, artifactsDir)
26+	}
27+}
28+
29+func TestGenerateJobIndex_WalksSubdirectories(t *testing.T) {
30+	tmpDir := t.TempDir()
31+	repoName := "testrepo"
32+	jobID := "j999"
33+	artifactsDir := filepath.Join(tmpDir, repoName, jobID, "artifacts")
34+
35+	if err := os.MkdirAll(filepath.Join(artifactsDir, "build"), 0755); err != nil {
36+		t.Fatalf("mkdir failed: %v", err)
37+	}
38+	if err := os.WriteFile(filepath.Join(artifactsDir, "build", "output.json"), []byte("{}"), 0644); err != nil {
39+		t.Fatalf("write file failed: %v", err)
40+	}
41+
42+	htmlContent, txtContent := generateJobIndex(tmpDir, repoName, jobID, nil)
43+	if !strings.Contains(htmlContent, "build/output.json") {
44+		t.Errorf("html index missing nested artifact build/output.json: %s", htmlContent)
45+	}
46+	if !strings.Contains(txtContent, "build/output.json") {
47+		t.Errorf("txt index missing nested artifact build/output.json: %s", txtContent)
48+	}
49+}
50+