Skip to content

Commit 6579491

Browse files
committed
Tighten the release notes comments without losing the reasons they record
1 parent 45c4385 commit 6579491

17 files changed

Lines changed: 312 additions & 402 deletions

.github/workflows/release-desktop.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,7 @@ jobs:
912912
notes = pathlib.Path(os.environ['RUNNER_TEMP'], 'desktop-release-notes.md').read_text()
913913
metadata = {
914914
'version': os.environ['APP_VERSION'],
915-
# The app version is SemVer; CHANGELOG.md is keyed by the backend
916-
# release, so the popup needs that version too.
915+
# App version is SemVer; CHANGELOG.md is keyed by the backend release.
917916
'pypi_version': os.environ['PYPI_VERSION'],
918917
'notes': notes,
919918
'pub_date': datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'),

_changelog_build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121

2222
class build_py(_build_py):
2323
def run(self) -> None:
24-
# Copy beside the sources only if writable (a PEP 517 build may run on
25-
# an immutable checkout), always into the staging directory.
24+
# Beside the sources only if writable (PEP 517 may build an immutable
25+
# checkout); into the staging directory always.
2626
if SOURCE.is_file():
2727
try:
2828
shutil.copyfile(SOURCE, SNAPSHOT)

build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ else
103103
STUDIO_STAMPED_VERSION="$(python scripts/stamp_studio_release.py)"
104104
fi
105105

106-
# 4. Build wheel/sdist. The build backend snapshots CHANGELOG.md into the
107-
# studio package (see _changelog_build.py) so release notes render offline.
106+
# 4. Build wheel/sdist. _changelog_build.py snapshots CHANGELOG.md into the studio
107+
# package so release notes render offline.
108108
python -m build
109109

110110
# Drop the snapshot so a source checkout never serves a stale copy.

studio/backend/utils/changelog.py

Lines changed: 48 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
CHANGELOG_FAILURE_TTL_SECONDS = 5 * 60
4040
RELEASE_NOTES_MAX_CHARS = 20_000
4141

42-
# CommonMark requires a space, a tab or the end of the line after the hashes: a
43-
# non-breaking space copied from rich text renders as ordinary text, not a
44-
# heading, but a bare `##` is an empty heading and still ends the release above.
42+
# CommonMark requires a space, tab or line end after the hashes: a non-breaking
43+
# space copied from rich text renders as text, not a heading, but a bare `##` is
44+
# an empty heading and still ends the release above.
4545
_HEADING_PATTERN = re.compile(r"^ {0,3}##(?:[ \t]+(?P<title>.*?))?[ \t]*$")
4646
_FENCE_PATTERN = re.compile(r"^ {0,3}(?P<marker>`{3,}|~{3,})(?P<rest>.*)$")
4747
# CommonMark type 1 HTML blocks: contents are literal until a closing tag,
@@ -60,8 +60,8 @@
6060
# Type 6 blocks run to the next blank line, so `<details>` only holds Markdown
6161
# once a blank line has closed the block. Open and close tags both start one.
6262
_HTML_BLOCK_OPEN = re.compile(r"^ {0,3}</?([a-zA-Z][a-zA-Z0-9-]*)(?=[\s/>]|$)")
63-
# Blocks that break into an open paragraph, so no paragraph is open after them
64-
# and one they are written below is closed rather than continued.
63+
# Blocks that break into an open paragraph, so none is open after them and one
64+
# they are written below is closed rather than continued.
6565
_INTERRUPTS = re.compile(
6666
r"^ {0,3}(?:#{1,6}([ \t]|$)|(?:\*[ \t]*){3,}$|(?:-[ \t]*){3,}$|(?:_[ \t]*){3,}$)"
6767
)
@@ -107,8 +107,7 @@
107107
_COMMENT_OPEN = "<!--"
108108
_COMMENT_CLOSE = "-->"
109109
# Stands in for a line the renderer hides. `#` is a block of its own, so list
110-
# tracking reads it the way it reads a comment: never a marker, never a lazy
111-
# paragraph continuation.
110+
# tracking reads it like a comment: never a marker, never a lazy continuation.
112111
_HIDDEN_BLOCK = "#"
113112
_VERSION_TOKEN_PATTERN = re.compile(r"^[\[(]?v?(?P<version>[0-9][0-9A-Za-z.!+-]*?)[\])]?$")
114113
_SAFE_VERSION_PATTERN = re.compile(r"^[0-9A-Za-z][0-9A-Za-z.!+-]{0,63}$")
@@ -195,8 +194,8 @@ def parse_changelog(text: str) -> list[ChangelogEntry]:
195194
body: list[str] = []
196195
open_fence: str | None = None
197196
# Content column of the list item the open block belongs to, 0 at document
198-
# level. A fence and an HTML block are both scoped to their container, so
199-
# the item's end closes them. Only one of the three is ever open.
197+
# level. A fence and an HTML block are scoped to their container, so the
198+
# item's end closes them. Only one of the three is ever open.
200199
block_column = 0
201200
in_comment = False
202201
in_raw_html: int | None = None
@@ -222,12 +221,11 @@ def flush() -> None:
222221
structural = ""
223222
opened_block = False
224223
in_block = open_fence is not None or in_html_block or in_raw_html is not None or in_comment
225-
# A fence, a comment or an HTML block inside a list item runs only to
226-
# the end of that item, so a line dedented out of the item closes both.
227-
# Lazy continuation cannot reach into any of them, so any content to the
228-
# left of the item ends the block along with the item. A raw block or a
229-
# comment inside an item ends on a blank line as well: the item takes
230-
# the break, so what follows it is a block of the item's own.
224+
# A fence, comment or HTML block inside a list item runs only to the end
225+
# of that item, so a line dedented out of the item closes both. Lazy
226+
# continuation reaches into none of them. A raw block or comment inside an
227+
# item also ends on a blank line: the item takes the break, so what
228+
# follows is a block of the item's own.
231229
leaves = (
232230
_indent_width(line) < block_column
233231
if line.strip()
@@ -242,9 +240,9 @@ def flush() -> None:
242240
# The paragraph the line could have continued is block content, so
243241
# it closes the item rather than reading as more of it.
244242
after_paragraph = False
245-
# A fence written as a list item's first content opens inside that
246-
# item, so an opener is read past a marker on the same line. Only an
247-
# opener: fenced content is literal, and a closer carries no marker.
243+
# A fence written as a list item's first content opens inside that item, so
244+
# an opener is read past a marker on the same line. Only an opener: fenced
245+
# content is literal and a closer carries no marker.
248246
fence_line = line if open_fence else _item_content(line, after_paragraph)
249247
# Raw HTML first: its contents are literal, so a fence in it is not one.
250248
if in_raw_html is not None:
@@ -263,35 +261,33 @@ def flush() -> None:
263261
elif open_fence:
264262
visible = ""
265263
else:
266-
# A block already open owns this line, so the line is its content
267-
# rather than a block written at the column it happens to start in.
264+
# A block already open owns this line, so it is content rather than a
265+
# block written at the column it happens to start in.
268266
hidden = in_comment or in_raw_html is not None
269-
# A comment is an HTML block too, so one written as a list item's
270-
# first content opens inside that item exactly as a fence does: the
271-
# opener is read past a marker on the same line.
267+
# A comment is an HTML block too, so one written as a list item's first
268+
# content opens inside it exactly as a fence does: the opener is read
269+
# past a marker on the same line.
272270
block_open = (
273271
not in_comment
274272
and _COMMENT_BLOCK_OPEN.match(_item_content(line, after_paragraph)) is not None
275273
)
276274
# Commented-out sections are not rendered, so they are not releases.
277275
visible, in_comment = _strip_comments(line, in_comment, block_open)
278276
# An HTML block written as a list item's first content opens inside
279-
# that item, as a fence does, so an opener is read past a marker on
280-
# the same line. The marker itself stays, so the item it opens is
281-
# still tracked. A comment blanks its own line, so that line is read
282-
# as written instead: the block renders as nothing, but the item it
283-
# is the content of still opens.
277+
# that item, as a fence does, so an opener is read past a marker on the
278+
# same line. The marker stays, so its item is still tracked. A comment
279+
# blanks its own line, so that line is read as written: the block
280+
# renders as nothing, but the item it is content of still opens.
284281
source = line if block_open else visible
285282
content = _item_content(source, after_paragraph)
286283
marker = source[: len(source) - len(content)]
287284
# Nor is anything inside a raw HTML block such as <pre>.
288285
stripped, in_raw_html = _strip_raw_html(content, in_raw_html)
289286
opened_block = in_raw_html is not None or (block_open and in_comment)
290287
# Taken before the opener is hidden: it renders as nothing, but its
291-
# indentation still closes a list item it sits to the left of, and a
292-
# marker on its line still opens one. A comment or a raw block keeps
293-
# only those, since the text it hides is not Markdown and must not
294-
# open a list of its own.
288+
# indent still closes a list item it sits left of, and a marker on its
289+
# line still opens one. A comment or raw block keeps only those, since
290+
# the text it hides is not Markdown and must open no list.
295291
if block_open or stripped != content:
296292
if not hidden:
297293
structural = _hidden_structure(line, marker)
@@ -352,21 +348,20 @@ def flush() -> None:
352348
# Only ordinary text continues a paragraph. Indented code counts four
353349
# spaces past the container, so an item's own indent does not count.
354350
indented_code = not after_paragraph and _indent_width(visible) - column >= 4
355-
# An underline ends the paragraph it underlines, so it needs one open
356-
# in its own container: the quote above owns its own, and a row written
357-
# left of an open item is lazy text of the item's paragraph rather than
358-
# a heading. A row of three dashes is a thematic break either way, which
359-
# `_INTERRUPTS` already ends the paragraph on.
351+
# An underline ends the paragraph it underlines, so it needs one open in
352+
# its own container: the quote above owns its own, and a row left of an
353+
# open item is lazy text of the item's paragraph. Three dashes are a
354+
# thematic break either way, which `_INTERRUPTS` already ends on.
360355
underline = (
361356
_SETEXT_UNDERLINE.match(visible) is not None
362357
and after_paragraph
363358
and not quoted
364359
and _indent_width(visible) >= column
365360
)
366361
after_paragraph = (
367-
# Read inside its container, so an empty item and a fence written
368-
# as an item's own content leave no paragraph open below them. A
369-
# marker the paragraph above swallows is its text, not an item.
362+
# Read inside its container, so an empty item and a fence written as an
363+
# item's own content leave no paragraph open below them. A marker the
364+
# paragraph above swallows is its text, not an item.
370365
(bool(content.strip()) or lazy_marker)
371366
and match is None
372367
and _HEADING_PATTERN.match(content) is None
@@ -389,9 +384,9 @@ def flush() -> None:
389384
# The only paragraph a quote line leaves open is the quote's own,
390385
# and a quote holding a heading or nothing at all leaves none.
391386
after_paragraph = in_quote
392-
# Whose paragraph the line below would continue. A quote owns the one
393-
# its own lines hold, so a marker written outside the quote is a block
394-
# of its own rather than more of the text above it.
387+
# Whose paragraph the line below would continue. A quote owns the one its
388+
# own lines hold, so a marker outside the quote is a block of its own
389+
# rather than more of the text above it.
395390
quoted = quote_line or in_quote
396391
# The lines a later underline turns into one heading. A paragraph opens
397392
# only on plain text and then runs on until something interrupts it.
@@ -412,9 +407,8 @@ def flush() -> None:
412407
continue
413408

414409
flush()
415-
# An empty heading has no title at all, so it ends the release above it
416-
# without indexing one of its own: `_version_from_heading` finds no
417-
# version and `flush` then skips the section.
410+
# An empty heading has no title, so it ends the release above without
411+
# indexing one: `_version_from_heading` finds no version and `flush` skips.
418412
heading = match.group("title") or ""
419413
version = _version_from_heading(heading)
420414
body = []
@@ -749,8 +743,8 @@ def _strip_comments(line: str, in_comment: bool, block_open: bool) -> tuple[str,
749743
index = 0
750744
spans = _code_span_ranges(line)
751745
# Spans are ordered and disjoint and each opener sits at or past the one
752-
# before, so the search resumes where it stopped. Restarting it per opener
753-
# is quadratic, and a long line of code spans is reparsed on every request.
746+
# before, so the search resumes rather than restarts: restarting per opener is
747+
# quadratic, and a long line of code spans is reparsed on every request.
754748
cursor = 0
755749
while index < len(line):
756750
opening = line.find(_COMMENT_OPEN, index)
@@ -845,8 +839,8 @@ def _item_content(line: str, after_paragraph: bool) -> str:
845839
if item is None:
846840
return line
847841
padding = _indent_width(item.group("space"))
848-
# Over-indented content starts one column past the marker, so the rest of
849-
# the padding is the content's own indentation.
842+
# Over-indented content starts one column past the marker; the rest of the
843+
# padding is the content's own indentation.
850844
over = padding - 1 if padding > _MAX_ITEM_PADDING else 0
851845
return " " * over + line[item.end() :]
852846

@@ -872,7 +866,7 @@ def _may_be_lazy(line: str) -> bool:
872866
and _INTERRUPTS.match(line) is None
873867
and _FENCE_PATTERN.match(line) is None
874868
# Types 1 to 6 interrupt a paragraph, so a `<div>` left of an open item
875-
# closes it. Type 7 cannot, and is deliberately excluded here.
869+
# closes it. Type 7 cannot, and is deliberately excluded.
876870
and not _opens_html_block(line, True)
877871
)
878872

@@ -1020,10 +1014,9 @@ def _renders_visibly(markdown: str) -> bool:
10201014
if not in_comment and (_FENCE_PATTERN.match(line) or opens_raw):
10211015
# A code block or raw HTML block renders even when it is empty.
10221016
return True
1023-
# No containers are tracked here, so the opener is read at the margin.
1024-
# The answer does not turn on the difference: an item renders its marker
1025-
# whatever the block inside it hides, so a section whose only content is
1026-
# a commented-out item renders something either way.
1017+
# No containers are tracked here, so the opener is read at the margin. The
1018+
# answer does not turn on it: an item renders its marker whatever the block
1019+
# inside hides, so a commented-out item renders something either way.
10271020
visible, in_comment = _strip_comments(
10281021
line, in_comment, _COMMENT_BLOCK_OPEN.match(line) is not None
10291022
)

studio/backend/utils/update_status.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ def get_studio_update_status(current_version: str) -> dict[str, Any]:
121121
install_source = detect_install_source()
122122
disabled = os.environ.get(DISABLE_ENV_VAR) == "1"
123123

124-
# Dev-only: the popup is PyPI-install-only, so set the env var below to a
125-
# version to review it from a checkout. The documented opt-out still wins.
124+
# Dev-only: the popup is PyPI-install-only, so fake a version to review it
125+
# from a checkout. The documented opt-out still wins.
126126
forced_version = os.environ.get(FAKE_UPDATE_ENV_VAR, "").strip()
127127
if forced_version and not disabled and _is_version(forced_version):
128128
return _status_response(

studio/frontend/src/app/provider.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,7 @@ function TauriUpdateLayer({
213213
}
214214

215215
return (
216-
// Capped like the browser stack: the download panel shares it, so both
217-
// together must still fit the window.
216+
// Capped like the browser stack: the download panel shares it, so both must fit.
218217
<div className="pointer-events-none fixed bottom-4 right-4 z-[9998] flex max-h-[calc(100dvh_-_2rem)] flex-col items-end gap-2">
219218
<UpdateBanner
220219
status={update.status}
@@ -381,10 +380,10 @@ function TauriWrapper({ children }: { children: ReactNode }) {
381380
return (
382381
<>
383382
{children}
384-
{/* One bottom-right stack so overlays never overlap: download panel at
385-
the corner, banners above. Each overlay owns its width. */}
383+
{/* One bottom-right stack so overlays never overlap: download panel at the
384+
corner, banners above, each owning its width. */}
386385
{/* Capped to the viewport, or a long download list plus expanded notes
387-
would push the top of the stack off screen. */}
386+
pushes the top of the stack off screen. */}
388387
<div className="pointer-events-none fixed bottom-4 right-4 z-[9998] flex max-h-[calc(100dvh_-_2rem)] flex-col items-end gap-2">
389388
<WebUpdateBanner
390389
positioned={false}

studio/frontend/src/components/update/release-notes-panel.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,9 @@ export function ReleaseNotesPanel({
6868
const { state, notes, retry } = useReleaseNotes({ version, enabled: true });
6969
const scrollRef = useRef<HTMLElement | null>(null);
7070

71-
// The fallback stands in for "this version has no section in the changelog",
72-
// which the hook reports as ready. A fetch that failed is reported as error and
73-
// is retryable, and on desktop the fallback is the updater's static install
74-
// blurb, so taking it there would replace a Retry button with generic text
75-
// until the cache expires.
71+
// The fallback stands in for "no section in the changelog", which the hook
72+
// reports as ready. An error is retryable, and the desktop fallback is the
73+
// updater's static blurb, so taking it there would hide Retry until cache expiry.
7674
const source = notes?.matched
7775
? notes.markdown
7876
: state === "error"
@@ -97,7 +95,7 @@ export function ReleaseNotesPanel({
9795
}
9896
}, [open, markdown]);
9997

100-
// Caller's URL wins: the API only returns the generic changelog, while the
98+
// Caller's URL wins: the API returns only the generic changelog, while the
10199
// desktop banner passes this version's release page.
102100
const notesUrl = releaseNotesUrl ?? notes?.releaseNotesUrl;
103101
const link = notesUrl ? <ChangelogLink href={notesUrl} /> : null;

studio/frontend/src/features/hub/download-manager/download-manager-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ export function DownloadManagerPanel({
201201
className={cn(
202202
// Standalone: anchor bottom-right. In a shared stack (positioned=false)
203203
// flow as a right-aligned row so overlays stack instead of overlapping.
204-
// min-h-0 there because a flex item defaults to min-height:auto, so the
205-
// capped stack would squeeze the update card instead of this list.
204+
// min-h-0 there: a flex item's min-height defaults to auto, so the capped
205+
// stack would squeeze the update card instead of this list.
206206
"pointer-events-none",
207207
positioned ? "fixed bottom-4 right-4 z-50" : "flex min-h-0 justify-end",
208208
)}

studio/frontend/src/hooks/use-release-notes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export interface ReleaseNotes {
1919

2020
export type ReleaseNotesState = "idle" | "loading" | "ready" | "error";
2121

22-
// Desktop auto-auth installs its token after first paint, so a popup shown at
23-
// startup can ask before one exists. Wait briefly rather than fail.
22+
// Desktop auto-auth installs its token after first paint, so a startup popup can
23+
// ask before one exists. Wait briefly rather than fail.
2424
const AUTH_POLL_MS = 250;
2525
const AUTH_POLL_LIMIT = 40;
2626

@@ -42,7 +42,7 @@ function toReleaseNotes(value: unknown, version: string): ReleaseNotes | null {
4242
}
4343
const payload = value as ApiObject;
4444
const notesVersion = stringOrNull(payload, "version");
45-
// Defensive: a response for another version is not usable here.
45+
// A response for another version is not usable here.
4646
if (notesVersion !== version) {
4747
return null;
4848
}

studio/frontend/src/hooks/use-tauri-update.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ export type UpdateStatus =
2121
export interface UpdateInfo {
2222
version: string;
2323
currentVersion: string;
24-
// Backend release this desktop build pins. The app version is SemVer, but
25-
// CHANGELOG.md is keyed by this one, so notes look it up.
24+
// Backend release this build pins; CHANGELOG.md is keyed by it, not the SemVer.
2625
pypiVersion?: string;
2726
body?: string;
2827
date?: string;

0 commit comments

Comments
 (0)