Pulse/scripts/installtests/release_notes_contract_test.go
pulse-triage[bot] 022b55083d Keep authored release notes compatible with safety checks
Bind stable notes to version and operator disclosures rather than obsolete prose. Support bounded grouped customer sections for v6.4.6 and later, align generation and rendering, and preserve published history and all qualification gates.

Change-source: pulse-maintainer
2026-10-01 05:28:54 +01:00

110 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package installtests
import (
"os"
"regexp"
"strings"
"testing"
)
// These are reader-facing change sections, not internal qualification headings.
var releaseNoteChangeHeadings = map[string]bool{
"what's improved": true, "what’s improved": true,
"alerts and notifications": true, "disks and storage": true,
"proxmox, pbs and backups": true, "truenas, vsphere and docker": true,
"install, updates and agents": true, "updates and agents": true,
"pulse pro, ai and hosted": true, "monitoring and service health": true,
"security": true, "other improvements": true,
}
func stablePatchReleaseNotesIssues(notes, version, previous string) []string {
var issues []string
lines := strings.Split(strings.TrimSpace(notes), "\n")
title := regexp.MustCompile(`^# Pulse v` + regexp.QuoteMeta(version) + `(?: Release Notes)?$`)
if !title.MatchString(strings.TrimSpace(lines[0])) {
issues = append(issues, "release title must name the exact current version")
}
changesSection, hasChanges := false, false
for _, line := range lines[1:] {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "## ") {
changesSection = releaseNoteChangeHeadings[strings.ToLower(strings.TrimPrefix(line, "## "))]
} else if strings.HasPrefix(line, "#") {
changesSection = false
} else if changesSection && strings.HasPrefix(line, "- ") && len(strings.TrimSpace(line[2:])) > 0 {
hasChanges = true
}
}
if !hasChanges {
issues = append(issues, "notes need non-empty user-facing change bullets")
}
normalized := strings.ToLower(strings.Join(strings.Fields(notes), " "))
for _, required := range []string{
"not authenticode-signed", "unknown publisher warning",
"rollback target is stable `v" + previous + "`",
} {
if !strings.Contains(normalized, required) {
issues = append(issues, "missing operator safety disclosure: "+required)
}
}
compatibleMobile := strings.Contains(normalized, "does not require a companion mobile release") ||
strings.Contains(normalized, "pulse mobile works with this release unchanged") ||
strings.Contains(normalized, "pulse mobile is being retired on 31 march 2027. it keeps working until then.")
if !compatibleMobile {
issues = append(issues, "missing companion mobile compatibility or approved retirement disclosure")
}
return issues
}
func TestStablePatchReleaseNotesAcceptAuthoredPublishedCopy(t *testing.T) {
// Exact public source b50aeb6a8d9e38ab362b23f4ed15a010f675a90f.
// A fixture is not permission to edit or re-render the published release.
notes, err := os.ReadFile("testdata/release-notes-v6.4.5-authored.md")
if err != nil {
t.Fatal(err)
}
if issues := stablePatchReleaseNotesIssues(string(notes), "6.4.5", "6.4.1"); len(issues) > 0 {
t.Fatal(issues)
}
}
func TestStablePatchReleaseNotesKeepIdentityAndSafety(t *testing.T) {
notes := "# Pulse v6.4.6 Release Notes\n\nPulse updates and History are clearer.\n\n" +
"## What's improved\n\n- History stays with the current host.\n\n" +
"## Before you upgrade\n\nWindows binaries are not Authenticode-signed. " +
"Windows may show an Unknown Publisher warning. " +
"This does not require a companion mobile release. " +
"The rollback target is stable `v6.4.5`.\n"
if issues := stablePatchReleaseNotesIssues(notes, "6.4.6", "6.4.5"); len(issues) > 0 {
t.Fatal(issues)
}
for _, change := range []struct{ name, from, to string }{
{"wrong title", "# Pulse v6.4.6", "# Pulse v6.4.5"},
{"no changes", "- History stays with the current host.", ""},
{"internal changes only", "## What's improved", "## Qualification"},
{"signing omitted", "not Authenticode-signed", "signed"},
{"publisher warning omitted", "Unknown Publisher warning", "warning"},
{"mobile omitted", "does not require a companion mobile release", "requires a new mobile build"},
{"wrong rollback", "rollback target is stable `v6.4.5`", "rollback target is stable `v6.4.1`"},
} {
t.Run(change.name, func(t *testing.T) {
if issues := stablePatchReleaseNotesIssues(strings.ReplaceAll(notes, change.from, change.to), "6.4.6", "6.4.5"); len(issues) == 0 {
t.Fatal("unsafe or mismatched notes accepted")
}
})
}
// Preserve the Markdown title/heading while exercising wrapped safety text.
wrapped := strings.ReplaceAll(notes, "Windows binaries are", "Windows\nbinaries are")
if issues := stablePatchReleaseNotesIssues(wrapped, "6.4.6", "6.4.5"); len(issues) > 0 {
t.Fatal(issues)
}
for _, disclosure := range []string{
"Pulse Mobile works with this release unchanged",
"Pulse Mobile is being retired on 31 March 2027. It keeps working until then.",
} {
candidate := strings.ReplaceAll(notes, "This does not require a companion mobile release", disclosure)
if issues := stablePatchReleaseNotesIssues(candidate, "6.4.6", "6.4.5"); len(issues) > 0 {
t.Fatal(issues)
}
}
}