package main
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"craftdesign.group/quarry/internal/ci"
)
// Log retention (RFC 0009 §logs, as amended for dagnats#624's scope):
// the engine owns the hot lane; a forge that needs a verdict to stay
// explainable for years stores the logs next to the verdict. On a
// run's terminal transition the poller drains every step's chunks into
// one object per run under the derived-data directory. An engine that
// predates the lane simply leaves nothing to drain.
// ciLogPath is the drained object: data/<repo>/ci-logs/<runID>.json.
func ciLogPath(dir, repo, runID string) string {
return filepath.Join(dir, repo, "ci-logs", runID+".json")
}
// drainLogs pulls a finished run's chunks and stores them, each chunk
// stamped with its step. Draining twice is a cheap overwrite with the
// same bytes; an engine without the lane is not an error, it is an
// empty drain.
func drainLogs(e *ci.Engine, dir, repo, runID string, steps []string) error {
var all []ci.LogChunk
for _, step := range steps {
chunks, err := e.Logs(runID, step, 0, false)
if errors.Is(err, ci.ErrNoLogs) {
return nil
}
if err != nil {
return err
}
for _, c := range chunks {
c.Step = step
all = append(all, c)
}
}
if len(all) == 0 {
return nil
}
p := ciLogPath(dir, repo, runID)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return err
}
b, err := json.Marshal(all)
if err != nil {
return err
}
return os.WriteFile(p, b, 0o644)
}
// drainedLogs is the read side: the stored chunks, or nothing.
type ciLogStore struct{ dir string }
func (s ciLogStore) DrainedLogs(repo, runID string) ([]ci.LogChunk, bool) {
b, err := os.ReadFile(ciLogPath(s.dir, repo, runID))
if err != nil {
return nil, false
}
var chunks []ci.LogChunk
if json.Unmarshal(b, &chunks) != nil {
return nil, false
}
return chunks, true
}