refactor: share one atomic write path for generated files

README.md and site/data.json were written with os.Create/os.WriteFile,
so a write that failed midway left the repo front page truncated, while
history.jsonl already used a temp-file-plus-rename. Extract that pattern
into atomicWriteFile and route all three writers through it.

Also sort snapshots by date when reading history.jsonl: delta windows
pick the newest snapshot inside the window by scan order, which silently
produces wrong deltas if a hand edit or a merge of two concurrent runs
interleaves lines.

Cover the README renderer, which had no test beyond sanitizeCell, and
name the generated paths as constants instead of repeating literals.
Coverage 65.1% -> 75.7%.
This commit is contained in:
tiennm99 committed 2026-09-11 14:29:49 +07:00
1 parent 1262e3435b
commit fde3ae7efd
8 files changed
+280 -62

No files matched your search

+8 -16
View File
@@ -2,7 +2,7 @@ package main
import (
"fmt"
"os"
"io"
"path/filepath"
"strings"
"text/template"
@@ -84,22 +84,14 @@ func renderReadme(tmplPath, outPath string, stats []Stat, deltas map[string]int)
return err
}
f, err := os.Create(outPath)
if err != nil {
return err
}
execErr := tmpl.ExecuteTemplate(f, filepath.Base(tmplPath), map[string]any{
"Rows": rows,
"UpdatedAt": timeNow().UTC().Format("2006-01-02 15:04 UTC"),
"Total": len(rows),
"TopMover": topMover,
return atomicWriteFile(outPath, func(w io.Writer) error {
return tmpl.ExecuteTemplate(w, filepath.Base(tmplPath), map[string]any{
"Rows": rows,
"UpdatedAt": timeNow().UTC().Format("2006-01-02 15:04 UTC"),
"Total": len(rows),
"TopMover": topMover,
})
})
// A failed close on a write path can hide lost data — surface it.
if closeErr := f.Close(); closeErr != nil && execErr == nil {
execErr = closeErr
}
return execErr
}
// sanitizeCell makes a third-party repo description safe to embed in a