Centralize atomic artifact writes

This commit is contained in:
2026-05-29 20:33:35 +00:00
parent 7dc2ac9253
commit 1355605e70
13 changed files with 207 additions and 181 deletions

View File

@@ -10,9 +10,9 @@ import (
"strings"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
)
@@ -99,7 +99,7 @@ func (s *FilesystemStore) SaveBriefing(_ context.Context, resolved report.Resolv
if err != nil {
return "", err
}
if err := writeJSONAtomic(paths.Briefing, pkg); err != nil {
if err := fileutil.WriteJSONAtomic(paths.Briefing, pkg); err != nil {
return "", err
}
return paths.Briefing, nil
@@ -113,21 +113,18 @@ func (s *FilesystemStore) SaveDataPackage(_ context.Context, resolved report.Res
if err := promptinput.Validate(pkg); err != nil {
return "", err
}
if err := writeJSONAtomic(paths.DataPackage, pkg); err != nil {
if err := fileutil.WriteJSONAtomic(paths.DataPackage, pkg); err != nil {
return "", err
}
return paths.DataPackage, nil
}
func (s *FilesystemStore) SavePreflight(_ context.Context, resolved report.Resolved, result *scriptorium.RenderResult) (string, error) {
if result == nil {
return "", fmt.Errorf("render result is required")
}
func (s *FilesystemStore) SavePreflight(_ context.Context, resolved report.Resolved, artifact PreflightArtifact) (string, error) {
paths, err := s.Paths(resolved)
if err != nil {
return "", err
}
if err := writeJSONAtomic(paths.Preflight, result); err != nil {
if err := fileutil.WriteJSONAtomic(paths.Preflight, artifact); err != nil {
return "", err
}
return paths.Preflight, nil
@@ -161,7 +158,7 @@ func (s *FilesystemStore) SaveMetadata(_ context.Context, metadata Metadata) (st
if path == "" {
return "", fmt.Errorf("metadata path cannot be resolved")
}
if err := writeJSONAtomic(path, metadata); err != nil {
if err := fileutil.WriteJSONAtomic(path, metadata); err != nil {
return "", err
}
return path, nil
@@ -362,33 +359,6 @@ func validateRelativeDir(name string, value string) error {
return nil
}
func writeJSONAtomic(path string, value any) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return fmt.Errorf("marshal %q: %w", path, err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary file for %q: %w", path, err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary file for %q: %w", path, err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary file for %q: %w", path, err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save %q: %w", path, err)
}
return nil
}
func readJSON(path string, target any) error {
data, err := os.ReadFile(path)
if err != nil {

View File

@@ -9,7 +9,6 @@ import (
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
@@ -56,7 +55,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if err != nil {
t.Fatalf("SaveDataPackage() error = %v", err)
}
preflightPath, err := store.SavePreflight(context.Background(), resolved, &scriptorium.RenderResult{Stdout: `{"ok":true}`})
preflightPath, err := store.SavePreflight(context.Background(), resolved, PreflightArtifact{Stdout: `{"ok":true}`})
if err != nil {
t.Fatalf("SavePreflight() error = %v", err)
}
@@ -67,6 +66,17 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if err := os.WriteFile(renderedReportPath, []byte("# Daily Report\n"), 0o600); err != nil {
t.Fatalf("write rendered report: %v", err)
}
var preflight PreflightArtifact
preflightData, err := os.ReadFile(preflightPath)
if err != nil {
t.Fatalf("read preflight: %v", err)
}
if err := json.Unmarshal(preflightData, &preflight); err != nil {
t.Fatalf("decode preflight: %v", err)
}
if preflight.Stdout != `{"ok":true}` {
t.Fatalf("preflight stdout = %q, want render stdout", preflight.Stdout)
}
paths, err := store.Paths(resolved)
if err != nil {
t.Fatalf("Paths() error = %v", err)

View File

@@ -4,7 +4,6 @@ package state
import (
"context"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
@@ -14,7 +13,7 @@ type Store interface {
Paths(report.Resolved) (ArtifactPaths, error)
SaveBriefing(context.Context, report.Resolved, briefing.Package) (string, error)
SaveDataPackage(context.Context, report.Resolved, promptinput.Package) (string, error)
SavePreflight(context.Context, report.Resolved, *scriptorium.RenderResult) (string, error)
SavePreflight(context.Context, report.Resolved, PreflightArtifact) (string, error)
PrepareRenderedReport(context.Context, report.Resolved) (string, error)
SaveMetadata(context.Context, Metadata) (string, error)
FindPriorSnapshot(context.Context, report.Resolved) (*PriorSnapshot, error)
@@ -26,3 +25,12 @@ type PriorSnapshot struct {
Metadata Metadata
BriefingPath string
}
type PreflightArtifact struct {
Command []string `json:"command"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
StdoutTruncated bool `json:"stdoutTruncated,omitempty"`
StderrTruncated bool `json:"stderrTruncated,omitempty"`
ExitCode int `json:"exitCode"`
}