Commit 25ab3ff

Eric Bower  ·  2026-08-30 09:45:36 -0400 EDT
parent e54894f
feat: cache prev run based on PICI_EVENT
5 files changed,  +108, -36
+6, -7
......@@ -1041,11 +1041,11 @@ See: https://github.com/picosh/pici
10411041
10421042 // waitAndReport polls the job's sessions until all complete, prints live
10431043 // interactive progress to stdout, updates stats.json, and outputs a final summary.
1044-func waitAndReport(cfg *Cfg, log *slog.Logger, name, jobID, eventType string) error {
1044+func waitAndReport(cfg *Cfg, log *slog.Logger, name, jobID, domain, eventType string) error {
10451045 cwd, _ := os.Getwd()
10461046 branch := detectGitBranch(cwd)
10471047 commit := detectGitCommit(cwd)
1048- return waitAndReportUI(cfg, log, name, jobID, eventType, branch, commit)
1048+ return waitAndReportUI(cfg, log, name, jobID, domain, eventType, branch, commit)
10491049 }
10501050
10511051 func writeSummaryMarkdown(filePath string, repoName, jobID string, jobSessions []SessionInfo, known map[string]*sessionState, debugActive bool, debugTopic string) error {
......@@ -2467,7 +2467,7 @@ func runLocal(cfg *Cfg, dest string) error {
24672467
24682468 // Wait for completion & print live progress
24692469 var waitErr error
2470- if err := waitAndReport(cfg, log, eventData.Name, jobID, "local"); err != nil {
2470+ if err := waitAndReport(cfg, log, eventData.Name, jobID, "local", eventData.Type); err != nil {
24712471 waitErr = err
24722472 }
24732473
......@@ -2528,10 +2528,9 @@ func writePublishedSentinel(artifactDir, repoName, jobID, status string, exitCod
25282528 return os.WriteFile(sentinel, publishedJSON, 0644)
25292529 }
25302530
2531-func stageLocalArtifacts(cfg *Cfg, log *slog.Logger, repoName, jobID, domainOrEvent string) error {
2532- domain := domainOrEvent
2533- if domain != "local" && domain != "ci" {
2534- domain = getDomain(domainOrEvent)
2531+func stageLocalArtifacts(cfg *Cfg, log *slog.Logger, repoName, jobID, domain string) error {
2532+ if domain == "" {
2533+ domain = "local"
25352534 }
25362535 prefix := fmt.Sprintf("%s.%s.%s.", domain, repoName, jobID)
25372536
+3, -3
......@@ -953,7 +953,7 @@ func TestWaitAndReport_Cancellation(t *testing.T) {
953953 cancel()
954954 }()
955955
956- err := waitAndReport(cfg, nil, "testrepo", jobID, "local")
956+ err := waitAndReport(cfg, nil, "testrepo", jobID, "local", "local")
957957 if err == nil {
958958 t.Error("expected error when waitAndReport is cancelled, got nil")
959959 }
......@@ -1158,7 +1158,7 @@ func TestWaitAndReport_ExitCodeFailure(t *testing.T) {
11581158 t.Fatalf("failed to start zmx session: %v", err)
11591159 }
11601160
1161- err := waitAndReport(cfg, nil, "testrepo", jobID, "local")
1161+ err := waitAndReport(cfg, nil, "testrepo", jobID, "local", "local")
11621162 if err == nil {
11631163 t.Fatal("expected error from waitAndReport for failed job, got nil")
11641164 }
......@@ -1196,7 +1196,7 @@ func TestWaitAndReport_Success(t *testing.T) {
11961196 t.Fatalf("failed to start zmx session: %v", err)
11971197 }
11981198
1199- err := waitAndReport(cfg, nil, "testrepo", jobID, "local")
1199+ err := waitAndReport(cfg, nil, "testrepo", jobID, "local", "local")
12001200 if err != nil {
12011201 t.Fatalf("expected nil error for successful job, got %v", err)
12021202 }
+7, -5
......@@ -755,14 +755,16 @@ func isTerminal(w io.Writer) bool {
755755 }
756756
757757 // waitAndReportUI executes the rich interactive reporting loop, updating stats.json.
758-func waitAndReportUI(cfg *Cfg, log *slog.Logger, repoName, jobID, domainOrEvent, branch, commit string) error {
759- domain := domainOrEvent
760- if domain != "local" && domain != "ci" {
761- domain = getDomain(domainOrEvent)
758+func waitAndReportUI(cfg *Cfg, log *slog.Logger, repoName, jobID, domain, eventType, branch, commit string) error {
759+ if domain == "" {
760+ domain = "local"
761+ }
762+ if eventType == "" {
763+ eventType = "local"
762764 }
763765 prefix := fmt.Sprintf("%s.%s.%s.", domain, repoName, jobID)
764766
765- stats, _ := loadRepoStats(repoName)
767+ stats, _ := loadRepoStats(repoName, eventType)
766768 isTTY := isTerminal(os.Stdout)
767769
768770 ctrl := newInteractiveController(cfg.Ctx, os.Stdout, repoName, branch, commit, jobID, stats, isTTY)
+22, -7
......@@ -11,6 +11,7 @@ import (
1111 // RepoStats tracks historical run durations and outcomes across runs.
1212 type RepoStats struct {
1313 Repo string `json:"repo"`
14+ Event string `json:"event,omitempty"`
1415 LastRunAt string `json:"last_run_at"`
1516 LastOverallStatus string `json:"last_overall_status"`
1617 RunsCount int `json:"runs_count"`
......@@ -29,8 +30,11 @@ type TaskStats struct {
2930 LastStartedAt int64 `json:"last_started_at,omitempty"`
3031 }
3132
32-// getStatsPath returns the path to stats.json for the specified repository.
33-func getStatsPath(repo string) string {
33+// getStatsPath returns the path to stats.json for the specified repository and event.
34+func getStatsPath(repo, event string) string {
35+ if event == "" {
36+ event = "local"
37+ }
3438 cacheDir := os.Getenv("XDG_CACHE_HOME")
3539 if cacheDir == "" {
3640 home, err := os.UserHomeDir()
......@@ -40,17 +44,21 @@ func getStatsPath(repo string) string {
4044 cacheDir = filepath.Join(home, ".cache")
4145 }
4246 }
43- return filepath.Join(cacheDir, "pici", repo, "stats.json")
47+ return filepath.Join(cacheDir, "pici", repo, event, "stats.json")
4448 }
4549
46-// loadRepoStats loads historical stats for a repo, or returns an initialized empty struct.
47-func loadRepoStats(repo string) (*RepoStats, error) {
48- path := getStatsPath(repo)
50+// loadRepoStats loads historical stats for a repo and event, or returns an initialized empty struct.
51+func loadRepoStats(repo, event string) (*RepoStats, error) {
52+ if event == "" {
53+ event = "local"
54+ }
55+ path := getStatsPath(repo, event)
4956 data, err := os.ReadFile(path)
5057 if err != nil {
5158 if os.IsNotExist(err) {
5259 return &RepoStats{
5360 Repo: repo,
61+ Event: event,
5462 Tasks: make(map[string]*TaskStats),
5563 }, nil
5664 }
......@@ -61,9 +69,13 @@ func loadRepoStats(repo string) (*RepoStats, error) {
6169 if err := json.Unmarshal(data, &stats); err != nil {
6270 return &RepoStats{
6371 Repo: repo,
72+ Event: event,
6473 Tasks: make(map[string]*TaskStats),
6574 }, nil
6675 }
76+ if stats.Event == "" {
77+ stats.Event = event
78+ }
6779 if stats.Tasks == nil {
6880 stats.Tasks = make(map[string]*TaskStats)
6981 }
......@@ -75,7 +87,10 @@ func saveRepoStats(stats *RepoStats) error {
7587 if stats == nil || stats.Repo == "" {
7688 return nil
7789 }
78- path := getStatsPath(stats.Repo)
90+ if stats.Event == "" {
91+ stats.Event = "local"
92+ }
93+ path := getStatsPath(stats.Repo, stats.Event)
7994 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
8095 return err
8196 }
+70, -14
......@@ -84,15 +84,28 @@ func TestStats_StorageAndPath(t *testing.T) {
8484 t.Setenv("XDG_CACHE_HOME", tempCache)
8585
8686 repo := "my-project"
87- expectedPath := filepath.Join(tempCache, "pici", repo, "stats.json")
88- path := getStatsPath(repo)
89- if path != expectedPath {
90- t.Errorf("expected stats path %s, got %s", expectedPath, path)
87+ expectedLocalPath := filepath.Join(tempCache, "pici", repo, "local", "stats.json")
88+ pathLocal := getStatsPath(repo, "local")
89+ if pathLocal != expectedLocalPath {
90+ t.Errorf("expected stats path %s, got %s", expectedLocalPath, pathLocal)
9191 }
9292
93- // Test Save & Load
94- stats := &RepoStats{
93+ // Default event should be "local"
94+ pathDefault := getStatsPath(repo, "")
95+ if pathDefault != expectedLocalPath {
96+ t.Errorf("expected default stats path %s, got %s", expectedLocalPath, pathDefault)
97+ }
98+
99+ expectedTagPath := filepath.Join(tempCache, "pici", repo, "git.tag", "stats.json")
100+ pathTag := getStatsPath(repo, "git.tag")
101+ if pathTag != expectedTagPath {
102+ t.Errorf("expected stats path %s, got %s", expectedTagPath, pathTag)
103+ }
104+
105+ // Test Save & Load for "local" event
106+ statsLocal := &RepoStats{
95107 Repo: repo,
108+ Event: "local",
96109 LastRunAt: time.Now().UTC().Format(time.RFC3339),
97110 LastOverallStatus: "success",
98111 RunsCount: 5,
......@@ -109,19 +122,62 @@ func TestStats_StorageAndPath(t *testing.T) {
109122 },
110123 }
111124
112- if err := saveRepoStats(stats); err != nil {
113- t.Fatalf("failed to save stats: %v", err)
125+ if err := saveRepoStats(statsLocal); err != nil {
126+ t.Fatalf("failed to save local stats: %v", err)
127+ }
128+
129+ // Test Save & Load for "git.tag" event
130+ statsTag := &RepoStats{
131+ Repo: repo,
132+ Event: "git.tag",
133+ LastRunAt: time.Now().UTC().Format(time.RFC3339),
134+ LastOverallStatus: "failed",
135+ RunsCount: 1,
136+ AvgWallDurationMs: 60000,
137+ Tasks: map[string]*TaskStats{
138+ "deploy": {
139+ AvgDurationMs: 30000,
140+ LastDurationMs: 30000,
141+ LastStatus: "failed",
142+ LastExitCode: 1,
143+ SeenCount: 1,
144+ LastStartedAt: 1700000100,
145+ },
146+ },
147+ }
148+
149+ if err := saveRepoStats(statsTag); err != nil {
150+ t.Fatalf("failed to save tag stats: %v", err)
114151 }
115152
116- loaded, err := loadRepoStats(repo)
153+ // Verify local stats
154+ loadedLocal, err := loadRepoStats(repo, "local")
117155 if err != nil {
118- t.Fatalf("failed to load stats: %v", err)
156+ t.Fatalf("failed to load local stats: %v", err)
157+ }
158+ if loadedLocal.Repo != repo || loadedLocal.Event != "local" || loadedLocal.RunsCount != 5 || loadedLocal.AvgWallDurationMs != 12000 {
159+ t.Errorf("loaded local stats mismatch: %+v", loadedLocal)
160+ }
161+ if task, ok := loadedLocal.Tasks["build"]; !ok || task.AvgDurationMs != 6000 || task.LastStartedAt != 1700000000 {
162+ t.Errorf("loaded local task stats mismatch: %+v", task)
163+ }
164+ if _, ok := loadedLocal.Tasks["deploy"]; ok {
165+ t.Errorf("expected deploy task NOT to be in local stats")
166+ }
167+
168+ // Verify tag stats
169+ loadedTag, err := loadRepoStats(repo, "git.tag")
170+ if err != nil {
171+ t.Fatalf("failed to load tag stats: %v", err)
172+ }
173+ if loadedTag.Repo != repo || loadedTag.Event != "git.tag" || loadedTag.RunsCount != 1 || loadedTag.AvgWallDurationMs != 60000 {
174+ t.Errorf("loaded tag stats mismatch: %+v", loadedTag)
119175 }
120- if loaded.Repo != repo || loaded.RunsCount != 5 || loaded.AvgWallDurationMs != 12000 {
121- t.Errorf("loaded stats mismatch: %+v", loaded)
176+ if task, ok := loadedTag.Tasks["deploy"]; !ok || task.AvgDurationMs != 30000 || task.LastStartedAt != 1700000100 {
177+ t.Errorf("loaded tag task stats mismatch: %+v", task)
122178 }
123- if task, ok := loaded.Tasks["build"]; !ok || task.AvgDurationMs != 6000 || task.LastStartedAt != 1700000000 {
124- t.Errorf("loaded task stats mismatch: %+v", task)
179+ if _, ok := loadedTag.Tasks["build"]; ok {
180+ t.Errorf("expected build task NOT to be in tag stats")
125181 }
126182 }
127183