From ff27c3881f0c418de856b26175cb2fcd96a962ec Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 27 Aug 2026 11:56:06 +0100 Subject: [PATCH] fix(release): make changelog ranges channel-aware --- .../subsystems/deployment-installability.md | 15 +++ docs/releases/RELEASE_NOTES_TEMPLATE.md | 22 ++-- docs/releases/RELEASE_NOTES_v6.4.0-rc.6.md | 21 +--- docs/releases/V6_CHANGELOG_v6.4.0-rc.6.md | 11 +- scripts/generate-release-notes.sh | 99 +++++++++++++--- .../installtests/build_release_assets_test.go | 111 +++++++++++++++++- .../release_control/render_release_body.py | 23 +++- .../render_release_body_test.py | 35 ++++-- scripts/trigger-release.sh | 5 +- 9 files changed, 276 insertions(+), 66 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b974f22c3..b10a37872 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -29,6 +29,21 @@ The next-candidate release notes and changelog must also describe newly stable integration fields when external receivers need them to consume the release; packet proof keeps the customer-facing summary and detailed changelog aligned. +Release-note comparison ranges are channel-specific. Each RC compares against +the immediately preceding RC on the same version line, with RC1 comparing +against the previous stable release. A stable GA release compares against the +previous stable release, not the final RC, so it can retell the complete release +train as a small number of user-recognizable themes. Stable notes must synthesize +the final product outcomes instead of concatenating RC notes or enumerating the +underlying commits. + +Customer-facing notes use one outcome list for features and fixes. Each visible +change is described once under `What's improved`; a parallel `Fixes` section is +forbidden for packets from `v6.4.0-rc.6` onward because it encourages the same +change to be restated with slightly different implementation detail. Internal +toolchain and architecture work stays in the detailed changelog unless it +changes something users can recognize or act on. + Provider-hosted MSP deploy artifacts must package the provider control plane as a least-privilege Docker provisioner. The packaged compose/setup path must avoid whole-host and Docker-data read mounts, expose storage admission only through diff --git a/docs/releases/RELEASE_NOTES_TEMPLATE.md b/docs/releases/RELEASE_NOTES_TEMPLATE.md index 33eef5338..a4188586c 100644 --- a/docs/releases/RELEASE_NOTES_TEMPLATE.md +++ b/docs/releases/RELEASE_NOTES_TEMPLATE.md @@ -7,18 +7,24 @@ what feels better or works now, not how it was implemented. - **Short outcome** — Explain where users notice it and why it matters. - **Short outcome** — Keep each item concrete and independently useful. -- **Short outcome** — Prefer observable behavior over component names. -- **Short outcome** — Use plain language and avoid implementation detail. +- **Short outcome** — Include fixes here as outcomes rather than repeating + them in a second section. -Use four to six meaningful improvements for a normal RC or minor release. A +Use up to six meaningful improvements for a normal RC or minor release. A narrow patch may use fewer rather than padding the notes with internal work. +Each user-visible change belongs in this list exactly once. Prefer observable +behavior over component names, group related implementation work into one +user-recognizable theme, and use plain language. -## Fixes +For an RC, cover only changes since the immediately preceding RC (or the +previous stable release for RC1). Do not repeat improvements already announced +in an earlier RC. For a stable GA release, cover the complete release train +since the previous stable release and boil the full commit range down to a few +themes that explain what users will experience differently. Do not concatenate +the RC notes or attempt to list every commit. -- State a visible problem that no longer happens. -- Name the affected page, workflow, integration, or platform when useful. - -Omit this section only when there are genuinely no user-facing fixes. +Do not add a separate `Fixes` section. That shape encourages the same change to +be described twice as both an improvement and a fix. ## Before you upgrade diff --git a/docs/releases/RELEASE_NOTES_v6.4.0-rc.6.md b/docs/releases/RELEASE_NOTES_v6.4.0-rc.6.md index 439b22c57..1dc7dd1c0 100644 --- a/docs/releases/RELEASE_NOTES_v6.4.0-rc.6.md +++ b/docs/releases/RELEASE_NOTES_v6.4.0-rc.6.md @@ -1,23 +1,14 @@ # Pulse v6.4.0-rc.6 Release Notes -`v6.4.0-rc.6` is a release candidate for the next v6 minor release. It restores complete hardware detail for standalone Proxmox Backup Server hosts, prevents large LXC estates from losing filesystem usage, warns when SMART CRC counters grow, and completes the alert engine's policy consolidation. +`v6.4.0-rc.6` focuses on more complete host data and earlier hardware-fault detection, while making alert behavior more predictable ahead of the stable release. ## What's improved -- **Complete standalone PBS details** — Backup-server rows now open the full resource drawer with system, hardware, network, disk, thermal, service, history, and management information when a Pulse Agent source is available. -- **Complete LXC filesystem coverage** — Host agents read LXC mount usage through `/proc`, avoiding `pct df` lock contention that could leave later containers without filesystem data on larger hosts. -- **Earlier disk-cabling warnings** — Pulse establishes a SMART UDMA CRC baseline and raises a disk-health warning when the counter grows, helping expose link or cabling faults that a current-state health check can miss. -- **More consistent alerts** — Alert policy is resolved consistently through one declarative path, with the superseded transition-tracking maps removed. -- **Current release toolchain** — Release builds use Go 1.26.7 and its current security fixes. - -## Fixes - -- Standalone PBS hosts no longer lose their agent-reported hardware details when they are represented on the Proxmox backup surface ([#1723](https://github.com/rcourtman/Pulse/issues/1723)). -- LXC filesystem collection now uses host-namespace `statfs` with a bounded per-container fallback, preventing shared-budget starvation on larger hosts ([#1477](https://github.com/rcourtman/Pulse/issues/1477)). -- SMART UDMA CRC growth now creates a warning, treats a counter reset or disk replacement as a new baseline, and resolves the active growth event after a stable sample ([#1776](https://github.com/rcourtman/Pulse/issues/1776)). -- Alert configuration values, built-in defaults, and enabled state now use one policy fold across resource families instead of parallel resolution paths. -- Alert-event queries now allocate from the bounded effective result limit rather than an untrusted requested limit. -- Legacy alert transition state has been removed after the reducer cutover, eliminating a second mutable source of lifecycle truth. +- **Complete standalone PBS details** — Standalone PBS entries retain agent-reported system, hardware, network, disk, thermal, service, history, and management details in the full drawer ([#1723](https://github.com/rcourtman/Pulse/issues/1723)). +- **Complete LXC filesystem coverage** — Filesystem usage continues to populate across larger LXC hosts instead of later containers being left without mount data when earlier collection is slow ([#1477](https://github.com/rcourtman/Pulse/issues/1477)). +- **Earlier disk-cabling warnings** — SMART UDMA CRC growth now raises a warning, resets its baseline after a counter reset or disk replacement, and clears the active event after a stable sample ([#1776](https://github.com/rcourtman/Pulse/issues/1776)). +- **More predictable alerts** — Enabled state, configured values, delays, and built-in defaults now follow the same policy path across resource types, with the superseded lifecycle state removed. +- **Safer alert-history queries** — Oversized requested result limits are bounded before memory is reserved, preventing the request from driving an unnecessarily large allocation. ## Before you upgrade diff --git a/docs/releases/V6_CHANGELOG_v6.4.0-rc.6.md b/docs/releases/V6_CHANGELOG_v6.4.0-rc.6.md index 6c7f4e974..b7b32e70b 100644 --- a/docs/releases/V6_CHANGELOG_v6.4.0-rc.6.md +++ b/docs/releases/V6_CHANGELOG_v6.4.0-rc.6.md @@ -5,23 +5,18 @@ _This changelog describes the changes since `v6.4.0-rc.5` included in ## Added -- SMART UDMA CRC counter growth now raises a disk-health warning after the initial baseline sample. -- Standalone PBS backup-server entries expose the full merged host resource drawer when agent hardware evidence is available. +- SMART UDMA CRC counter growth now raises a disk-health warning after the initial baseline sample, treats counter resets and disk replacements as a new baseline, and resolves an active growth event after a stable follow-up sample ([#1776](https://github.com/rcourtman/Pulse/issues/1776)). ## Changed -- Alert configuration resolves through one declarative policy fold for enabled state, thresholds, delays, and built-in defaults. -- Legacy transition-tracking maps have been removed now that reducer-owned alert lifecycle state is authoritative. -- Proxmox LXC filesystem collection uses host-namespace `statfs` for configured mounts, with `pct df` retained as a bounded per-container fallback when `/proc//root` is unavailable. +- Alert configuration resolves through one declarative policy fold for enabled state, thresholds, delays, and built-in defaults. Legacy transition-tracking maps have been removed now that reducer-owned alert lifecycle state is authoritative. +- Proxmox LXC filesystem collection uses host-namespace `statfs` for configured mounts, with `pct df` retained as a bounded per-container fallback when `/proc//root` is unavailable, so slow earlier containers no longer starve later containers of filesystem data ([#1477](https://github.com/rcourtman/Pulse/issues/1477)). - Release builds use Go 1.26.7 across the public server, agent, provider control plane, and release scripts. ## Fixed - Standalone PBS rows open the canonical resource drawer instead of a limited four-field summary, restoring system, hardware, network, disks, thermals, services, history, and management context ([#1723](https://github.com/rcourtman/Pulse/issues/1723)). -- LXC filesystem discovery no longer gives every container's `pct df` call one shared deadline, which previously starved later containers and left their mounted filesystems unreported on larger hosts ([#1477](https://github.com/rcourtman/Pulse/issues/1477)). -- SMART CRC growth treats resets and disk replacements as a new baseline and resolves an active growth event on a stable follow-up sample ([#1776](https://github.com/rcourtman/Pulse/issues/1776)). - Alert-event queries allocate from the bounded effective result limit rather than the raw client-supplied limit. -- Alert policy evaluation no longer relies on duplicated resource-family lookup branches or legacy mutable transition mirrors. ## Release Metadata diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh index a528fccef..1657590d6 100755 --- a/scripts/generate-release-notes.sh +++ b/scripts/generate-release-notes.sh @@ -7,7 +7,8 @@ # The agent runs read-only git/gh commands itself instead of being fed # pre-chewed diff fragments, so nothing user-visible is missed by grep luck. # -# Usage: ./scripts/generate-release-notes.sh [previous-tag] +# Usage: ./scripts/generate-release-notes.sh [comparison-tag] +# ./scripts/generate-release-notes.sh --resolve-base # # Contract: the release notes markdown is written to STDOUT (trigger-release.sh # captures it); all progress/diagnostics go to STDERR. SAVE_TO_FILE=1 also @@ -19,30 +20,91 @@ set -euo pipefail +MODE=generate +if [ "${1:-}" = "--resolve-base" ]; then + MODE=resolve-base + shift +fi + VERSION=${1:-} -PREVIOUS_TAG=${2:-} +REQUESTED_COMPARISON_TAG=${2:-} if [ -z "$VERSION" ]; then - echo "Usage: $0 [previous-tag]" >&2 - echo "Example: $0 6.1.0 v6.0.6" >&2 + echo "Usage: $0 [comparison-tag]" >&2 + echo " $0 --resolve-base " >&2 + echo "Example: $0 6.4.0-rc.6" >&2 exit 1 fi cd "$(git rev-parse --show-toplevel)" -if [ -z "$PREVIOUS_TAG" ]; then - PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -z "$PREVIOUS_TAG" ]; then - echo "No previous tag found, cannot generate diff-based release notes" >&2 - exit 1 +VERSION=${VERSION#v} + +latest_stable_before() { + local target_tag="v$1" + local candidate + + while IFS= read -r candidate; do + [ "$candidate" = "$target_tag" ] && continue + if [ "$(printf '%s\n%s\n' "$candidate" "$target_tag" | sort -V | head -n 1)" = "$candidate" ]; then + printf '%s\n' "$candidate" + fi + done < <(git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V) +} + +resolve_comparison_tag() { + local version=$1 + local base rc expected + + if [[ "$version" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-rc\.([0-9]+)$ ]]; then + base=${BASH_REMATCH[1]} + rc=${BASH_REMATCH[2]} + if (( rc > 1 )); then + expected="v${base}-rc.$((rc - 1))" + if ! git merge-base --is-ancestor "$expected" HEAD 2>/dev/null; then + echo "Expected immediately preceding RC tag '$expected' is not an ancestor of HEAD" >&2 + return 1 + fi + printf '%s\n' "$expected" + return + fi + elif [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Unsupported release version '$version'; expected X.Y.Z or X.Y.Z-rc.N" >&2 + return 1 fi + + latest_stable_before "${base:-$version}" | tail -n 1 +} + +EXPECTED_COMPARISON_TAG=$(resolve_comparison_tag "$VERSION") +if [ -z "$EXPECTED_COMPARISON_TAG" ]; then + echo "No valid comparison tag found for v${VERSION}" >&2 + exit 1 fi -if ! git rev-parse -q --verify "${PREVIOUS_TAG}^{commit}" >/dev/null; then - echo "Previous tag '${PREVIOUS_TAG}' does not exist" >&2 +if [ -n "$REQUESTED_COMPARISON_TAG" ] && [ "$REQUESTED_COMPARISON_TAG" != "$EXPECTED_COMPARISON_TAG" ]; then + echo "Comparison tag '$REQUESTED_COMPARISON_TAG' violates the release-note range for v${VERSION}; expected '$EXPECTED_COMPARISON_TAG'" >&2 exit 1 fi +PREVIOUS_TAG=$EXPECTED_COMPARISON_TAG + +if ! git rev-parse -q --verify "${PREVIOUS_TAG}^{commit}" >/dev/null; then + echo "Comparison tag '${PREVIOUS_TAG}' does not exist" >&2 + exit 1 +fi + +if [ "$MODE" = "resolve-base" ]; then + printf '%s\n' "$PREVIOUS_TAG" + exit 0 +fi + +if [[ "$VERSION" == *-rc.* ]]; then + RELEASE_RANGE_GUIDANCE="This is an RC release. Cover only the user-visible changes since the immediately preceding RC, ${PREVIOUS_TAG}. Do not repeat changes already announced in an earlier RC." +else + RELEASE_RANGE_GUIDANCE="This is a stable GA release. Cover the complete release train since the previous stable release, ${PREVIOUS_TAG}. Synthesize that potentially large commit range into a few user-relevant themes; do not concatenate RC notes or enumerate every commit." +fi + echo "Generating release notes for v${VERSION} (changes since ${PREVIOUS_TAG})..." >&2 read -r -d '' PROMPT < bool: return core is not None and core >= _CUSTOMER_FORMAT_MINIMUM +def _requires_single_change_list(version: str) -> bool: + """Return whether fixes must be folded into the one customer outcome list.""" + + normalized = version.lower().removeprefix("v") + core = _release_core(normalized) + if core is None or core < (6, 4, 0): + return False + if core > (6, 4, 0): + return True + + rc_match = re.fullmatch(r"6\.4\.0-rc\.(\d+)", normalized) + return rc_match is None or int(rc_match.group(1)) >= 6 + + def _section_lines(text: str, heading_index: int) -> list[str]: lines = _normalize_newlines(text).splitlines() section: list[str] = [] @@ -198,7 +212,7 @@ def _flat_bullet_items(lines: list[str], section_name: str) -> list[str]: return items -def _validate_customer_facing_release_notes(text: str) -> None: +def _validate_customer_facing_release_notes(text: str, version: str) -> None: """Enforce concise public notes without release-control implementation prose.""" lines = _normalize_newlines(text).strip().splitlines() @@ -265,6 +279,11 @@ def _validate_customer_facing_release_notes(text: str) -> None: ) if "fixes" in headings: + if _requires_single_change_list(version): + raise ReleaseBodyIntegrityError( + "customer-facing release notes must describe features and fixes once " + "in What's improved instead of adding a separate Fixes section" + ) fixes = _flat_bullet_items(_section_lines(text, headings["fixes"]), "Fixes") if not fixes or len(fixes) > _MAX_CUSTOMER_FIX_ITEMS: raise ReleaseBodyIntegrityError( @@ -314,7 +333,7 @@ def validate_release_notes_shape(raw_text: str, version: str) -> None: _highlight_items(text) if _requires_customer_facing_standard(version): - _validate_customer_facing_release_notes(text) + _validate_customer_facing_release_notes(text, version) def strip_validation_status_block(text: str) -> str: diff --git a/scripts/release_control/render_release_body_test.py b/scripts/release_control/render_release_body_test.py index b237a1a01..059773c89 100644 --- a/scripts/release_control/render_release_body_test.py +++ b/scripts/release_control/render_release_body_test.py @@ -190,7 +190,7 @@ class RenderReleaseBodyTest(unittest.TestCase): render_release_body.validate_release_notes_shape(notes, "6.2.1") def test_future_release_notes_require_customer_facing_structure(self) -> None: - notes = """# Pulse v6.4.0-rc.2 Release Notes + notes = """# Pulse v6.4.0-rc.6 Release Notes Pulse is faster and more predictable in larger environments. @@ -198,17 +198,34 @@ Pulse is faster and more predictable in larger environments. - **Faster infrastructure views** — Tables stay responsive as estates grow. - **Lighter realtime updates** — Pages do less work when resources change. - -## Fixes - -- Saved API keys are no longer returned to the browser. +- **Safer API key handling** — Saved API keys are no longer returned to the browser. ## Before you upgrade No manual migration is required. """ - render_release_body.validate_release_notes_shape(notes, "6.4.0-rc.2") + render_release_body.validate_release_notes_shape(notes, "6.4.0-rc.6") + + def test_current_release_notes_reject_a_second_fixes_list(self) -> None: + notes = """# Pulse v6.4.0-rc.6 Release Notes + +Pulse is faster and more predictable in larger environments. + +## What's improved + +- **Faster infrastructure views** — Tables stay responsive as estates grow. + +## Fixes + +- Infrastructure tables no longer stall in larger estates. +""" + + with self.assertRaisesRegex( + render_release_body.ReleaseBodyIntegrityError, + "features and fixes once", + ): + render_release_body.validate_release_notes_shape(notes, "6.4.0-rc.6") def test_customer_facing_standard_exempts_only_the_already_cut_rc1(self) -> None: self.assertFalse( @@ -300,9 +317,11 @@ Pulse is faster and more predictable in larger environments. ).read_text(encoding="utf-8") self.assertIn("## What's improved", template) - self.assertIn("## Fixes", template) self.assertIn("## Before you upgrade", template) - self.assertIn("four to six meaningful improvements", template) + self.assertIn("For an RC, cover only changes since the immediately preceding RC", template) + self.assertIn("For a stable GA release", template) + self.assertIn("Do not add a separate `Fixes` section", template) + self.assertNotIn("\n## Fixes\n", template) self.assertIn("pipeline appends the `Install` and `Roll back` sections", template) self.assertNotIn("## Release Qualification", template) self.assertNotIn("## Promotion Metadata", template) diff --git a/scripts/trigger-release.sh b/scripts/trigger-release.sh index 14797cb19..181a1c40f 100755 --- a/scripts/trigger-release.sh +++ b/scripts/trigger-release.sh @@ -156,10 +156,7 @@ else echo "" if [[ ! $REPLY =~ ^[Nn]$ ]]; then echo "Generating release notes..." - # Try to find previous tag for better context - PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - - if ./scripts/generate-release-notes.sh "$VERSION" "$PREV_TAG" > "$NOTES_FILE"; then + if ./scripts/generate-release-notes.sh "$VERSION" > "$NOTES_FILE"; then echo "Release notes generated at ${NOTES_FILE}" echo "" # Show first few lines