Skip to content

release-go-daemons: send telegram notifications as escaped HTML - #7

Merged
mighty-sponge merged 1 commit into
mainfrom
fix/telegram-notify-parse-entities
Jul 17, 2026
Merged

release-go-daemons: send telegram notifications as escaped HTML#7
mighty-sponge merged 1 commit into
mainfrom
fix/telegram-notify-parse-entities

Conversation

@requilence

Copy link
Copy Markdown
Contributor

Problem

The notify step sends the GitHub-generated changelog to Telegram with format: markdown. Legacy Markdown has no escape mechanism, so any lone delimiter in the release notes breaks the send:

Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 505

Reconstructing the failing message from any-sync-node v0.12.1 puts byte 505 exactly on an asterisk — the closing one of **Full Changelog**, 13 total (odd).

Release Asterisks Result
any-sync-node v0.12.1 13 (odd) ❌ fails at byte 505
any-sync-coordinator v0.13.0-alpha.1 12 (even) ✅ "passed"

Each * bullet contributes one unpaired asterisk, so it's a coin flip on changelog length. This predates the recent retagging: node v0.11.2, filenode v0.11.3, coordinator v0.10.2 all failed the same way.

Two things that make this worse than it looks

The releases that "passed" were also broken. With an even count Telegram pairs the asterisks up and renders the message wrong — bullets consumed as bold delimiters, the first entry bolded, **Full Changelog** losing its bold entirely. Parity only decides whether it fails loudly or mangles silently.

Asterisks aren't the only trigger. _ is the italic delimiter, and real changelogs here contain client_golang and start_redis, so every dependabot PR title is an independent trigger.

The root cause is therefore not bullet parity but unescaped arbitrary text in a markup-formatted message.

The one-character fix that doesn't work

appleboy/telegram-action has a built-in escaper, but it's gated on p.Config.Format == "Markdown" (capital M) — lowercase markdown silently skipped it. Fixing only the capitalisation looks like the answer, but escapeMarkdownOne only escapes _ and never touches *. It would fix the dependabot vector and leave this bug intact.

Fix

Compose the message in github-script: escape & < >, map ##→bold, **x**→bold, * , and send as HTML — the only format where the input can be fully escaped.

  • Release notes go in via env rather than ${{ }} interpolation, so a PR title can't execute as JavaScript.
  • Body capped at 3500 chars; long changelogs otherwise exceed Telegram's 4096 limit and fail a different way.
  • format: HTML uses the canonical Bot API parse_mode — the action forwards the value verbatim and case-insensitivity there is undocumented.

Verification

The script was extracted from the YAML as shipped and executed against real and hostile inputs, validated against Telegram's HTML-mode rules (10/10 pass):

✅ node v0.12.1 (THE PROD FAILURE)     ✅ hostile: html/script injection
✅ coordinator v0.13.0-alpha.1         ✅ hostile: unpaired * and _
✅ node v0.7.4 (dependabot _)          ✅ hostile: 120-bullet changelog (truncates)
✅ filenode v0.10.5 (_)                ✅ hostile: workflow name w/ &
✅ edge: empty body                    ✅ edge: release job skipped

The validator was itself checked to reject all six failure classes (bare <, bare &, unclosed tag, disallowed tag, mismatched nesting, >4096) so it isn't passing vacuously.

What Telegram now receives for the release that broke prod:

<b>Release</b> anyproto/any-sync-node

<b>Release:</b> <a href="https://github.com/anyproto/any-sync-node/releases/tag/v0.12.1">v0.12.1</a>
<b>Description:</b>
<b>What's Changed</b>
• GO-7185 fix calcMedian panic on empty input by @cheggaaa in https://github.com/anyproto/any-sync-node/pull/220
• Update any-sync to v0.12.14 by @requilence in https://github.com/anyproto/any-sync-node/pull/228

<b>New Contributors</b>
• @requilence made their first contribution in https://github.com/anyproto/any-sync-node/pull/228

<b>Full Changelog</b>: https://github.com/anyproto/any-sync-node/compare/v0.11.1...v0.12.1

Note

Consumers pin release-go-daemons-reusable.yml@v1, so the v1 tag needs moving after merge for this to take effect.

The notify step passed the GitHub-generated changelog to Telegram with
format: markdown. Legacy Markdown has no escape mechanism, so any lone
delimiter in the release notes breaks the send:

  Bad Request: can't parse entities: Can't find end of the entity
  starting at byte offset 505

Every "* " bullet contributes one unpaired asterisk, so an odd number of
bullets aborts the send (any-sync-node v0.12.1, filenode v0.11.3,
coordinator v0.10.2). An even number does not make it correct: the
bullets are consumed as bold delimiters and the message renders mangled,
which is what the releases that appeared to succeed actually did.
Asterisks are also not the only trigger -- _ is the italic delimiter, so
dependabot titles such as "Bump github.com/prometheus/client_golang"
fail the same way.

Compose the message in github-script instead: escape & < > and map the
changelog to Telegram HTML, the only format where the input can be fully
escaped. Release notes are passed via env rather than interpolated into
the script, so a PR title cannot execute as JavaScript. The body is
capped at 3500 chars, since long changelogs otherwise exceed Telegram's
4096 limit and fail a different way.

Note that format: markdown also silently disabled the action's own
escaper, which is gated on the capitalised "Markdown"; that escaper only
handles _ and never *, so it would not have fixed this on its own.
@requilence
requilence requested a review from mighty-sponge July 17, 2026 10:20
@mighty-sponge
mighty-sponge merged commit 07a9bc2 into main Jul 17, 2026
1 check passed
@mighty-sponge
mighty-sponge deleted the fix/telegram-notify-parse-entities branch July 17, 2026 10:38
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants