// Package status records per-repo deploy state (which commit is in production / // preview) so the forge can badge commits. Backed by a single JSON file. package status import ( "encoding/json" "os" "path/filepath" "sync" ) // Entry is the commit + URL for a deploy state. type Entry struct { Commit string `json:"commit"` URL string `json:"url"` } // Repo holds the current production + preview pointers for one repo. type Repo struct { Production Entry `json:"production"` Preview Entry `json:"preview"` } // Store is a concurrency-safe JSON-backed status store. type Store struct { path string mu sync.Mutex data map[string]Repo } // Open loads the store from path (creating an empty one if absent). func Open(path string) (*Store, error) { s := &Store{path: path, data: map[string]Repo{}} b, err := os.ReadFile(path) if err == nil { _ = json.Unmarshal(b, &s.data) } else if !os.IsNotExist(err) { return nil, err } return s, nil } // Set updates a repo's production or preview pointer and persists. func (s *Store) Set(repo, state, commit, url string) error { s.mu.Lock() defer s.mu.Unlock() r := s.data[repo] switch state { case "prod", "production": r.Production = Entry{Commit: commit, URL: url} case "preview": r.Preview = Entry{Commit: commit, URL: url} default: return nil // ignore unknown states } s.data[repo] = r return s.persist() } // Badge returns the deploy state of a commit ("production"|"preview"|"") and its // URL. Empty state means unverified. func (s *Store) Badge(repo, commit string) (state, url string) { if commit == "" { return "", "" } s.mu.Lock() defer s.mu.Unlock() r := s.data[repo] switch commit { case r.Production.Commit: return "production", r.Production.URL case r.Preview.Commit: return "preview", r.Preview.URL } return "", "" } // Has reports whether a repo has any recorded deploy status. func (s *Store) Has(repo string) bool { s.mu.Lock() defer s.mu.Unlock() r, ok := s.data[repo] return ok && (r.Production.Commit != "" || r.Preview.Commit != "") } func (s *Store) persist() error { if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return err } b, err := json.MarshalIndent(s.data, "", " ") if err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, s.path) }