diff --git a/.github/nightly-step-coverage.json b/.github/nightly-step-coverage.json index e0690b792623..8646a2b7ca56 100644 --- a/.github/nightly-step-coverage.json +++ b/.github/nightly-step-coverage.json @@ -245,6 +245,8 @@ "Start the span at the newest published notes": {"pr": "ci-guards.yml:workflow-guard-tests:Test What's New highlights and release notes"}, "Fetch the commits since the newest published notes": {"pr": "ci-guards.yml:workflow-guard-tests:Validate the nightly-next release notes fetch (published span only)"}, "Build and sign the release notes": {"nightly_only": "signs notes with content key"}}, + "check-release-notes": { + "Check this build's notes as the page and the update card show them": {"pr": "ci-web.yml:react-apps-check-shard:Test webviews", "note": "check-release-notes.ts runs notesCheck.ts, tested by webviews/test/changelog-notes-check.test.ts"}}, "publish-release-notes": { "Recheck the NEXT publication head": {"nightly_only": "reads live published tag, feed"}, "Upload the signed release notes to R2": {"nightly_only": "uploads signed notes to R2"}}, diff --git a/.github/workflows/ci-guards.yml b/.github/workflows/ci-guards.yml index 4883f8a2c4bb..2ed996c3a701 100644 --- a/.github/workflows/ci-guards.yml +++ b/.github/workflows/ci-guards.yml @@ -1426,6 +1426,10 @@ jobs: if: ${{ !cancelled() && matrix.group == 'release-notary' }} run: python3 tests/test_nightly_next_release_notes_fetch.py + - name: Validate the nightly-next release notes items, summary and digest + if: ${{ !cancelled() && matrix.group == 'release-notary' }} + run: bash scripts/cmux-next/tests/release-notes-items.test.sh + - name: Validate the nightly cmux-tui resolver (newest published tree, no wait) if: ${{ !cancelled() && matrix.group == 'release-notary' }} run: python3 tests/test_nightly_cmux_tui_resolve.py diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 212c31ac4864..6b94259479c2 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -7383,12 +7383,81 @@ jobs: path: notes/ retention-days: 7 + # The pre-publish check of the signed notes (Leo 2026-10-18): this build's notes read as the + # What's New page shows an update of 1 and of 10 nightlies, and as the update card and popup + # show them. A title cut with "…", a bead id, a title shown twice or as its own description, or + # internal work in New, Improved or Fixed fails it, and publish-release-notes waits for it + # (scripts/cmux-next/check-release-notes.ts). Only this build's defects count, so a defect an + # older nightly already published never blocks the notes after it. Withheld notes are not lost: + # the next build's span starts at the newest published notes, so it carries the same commits and + # fails the same way until the rule or the line is fixed (report-nightly-failure opens the issue). + # No secrets: it reads the artifact and the public feed (an unverified older file can only fail + # the check, never pass it). + check-release-notes: + needs: [decide, build-nightly-app, sign-release-notes, resolve-nightly-cmux-tui-client] + if: "!cancelled() && needs.decide.outputs.track == 'nightly-next' && needs.sign-release-notes.result == 'success'" + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + env: + NIGHTLY_BUILD: ${{ needs.build-nightly-app.outputs.daemon_build }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.resolve-nightly-cmux-tui-client.outputs.build_sha }} + persist-credentials: false + sparse-checkout: | + scripts/cmux-next/check-release-notes.ts + webviews/src/pages/changelog/ + sparse-checkout-cone-mode: false + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-notes + path: notes + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.4.2" + - name: Check this build's notes as the page and the update card show them + run: | + set -euo pipefail + # A build commit from before the check landed has no script to run. + if [ ! -f scripts/cmux-next/check-release-notes.ts ]; then + echo "::warning title=Release notes::$NIGHTLY_BUILD: the build commit has no notes check; not checked" + exit 0 + fi + # The nine nightlies listed before this one, newest first: the artifact's copy when the + # backfill rewrote it, else the published file. A bad index fails here, not silently. + builds="$(python3 -c 'import json, sys; builds = [b["build"] for b in json.load(open(sys.argv[1]))["builds"] if b["build"] != sys.argv[2]]; print("\n".join(builds[:9]))' notes/index.json "$NIGHTLY_BUILD")" + mkdir -p previous + previous=() + missing=() + while IFS= read -r build; do + [ -n "$build" ] || continue + file="notes/$build.json" + if [ ! -f "$file" ]; then + file="previous/$build.json" + if ! curl -fsSL --retry 3 -A "cmux-release-notes/1" -o "$file" "$NIGHTLY_NEXT_FEED_BASE/notes/$build.json"; then + missing+=("$build") + continue + fi + fi + previous+=("$file") + done <<< "$builds" + # A feed outage would narrow the wider page and could pass a duplicate across nightlies, so + # it fails the check (and holds the notes) like a bad index does; a rerun checks again. + if [ "${#missing[@]}" -gt 0 ]; then + echo "::error title=Release notes::$NIGHTLY_BUILD: the feed did not serve ${#missing[@]} of the nightlies the check compares with: ${missing[*]}" + exit 1 + fi + bun scripts/cmux-next/check-release-notes.ts "notes/$NIGHTLY_BUILD.json" "${previous[@]}" + publish-release-notes: - needs: [decide, publish-nightly, sign-release-notes, resolve-nightly-cmux-tui-client] + needs: [decide, publish-nightly, sign-release-notes, check-release-notes, resolve-nightly-cmux-tui-client] concurrency: group: release-publish-${{ needs.decide.outputs.release_tag }} cancel-in-progress: false - if: "!cancelled() && needs.decide.outputs.track == 'nightly-next' && needs.publish-nightly.result == 'success' && needs.publish-nightly.outputs.already_published != 'true' && needs.publish-nightly.outputs.refused != 'true' && needs.sign-release-notes.result == 'success'" + if: "!cancelled() && needs.decide.outputs.track == 'nightly-next' && needs.publish-nightly.result == 'success' && needs.publish-nightly.outputs.already_published != 'true' && needs.publish-nightly.outputs.refused != 'true' && needs.sign-release-notes.result == 'success' && needs.check-release-notes.result == 'success'" environment: ${{ needs.decide.outputs.environment }} runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 10 @@ -7538,7 +7607,7 @@ jobs: # published build whose notes jobs did not both succeed is reported too # (run 37526646874 published with its notes job cancelled and no alarm). report-nightly-failure: - needs: [decide, build-nightly-ghostty-cli-helper, build-nightly-app, resolve-nightly-cmux-tui-client, prepare-nightly-variant, build-sign-notarize-nightly, verify-signed-nightly, verify-final-dmg-nightly, verify-recovered-nightly-next, publish-nightly, generate-nightly-deltas, republish-nightly-deltas, sign-release-notes, publish-release-notes] + needs: [decide, build-nightly-ghostty-cli-helper, build-nightly-app, resolve-nightly-cmux-tui-client, prepare-nightly-variant, build-sign-notarize-nightly, verify-signed-nightly, verify-final-dmg-nightly, verify-recovered-nightly-next, publish-nightly, generate-nightly-deltas, republish-nightly-deltas, sign-release-notes, check-release-notes, publish-release-notes] if: always() && needs.decide.outputs.should_publish == 'true' && needs.decide.outputs.notary_test != 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || needs.publish-nightly.outputs.tag_status == 'stale' || needs.publish-nightly.outputs.refusal_reason != '' || (needs.decide.outputs.track == 'nightly-next' && needs.publish-nightly.result == 'success' && needs.publish-nightly.outputs.already_published != 'true' && needs.publish-nightly.outputs.refused != 'true' && needs.publish-nightly.outputs.source != 'recovered' && (needs.sign-release-notes.result != 'success' || needs.publish-release-notes.result != 'success'))) runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} permissions: @@ -7604,7 +7673,7 @@ jobs: // nightly-next: notes jobs that were skipped after a publication // (a recovered one skips them on purpose). if (process.env.TRACK === 'nightly-next' && results['publish-nightly']?.result === 'success' && process.env.PUBLISH_SOURCE !== 'recovered') { - for (const job of ['sign-release-notes', 'publish-release-notes']) { + for (const job of ['sign-release-notes', 'check-release-notes', 'publish-release-notes']) { const result = results[job]?.result; if (result === 'skipped') failed.push(`${job} (skipped)`); } diff --git a/scripts/cmux-next/check-release-notes.ts b/scripts/cmux-next/check-release-notes.ts new file mode 100644 index 000000000000..eb17b05a5d1e --- /dev/null +++ b/scripts/cmux-next/check-release-notes.ts @@ -0,0 +1,22 @@ +// bun scripts/cmux-next/check-release-notes.ts NOTES.json [PREVIOUS.json ...] +// +// The nightly-next pre-publish check (nightly.yml check-release-notes): reads this build's notes +// the way the What's New page (1 and 10 nightlies) and the update card will show them, and fails +// with one error annotation per defect (internal work among the machine lines only warns) (webviews/src/pages/changelog/notesCheck.ts). PREVIOUS are +// the nightlies published before it, newest first. +import { readFileSync } from "node:fs"; +import { annotation, blocksPublish, checkNightlyNotes, RANGE } from "../../webviews/src/pages/changelog/notesCheck"; +import type { ReleaseNotes } from "../../webviews/src/pages/changelog/types"; + +const [path, ...previousPaths] = process.argv.slice(2); +if (!path) { + console.error("usage: check-release-notes.ts NOTES.json [PREVIOUS.json ...]"); + process.exit(2); +} +const read = (file: string) => JSON.parse(readFileSync(file, "utf8")) as ReleaseNotes; +const notes = read(path); +const previous = previousPaths.slice(0, RANGE - 1).map(read); +const defects = checkNightlyNotes(notes, previous); +for (const defect of defects) console.log(annotation(notes.build, defect)); +if (defects.some(blocksPublish)) process.exit(1); +console.log(`${notes.build}: notes, page (1 and ${previous.length + 1} nightlies) and card read clean`); diff --git a/scripts/cmux-next/release-notes.py b/scripts/cmux-next/release-notes.py index 8b1e6804d55f..524a89b31d41 100755 --- a/scripts/cmux-next/release-notes.py +++ b/scripts/cmux-next/release-notes.py @@ -73,11 +73,17 @@ r"frame budget|osc \d+|terminal host|seed|record\w*|nightly|headless|ffi|rust|regenerate|decode|renderer|texture|" r"machine code|scene captures?|policy log|host log|force unwrap|store intent|property model|false positive|inventory|" r"boundary|on the wire|debug reports?|dom events?|keybind actions?|main actor|shims?|" - r"consumers?|metrics|review fixes|byte limit)\b", re.I) -# Product names whose casing is not code. -BRANDS = re.compile(r"\b(?:macOS|iOS|iPadOS|iCloud|iPhone|iPad|GitHub|YouTube|SwiftUI|WebKit|JavaScript|TypeScript|ChatGPT|OpenAI)\b") -# Code-looking subjects (dotted names, snake_case, versioned ids, backticks) are internal. -CODE_LIKE = re.compile(r"`|\b[A-Za-z_]\w*\.[A-Za-z_]\w*\b|\w_\w|\w-v\d\b|\[cx-|\b[a-z]+[A-Z]\w*|\b[A-Z][a-z]+[A-Z]\w*|\b[A-Z]{2,}(?:-[A-Z]+)+\b|\{\w+\}") + r"consumers?|metrics|review fixes|base reds?|feat-cmux-next|compile fix(?:es)?|byte limit)\b", re.I | re.A) +# Product names whose casing is not code ("MacOS": a title capitalized before LEADING_BRAND). +BRANDS = re.compile(r"\b(?:[Mm]acOS|[Ii]OS|[Ii]PadOS|[Ii]Cloud|[Ii]Phone|[Ii]Pad|GitHub|YouTube|SwiftUI|WebKit|JavaScript|TypeScript|ChatGPT|OpenAI)\b") +# Code-looking subjects (dotted names, snake_case, versioned ids, bead ids, backticks) are internal. +# ASCII classes, as JavaScript reads them in readableChange.ts. +CODE_LIKE = re.compile(r"`|\b[A-Za-z_]\w*\.[A-Za-z_]\w*\b|\w_\w|\w-v\d\b|\[cx-|\bcx-[a-z0-9]{3,}|\b[a-z]+[A-Z]\w*|\b[A-Z][a-z]+[A-Z]\w*|\b[A-Z]{2,}(?:-[A-Z]+)+\b|\{\w+\}", re.A) +# Work no user sees by its shape (readableChange.ts TOOL_NAME, PR_NUMBER, SQUASH_REPAIR): a tool name +# of three or more hyphenated parts, a pull request number, a repair after a squash. +TOOL_NAME = re.compile(r"(?[A-Za-z-]+)(?:\((?P[^)]*)\))?!?:\s*(?P.+)$") AREA_PREFIX = re.compile(r"^(cmux-next(?: \w+){0,2}|cmuxnext|cmux next|acpmux|apps?|browser|gallery|release notes?|cx-[\w.]+|feed [\d.]+|cmux-tui)\s*:\s*", re.I) # "area words: rest" after the conventional type: "cmux NEXT perf-split: review: …", "cmux-next strip: …". @@ -85,12 +91,21 @@ # Work streams that measure or check the app (perf-split, perf-gate, motion-record, strip-scroll-check). INTERNAL_AREA_SHAPE = re.compile(r"^(?:perf-[\w-]+|[\w-]+-(?:check|record|gate|bench|probe)|motion-record|review|gate|base-red)$", re.I) PRODUCT_WORDS = {"cmux", "next", "cmux-next", "cmuxnext"} -# "(#123)", "(hq#45)", "(cx-abc)", "(cx-2y2 items 6, 10)", "(red)", anywhere in the subject, and a -# bead reference cut off at the end. +# "(#123)", "(hq#45)", "(cx-abc)", "(cx-2y2 items 6, 10)", "(red)", "(review)", anywhere in the +# subject, and a bead reference cut off at the end. # Also "(a283)", "(u3320)", "(a121, a211)": the Chief's agent and prompt ids, never for users. -REFS = re.compile(r"\s*\((?:[^()]*\bcx-[\w.]+[^()]*|[\w-]*#\d+|red)\)|\s*\((?:[^()]*\b)?cx-[\w.]*$" +REFS = re.compile(r"\s*\((?:[^()]*\bcx-[\w.]+[^()]*|[\w-]*#\d+|red|review)\)|\s*\((?:[^()]*\b)?cx-[\w.]*$" r"|\s*\([au]\d+(?:\s*[,/]\s*[au]?\d+)*\)", re.I) +# A brand that starts lowercase keeps its casing at the start of a title: "macOS 26 …". +LEADING_BRAND = re.compile(r"^(?:macOS|iOS|iPadOS|iCloud|iPhone|iPad)\b") + + +def capitalized(text): + """The text with a capital first letter, unless it starts with a lowercase brand.""" + return text if LEADING_BRAND.match(text) else text[:1].upper() + text[1:] + + def summary_entry(subject): """(group, title) for a user-facing commit subject, else None.""" text = " ".join(subject.split()) @@ -117,14 +132,15 @@ def summary_entry(subject): text = label["rest"] text = AREA_PREFIX.sub("", text) text = REFS.sub("", text).strip(" .") - if not text or INTERNAL_WORDS.search(text) or CODE_LIKE.search(BRANDS.sub("", text)): + if (not text or INTERNAL_WORDS.search(text) or CODE_LIKE.search(BRANDS.sub("", text)) + or TOOL_NAME.search(text) or PR_NUMBER.search(text) or SQUASH_REPAIR.search(text)): return None if group == "changed" and re.match(r"^(add|adds|new|introduce|support|show)\b", text, re.I): group = "new" elif group == "changed" and re.match(r"^(fix|fixes|repair|stop|keep)\b", text, re.I): group = "fixed" # Whole: the page wraps a long line, and short_title gives the digest a title that fits. - return group, text[:1].upper() + text[1:] + return group, capitalized(text) CLAUSE = re.compile(r",|;| —| - | \(| so | because | while ") @@ -152,7 +168,7 @@ def title_tail(full, title): tail = full[len(title):].lstrip(" ,;:—-").strip() if tail.startswith("(") and tail.endswith(")"): tail = tail[1:-1] - return tail[:1].upper() + tail[1:] + return capitalized(tail) def summarize_changes(subjects, limit=SUMMARY_LIMIT): @@ -386,11 +402,13 @@ def summary_whats_new_digest(args, lines): document = {"schemaVersion": 1, "version": args.short, "channel": "nightly", "date": args.date, "headline": {"en": "Fixes and improvements"}, "entries": []} categories = {"new": "new", "fixed": "fixed", "changed": "improved"} + titles = set() for index, line in enumerate(lines): # A line with no clause short enough stays out of the digest; the page shows it whole # from the summary lines. title = short_title(line.get("title", ""), validate.LIMITS["title"]) - if not title or line.get("group") not in categories: + # Two long lines can share a first clause; the digest shows each title once. + if not title or line.get("group") not in categories or title.lower() in titles: continue # The summary says what the title leaves out, or repeats a title that is the whole line. full = " ".join(line["title"].split()).rstrip(". ") @@ -402,6 +420,7 @@ def summary_whats_new_digest(args, lines): print(f"whats-new summary: dropped {entry['id']}:\n" + "\n".join(problems), file=sys.stderr) continue document["entries"].append(entry) + titles.add(title.lower()) if not document["entries"]: return None document["headline"] = {"en": document["entries"][0]["title"]["en"]} diff --git a/scripts/cmux-next/tests/release-notes-items.test.sh b/scripts/cmux-next/tests/release-notes-items.test.sh index ff4d2ac6a0c9..5ff0f4e60da0 100755 --- a/scripts/cmux-next/tests/release-notes-items.test.sh +++ b/scripts/cmux-next/tests/release-notes-items.test.sh @@ -47,8 +47,8 @@ assert titles == [ ], titles entries = notes["whatsNew"]["entries"] assert [(e["title"]["en"], e["summary"]["en"]) for e in entries] == [ - ("A column's tab bar lays out for where its pane rests", "Never pinned to the traffic lights while the camera scrolls"), - ("Terminals in the keep-alive band keep drawing", "So a column scrolled in is never blank"), + ("A column's tab bar lays out for where its pane rests", "Never pinned to the traffic lights while the camera scrolls."), + ("Terminals in the keep-alive band keep drawing", "So a column scrolled in is never blank."), ], entries assert not any("…" in e["title"]["en"] for e in entries) print("ok") diff --git a/tests/test_cmux_next_release_notes.py b/tests/test_cmux_next_release_notes.py index e9a12d8c7bd4..b7421be0c250 100644 --- a/tests/test_cmux_next_release_notes.py +++ b/tests/test_cmux_next_release_notes.py @@ -16,6 +16,23 @@ def git(cwd, *args): class ReleaseNotesTests(unittest.TestCase): + def test_digest_titles_are_unique_when_long_lines_share_a_first_clause(self): + # Review of #22099: short_title cuts both lines to "Sidebar rows keep their order", + # and the pre-publish check rejects a digest that shows one title twice. + import argparse, importlib.util + script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "scripts", "cmux-next", "release-notes.py") + spec = importlib.util.spec_from_file_location("release_notes_digest", script) + notes = importlib.util.module_from_spec(spec) + spec.loader.exec_module(notes) + lines = [ + {"group": "fixed", "title": "Sidebar rows keep their order, even when a workspace is renamed while another one is dragged"}, + {"group": "fixed", "title": "Sidebar rows keep their order, after a relaunch restores every window and each of its workspaces"}, + ] + args = argparse.Namespace(short="1.0.0-nightly.1", date="2026-10-19") + digest = notes.summary_whats_new_digest(args, lines) + titles = [entry["title"]["en"].lower() for entry in digest["entries"]] + self.assertEqual(len(titles), len(set(titles)), titles) + def setUp(self): self.repo = tempfile.mkdtemp() git(self.repo, "init", "-q") diff --git a/tests/test_nightly_next_recovery.py b/tests/test_nightly_next_recovery.py index 1f60c0bd5402..4b53f0c466f0 100755 --- a/tests/test_nightly_next_recovery.py +++ b/tests/test_nightly_next_recovery.py @@ -431,12 +431,13 @@ def publication(self, source, **results): jobs = ("decide", "build-nightly-ghostty-cli-helper", "build-nightly-app", "resolve-nightly-cmux-tui-client", "prepare-nightly-variant", "build-sign-notarize-nightly", "verify-signed-nightly", "verify-final-dmg-nightly", "verify-recovered-nightly-next", "publish-nightly", "generate-nightly-deltas", "republish-nightly-deltas", - "sign-release-notes", "publish-release-notes") + "sign-release-notes", "check-release-notes", "publish-release-notes") outcome = dict.fromkeys(jobs, "success") if source == "own": outcome["verify-recovered-nightly-next"] = "skipped" if source == "recovered": - outcome.update({"generate-nightly-deltas": "skipped", "sign-release-notes": "skipped", "publish-release-notes": "skipped"}) + outcome.update({"generate-nightly-deltas": "skipped", "sign-release-notes": "skipped", + "check-release-notes": "skipped", "publish-release-notes": "skipped"}) outcome.update(results) outputs = dict(self.OUTPUTS, **{"publish-nightly": dict(self.OUTPUTS["publish-nightly"], source=source)}) return job_runs("report-nightly-failure", outcome, outputs), job_runs("close-nightly-failure-issue", outcome, outputs) @@ -460,6 +461,22 @@ def test_a_recovered_publication_after_this_run_s_build_failed_reports(self): def test_a_recovered_publication_that_failed_its_delta_republish_reports(self): self.assertEqual(self.publication("recovered", **{"republish-nightly-deltas": "failure"}), (True, False)) + # The pre-publish check of the signed notes (scripts/cmux-next/check-release-notes.ts, Leo + # 2026-10-18): notes that fail it are not uploaded, and the failure issue names the check. + def test_notes_that_fail_their_check_are_not_published_and_report(self): + jobs = workflow()["jobs"] + check = jobs["check-release-notes"] + self.assertIn("sign-release-notes", check["needs"]) + self.assertIn("bun scripts/cmux-next/check-release-notes.ts", json.dumps(check)) + self.assertNotIn("secrets.", json.dumps(check)) + self.assertIn("check-release-notes", jobs["report-nightly-failure"]["needs"]) + publish = dict.fromkeys(jobs["publish-release-notes"]["needs"], "success") + outputs = {"decide": {"should_publish": "true", "track": "nightly-next"}, "publish-nightly": {"source": "own"}} + self.assertTrue(job_runs("publish-release-notes", publish, outputs)) + self.assertFalse(job_runs("publish-release-notes", dict(publish, **{"check-release-notes": "failure"}), outputs)) + self.assertEqual(self.publication("own", **{"check-release-notes": "failure", "publish-release-notes": "skipped"}), + (True, False)) + def test_an_already_published_recovered_build_neither_reports_nor_closes(self): # Another run published this build and owns the issue; this run's # no-op moved no marker (tag_status empty) and republished nothing. diff --git a/webviews/src/pages/changelog/UpdateSurfaces.tsx b/webviews/src/pages/changelog/UpdateSurfaces.tsx index 7817ca4cc763..6d2bc2fd51be 100644 --- a/webviews/src/pages/changelog/UpdateSurfaces.tsx +++ b/webviews/src/pages/changelog/UpdateSurfaces.tsx @@ -21,7 +21,7 @@ import { useEffect, useId, useRef, useSyncExternalStore } from "react"; import { CARD_SCROLL_ATTRIBUTE, installScrollEdges } from "../../scrollers"; import { getColorScheme, subscribeColorScheme } from "./colorScheme"; -import { distinctSummary } from "./model"; +import { distinctSummary, readableUpdate } from "./model"; import type { Strings } from "../shared/i18n"; import type { ReleaseNotes } from "./types"; import type { UpdateHighlightMedia, UpdateHighlights, UpdateSurfaceActions } from "./updateHighlights"; @@ -71,7 +71,7 @@ export function UpdatedCard({ closeButton?: boolean; }) { const titleId = useId(); - const summary = span ? summarizeUpdate(span, strings.language) : summarizeHighlights(data); + const summary = span ? summarizeUpdate(span, strings.language) : summarizeHighlights(readableUpdate(data)); const empty = strings.t(span ? "changelog.page.maintenance" : "updates.card.whatsNewInBuild"); return (
@@ -126,7 +126,9 @@ export function UpdateReadyHighlights({ }) { const titleId = useId(); const scroller = useRef(null); - const rest = data.rest ?? null; + const readable = readableUpdate(data); + const items = readable.items; + const rest = readable.rest ?? null; useEffect(() => (scroller.current ? installScrollEdges(scroller.current) : undefined), [data]); return (
@@ -137,9 +139,9 @@ export function UpdateReadyHighlights({ {data.fromVersion && data.fromVersion !== data.version && (
{strings.format("updates.popover.from", cardVersion(data.fromVersion))}
)} - {data.items.length > 0 && ( + {items.length > 0 && (
    - {data.items.map((item, index) => ( + {items.map((item, index) => (