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
| ... | ... | @@ -238,6 +238,8 @@ ENVIRONMENT VARIABLES: pici injects these vars into pico.sh: | |
| 238 | 238 | PICI_BRANCH Git branch name (auto-detected) | |
| 239 | 239 | PICI_COMMIT Git commit SHA (auto-detected) | |
| 240 | 240 | PICI_TAG Git tag name (set for tag events) | |
| 241 | + | PICI_ARTIFACTS_DIR Path to job artifacts directory for staging assets | |
| 242 | + | PICI_ARTIFACT_DIR Alias for PICI_ARTIFACTS_DIR | |
| 241 | 243 | ZMX_SESSION_PREFIX Internal session prefix managed by pici | |
| 242 | 244 | ||
| 243 | 245 | Pass custom env vars or overrides with -e / -env (e.g. pici -e CGO_ENABLED=0). |
| ... | ... | @@ -415,11 +417,12 @@ EXAMPLE | |
| 415 | 417 | ||
| 416 | 418 | ENV VARS IN pico.sh | |
| 417 | 419 | The runner exports these environment variables for your pico.sh script: | |
| 418 | - | PICI_JOB Unique job identifier (e.g. "a3f2b8c1") | |
| 420 | + | PICI_JOB Unique job identifier (e.g. "a3f2b8c1") | |
| 419 | 421 | PICI_REPO Repository name (from event "name" field) | |
| 420 | 422 | PICI_EVENT Event type (e.g. "git.push", "git.tag") | |
| 421 | 423 | PICI_TAG Git tag name (set only for git.tag events) | |
| 422 | 424 | PICI_BRANCH Git branch name (set only for git.push events) | |
| 425 | + | PICI_ARTIFACTS_DIR Path to job artifacts directory for staging assets | |
| 423 | 426 | ||
| 424 | 427 | Note: pico.sh runs with the workspace as its working directory, so | |
| 425 | 428 | $(pwd) gives you the workspace path directly. |
| ... | ... | @@ -750,7 +753,12 @@ func (eng *JobEngine) Run(manifest string) error { | |
| 750 | 753 | // outer zmx run call — it would be prepended to the runner's own name. | |
| 751 | 754 | // The prefix is only for child sessions spawned inside pico.sh (via the | |
| 752 | 755 | // exported env var). | |
| 753 | - | cmdEnv := make([]string, 0, len(os.Environ())+5) | |
| 756 | + | artifactsDir := filepath.Join(eng.Cfg.ArtifactDir, eng.Ev.Name, eng.JobID, "artifacts") | |
| 757 | + | if err := os.MkdirAll(artifactsDir, 0755); err != nil { | |
| 758 | + | log.Error("create artifacts dir", "err", err) | |
| 759 | + | } | |
| 760 | + | ||
| 761 | + | cmdEnv := make([]string, 0, len(os.Environ())+7) | |
| 754 | 762 | for _, e := range os.Environ() { | |
| 755 | 763 | if !strings.HasPrefix(e, "ZMX_SESSION_PREFIX=") { | |
| 756 | 764 | cmdEnv = append(cmdEnv, e) |
| ... | ... | @@ -761,6 +769,8 @@ func (eng *JobEngine) Run(manifest string) error { | |
| 761 | 769 | fmt.Sprintf("PICI_REPO=%s", eng.Ev.Name), | |
| 762 | 770 | fmt.Sprintf("PICI_EVENT=%s", eng.Ev.Type), | |
| 763 | 771 | fmt.Sprintf("PICI_BRANCH=%s", eng.Ev.Branch), | |
| 772 | + | fmt.Sprintf("PICI_ARTIFACTS_DIR=%s", artifactsDir), | |
| 773 | + | fmt.Sprintf("PICI_ARTIFACT_DIR=%s", artifactsDir), | |
| 764 | 774 | ) | |
| 765 | 775 | if eng.Ev.Tag != "" { | |
| 766 | 776 | cmdEnv = append(cmdEnv, fmt.Sprintf("PICI_TAG=%s", eng.Ev.Tag)) |
| ... | ... | @@ -2200,21 +2210,29 @@ func generateJobIndex(artifactDir, name, jobID string, sessions []SessionInfo) ( | |
| 2200 | 2210 | var artifacts []artifactRow | |
| 2201 | 2211 | artifactsDir := filepath.Join(artifactDir, name, jobID, "artifacts") | |
| 2202 | 2212 | ||
| 2203 | - | if entries, err := os.ReadDir(artifactsDir); err == nil { | |
| 2204 | - | for _, entry := range entries { | |
| 2205 | - | if entry.IsDir() { | |
| 2206 | - | continue | |
| 2213 | + | if _, err := os.Stat(artifactsDir); err == nil { | |
| 2214 | + | _ = filepath.WalkDir(artifactsDir, func(path string, d os.DirEntry, err error) error { | |
| 2215 | + | if err != nil || d.IsDir() { | |
| 2216 | + | return nil | |
| 2207 | 2217 | } | |
| 2208 | - | info, err := entry.Info() | |
| 2218 | + | rel, err := filepath.Rel(artifactsDir, path) | |
| 2209 | 2219 | if err != nil { | |
| 2210 | - | continue | |
| 2220 | + | return nil | |
| 2221 | + | } | |
| 2222 | + | if strings.HasPrefix(d.Name(), ".") { | |
| 2223 | + | return nil | |
| 2224 | + | } | |
| 2225 | + | info, err := d.Info() | |
| 2226 | + | if err != nil { | |
| 2227 | + | return nil | |
| 2211 | 2228 | } | |
| 2212 | 2229 | artifacts = append(artifacts, artifactRow{ | |
| 2213 | - | Name: entry.Name(), | |
| 2230 | + | Name: rel, | |
| 2214 | 2231 | Size: formatFileSize(info.Size()), | |
| 2215 | 2232 | ModTime: formatTimestamp(fmt.Sprintf("%d", info.ModTime().Unix())), | |
| 2216 | 2233 | }) | |
| 2217 | - | } | |
| 2234 | + | return nil | |
| 2235 | + | }) | |
| 2218 | 2236 | } | |
| 2219 | 2237 | ||
| 2220 | 2238 | // Resolve overall job status |
| ... | ... | @@ -2262,6 +2280,12 @@ func generateJobIndex(artifactDir, name, jobID string, sessions []SessionInfo) ( | |
| 2262 | 2280 | fmt.Fprintf(&buf, " %s %-20s exit: %-5s duration: %-8s ended: %s\n", | |
| 2263 | 2281 | rIcon, r.Short, r.ExitCode, r.Duration, r.Ended) | |
| 2264 | 2282 | } | |
| 2283 | + | if len(artifacts) > 0 { | |
| 2284 | + | fmt.Fprintln(&buf, "Artifacts:") | |
| 2285 | + | for _, a := range artifacts { | |
| 2286 | + | fmt.Fprintf(&buf, " %-30s %-10s %s\n", a.Name, a.Size, a.ModTime) | |
| 2287 | + | } | |
| 2288 | + | } | |
| 2265 | 2289 | fmt.Fprintln(&buf, strings.Repeat("-", 60)) | |
| 2266 | 2290 | txtContent = buf.String() | |
| 2267 | 2291 |
| ... | ... | @@ -2387,10 +2411,10 @@ func runLocal(cfg *Cfg, dest string) error { | |
| 2387 | 2411 | }() | |
| 2388 | 2412 | ||
| 2389 | 2413 | fmt.Fprintf(os.Stdout, "🚀 starting local job local.%s.%s\n", eventData.Name, jobID) //nolint:errcheck | |
| 2390 | - | fmt.Fprintln(os.Stdout, "📦 syncing workspace to temp directory...") //nolint:errcheck | |
| 2391 | 2414 | if err := eng.Setup(); err != nil { | |
| 2392 | 2415 | return fmt.Errorf("workspace setup: %w", err) | |
| 2393 | 2416 | } | |
| 2417 | + | fmt.Fprintf(os.Stdout, "📦 syncing workspace to temp directory %s\n", eng.Wk.GetDir()) //nolint:errcheck | |
| 2394 | 2418 | log.Debug("workspace directory", "dir", eng.Wk.GetDir()) | |
| 2395 | 2419 | ||
| 2396 | 2420 | // Store event.json in artifact dir |
+46,
-0
| ... | ... | @@ -1036,3 +1036,49 @@ func (w *mockLocalWorkspace) Setup() error { return nil } | |
| 1036 | 1036 | func (w *mockLocalWorkspace) Cleanup() error { return nil } | |
| 1037 | 1037 | func (w *mockLocalWorkspace) GetDir() string { return w.dir } | |
| 1038 | 1038 | func (w *mockLocalWorkspace) Checksum() string { return "" } | |
| 1039 | + | ||
| 1040 | + | func TestJobEngine_ExportsArtifactsDir(t *testing.T) { | |
| 1041 | + | tmpDir := t.TempDir() | |
| 1042 | + | eng := &JobEngine{ | |
| 1043 | + | Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), | |
| 1044 | + | Cfg: &Cfg{ | |
| 1045 | + | ArtifactDir: tmpDir, | |
| 1046 | + | }, | |
| 1047 | + | Ev: &Event{ | |
| 1048 | + | Name: "myrepo", | |
| 1049 | + | Type: "local", | |
| 1050 | + | }, | |
| 1051 | + | JobID: "job123", | |
| 1052 | + | } | |
| 1053 | + | ||
| 1054 | + | expectedArtifactsDir := filepath.Join(tmpDir, "myrepo", "job123", "artifacts") | |
| 1055 | + | ||
| 1056 | + | // Verify that artifacts directory path matches expected layout | |
| 1057 | + | artifactsDir := filepath.Join(eng.Cfg.ArtifactDir, eng.Ev.Name, eng.JobID, "artifacts") | |
| 1058 | + | if artifactsDir != expectedArtifactsDir { | |
| 1059 | + | t.Errorf("expected artifactsDir %s, got %s", expectedArtifactsDir, artifactsDir) | |
| 1060 | + | } | |
| 1061 | + | } | |
| 1062 | + | ||
| 1063 | + | func TestGenerateJobIndex_WalksSubdirectories(t *testing.T) { | |
| 1064 | + | tmpDir := t.TempDir() | |
| 1065 | + | repoName := "testrepo" | |
| 1066 | + | jobID := "j999" | |
| 1067 | + | artifactsDir := filepath.Join(tmpDir, repoName, jobID, "artifacts") | |
| 1068 | + | ||
| 1069 | + | if err := os.MkdirAll(filepath.Join(artifactsDir, "build"), 0755); err != nil { | |
| 1070 | + | t.Fatalf("mkdir failed: %v", err) | |
| 1071 | + | } | |
| 1072 | + | if err := os.WriteFile(filepath.Join(artifactsDir, "build", "output.json"), []byte("{}"), 0644); err != nil { | |
| 1073 | + | t.Fatalf("write file failed: %v", err) | |
| 1074 | + | } | |
| 1075 | + | ||
| 1076 | + | htmlContent, txtContent := generateJobIndex(tmpDir, repoName, jobID, nil) | |
| 1077 | + | if !strings.Contains(htmlContent, "build/output.json") { | |
| 1078 | + | t.Errorf("html index missing nested artifact build/output.json: %s", htmlContent) | |
| 1079 | + | } | |
| 1080 | + | if !strings.Contains(txtContent, "build/output.json") { | |
| 1081 | + | t.Errorf("txt index missing nested artifact build/output.json: %s", txtContent) | |
| 1082 | + | } | |
| 1083 | + | } | |
| 1084 | + |