Notify hourly generated reports after success
This commit is contained in:
@@ -720,6 +720,27 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputPath := reportPath
|
||||
if req.OutputPath != "" {
|
||||
outputPath = req.OutputPath
|
||||
if req.OutputPath != reportPath {
|
||||
if err := fileutil.CopyFileAtomic(reportPath, req.OutputPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notification, notificationPath, notificationErr := notifyReport(ctx, req.Config, req.Resolved, reportPath, req.metadata, req.Notifier, req.store)
|
||||
if notificationPath != "" {
|
||||
req.metadata.NotificationPath = notificationPath
|
||||
metadataPath, err = req.store.SaveMetadata(ctx, req.metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if notificationErr != nil {
|
||||
return nil, notificationErr
|
||||
}
|
||||
|
||||
return &ReportResult{
|
||||
ModuleSnapshot: req.moduleSnapshot,
|
||||
@@ -728,7 +749,8 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest)
|
||||
DataPackagePath: req.dataPackagePath,
|
||||
PreflightPath: req.preflightPath,
|
||||
ReportPath: reportPath,
|
||||
OutputPath: reportPath,
|
||||
OutputPath: outputPath,
|
||||
NotificationPath: notificationPath,
|
||||
Metadata: req.metadata,
|
||||
MetadataPath: metadataPath,
|
||||
RecentChanges: req.recentChanges,
|
||||
@@ -738,6 +760,7 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest)
|
||||
GeneratedTextResultPath: generatedTextResultPath,
|
||||
GeneratedTextPath: generatedTextPath,
|
||||
RenderContextPath: renderContextPath,
|
||||
Notification: notification,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -501,6 +501,137 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHourlyReportCopiesOutputAndNotifiesManagedReport(t *testing.T) {
|
||||
cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t)
|
||||
notifier.result = &NotificationResult{
|
||||
RunID: "distributor-run",
|
||||
Status: "succeeded",
|
||||
UploadStatus: "accepted",
|
||||
PipelineID: "reports",
|
||||
Report: []byte(`{"actions":[{"action":"replace_older"}]}`),
|
||||
}
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||
structuredRunBody: validHourlyGeneratedTextJSON(),
|
||||
}
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
OutputPath: outputPath,
|
||||
Renderer: renderer,
|
||||
Store: store,
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
if result.OutputPath != outputPath {
|
||||
t.Fatalf("OutputPath = %q, want requested copy %q", result.OutputPath, outputPath)
|
||||
}
|
||||
assertPathsExist(t, result.ReportPath, outputPath, result.NotificationPath)
|
||||
reportData, err := os.ReadFile(result.ReportPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read managed report: %v", err)
|
||||
}
|
||||
copyData, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read output copy: %v", err)
|
||||
}
|
||||
if string(copyData) != string(reportData) {
|
||||
t.Fatalf("output copy differs from managed report")
|
||||
}
|
||||
if len(notifier.requests) != 1 {
|
||||
t.Fatalf("notification requests = %d, want 1", len(notifier.requests))
|
||||
}
|
||||
req := notifier.requests[0]
|
||||
if req.ReportPath != result.ReportPath {
|
||||
t.Fatalf("notification ReportPath = %q, want managed report path %q", req.ReportPath, result.ReportPath)
|
||||
}
|
||||
if req.ReportPath == outputPath {
|
||||
t.Fatalf("notification used output copy %q, want managed report path", outputPath)
|
||||
}
|
||||
wantBundlePaths := []string{"2026-05-29/hourly/hourly.md"}
|
||||
if strings.Join(req.BundlePaths, "\n") != strings.Join(wantBundlePaths, "\n") {
|
||||
t.Fatalf("notification BundlePaths = %#v, want %#v", req.BundlePaths, wantBundlePaths)
|
||||
}
|
||||
if req.PipelineID != "weatherreporter.hourly" {
|
||||
t.Fatalf("notification PipelineID = %q, want weatherreporter.hourly", req.PipelineID)
|
||||
}
|
||||
if req.BundleID != "weatherreporter.home.hourly" {
|
||||
t.Fatalf("notification BundleID = %q, want weatherreporter.home.hourly", req.BundleID)
|
||||
}
|
||||
if req.IdempotencyKey != req.BundleID+"."+result.Metadata.RunID {
|
||||
t.Fatalf("notification IdempotencyKey = %q, want per-run key", req.IdempotencyKey)
|
||||
}
|
||||
if result.Notification == nil || result.Notification.RunID != "distributor-run" {
|
||||
t.Fatalf("Notification = %#v, want distributor result", result.Notification)
|
||||
}
|
||||
if result.Metadata.NotificationPath != result.NotificationPath {
|
||||
t.Fatalf("metadata NotificationPath = %q, want %q", result.Metadata.NotificationPath, result.NotificationPath)
|
||||
}
|
||||
notificationData, err := os.ReadFile(result.NotificationPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read notification artifact: %v", err)
|
||||
}
|
||||
var notificationArtifact state.DistributorNotificationArtifact
|
||||
if err := json.Unmarshal(notificationData, ¬ificationArtifact); err != nil {
|
||||
t.Fatalf("decode notification artifact: %v", err)
|
||||
}
|
||||
if notificationArtifact.SourcePath != result.ReportPath || strings.Join(notificationArtifact.BundlePaths, "\n") != strings.Join(wantBundlePaths, "\n") || notificationArtifact.RunStatus == nil {
|
||||
t.Fatalf("notification artifact = %#v, want managed source and run status", notificationArtifact)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHourlyReportNotificationFailureFailsReport(t *testing.T) {
|
||||
cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t)
|
||||
notifier.err = errors.New("upload rejected")
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||
structuredRunBody: validHourlyGeneratedTextJSON(),
|
||||
}
|
||||
|
||||
_, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
OutputPath: outputPath,
|
||||
Renderer: renderer,
|
||||
Store: store,
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("GenerateReport() error = nil, want notification error")
|
||||
}
|
||||
var notificationErr *NotificationError
|
||||
if !errors.As(err, ¬ificationErr) {
|
||||
t.Fatalf("GenerateReport() error = %T %v, want NotificationError", err, err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), `notify report "hourly"`) || !strings.Contains(err.Error(), "upload rejected") {
|
||||
t.Fatalf("error = %q, want hourly notification context", err.Error())
|
||||
}
|
||||
if len(notifier.requests) != 1 {
|
||||
t.Fatalf("notification requests = %d, want 1", len(notifier.requests))
|
||||
}
|
||||
paths := hourlyArtifactPaths(t, store, resolved)
|
||||
assertPathsExist(t, paths.RenderedReport, outputPath, paths.Metadata, paths.Notification)
|
||||
if notifier.requests[0].ReportPath != paths.RenderedReport {
|
||||
t.Fatalf("notification ReportPath = %q, want managed report path %q", notifier.requests[0].ReportPath, paths.RenderedReport)
|
||||
}
|
||||
notificationData, readErr := os.ReadFile(paths.Notification)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read notification artifact after failure: %v", readErr)
|
||||
}
|
||||
var notification state.DistributorNotificationArtifact
|
||||
if err := json.Unmarshal(notificationData, ¬ification); err != nil {
|
||||
t.Fatalf("decode notification artifact: %v", err)
|
||||
}
|
||||
if notification.Status != "failed" || !strings.Contains(notification.Error, "upload rejected") || notification.SourcePath != paths.RenderedReport {
|
||||
t.Fatalf("notification failure artifact = %+v, want failed managed-source context", notification)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHourlyReportPersistsPreflightFailure(t *testing.T) {
|
||||
cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t)
|
||||
renderer := &recordingRenderer{
|
||||
@@ -1876,6 +2007,18 @@ func TestRunBatchUsesOutputDirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOutputPathUsesHourlyOutputName(t *testing.T) {
|
||||
definition, err := report.DefaultRegistry().Lookup(report.Hourly)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup(hourly) error = %v", err)
|
||||
}
|
||||
outputDir := filepath.Join(t.TempDir(), "reports")
|
||||
want := filepath.Join(outputDir, "hourly.md")
|
||||
if got := batchOutputPath(outputDir, definition); got != want {
|
||||
t.Fatalf("batchOutputPath() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func mustParse(value string) time.Time {
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user