Keep failed processing reports consistent

This commit is contained in:
2026-11-10 20:16:59 +00:00
parent 3eacb33eb0
commit af0f9ac960
9 changed files with 115 additions and 41 deletions

View File

@@ -96,10 +96,15 @@ func prepareProcess(ctx context.Context, input processInput, loaded *processInpu
if err != nil {
return nil, err
}
snapshot.Status = reporting.StatusFailed
failed, err := renderReportBundle(snapshot, secrets...)
if err != nil {
return nil, err
failed := func(primary error, failures []diagnostics.FileFailure) diagnostics.ReportBundle {
failedSnapshot := snapshot
completed := time.Now().UTC()
failedSnapshot.CompletedAt = &completed
if primary != nil {
failedSnapshot.Errors = []reporting.ErrorRecord{{Scope: "run", Phase: "initialization", Reason: "canceled", Message: "Processing was canceled."}}
}
bundle, _ := renderReportBundle(failedOutputSnapshot(failedSnapshot, failures), secrets...)
return bundle
}
var debug *preflight.DebugInput
var warnings []error

View File

@@ -23,8 +23,10 @@ func finishProcess(inv commandkit.Invocation, input processInput, loaded *proces
snapshot.TranscriptPath, snapshot.OutputPath = input.Transcript, input.Output.Value
if primary != nil {
snapshot.Status = reporting.StatusFailed
if len(snapshot.Errors) == 0 {
snapshot.Errors = []reporting.ErrorRecord{{Scope: "run", Reason: "processing_failure", Message: "Processing failed"}}
snapshot = failedOutputSnapshot(snapshot, nil)
if *snapshot.Summary.RunErrors == 0 {
snapshot.Errors = append(snapshot.Errors, reporting.ErrorRecord{Scope: "run", Reason: "processing_failure", Message: "Processing failed"})
snapshot = failedOutputSnapshot(snapshot, nil)
}
} else {
snapshot.Status = reporting.StatusCompleted
@@ -37,13 +39,7 @@ func finishProcess(inv commandkit.Invocation, input processInput, loaded *proces
snapshot.Status = reporting.StatusFailed
}
failed := func(failures []diagnostics.FileFailure) diagnostics.ReportBundle {
copySnapshot := snapshot
copySnapshot.Status = reporting.StatusFailed
copySnapshot.Errors = append([]reporting.ErrorRecord(nil), snapshot.Errors...)
for _, failure := range failures {
copySnapshot.Errors = append(copySnapshot.Errors, reporting.ErrorRecord{Scope: "run", Reason: "output_failure", Message: "Required output delivery failed", OutputPath: failure.Path})
}
result, bundleErr := renderReportBundle(copySnapshot, secrets...)
result, bundleErr := renderReportBundle(failedOutputSnapshot(snapshot, failures), secrets...)
if bundleErr != nil {
return reports
}
@@ -63,3 +59,27 @@ func finishProcess(inv commandkit.Invocation, input processInput, loaded *proces
}
return commandkit.Success()
}
// failedOutputSnapshot derives the count from the same run records sent to each report.
func failedOutputSnapshot(snapshot reporting.Snapshot, failures []diagnostics.FileFailure) reporting.Snapshot {
snapshot.Status = reporting.StatusFailed
if len(failures) > 0 {
snapshot.Errors = append([]reporting.ErrorRecord(nil), snapshot.Errors...)
}
for _, failure := range failures {
record := reporting.ErrorRecord{Scope: "run", Reason: "output_failure", Message: "Required output delivery failed", OutputPath: failure.Path}
if failure.Kind == "report initialization" {
record.Phase = "initialization"
record.Message = "Required report initialization failed"
}
snapshot.Errors = append(snapshot.Errors, record)
}
count := 0
for _, record := range snapshot.Errors {
if record.Scope == "run" {
count++
}
}
snapshot.Summary.RunErrors = &count
return snapshot
}

View File

@@ -10,6 +10,7 @@ import (
"gitea.maximumdirect.net/eric/audita/v3/internal/core/reporting"
commandkit "go.maximumdirect.net/commandkit"
"gopkg.in/yaml.v3"
)
func TestPreparedDeliveryFollowsReportingMode(t *testing.T) {
@@ -76,8 +77,9 @@ func TestPreparedDeliveryFollowsReportingMode(t *testing.T) {
func TestDeliveryFailureSuppressesCompletionSummary(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
for name, contents := range map[string]string{
"config.yml": "version: 3\npipeline:\n stages: []\nreporting:\n enabled: false\n",
"config.yml": "version: 3\npipeline:\n stages: []\n",
"transcript.json": `[{"speaker":"Alice","start":0,"end":1,"text":"hello"}]`,
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(contents), 0644); err != nil {
@@ -105,6 +107,24 @@ func TestDeliveryFailureSuppressesCompletionSummary(t *testing.T) {
if outcome.Status == 0 || strings.Contains(stderr.String(), "Completed:") {
t.Fatalf("status=%d stderr=%q", outcome.Status, stderr.String())
}
var errs reporting.ErrorsDocument
var recoveries reporting.RecoveriesDocument
for path, target := range map[string]any{"errors.yml": &errs, "recoveries.yml": &recoveries} {
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if err := yaml.Unmarshal(data, target); err != nil {
t.Fatal(err)
}
}
if errs.Status != reporting.StatusFailed || recoveries.Status != reporting.StatusFailed ||
errs.Summary.RunErrors == nil || *errs.Summary.RunErrors != 1 ||
recoveries.Summary.RunErrors == nil || *recoveries.Summary.RunErrors != 1 ||
len(errs.Errors) != 1 || errs.Errors[0].Reason != "output_failure" {
t.Fatalf("failed output reports disagree: %+v %+v", errs, recoveries)
}
}
type shortProcessWriter struct{}

View File

@@ -2,6 +2,7 @@ package cli
import (
"context"
"errors"
"fmt"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/diagnostics"
@@ -13,6 +14,7 @@ import (
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/processreport"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/runner"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput"
)
func executePipeline(ctx context.Context, loaded *processInputs, prepared *preparedProcess, normalized *schema.Transcript, warn func(error)) (processResult, error) {
@@ -47,22 +49,18 @@ func executePipeline(ctx context.Context, loaded *processInputs, prepared *prepa
var failures []processreport.RunFailure
if runErr != nil {
failure := processreport.RunFailure{Phase: "processing", Reason: outcomes.ReasonInternalFailure}
if ctx.Err() != nil {
var cause *structuredoutput.Failure
if errors.As(runErr, &cause) {
failure.Reason = cause.Reason
} else if errors.Is(runErr, context.Canceled) || (ctx.Err() != nil && errors.Is(runErr, ctx.Err())) {
failure.Reason = outcomes.ReasonCanceled
}
if len(output.ModuleResults) > 0 {
last := output.ModuleResults[len(output.ModuleResults)-1]
index := last.StageIndex
failure.StageIndex = &index
origin := outcomes.Reference{StageIndex: &index, StageID: last.StageID, ModuleKey: last.ModuleKey, ModuleInstance: last.ModuleInstance}
for _, operation := range last.Operations {
if operation.FailureReason != "" {
origin = operation.Reference
failure.Reason = operation.FailureReason
break
}
if last.Status == runner.ModuleStatusFailed {
index := last.StageIndex
failure.Origin = &outcomes.Reference{StageIndex: &index, StageID: last.StageID, ModuleKey: last.ModuleKey, ModuleInstance: last.ModuleInstance}
}
failure.Origin = &origin
}
failures = append(failures, failure)
}