mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 04:38:48 +00:00
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
110 lines
4.7 KiB
Go
110 lines
4.7 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|