mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 05:30:35 +00:00
fix(release): simplify in-app highlights
Some checks are pending
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Public docs / check (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Waiting to run
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
Some checks are pending
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Public docs / check (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Waiting to run
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
This commit is contained in:
parent
f0e2243b44
commit
8fd43b307b
4 changed files with 195 additions and 12 deletions
|
|
@ -657,6 +657,11 @@ upgrade, update, release, or artifact-selection behavior.
|
|||
while the post-update card may show that same section once per later
|
||||
installed release and must stay silent for a first baseline, malformed or
|
||||
development versions, missing releases, and releases without highlights.
|
||||
When present, `Highlights` is the complete in-app overview: release
|
||||
rendering must keep it to at most three short plain-text bullets of no more
|
||||
than 140 characters each, with links, code, issue references, nested
|
||||
structure, and implementation-oriented detail reserved for the full release
|
||||
notes.
|
||||
The same post-update communication boundary owns the one-time schema-v2
|
||||
telemetry payload notice. It must use a non-blocking shared notice banner,
|
||||
appear only for existing installations on a published build, stay silent
|
||||
|
|
@ -1416,9 +1421,10 @@ experience. Update checks can preview a curated `Highlights` section, and an
|
|||
authenticated running-version endpoint lets the update surface show those
|
||||
same published highlights once after a later upgrade. Missing highlights stay
|
||||
quiet by design, and source or development builds never masquerade as
|
||||
published releases. Post-update highlights use the shared dialog so multi-item
|
||||
release content has a readable measure without pushing the dashboard down;
|
||||
every close path still records the running release as seen.
|
||||
published releases. Post-update highlights are limited to three short,
|
||||
plain-text user outcomes and use the shared dialog so the overview stays easy
|
||||
to scan without pushing the dashboard down; every close path still records the
|
||||
running release as seen.
|
||||
The initial GA promotion
|
||||
metadata remains
|
||||
`promoted_from_tag=v6.0.0-rc.7`, `rollback_version=v5.1.35`,
|
||||
|
|
|
|||
|
|
@ -68,13 +68,15 @@ Write the release notes in exactly this format:
|
|||
## v${VERSION}
|
||||
|
||||
### Highlights
|
||||
[2-4 short bullets, plain English, covering only what a typical user would
|
||||
notice and care about. This section is rendered inside the Pulse UI itself
|
||||
(the post-update "What's New" banner and the update-banner preview), so each
|
||||
bullet must be self-contained and jargon-free. IMPORTANT: if this release has
|
||||
nothing a typical user would notice (only internal fixes or minor patches),
|
||||
OMIT this entire section — that deliberately keeps the in-app banner silent
|
||||
for maintenance releases. Keep the heading at level 3 (###).]
|
||||
[2-3 short bullets covering only what a typical user would notice and care
|
||||
about. This is the entire overview shown inside Pulse after an update. Use one
|
||||
plain-text sentence per bullet, no more than 140 characters. Say what improved
|
||||
and why it matters in everyday words; avoid component names, acronyms,
|
||||
implementation details, links, issue numbers, and Markdown formatting.
|
||||
IMPORTANT: if this release has nothing a typical user would notice (only
|
||||
internal fixes or minor patches), OMIT this entire section — that deliberately
|
||||
keeps the in-app banner silent for maintenance releases. Keep the heading at
|
||||
level 3 (###).]
|
||||
|
||||
### New Features
|
||||
[Genuinely new user-facing capabilities. Be specific about what users can now do.]
|
||||
|
|
@ -92,8 +94,8 @@ Guidelines:
|
|||
- Do NOT write an Installation section or anything after Improvements — the
|
||||
release pipeline appends those.
|
||||
- Highlights is the ONE exception to "boring": it is shown in-app to users who
|
||||
just updated, so pick the few changes they would actually notice — but still
|
||||
facts, no hype.
|
||||
just updated, so make it the shortest useful explanation of what changed —
|
||||
still factual, with no hype.
|
||||
|
||||
Your reply must be ONLY the release-notes markdown, starting with
|
||||
"## v${VERSION}" — no preamble, no code fences, no commentary.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ _VALIDATION_STATUS_BLOCK_RE = re.compile(
|
|||
re.DOTALL,
|
||||
)
|
||||
|
||||
_HIGHLIGHTS_HEADING_RE = re.compile(r"^(#{2,6})[ \t]+Highlights[ \t]*$", re.IGNORECASE)
|
||||
_HIGHLIGHT_BULLET_RE = re.compile(r"^-[ \t]+(.+)$")
|
||||
_MAX_HIGHLIGHT_ITEMS = 3
|
||||
_MAX_HIGHLIGHT_LENGTH = 140
|
||||
|
||||
|
||||
def _normalize_newlines(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
|
@ -40,6 +45,75 @@ def _find_inline_markdown_markers(text: str) -> list[str]:
|
|||
return markers
|
||||
|
||||
|
||||
def _highlight_items(text: str) -> list[str] | None:
|
||||
"""Return the optional in-app Highlights list after validating its shape."""
|
||||
|
||||
lines = _normalize_newlines(text).splitlines()
|
||||
headings: list[tuple[int, int]] = []
|
||||
for index, line in enumerate(lines):
|
||||
match = _HIGHLIGHTS_HEADING_RE.fullmatch(line.strip())
|
||||
if match:
|
||||
headings.append((index, len(match.group(1))))
|
||||
|
||||
if not headings:
|
||||
return None
|
||||
if len(headings) > 1:
|
||||
raise ReleaseBodyIntegrityError(
|
||||
"release notes must contain at most one Highlights section"
|
||||
)
|
||||
|
||||
start_index, start_level = headings[0]
|
||||
section_lines: list[str] = []
|
||||
for line in lines[start_index + 1 :]:
|
||||
heading = re.fullmatch(r"(#{1,6})[ \t]+\S.*", line.strip())
|
||||
if heading and len(heading.group(1)) <= start_level:
|
||||
break
|
||||
section_lines.append(line)
|
||||
|
||||
items: list[str] = []
|
||||
for line in section_lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
bullet = _HIGHLIGHT_BULLET_RE.fullmatch(line)
|
||||
if bullet:
|
||||
items.append(bullet.group(1).strip())
|
||||
continue
|
||||
if line[:1].isspace() and items and not line.lstrip().startswith(("- ", "* ", "+ ")):
|
||||
items[-1] = f"{items[-1]} {line.strip()}"
|
||||
continue
|
||||
raise ReleaseBodyIntegrityError(
|
||||
"Highlights must be a flat list of short plain-text bullets"
|
||||
)
|
||||
|
||||
if not items:
|
||||
raise ReleaseBodyIntegrityError("Highlights must contain at least one bullet")
|
||||
if len(items) > _MAX_HIGHLIGHT_ITEMS:
|
||||
raise ReleaseBodyIntegrityError(
|
||||
f"Highlights may contain at most {_MAX_HIGHLIGHT_ITEMS} bullets"
|
||||
)
|
||||
|
||||
for item in items:
|
||||
if len(item) > _MAX_HIGHLIGHT_LENGTH:
|
||||
raise ReleaseBodyIntegrityError(
|
||||
"each Highlights bullet must be "
|
||||
f"{_MAX_HIGHLIGHT_LENGTH} characters or fewer"
|
||||
)
|
||||
if (
|
||||
"`" in item
|
||||
or "*" in item
|
||||
or "_" in item
|
||||
or re.search(r"!?\[[^\]]+\]\([^\)]+\)", item)
|
||||
or re.search(r"<[^>]+>", item)
|
||||
or re.search(r"\(#[0-9]+\)", item)
|
||||
):
|
||||
raise ReleaseBodyIntegrityError(
|
||||
"Highlights bullets must use plain text without links, code, HTML, "
|
||||
"or issue references"
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def validate_release_notes_shape(raw_text: str, version: str) -> None:
|
||||
"""Fail closed when authored release-note Markdown has lost its structure."""
|
||||
|
||||
|
|
@ -71,6 +145,8 @@ def validate_release_notes_shape(raw_text: str, version: str) -> None:
|
|||
"release notes contain flattened Markdown: " + ", ".join(inline_markers)
|
||||
)
|
||||
|
||||
_highlight_items(text)
|
||||
|
||||
|
||||
def strip_validation_status_block(text: str) -> str:
|
||||
"""Remove the workflow-owned validation annotation from a release body."""
|
||||
|
|
|
|||
|
|
@ -29,6 +29,105 @@ def _discover_rc_draft_packet_paths() -> tuple[str, ...]:
|
|||
|
||||
|
||||
class RenderReleaseBodyTest(unittest.TestCase):
|
||||
def test_highlights_are_a_small_plain_language_overview(self) -> None:
|
||||
notes = """# Pulse v6.2.0 Release Notes
|
||||
|
||||
## Highlights
|
||||
|
||||
- Alerts now explain what went wrong and what to do next.
|
||||
- Tables are easier to use on phones and small screens.
|
||||
- Updates recover cleanly when an earlier installation was interrupted.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Corrected a release issue.
|
||||
"""
|
||||
|
||||
render_release_body.validate_release_notes_shape(notes, "6.2.0")
|
||||
|
||||
def test_generated_level_three_highlights_and_wrapped_bullets_are_supported(self) -> None:
|
||||
notes = """# Pulse v6.2.0 Release Notes
|
||||
|
||||
## v6.2.0
|
||||
|
||||
### Highlights
|
||||
|
||||
- Alerts now explain what went wrong and what to do
|
||||
next.
|
||||
- Tables are easier to use on small screens.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Corrected a release issue.
|
||||
"""
|
||||
|
||||
render_release_body.validate_release_notes_shape(notes, "6.2.0")
|
||||
|
||||
def test_highlights_reject_more_than_three_items(self) -> None:
|
||||
notes = """# Pulse v6.2.0 Release Notes
|
||||
|
||||
## Highlights
|
||||
|
||||
- One.
|
||||
- Two.
|
||||
- Three.
|
||||
- Four.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Corrected a release issue.
|
||||
"""
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
render_release_body.ReleaseBodyIntegrityError,
|
||||
"at most 3 bullets",
|
||||
):
|
||||
render_release_body.validate_release_notes_shape(notes, "6.2.0")
|
||||
|
||||
def test_highlights_reject_long_or_formatted_items(self) -> None:
|
||||
long_item = "A" * 141
|
||||
long_notes = f"""# Pulse v6.2.0 Release Notes
|
||||
|
||||
## Highlights
|
||||
|
||||
- {long_item}
|
||||
|
||||
## Fixed
|
||||
|
||||
- Corrected a release issue.
|
||||
"""
|
||||
formatted_notes = """# Pulse v6.2.0 Release Notes
|
||||
|
||||
## Highlights
|
||||
|
||||
- Read the [upgrade guide](https://example.com) for details.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Corrected a release issue.
|
||||
"""
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
render_release_body.ReleaseBodyIntegrityError,
|
||||
"140 characters or fewer",
|
||||
):
|
||||
render_release_body.validate_release_notes_shape(long_notes, "6.2.0")
|
||||
with self.assertRaisesRegex(
|
||||
render_release_body.ReleaseBodyIntegrityError,
|
||||
"must use plain text",
|
||||
):
|
||||
render_release_body.validate_release_notes_shape(formatted_notes, "6.2.0")
|
||||
|
||||
def test_release_notes_may_omit_highlights(self) -> None:
|
||||
notes = """# Pulse v6.2.1 Release Notes
|
||||
|
||||
## Fixed
|
||||
|
||||
- Corrected a maintenance issue.
|
||||
"""
|
||||
|
||||
render_release_body.validate_release_notes_shape(notes, "6.2.1")
|
||||
|
||||
def test_sanitize_release_notes_strips_draft_markers_duplicate_sections_and_draft_links(self) -> None:
|
||||
raw = """# Pulse v6.0.0-rc.2 Draft Release Notes
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue