From ef56410139e5312870d92b85aa653d17e3cb3ac1 Mon Sep 17 00:00:00 2001 From: X9 Dev Date: Tue, 28 Jul 2026 18:03:45 +0200 Subject: [PATCH] fix(report): footer advertises log attachment only when actually attached buildHTML ran before zipLog, so the email footer always claimed "log v priloze (-Deploy.log.zip)" even when the log was unreadable and no attachment was added. Zip the log first and pass the attachment name into buildHTML; footer now names the zip only when present, otherwise points solely to the local copy. Covered by TestBuildHTMLFooterMatchesAttachment. Co-Authored-By: Claude Opus 4.8 --- internal/report/report.go | 26 +++++++++++++++++++------- internal/report/report_test.go | 21 ++++++++++++++++++--- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/internal/report/report.go b/internal/report/report.go index 3c11a14..aee20ca 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -56,7 +56,11 @@ func Send(results []StepResult, issueURL, logPath string) error { now := time.Now().Format("2006-01-02 15:04") subject := fmt.Sprintf("xetup report %s", hostname) - body := buildHTML(results, hostname, now, issueURL) + + // Zip the log first (best-effort; nil attachment => plain email) so the + // body's footer can state accurately whether the log is actually attached. + attachName, attachData := zipLog(logPath, hostname) + body := buildHTML(results, hostname, now, issueURL, attachName) // Always save local copy so technician has a record even if SMTP fails _ = os.MkdirAll(filepath.Dir(localReportPath), 0755) @@ -64,9 +68,6 @@ func Send(results []StepResult, issueURL, logPath string) error { fmt.Fprintf(os.Stderr, "[WARN] Failed to save local report: %v\n", err) } - // Zip the log for attachment (best-effort; nil attachment => plain email) - attachName, attachData := zipLog(logPath, hostname) - // Retry SMTP up to 3 times with exponential backoff (1s, 5s, 15s) delays := []time.Duration{0, 1 * time.Second, 5 * time.Second} var lastErr error @@ -168,7 +169,7 @@ func writeBase64(w *bytes.Buffer, data []byte) { w.WriteString("\r\n") } -func buildHTML(results []StepResult, hostname, dateTime, issueURL string) string { +func buildHTML(results []StepResult, hostname, dateTime, issueURL, attachName string) string { var ok, errs, skipped int var rows strings.Builder @@ -235,14 +236,25 @@ func buildHTML(results []StepResult, hostname, dateTime, issueURL string) string %s

- Odeslano z xetup.exe — log v priloze (%s-Deploy.log.zip) i lokalne: C:\Windows\Setup\Scripts\Deploy.log + %s

`, hostname, dateTime, rows.String(), summaryColor, summaryText, ok, errs, skipped, trackerLinks(hostname, issueURL), - hostname) + logFooter(attachName)) +} + +// logFooter names the zipped log in the footer only when it is actually +// attached; otherwise it points solely to the local copy on the machine. +func logFooter(attachName string) string { + if attachName != "" { + return fmt.Sprintf("Odeslano z xetup.exe — log v priloze (%s) i lokalne: "+ + `C:\Windows\Setup\Scripts\Deploy.log`, attachName) + } + return "Odeslano z xetup.exe — log lokalne: " + + `C:\Windows\Setup\Scripts\Deploy.log` } // trackerLinks renders the tracker links block: a direct link to this run's diff --git a/internal/report/report_test.go b/internal/report/report_test.go index 1079ba9..3f085b4 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -33,7 +33,7 @@ func writeLog(t *testing.T, content []byte) string { // buildHTML must embed the direct issue link and the per-machine tracker filter. func TestBuildHTMLLinks(t *testing.T) { issueURL := "https://git.xetup.x9.cz/x9/xetup-runs/issues/42" - html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", issueURL) + html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", issueURL, "PC-TEST-Deploy.log.zip") if !strings.Contains(html, issueURL) { t.Errorf("html does not link the run issue %q", issueURL) @@ -45,7 +45,7 @@ func TestBuildHTMLLinks(t *testing.T) { // With no issue (clean run / post failed), only the tracker link is shown. func TestBuildHTMLNoIssue(t *testing.T) { - html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "") + html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "PC-TEST-Deploy.log.zip") if strings.Contains(html, "Chyby tohoto behu") { t.Error("issue-specific line shown when issueURL is empty") } @@ -54,6 +54,21 @@ func TestBuildHTMLNoIssue(t *testing.T) { } } +// The footer must only advertise the attachment when one is actually present. +func TestBuildHTMLFooterMatchesAttachment(t *testing.T) { + with := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "PC-TEST-Deploy.log.zip") + if !strings.Contains(with, "v priloze (PC-TEST-Deploy.log.zip)") { + t.Error("footer should name the attachment when one is present") + } + without := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "", "") + if strings.Contains(without, "v priloze") { + t.Errorf("footer claims an attachment when none is attached:\n%s", without) + } + if !strings.Contains(without, "log lokalne") { + t.Error("footer should still point to the local log copy") + } +} + // zipLog must produce a valid zip whose Deploy.log entry round-trips. func TestZipLogRoundTrip(t *testing.T) { content := []byte("line one\nERROR something failed\nline three\n") @@ -90,7 +105,7 @@ func TestZipLogMissing(t *testing.T) { func TestBuildMessageMultipart(t *testing.T) { logContent := []byte("deploy log body\nERROR boom\n") _, zipData := zipLog(writeLog(t, logContent), "PC-TEST") - html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "https://x/issues/1") + html := buildHTML(sampleResults, "PC-TEST", "2026-07-28 10:00", "https://x/issues/1", "PC-TEST-Deploy.log.zip") raw := buildMessage("xetup report PC-TEST", html, "PC-TEST-Deploy.log.zip", zipData)