package cli import ( "encoding/json" "fmt" "io" "path/filepath" "strings" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) const runResultSchemaVersion = "notarius.run-result.v1" type runResult struct { SchemaVersion string `json:"schema_version"` RunID string `json:"run_id"` PipelineID string `json:"pipeline_id"` OutputDirectory string `json:"output_directory"` IndexFile string `json:"index_file,omitempty"` NormalizedOutputCount int `json:"normalized_output_count"` RejectedOutputCount int `json:"rejected_output_count"` WarningCount int `json:"warning_count"` ValidationStatus string `json:"validation_status"` DebugDirectory string `json:"debug_directory,omitempty"` } func newRunResult(resolved pipeline.ResolvedPipeline, output pipeline.RunOutput, outputDirectory, debugDirectory string) (runResult, error) { if strings.TrimSpace(output.Manifest.RunID) == "" { return runResult{}, fmt.Errorf("run result requires a run ID") } if strings.TrimSpace(resolved.ID) == "" { return runResult{}, fmt.Errorf("run result requires a resolved pipeline ID") } if strings.TrimSpace(output.Manifest.PipelineID) == "" { return runResult{}, fmt.Errorf("run result requires a manifest pipeline ID") } if output.Manifest.PipelineID != resolved.ID { return runResult{}, fmt.Errorf("run result pipeline ID does not match resolved pipeline") } if strings.TrimSpace(output.Manifest.ValidationStatus) == "" { return runResult{}, fmt.Errorf("run result requires a validation status") } if strings.TrimSpace(outputDirectory) == "" { return runResult{}, fmt.Errorf("run result requires an output directory") } absOutputDirectory, err := filepath.Abs(outputDirectory) if err != nil { return runResult{}, fmt.Errorf("make output directory absolute: %w", err) } result := runResult{ SchemaVersion: runResultSchemaVersion, RunID: output.Manifest.RunID, PipelineID: output.Manifest.PipelineID, OutputDirectory: absOutputDirectory, NormalizedOutputCount: len(output.NormalizeOutputs), RejectedOutputCount: len(output.Rejected), WarningCount: len(output.Warnings), ValidationStatus: output.Manifest.ValidationStatus, } if strings.TrimSpace(debugDirectory) != "" { absDebugDirectory, err := filepath.Abs(debugDirectory) if err != nil { return runResult{}, fmt.Errorf("make debug directory absolute: %w", err) } result.DebugDirectory = absDebugDirectory } if resolved.Output.Module == pipeline.DefaultOutputModule { indexCount := 0 for _, file := range output.OutputFiles { if file.Name == "index.json" { indexCount++ } } if indexCount != 1 { return runResult{}, fmt.Errorf("production JSON output must contain exactly one index.json file") } result.IndexFile = "index.json" } return result, nil } func encodeRunResult(result runResult) ([]byte, error) { encoded, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("encode run result: %w", err) } return append(encoded, '\n'), nil } func writeRunResult(writer io.Writer, content []byte) error { for len(content) > 0 { written, err := writer.Write(content) if written < 0 || written > len(content) { return io.ErrShortWrite } content = content[written:] if err != nil { return err } if written == 0 { return io.ErrShortWrite } } return nil }