Commit a45593d
Eric Bower
·
2026-08-12 10:05:47 -0400 EDT
parent 07ea8af
feat: inject artifacts dir
2 files changed,
+81,
-11
M
main.go
M
main.go
+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+