// Package changelog manages the infrastructure changelog log. package changelog import ( "encoding/json" "fmt" "os" "sort" "time" ) // Categories available for changelog entries. var Categories = []string{"hardware", "network", "software", "migration"} // Entry is a single changelog entry. type Entry struct { ID string `json:"id"` Date string `json:"date"` // YYYY-MM-DD Title string `json:"title"` Description string `json:"description"` Category string `json:"category"` // hardware, network, software, migration } // Log holds all changelog entries. type Log struct { Entries []Entry `json:"entries"` } // Load reads the changelog from path. Returns an empty log if the file does not exist. func Load(path string) (*Log, error) { raw, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return &Log{}, nil } return nil, err } var l Log if err := json.Unmarshal(raw, &l); err != nil { return nil, err } sort.Slice(l.Entries, func(i, j int) bool { return l.Entries[i].Date > l.Entries[j].Date }) return &l, nil } // save writes the log to path without sorting (internal use). func save(path string, l *Log) error { raw, err := json.MarshalIndent(l, "", " ") if err != nil { return err } return os.WriteFile(path, raw, 0644) } // Add appends a new entry and saves. func Add(path string, e Entry) error { l, err := Load(path) if err != nil { return err } e.ID = fmt.Sprintf("%d", time.Now().UnixNano()) l.Entries = append(l.Entries, e) return save(path, l) } // Update replaces an entry by ID and saves. func Update(path string, e Entry) error { l, err := Load(path) if err != nil { return err } for i, entry := range l.Entries { if entry.ID == e.ID { l.Entries[i] = e return save(path, l) } } return fmt.Errorf("entry %s not found", e.ID) } // Delete removes an entry by ID and saves. func Delete(path string, id string) error { l, err := Load(path) if err != nil { return err } kept := l.Entries[:0] for _, e := range l.Entries { if e.ID != id { kept = append(kept, e) } } l.Entries = kept return save(path, l) } // Get returns a single entry by ID. func Get(path string, id string) (*Entry, error) { l, err := Load(path) if err != nil { return nil, err } for _, e := range l.Entries { if e.ID == id { return &e, nil } } return nil, fmt.Errorf("entry %s not found", id) }