Skip to content

Commit 6c69244

Browse files
therealalephclaude
andcommitted
ci(telegram): brief English bullets in announcement + cross-link, drop Persian-full
Telegram channel posts up through v1.9.9 inlined the full Persian half of `docs/changelog/v{version}.md` (often >2000 chars), with sub-bullets, contributor mentions, and architectural prose. In a chat-client viewport the result was an unreadable wall of mixed RTL Persian + LTR `<code>` / `<b>` spans + nested bullets that scrolled past most readers. Switched to brief-extracted English instead: - Added `brief_changelog(text)` — keeps only top-level `• ` bullets (drops sub-bullets), strips "by @user with full root cause + fix" / "from @user" prefatory phrases, replaces `[#nnn](url)` with `#nnn` for inline issue refs, cuts each bullet at the first natural sentence boundary (`:` after pos 30, `. `, ` — `), hard-caps at 200 chars per bullet, and trims any dangling unbalanced `(` or `[` left by the truncation. - Both posts (files-channel announcement + main-channel cross-link) now use `english_brief = brief_changelog(english_notes)` instead of the full Persian. - Title and footer chrome of both posts switched to English ("released" / "Files (Android, Windows, ...)" / "Channel:" / "or:"). The full Persian + full English text stays in `docs/changelog/v*.md` for archival; only the channel post becomes brief. Verified locally on v1.9.7 / v1.9.8 / v1.9.9 — produces 246–458 char briefs with clean bullet structure, no dangling parens, no contributor noise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 38c359f commit 6c69244

1 file changed

Lines changed: 119 additions & 30 deletions

File tree

.github/scripts/telegram_publish_files.py

