Require explicit manifest schemas

This commit is contained in:
2026-09-02 11:45:40 +00:00
parent 9679117e94
commit 2cfca710ec
17 changed files with 307 additions and 123 deletions

View File

@@ -98,6 +98,9 @@ func (s *LocalStore) Save(ctx context.Context, path string, m *Manifest) error {
if m == nil {
return fmt.Errorf("save manifest: manifest is nil")
}
if err := validateSessionManifestSchemaVersion(m.SchemaVersion); err != nil {
return fmt.Errorf("save manifest: %w", err)
}
if strings.TrimSpace(m.SessionID) == "" {
return fmt.Errorf("save manifest: session_id is required")
}
@@ -110,10 +113,10 @@ func (s *LocalStore) Save(ctx context.Context, path string, m *Manifest) error {
if err := validatePostPublishCleanup(m.PostPublishCleanup); err != nil {
return fmt.Errorf("save manifest: %w", err)
}
if err := validateSessionStages(m.Stages); err != nil {
return fmt.Errorf("save manifest: %w", err)
}
for name, stage := range m.Stages {
if stage == nil {
continue
}
normalizeAnalyzeArtifactCollection(stage.AnalyzeArtifacts)
if err := validateAnalyzeStageState(name, stage.AnalyzeStateVersion, stage.AnalyzeArtifacts); err != nil {
return fmt.Errorf("save manifest: stages.%s: %w", name, err)
@@ -121,10 +124,6 @@ func (s *LocalStore) Save(ctx context.Context, path string, m *Manifest) error {
}
m.UpdatedAt = time.Now().UTC()
if m.Stages == nil {
m.Stages = map[string]*StageRecord{}
}
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
return fmt.Errorf("save manifest: marshal: %w", err)
@@ -213,6 +212,9 @@ func (s *LocalStore) SaveRun(ctx context.Context, path string, m *RunManifest) e
if m == nil {
return fmt.Errorf("save run manifest: manifest is nil")
}
if err := validateRunManifestSchemaVersion(m.SchemaVersion); err != nil {
return fmt.Errorf("save run manifest: %w", err)
}
if strings.TrimSpace(m.SessionID) == "" {
return fmt.Errorf("save run manifest: session_id is required")
}
@@ -225,10 +227,10 @@ func (s *LocalStore) SaveRun(ctx context.Context, path string, m *RunManifest) e
if err := validateManifestIdentities(m.SessionID, m.Campaign, m.RunID); err != nil {
return fmt.Errorf("save run manifest: %w", err)
}
if err := validateRunStages(m.Stages); err != nil {
return fmt.Errorf("save run manifest: %w", err)
}
for name, stage := range m.Stages {
if stage == nil {
continue
}
normalizeAnalyzeArtifactCollection(stage.AnalyzeArtifacts)
if err := validateAnalyzeStageState(name, stage.AnalyzeStateVersion, stage.AnalyzeArtifacts); err != nil {
return fmt.Errorf("save run manifest: stages.%s: %w", name, err)
@@ -236,10 +238,6 @@ func (s *LocalStore) SaveRun(ctx context.Context, path string, m *RunManifest) e
}
m.UpdatedAt = time.Now().UTC()
if m.Stages == nil {
m.Stages = map[string]*RunStageRecord{}
}
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
return fmt.Errorf("save run manifest: marshal: %w", err)
@@ -253,6 +251,9 @@ func validateLoadedManifest(m *Manifest) error {
if m == nil {
return fmt.Errorf("manifest is nil")
}
if err := validateSessionManifestSchemaVersion(m.SchemaVersion); err != nil {
return err
}
if strings.TrimSpace(m.SessionID) == "" {
return fmt.Errorf("session_id is required")
}
@@ -268,10 +269,10 @@ func validateLoadedManifest(m *Manifest) error {
if err := validatePostPublishCleanup(m.PostPublishCleanup); err != nil {
return err
}
if err := validateSessionStages(m.Stages); err != nil {
return err
}
for name, stage := range m.Stages {
if stage == nil {
continue
}
if err := validateAnalyzeStageState(name, stage.AnalyzeStateVersion, stage.AnalyzeArtifacts); err != nil {
return fmt.Errorf("stages.%s: %w", name, err)
}
@@ -286,17 +287,7 @@ func validateLoadedManifest(m *Manifest) error {
}
func normalizeManifest(m *Manifest) {
if m.Stages == nil {
m.Stages = map[string]*StageRecord{}
}
for name, stage := range m.Stages {
if stage == nil {
stage = &StageRecord{Name: name, Status: StatusPending, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt}
m.Stages[name] = stage
}
if stage.Name == "" {
stage.Name = name
}
for _, stage := range m.Stages {
normalizeAnalyzeArtifactCollection(stage.AnalyzeArtifacts)
}
}
@@ -326,6 +317,9 @@ func validateLoadedRunManifest(m *RunManifest) error {
if m == nil {
return fmt.Errorf("manifest is nil")
}
if err := validateRunManifestSchemaVersion(m.SchemaVersion); err != nil {
return err
}
if strings.TrimSpace(m.SessionID) == "" {
return fmt.Errorf("session_id is required")
}
@@ -341,10 +335,10 @@ func validateLoadedRunManifest(m *RunManifest) error {
if err := validateManifestIdentities(m.SessionID, m.Campaign, m.RunID); err != nil {
return err
}
if err := validateRunStages(m.Stages); err != nil {
return err
}
for name, stage := range m.Stages {
if stage == nil {
continue
}
if err := validateAnalyzeStageState(name, stage.AnalyzeStateVersion, stage.AnalyzeArtifacts); err != nil {
return fmt.Errorf("stages.%s: %w", name, err)
}
@@ -386,27 +380,65 @@ func validateManifestIdentities(sessionID, campaign, runID string) error {
}
func normalizeRunManifest(m *RunManifest) {
if m.Stages == nil {
m.Stages = map[string]*RunStageRecord{}
}
for name, stage := range m.Stages {
if stage == nil {
stage = &RunStageRecord{
Name: name,
Action: RunStageActionRun,
Status: StatusPending,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
m.Stages[name] = stage
}
if stage.Name == "" {
stage.Name = name
}
for _, stage := range m.Stages {
normalizeAnalyzeArtifactCollection(stage.AnalyzeArtifacts)
}
}
func validateSessionManifestSchemaVersion(version string) error {
return validateManifestSchemaVersion("session manifest", version, SessionManifestSchemaVersion)
}
func validateRunManifestSchemaVersion(version string) error {
return validateManifestSchemaVersion("run manifest", version, RunManifestSchemaVersion)
}
func validateManifestSchemaVersion(kind, version, supported string) error {
if version == supported {
return nil
}
if version == "" {
return fmt.Errorf("%s schema_version is required; move aside and regenerate this unsupported local state (supported: %s)", kind, supported)
}
return fmt.Errorf("unsupported %s schema_version %q; move aside and regenerate this unsupported local state (supported: %s)", kind, version, supported)
}
func validateSessionStages(stages map[string]*StageRecord) error {
if stages == nil {
return fmt.Errorf("stages is required")
}
for name, stage := range stages {
if stage == nil {
return fmt.Errorf("stages.%s is nil", name)
}
if stage.Name == "" {
return fmt.Errorf("stages.%s.name is required", name)
}
if stage.Name != name {
return fmt.Errorf("stages.%s.name %q does not match map key", name, stage.Name)
}
}
return nil
}
func validateRunStages(stages map[string]*RunStageRecord) error {
if stages == nil {
return fmt.Errorf("stages is required")
}
for name, stage := range stages {
if stage == nil {
return fmt.Errorf("stages.%s is nil", name)
}
if stage.Name == "" {
return fmt.Errorf("stages.%s.name is required", name)
}
if stage.Name != name {
return fmt.Errorf("stages.%s.name %q does not match map key", name, stage.Name)
}
}
return nil
}
func writeJSONAtomically(ctx context.Context, path string, data []byte) error {
dir := filepath.Dir(path)
if err := fileops.EnsureWorkspaceDirectory(dir); err != nil {