Lines changed: 119 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,89 @@ def load_changelog(repo_root: Path, version: str) -> tuple[str | None, str | Non
231231
return persian, english
232232

233233

234+
def brief_changelog(text: str, max_total: int = 1500) -> str:
235+
"""Compress a changelog body to top-level bullets only, with each bullet
236+
trimmed to a short readable headline.
237+
238+
Sub-bullets, prose explanations, contributor @-mentions, and embedded
239+
"by @user with full root cause + fix" prefatory phrases are stripped.
240+
Markdown link `[text](url)` becomes plain `text`, with the special case
241+
of `[#nnn](url)` → `#nnn` (issue/PR number stays readable without the
242+
visual clutter of the URL). The result still goes through
243+
`md_to_tg_html` for backtick → <code> conversion.
244+
245+
Why bullets-only: Telegram channel readers want "what shipped" in a
246+
glance, not the architectural detail that lives in the git log + the
247+
full `docs/changelog/v*.md` file. The full English text is still in
248+
the repo for archival.
249+
250+
`max_total` caps the assembled brief so the announcement stays well
251+
under Telegram's 4096-char sendMessage budget after header / footer
252+
chrome is added.
253+
"""
254+
out: list[str] = []
255+
total_len = 0
256+
257+
for raw in text.splitlines():
258+
if not raw.startswith("• "):
259+
continue
260+
body = raw[2:].strip()
261+
262+
# Strip "by @user with full root cause + fix" / "from @user" /
263+
# "by @user". The "with ..." clause after "by @user" runs to the
264+
# next closing paren — greedy `[^)]*` is what consumes it
265+
# cleanly. Without the greedy form, the trailing "with full
266+
# root cause + fix" remained in the headline.
267+
body = re.sub(r" by @[\w-]+(?: with [^)]*)?", "", body)
268+
body = re.sub(r" from @[\w-]+", "", body)
269+
270+
# `(PR [#nnn](url))` → `(#nnn)` and bare `[#nnn](url)` → `#nnn`.
271+
# Done before generic `[text](url)` so the issue-number form
272+
# wins over the catch-all (which would expand the link text).
273+
body = re.sub(r"PR \[#(\d+)\]\([^)]+\)", r"#\1", body)
274+
body = re.sub(r"\[#(\d+)\]\([^)]+\)", r"#\1", body)
275+
body = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", body)
276+
277+
# Cut at the first natural sentence boundary that isn't too
278+
# early. ":" anchored to position ≥ 30 catches "Title: details"
279+
# without truncating short headers like "Tests:" / "API:" /
280+
# "Build:" that are themselves the headline. ". " catches the
281+
# rest. " — " (em-dash with spaces) is our explicit "headline
282+
# — body" form in the changelogs.
283+
candidates = []
284+
for sep, min_pos in ((":", 30), (". ", 5), (" — ", 5)):
285+
idx = body.find(sep)
286+
if idx >= min_pos and idx < 200:
287+
candidates.append(idx)
288+
if candidates:
289+
body = body[: min(candidates)].rstrip()
290+
291+
# Hard cap at 200 chars so a single sentence-less bullet
292+
# (e.g. comma-separated list) can't dominate the brief.
293+
if len(body) > 200:
294+
body = body[:197].rstrip() + "…"
295+
296+
# If our truncation left an unclosed `(`, strip from there. A
297+
# dangling `(` reads as a typo in the channel post; better to
298+
# drop the parenthesised aside than to show a half-open one.
299+
# Same for `[`. Counts compare: if open > close, find the last
300+
# offending char and trim back to the previous space.
301+
for open_ch, close_ch in (("(", ")"), ("[", "]")):
302+
if body.count(open_ch) > body.count(close_ch):
303+
last = body.rfind(open_ch)
304+
if last > 0:
305+
body = body[:last].rstrip()
306+
307+
line = f"• {body}"
308+
# +1 for the line separator we'll insert when joining.
309+
if total_len + len(line) + 1 > max_total:
310+
break
311+
out.append(line)
312+
total_len += len(line) + 1
313+
314+
return "\n".join(out)
315+
316+
234317
def md_to_tg_html(md: str, max_len: int = TG_CHANGELOG_BUDGET) -> str:
235318
"""Convert a subset of Markdown to Telegram-flavoured HTML.
236319
@@ -479,40 +562,44 @@ def post_main_channel_pointer(
479562
hashtag: str,
480563
channel_username_link: str = "",
481564
channel_invite_link: str = "",
482-
persian_notes: str | None = None,
565+
english_notes_brief: str | None = None,
483566
) -> bool:
484567
"""Post a short cross-link to the main announcement channel pointing
485568
at the anchor post in the files channel. Replaces the previous
486569
behaviour of posting the universal APK + full changelog directly
487570
to the main channel — the main channel becomes a discovery surface
488571
while the files channel hosts the actual artifacts.
489572
490-
When `persian_notes` is supplied (the Persian half of the matching
491-
`docs/changelog/v{version}.md`), it's rendered between the title
492-
and the files-channel link so subscribers see what's actually new
493-
without needing to click through. Falls back to the bare pointer
494-
if notes aren't available.
573+
When `english_notes_brief` is supplied (the brief-extracted English
574+
half of `docs/changelog/v{version}.md` via `brief_changelog`), it's
575+
rendered between the title and the files-channel link so subscribers
576+
see what's new without clicking through. Falls back to the bare
577+
pointer if notes aren't available.
578+
579+
English brief (not Persian full) is what we ship to TG: the audience
580+
is the worldwide channel, and short brief-tone bullets read cleanly
581+
in a chat client where Persian RTL prose mixed with `<code>` /
582+
`<b>` spans rendered awkwardly. The full Persian + full English
583+
changelog stays in `docs/changelog/v*.md` for archival.
495584
496585
Includes channel-join links (public username + invite hash) at the
497586
bottom so recipients who aren't yet members can subscribe before
498587
clicking through to the specific release post.
499588
"""
500589
parts = [
501-
f"<b>📦 mhrv-rs v{html_escape(version)} منتشر شد</b>",
590+
f"<b>📦 mhrv-rs v{html_escape(version)} released</b>",
502591
"",
503592
]
504-
if persian_notes:
505-
# Use a slightly tighter budget here since the cross-link has
506-
# extra footer chrome (channel-join links) the files-channel
507-
# announcement doesn't.
508-
parts.append(md_to_tg_html(persian_notes, max_len=TG_CHANGELOG_BUDGET - 400))
593+
if english_notes_brief:
594+
# Tighter budget than the files-channel announcement since the
595+
# cross-link has extra footer chrome (channel-join links).
596+
parts.append(md_to_tg_html(english_notes_brief, max_len=TG_CHANGELOG_BUDGET - 400))
509597
parts.append("")
510598
parts.extend([
511-
f"برای دانلود فایل‌ها (Android، Windows، macOS، Linux و ...) "
512-
f"به کانال فایل‌ها مراجعه کنید:",
599+
f"Files (Android APKs, Windows, macOS, Linux, OpenWRT) on the files channel:",
513600
"",
514601
f"👉 <a href=\"{html_escape(files_channel_post_link)}\">"
515-
f"v{html_escape(version)}همه فایل‌ها + SHA-256</a>",
602+
f"v{html_escape(version)}all files with SHA-256</a>",
516603
])
517604
# Channel-join links. Two forms handle different states of the
518605
# files channel: the `t.me/<username>` form works for public
@@ -522,15 +609,15 @@ def post_main_channel_pointer(
522609
# is forgiving — recipients click whichever works for them.
523610
if channel_username_link or channel_invite_link:
524611
parts.append("")
525-
parts.append("لینک کانال:")
612+
parts.append("Channel:")
526613
if channel_username_link:
527614
# Render as plain URL (not HTML <a>) so the text shows the
528615
# link itself — useful when users share the message via
529616
# screenshot or copy-paste outside Telegram, which would
530617
# strip the <a href> wrapper.
531618
parts.append(html_escape(channel_username_link))
532619
if channel_invite_link:
533-
parts.append(f"و یا: {html_escape(channel_invite_link)}")
620+
parts.append(f"or: {html_escape(channel_invite_link)}")
534621
parts.append("")
535622
parts.append(hashtag)
536623
text = "\n".join(parts)
@@ -603,23 +690,25 @@ def main() -> int:
603690
# saying "new release, click here." Recipients land on this anchor
604691
# and scroll down to see all the platform-specific files.
605692
#
606-
# We pull the Persian half of `docs/changelog/v{version}.md` if it
607-
# exists and inject it into the announcement, so the channel post
608-
# actually tells subscribers what changed instead of just "new
609-
# release dropped." Falls back to the old skeleton when the file
610-
# isn't there (e.g. an out-of-band re-publish for an old tag whose
611-
# changelog file was never landed).
612-
persian_notes, _english_notes = load_changelog(repo_root_from_script(), args.version)
693+
# We pull the English half of `docs/changelog/v{version}.md`, run it
694+
# through `brief_changelog` to keep just the top-level bullets (no
695+
# sub-bullets, no contributor mentions, no embedded prose), and
696+
# inject that into the announcement. Brief English (not full Persian)
697+
# is the right tone for a Telegram channel post: subscribers want
698+
# "what shipped" in one glance; the full archival changelog stays in
699+
# the repo. Falls back to the bare skeleton if the changelog file
700+
# doesn't exist (e.g. an out-of-band re-publish for an old tag).
701+
_persian_notes, english_notes = load_changelog(repo_root_from_script(), args.version)
702+
english_brief = brief_changelog(english_notes) if english_notes else None
613703
announce_lines = [
614-
f"<b>📦 mhrv-rs {html_escape('v' + args.version)} منتشر شد</b>",
704+
f"<b>📦 mhrv-rs {html_escape('v' + args.version)} released</b>",
615705
"",
616706
]
617-
if persian_notes:
618-
announce_lines.append(md_to_tg_html(persian_notes))
707+
if english_brief:
708+
announce_lines.append(md_to_tg_html(english_brief))
619709
announce_lines.append("")
620710
announce_lines.extend([
621-
"فایل‌ها در ادامه به ترتیب پلتفرم ارسال می‌شن.",
622-
"هر فایل با SHA-256 (تایید اصالت) همراه هست.",
711+
"Per-platform files follow with SHA-256 captions for verification.",
623712
"",
624713
args.hashtag,
625714
])
@@ -689,7 +778,7 @@ def main() -> int:
689778
args.hashtag,
690779
channel_username_link=username_link,
691780
channel_invite_link=invite_link,
692-
persian_notes=persian_notes,
781+
english_notes_brief=english_brief,
693782
)
694783
if not ok:
695784
failures += 1

0 commit comments

Comments
 (0)