mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 05:44:35 +03:00
ci: post macOS/Linux/Windows/Android binaries as Telegram media group
The Telegram release notifier used to post just the universal APK with a single-document caption. This change ships the per-platform binaries for macOS (amd64+arm64 CLI), Linux (amd64+arm64 CLI), Windows (amd64 UI), and Android (universal APK) as a single Telegram media group with one caption listing every filename + SHA-256. Workflow side (.github/workflows/release.yml): - The telegram job now downloads ALL artifacts (was: APK only). - New `Prepare files for Telegram media group` step extracts the raw binaries out of each per-platform .tar.gz / .zip (no archive wrappers in the channel) and renames them with version suffixes (mhrv-rs-linux-amd64-v1.7.2, mhrv-rs-windows-amd64-ui-v1.7.2.exe, etc.). Per-platform extraction is best-effort: a missing artifact emits a `::warning::` and skips that platform rather than failing the whole post. - The post step builds a `--files <path>` arg list from tg-files/, sorted for deterministic order across runs, and invokes the notifier without --with-changelog (the script auto-replies with changelog whenever --files is used). Script side (.github/scripts/telegram_release_notify.py): - New --files arg (repeatable). 2..=10 files → sendMediaGroup; 1 file → sendDocument with the same caption shape; 0 → error. Telegram's sendMediaGroup rejects single-item groups, so the 1-file fallback isn't optional. - New build_media_group_caption() composes title + per-file filename+SHA list + repo/release URLs. Fits ~860 chars for a 6-file release; fallback to filename-only-list if a future swell pushes past Telegram's 1024-char caption cap. - send_media_group() handles the multipart/form-data shape with each file referenced as `attach://fileN` from the media JSON. Caption is attached to file 0 only (Telegram clients render per-item captions inconsistently for media groups; first-item-only is the safe pattern). - Legacy --apk path kept for any caller that hasn't migrated; either --apk or --files must be present (validated at startup). - _content_type_for() picks application/vnd.android.package-archive for .apk and application/octet-stream for everything else, so Telegram clients label the APK with the Android icon and label desktop binaries by filename without a misleading icon. Behavioural change for users: - The Telegram channel now sees one grouped post per release with all primary platform binaries inline, instead of just the APK. macOS users wanting the gatekeeper-friendly .app.zip still grab it from the GitHub Releases page; the Telegram drop is for the "give me the binary, I'll run it" path. - The Persian/English changelog reply that used to be opt-in (via TELEGRAM_INCLUDE_CHANGELOG=true) is now automatic in the --files path because the per-file SHA list eats the caption budget that previously held the FA brief-note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,81 @@ def sha256_of(path: str) -> str:
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def build_media_group_caption(files: list, version: str, repo: str) -> str:
|
||||
"""Build the single shared caption for a media-group post.
|
||||
|
||||
Caption shape (each file is one filename + one SHA line):
|
||||
|
||||
<b>mhrv-rs v1.7.1</b>
|
||||
|
||||
<b>mhrv-rs-linux-amd64-v1.7.1</b>
|
||||
<code>{sha256}</code>
|
||||
|
||||
<b>mhrv-rs-windows-amd64-v1.7.1.exe</b>
|
||||
<code>{sha256}</code>
|
||||
|
||||
...
|
||||
|
||||
مخزن گیتهاب + مطالعه راهنمای کامل فارسی:
|
||||
https://github.com/{repo}
|
||||
|
||||
لینک به این نسخه:
|
||||
https://github.com/{repo}/releases/tag/v{version}
|
||||
|
||||
Telegram caption hard-cap is 1024 chars. A typical 6-file release
|
||||
fits comfortably (~860 chars); the budget check at the bottom is a
|
||||
safety net for edge cases (longer filename suffixes, extra files).
|
||||
|
||||
The release-note `<blockquote>` block that the single-document path
|
||||
renders does NOT belong here — with N files in the group the SHA
|
||||
list eats the caption budget, and the release-note bullets move to
|
||||
the reply-threaded changelog message instead (sent unconditionally
|
||||
when sending a media group, since there's nowhere else for them).
|
||||
"""
|
||||
lines: list = [f"<b>mhrv-rs v{version}</b>", ""]
|
||||
for path in files:
|
||||
name = os.path.basename(path)
|
||||
sha = sha256_of(path)
|
||||
lines.append(f"<b>{name}</b>")
|
||||
lines.append(f"<code>{sha}</code>")
|
||||
lines.append("")
|
||||
lines.extend([
|
||||
"مخزن گیتهاب + مطالعه راهنمای کامل فارسی:",
|
||||
f"https://github.com/{repo}",
|
||||
"",
|
||||
"لینک به این نسخه:",
|
||||
f"https://github.com/{repo}/releases/tag/v{version}",
|
||||
])
|
||||
caption = "\n".join(lines)
|
||||
if len(caption) > 1024:
|
||||
# Truncate from the SHA list — header + footer URLs must stay.
|
||||
# In practice we never hit this with the current 4-platform
|
||||
# release; this branch is a guard for "what if we add 5 more
|
||||
# ABIs later" so the caller doesn't silently fail Telegram's
|
||||
# cap rejection. Falling back to "list filenames only, drop
|
||||
# SHAs" keeps the post useful while flagging the issue in CI.
|
||||
print(
|
||||
f"::warning::caption {len(caption)} chars > 1024; "
|
||||
f"falling back to filename-only list",
|
||||
file=sys.stderr,
|
||||
)
|
||||
compact: list = [f"<b>mhrv-rs v{version}</b>", ""]
|
||||
for path in files:
|
||||
compact.append(f"• <code>{os.path.basename(path)}</code>")
|
||||
compact.extend([
|
||||
"",
|
||||
"(SHA-256 hashes truncated; see GitHub release page.)",
|
||||
"",
|
||||
"مخزن گیتهاب:",
|
||||
f"https://github.com/{repo}",
|
||||
"",
|
||||
"این نسخه:",
|
||||
f"https://github.com/{repo}/releases/tag/v{version}",
|
||||
])
|
||||
caption = "\n".join(compact)
|
||||
return caption
|
||||
|
||||
|
||||
def tg_request(method: str, token: str, *, body: bytes, content_type: str) -> dict:
|
||||
"""POST `body` to https://api.telegram.org/bot<token>/<method>."""
|
||||
conn = http.client.HTTPSConnection(
|
||||
@@ -224,10 +299,30 @@ def tg_request(method: str, token: str, *, body: bytes, content_type: str) -> di
|
||||
return data["result"]
|
||||
|
||||
|
||||
def send_document(token: str, chat_id: str, apk_path: str, caption: str) -> int:
|
||||
"""Upload the APK file with a short HTML caption. Returns message_id."""
|
||||
def _content_type_for(path: str) -> str:
|
||||
"""Pick a sensible Content-Type so Telegram clients label the file
|
||||
with the right icon/extension hint. APKs get the Android-specific
|
||||
MIME so the channel preview shows the Android package icon; raw
|
||||
desktop binaries (Mach-O / ELF / PE) and tarballs fall through to
|
||||
octet-stream — Telegram still displays them with the user-supplied
|
||||
filename and downloads correctly.
|
||||
"""
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext == ".apk":
|
||||
return "application/vnd.android.package-archive"
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
def send_document(token: str, chat_id: str, file_path: str, caption: str) -> int:
|
||||
"""Upload a single file with a short HTML caption. Returns message_id.
|
||||
|
||||
Used for single-file releases (e.g. APK-only) and as the fallback
|
||||
when --files is passed exactly one path. Multi-file releases go
|
||||
through send_media_group instead — Telegram's sendDocument can only
|
||||
upload one file per call.
|
||||
"""
|
||||
boundary = "----" + uuid.uuid4().hex
|
||||
with open(apk_path, "rb") as f:
|
||||
with open(file_path, "rb") as f:
|
||||
file_bytes = f.read()
|
||||
|
||||
def text_field(name: str, value: str) -> bytes:
|
||||
@@ -237,13 +332,11 @@ def send_document(token: str, chat_id: str, apk_path: str, caption: str) -> int:
|
||||
f"{value}\r\n"
|
||||
).encode("utf-8")
|
||||
|
||||
def file_field(name: str, filename: str, content: bytes) -> bytes:
|
||||
def file_field(name: str, filename: str, content: bytes, content_type: str) -> bytes:
|
||||
head = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'
|
||||
# Proper MIME type — makes the Telegram client show the APK
|
||||
# with the Android package icon and honour its size/name.
|
||||
f"Content-Type: application/vnd.android.package-archive\r\n\r\n"
|
||||
f"Content-Type: {content_type}\r\n\r\n"
|
||||
).encode("utf-8")
|
||||
return head + content + b"\r\n"
|
||||
|
||||
@@ -251,7 +344,12 @@ def send_document(token: str, chat_id: str, apk_path: str, caption: str) -> int:
|
||||
text_field("chat_id", chat_id)
|
||||
+ text_field("caption", caption)
|
||||
+ text_field("parse_mode", "HTML")
|
||||
+ file_field("document", os.path.basename(apk_path), file_bytes)
|
||||
+ file_field(
|
||||
"document",
|
||||
os.path.basename(file_path),
|
||||
file_bytes,
|
||||
_content_type_for(file_path),
|
||||
)
|
||||
+ f"--{boundary}--\r\n".encode("utf-8")
|
||||
)
|
||||
|
||||
@@ -264,6 +362,86 @@ def send_document(token: str, chat_id: str, apk_path: str, caption: str) -> int:
|
||||
return int(result["message_id"])
|
||||
|
||||
|
||||
def send_media_group(
|
||||
token: str, chat_id: str, file_paths: list, caption: str
|
||||
) -> int:
|
||||
"""Upload 2–10 files as a single Telegram media group. Returns the
|
||||
message_id of the first message in the group (which is what any
|
||||
threaded reply should reference).
|
||||
|
||||
Telegram quirks:
|
||||
- `sendMediaGroup` accepts 2..=10 items. Zero/one is rejected;
|
||||
eleven+ is rejected. Caller must pre-check.
|
||||
- The `caption` field on `InputMediaDocument` is shown only when
|
||||
attached to the FIRST item in the group — Telegram clients
|
||||
render that caption above the document stack. Captions on
|
||||
later items in the group are silently dropped by some clients.
|
||||
We attach the caption to file 0 and leave the rest captionless.
|
||||
- `media` parameter is a JSON-encoded array; each item references
|
||||
its file via `attach://<form-data-name>`. We use `file0`,
|
||||
`file1`, ... for clarity in case Telegram ever surfaces the
|
||||
multipart name in error responses.
|
||||
- The total bytes of all files in a single media group must fit
|
||||
in Telegram's per-request limit (50 MB for bot uploads). For
|
||||
our typical release (~6 files × ~5–15 MB each) we're well
|
||||
under, but a future Android APK swell could hit this — caller
|
||||
should split into multiple groups in that case.
|
||||
"""
|
||||
if len(file_paths) < 2 or len(file_paths) > 10:
|
||||
raise SystemExit(
|
||||
f"send_media_group: need 2..=10 files, got {len(file_paths)}"
|
||||
)
|
||||
|
||||
boundary = "----" + uuid.uuid4().hex
|
||||
|
||||
media_array = []
|
||||
for i, path in enumerate(file_paths):
|
||||
item = {"type": "document", "media": f"attach://file{i}"}
|
||||
if i == 0:
|
||||
# Caption on the first item only — see docstring.
|
||||
item["caption"] = caption
|
||||
item["parse_mode"] = "HTML"
|
||||
media_array.append(item)
|
||||
|
||||
parts: list = []
|
||||
|
||||
def text_field(name: str, value: str) -> bytes:
|
||||
return (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="{name}"\r\n\r\n'
|
||||
f"{value}\r\n"
|
||||
).encode("utf-8")
|
||||
|
||||
parts.append(text_field("chat_id", chat_id))
|
||||
parts.append(text_field("media", json.dumps(media_array)))
|
||||
|
||||
for i, path in enumerate(file_paths):
|
||||
with open(path, "rb") as f:
|
||||
content = f.read()
|
||||
head = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="file{i}"; '
|
||||
f'filename="{os.path.basename(path)}"\r\n'
|
||||
f"Content-Type: {_content_type_for(path)}\r\n\r\n"
|
||||
).encode("utf-8")
|
||||
parts.append(head + content + b"\r\n")
|
||||
|
||||
parts.append(f"--{boundary}--\r\n".encode("utf-8"))
|
||||
body = b"".join(parts)
|
||||
|
||||
# sendMediaGroup returns an array of Message objects (one per item);
|
||||
# caller reply-threading targets the first message.
|
||||
result = tg_request(
|
||||
"sendMediaGroup",
|
||||
token,
|
||||
body=body,
|
||||
content_type=f"multipart/form-data; boundary={boundary}",
|
||||
)
|
||||
if not isinstance(result, list) or not result:
|
||||
raise SystemExit(f"sendMediaGroup: unexpected response shape: {result!r}")
|
||||
return int(result[0]["message_id"])
|
||||
|
||||
|
||||
def send_reply(token: str, chat_id: str, text: str, reply_to: int) -> None:
|
||||
"""Post a text message as a reply to the APK message."""
|
||||
from urllib.parse import urlencode
|
||||
@@ -286,27 +464,51 @@ def send_reply(token: str, chat_id: str, text: str, reply_to: int) -> None:
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apk", required=True)
|
||||
# Two ways to specify what to send:
|
||||
# --files <path> [--files <path> ...] (preferred, multi-platform)
|
||||
# Sends the files as a single Telegram media group with one
|
||||
# caption listing each filename + SHA-256. The follow-up changelog
|
||||
# reply is automatic for media-group posts (the FA bullet block
|
||||
# would normally live in the caption, but the per-file SHA list
|
||||
# eats that budget — see build_media_group_caption docstring).
|
||||
# --apk <path> (legacy, single file)
|
||||
# Sends one document with the original caption layout (title +
|
||||
# single SHA + brief FA note + two link rows). Reply with
|
||||
# changelog is gated on --with-changelog as before.
|
||||
# Exactly one of the two must be present; if --files is given multiple
|
||||
# times we use the media-group path even if --apk is also given.
|
||||
ap.add_argument("--apk", required=False,
|
||||
help="Single file to send via sendDocument (legacy). "
|
||||
"Prefer --files for new releases.")
|
||||
ap.add_argument("--files", action="append", default=[],
|
||||
help="Path to a release file. Pass once per file; "
|
||||
"2..=10 files are sent as a Telegram media group "
|
||||
"(one caption listing all filenames + SHA-256).")
|
||||
ap.add_argument("--version", required=True)
|
||||
ap.add_argument("--repo", required=True)
|
||||
ap.add_argument("--changelog", required=True,
|
||||
help="Path to docs/changelog/vX.Y.Z.md; only read when --with-changelog is passed.")
|
||||
# Default: just the APK + short caption (title + SHA-256 + repo URL +
|
||||
# release URL). The per-release Persian/English blockquote reply is
|
||||
# opt-in via `--with-changelog` so routine releases don't flood the
|
||||
# channel with bullet-point bodies. To re-enable for a specific tag:
|
||||
# set the repo variable TELEGRAM_INCLUDE_CHANGELOG=true before pushing
|
||||
# the tag (the workflow converts that into --with-changelog).
|
||||
help="Path to docs/changelog/vX.Y.Z.md; read for the "
|
||||
"FA brief-note in the legacy caption (--apk path) "
|
||||
"and for the reply-threaded changelog message.")
|
||||
# Default for --apk path: just the APK + short caption.
|
||||
# For --files path: the FA+EN reply is automatic since the caption is
|
||||
# full of SHA hashes; toggle is ignored in that case.
|
||||
ap.add_argument("--with-changelog", action="store_true",
|
||||
help="Include the Persian+English changelog as a reply-threaded message.")
|
||||
help="(--apk path only) Include the Persian+English "
|
||||
"changelog as a reply-threaded message. Ignored "
|
||||
"with --files: media group always replies with "
|
||||
"the changelog because the per-file SHA list "
|
||||
"leaves no caption room for the FA brief-note.")
|
||||
# Dry-run lets you verify the rendered caption locally without hitting
|
||||
# Telegram. Useful when changing the brief-release-note budget /
|
||||
# truncation logic — print, eyeball, push.
|
||||
# Telegram. Useful when changing caption layout — print, eyeball, push.
|
||||
ap.add_argument("--dry-run", action="store_true",
|
||||
help="Render the caption and print it instead of posting. "
|
||||
"Skips token/chat_id checks.")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.files and not args.apk:
|
||||
ap.error("either --apk or --files is required")
|
||||
|
||||
if not args.dry_run:
|
||||
token = os.environ.get("BOT_TOKEN", "")
|
||||
chat_id = os.environ.get("CHAT_ID", "")
|
||||
@@ -318,24 +520,64 @@ def main() -> int:
|
||||
chat_id = ""
|
||||
|
||||
ver = args.version
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Multi-file path (media group)
|
||||
# ------------------------------------------------------------------
|
||||
if args.files:
|
||||
files = list(args.files)
|
||||
if len(files) == 1:
|
||||
# Telegram's sendMediaGroup rejects single-item groups, so
|
||||
# one-file --files calls fall through to sendDocument with
|
||||
# the same multi-file caption shape (still has the SHA list
|
||||
# below the title). This makes --files a clean superset of
|
||||
# --apk semantically: callers can pass 1..=10 files without
|
||||
# branching on platform-specific build outputs.
|
||||
caption = build_media_group_caption(files, ver, args.repo)
|
||||
if args.dry_run:
|
||||
print(f"--- DRY RUN: single-file caption ({len(caption)} chars) ---")
|
||||
print(caption)
|
||||
return 0
|
||||
mid = send_document(token, chat_id, files[0], caption)
|
||||
print(f"sendDocument OK, message_id={mid}")
|
||||
else:
|
||||
caption = build_media_group_caption(files, ver, args.repo)
|
||||
if args.dry_run:
|
||||
print(f"--- DRY RUN: media-group caption ({len(caption)} chars) ---")
|
||||
print(caption)
|
||||
print(f"--- {len(files)} files would be uploaded ---")
|
||||
for f in files:
|
||||
print(f" {os.path.basename(f)}")
|
||||
return 0
|
||||
mid = send_media_group(token, chat_id, files, caption)
|
||||
print(f"sendMediaGroup OK ({len(files)} files), first message_id={mid}")
|
||||
|
||||
# Always reply with the changelog when sending a media group —
|
||||
# the per-file SHA list pushed the FA bullet headlines out of
|
||||
# the caption, so the reply is the only place they fit.
|
||||
# Single-file --files calls also reply, which matches the
|
||||
# multi-file behaviour and avoids surprising users who switch
|
||||
# back and forth between 1-file and N-file releases.
|
||||
fa, en = parse_changelog(args.changelog)
|
||||
if not fa and not en:
|
||||
print(f"No changelog at {args.changelog}, skipping reply.")
|
||||
return 0
|
||||
reply_parts: list = []
|
||||
if fa:
|
||||
reply_parts.append(f"<blockquote>{fa}</blockquote>")
|
||||
if en:
|
||||
reply_parts.append(f"<blockquote>{en}</blockquote>")
|
||||
send_reply(token, chat_id, "\n\n".join(reply_parts), mid)
|
||||
print("Reply OK")
|
||||
return 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Single-file path (legacy --apk, kept for any caller that hasn't
|
||||
# migrated to --files yet).
|
||||
# ------------------------------------------------------------------
|
||||
sha = sha256_of(args.apk)
|
||||
# Brief Persian release-note above the links. Pulled from the FA
|
||||
# half of `docs/changelog/v<ver>.md` so each release auto-includes
|
||||
# what's new without manual edits to this script. Truncated to fit
|
||||
# Telegram's 1024-char caption budget alongside title + SHA + the
|
||||
# two-link footer.
|
||||
fa_note = build_caption_release_note(args.changelog)
|
||||
|
||||
# Caption structure requested by the repo owner:
|
||||
# 1. Title + SHA-256 (as before)
|
||||
# 2. Brief Persian "what's new" note (extracted from changelog)
|
||||
# 3. Persian preamble labelling the repo link as
|
||||
# "GitHub repo + full Persian guide"
|
||||
# 4. Repo URL
|
||||
# 5. Persian preamble labelling the release link as
|
||||
# "this version's release — desktop/router builds live here"
|
||||
# 6. Release URL
|
||||
# Keeps total well under Telegram's 1024-char caption limit.
|
||||
caption_parts = [
|
||||
f"<b>mhrv-rs Android v{ver}</b>",
|
||||
"",
|
||||
@@ -376,7 +618,7 @@ def main() -> int:
|
||||
print(f"No changelog at {args.changelog}, skipping reply.")
|
||||
return 0
|
||||
|
||||
parts = []
|
||||
parts: list = []
|
||||
if fa:
|
||||
parts.append(f"<blockquote>{fa}</blockquote>")
|
||||
if en:
|
||||
|
||||
+151
-27
@@ -651,16 +651,28 @@ jobs:
|
||||
generate_release_notes: true
|
||||
|
||||
# Notify the Persian-speaking Telegram channel with the CI-built
|
||||
# Android APK + its sha256 + the per-version changelog from
|
||||
# `docs/changelog/v<tag>.md`.
|
||||
# binaries for macOS / Linux / Windows / Android, sent as a single
|
||||
# media group with one caption listing each filename + SHA-256, plus
|
||||
# a reply-threaded changelog from `docs/changelog/v<tag>.md`.
|
||||
#
|
||||
# Two Telegram API calls:
|
||||
# 1. sendDocument — APK file + a short caption (Telegram caps
|
||||
# captions at 1024 chars, and we have bigger changelogs than
|
||||
# that).
|
||||
# 2. sendMessage — full changelog as a reply to #1, Persian
|
||||
# quote-block first then English, same pattern as the
|
||||
# previous manual post. No emojis, as the user asked.
|
||||
# 1. sendMediaGroup — N (2..=10) documents in a single grouped post.
|
||||
# Telegram only renders the caption attached to the FIRST item,
|
||||
# so we put the whole filename+SHA list there and leave the rest
|
||||
# captionless. Caption budget is 1024 chars; a 6-file release
|
||||
# sits ~860 chars (script falls back to a filename-only list
|
||||
# with a warning if it ever overflows).
|
||||
# 2. sendMessage — full changelog as a reply to the first message
|
||||
# in the media group, Persian blockquote first then English.
|
||||
# Auto-sent for media-group posts since the per-file SHA list
|
||||
# pushes the FA brief-note out of the caption.
|
||||
#
|
||||
# We send the raw binaries (Mach-O / ELF / PE / APK) extracted from
|
||||
# the per-platform archives — no .zip / .tar.gz wrappers in the
|
||||
# channel, per the maintainer's request. macOS users wanting the
|
||||
# gatekeeper-friendly .app bundle still grab it from the GitHub
|
||||
# Releases page; the Telegram drop is for the "give me the binary
|
||||
# and let me run it" path.
|
||||
#
|
||||
# Needs two repo secrets:
|
||||
# TELEGRAM_BOT_TOKEN — bot the channel admits as poster
|
||||
@@ -683,51 +695,163 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Download every release artifact (build matrix + android job),
|
||||
# not just the universal APK. `merge-multiple: true` flattens the
|
||||
# per-job artifact dirs into one tree so we don't have to know
|
||||
# the artifact names in advance.
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: mhrv-rs-android-universal
|
||||
path: apk
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
# Pull the raw binary out of each per-platform archive and rename
|
||||
# it with the version suffix so users see what they're getting at
|
||||
# a glance in Telegram. Order in `tg-files/` doesn't matter to the
|
||||
# Telegram client (media-group items are displayed as a stack);
|
||||
# we sort with the find pipe at the end so the script always
|
||||
# passes them in the same order across runs (debugging-friendly).
|
||||
#
|
||||
# Per-platform binary picks:
|
||||
# - macOS amd64 / arm64: `mhrv-rs` (CLI). Mach-O binary; users
|
||||
# run from Terminal. The GUI .app bundle requires the .app
|
||||
# directory tree (can't ship as a single file), so .app users
|
||||
# grab the .app.zip from GitHub Releases.
|
||||
# - Linux amd64 / arm64: `mhrv-rs` (CLI), GLIBC build. Most
|
||||
# desktop Linux users run from terminal; arm64 doesn't have
|
||||
# a UI binary in our matrix anyway.
|
||||
# - Windows amd64: `mhrv-rs.exe` CLI, plus `mhrv-rs-ui.exe` if
|
||||
# it built (it currently does). The UI binary is what most
|
||||
# Windows users want; CLI is the backup.
|
||||
# - Android: the universal APK (already a single file).
|
||||
#
|
||||
# `if [ -f ... ]` guards on every extraction — a single-platform
|
||||
# build failure (rare but possible: the matrix has
|
||||
# `continue-on-error: true` for mipsel-softfloat) shouldn't kill
|
||||
# the Telegram post.
|
||||
- name: Prepare files for Telegram media group
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER="${{ inputs.version || github.ref_name }}"
|
||||
VER="${VER#v}"
|
||||
mkdir -p tg-files extract-tmp
|
||||
|
||||
# Helper: extract a named binary out of a tar.gz, rename
|
||||
# with the version-tagged target name, copy into tg-files/.
|
||||
# Empties extract-tmp/ between calls so cross-platform binary
|
||||
# name collisions (everyone packages a `mhrv-rs` inside their
|
||||
# archive) don't pick up the previous run's file.
|
||||
extract_tar() {
|
||||
local archive="$1" platform="$2" inner="${3:-mhrv-rs}" suffix="${4:-}"
|
||||
if [ ! -f "$archive" ]; then
|
||||
echo "::warning::missing $archive — skipping $platform"
|
||||
return 0
|
||||
fi
|
||||
rm -rf extract-tmp/* extract-tmp/.[!.]* 2>/dev/null || true
|
||||
tar xzf "$archive" -C extract-tmp
|
||||
if [ ! -f "extract-tmp/$inner" ]; then
|
||||
echo "::warning::$inner not in $archive — skipping $platform"
|
||||
return 0
|
||||
fi
|
||||
cp "extract-tmp/$inner" "tg-files/mhrv-rs-${platform}-v${VER}${suffix}"
|
||||
echo "prepared mhrv-rs-${platform}-v${VER}${suffix}"
|
||||
}
|
||||
|
||||
# Same shape but for .zip archives (Windows). `unzip -o`
|
||||
# overwrites without prompting, matching tar's behaviour.
|
||||
extract_zip() {
|
||||
local archive="$1" platform="$2" inner="$3" suffix="$4"
|
||||
if [ ! -f "$archive" ]; then
|
||||
echo "::warning::missing $archive — skipping $platform"
|
||||
return 0
|
||||
fi
|
||||
rm -rf extract-tmp/* extract-tmp/.[!.]* 2>/dev/null || true
|
||||
unzip -o "$archive" -d extract-tmp >/dev/null
|
||||
if [ ! -f "extract-tmp/$inner" ]; then
|
||||
echo "::warning::$inner not in $archive — skipping $platform"
|
||||
return 0
|
||||
fi
|
||||
cp "extract-tmp/$inner" "tg-files/mhrv-rs-${platform}-v${VER}${suffix}"
|
||||
echo "prepared mhrv-rs-${platform}-v${VER}${suffix}"
|
||||
}
|
||||
|
||||
# Desktop CLI binaries — primary platforms only. Linux armhf,
|
||||
# Linux musl, mipsel-softfloat are niche enough to leave on
|
||||
# the GitHub Releases page rather than crowd the Telegram
|
||||
# group (Telegram caps a media group at 10 items).
|
||||
extract_tar "artifacts/mhrv-rs-linux-amd64.tar.gz" "linux-amd64"
|
||||
extract_tar "artifacts/mhrv-rs-linux-arm64.tar.gz" "linux-arm64"
|
||||
extract_tar "artifacts/mhrv-rs-macos-amd64.tar.gz" "macos-amd64"
|
||||
extract_tar "artifacts/mhrv-rs-macos-arm64.tar.gz" "macos-arm64"
|
||||
|
||||
# Windows: ship the GUI binary since it's the typical case
|
||||
# for Windows users; the CLI still goes up via the GitHub
|
||||
# Releases page.
|
||||
extract_zip "artifacts/mhrv-rs-windows-amd64.zip" "windows-amd64-ui" "mhrv-rs-ui.exe" ".exe"
|
||||
|
||||
# Android universal APK is already a single file with the
|
||||
# version baked into the name — just copy it across.
|
||||
if [ -f "artifacts/mhrv-rs-android-universal-v${VER}.apk" ]; then
|
||||
cp "artifacts/mhrv-rs-android-universal-v${VER}.apk" tg-files/
|
||||
echo "prepared mhrv-rs-android-universal-v${VER}.apk"
|
||||
else
|
||||
echo "::warning::missing universal APK — skipping Android"
|
||||
fi
|
||||
|
||||
echo "--- tg-files/ ---"
|
||||
ls -la tg-files/
|
||||
count=$(find tg-files -maxdepth 1 -type f | wc -l)
|
||||
if [ "$count" -lt 1 ]; then
|
||||
echo "::error::no files prepared for Telegram post"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$count" -gt 10 ]; then
|
||||
echo "::error::Telegram media group caps at 10 items, got $count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Post to Telegram
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
INCLUDE_CHANGELOG: ${{ vars.TELEGRAM_INCLUDE_CHANGELOG }}
|
||||
# Python over curl/bash so we don't have to fight curl's -F
|
||||
# value-interpretation rules. curl treats `-F "caption=<..."`
|
||||
# as "read the caption from file named ..." when the value
|
||||
# starts with `<`, which matches our `<b>` HTML-bold tags and
|
||||
# silently turns the whole job into a "file not found" exit
|
||||
# 26. Python stdlib has no such wart.
|
||||
#
|
||||
# We pass --files once per file in tg-files/. The script
|
||||
# decides between sendMediaGroup (>=2 files) and sendDocument
|
||||
# (==1 file) automatically; the changelog reply is always sent
|
||||
# in the --files path because the per-file SHA list eats the
|
||||
# caption budget that previously held the FA brief-note.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER="${{ inputs.version || github.ref_name }}"
|
||||
VER="${VER#v}"
|
||||
APK="apk/mhrv-rs-android-universal-v${VER}.apk"
|
||||
|
||||
if [ -z "${BOT_TOKEN:-}" ] || [ -z "${CHAT_ID:-}" ]; then
|
||||
echo "::notice::TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID not set, skipping Telegram post"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$APK" ]; then
|
||||
echo "::error::expected $APK to exist; got:"
|
||||
ls -la apk/
|
||||
# Build the --files args. `find … -print0 | sort -z` sorts
|
||||
# the filenames so the order is deterministic across runs
|
||||
# (helps reading the channel post — same column order each
|
||||
# release). xargs -0 is used so spaces/newlines in filenames
|
||||
# don't break the arg list.
|
||||
FILES_ARGS=()
|
||||
while IFS= read -r -d '' f; do
|
||||
FILES_ARGS+=(--files "$f")
|
||||
done < <(find tg-files -maxdepth 1 -type f -print0 | sort -z)
|
||||
|
||||
if [ "${#FILES_ARGS[@]}" -eq 0 ]; then
|
||||
echo "::error::no --files args to pass; tg-files/ is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --with-changelog is opt-in. Default post is just the APK
|
||||
# plus a short caption with the SHA-256, repo URL, and release
|
||||
# URL — no long body. To include the Persian/English bullets
|
||||
# for a specific tag, set the repo variable
|
||||
# TELEGRAM_INCLUDE_CHANGELOG=true before pushing that tag.
|
||||
INCLUDE_CHANGELOG_FLAG=""
|
||||
if [ "${INCLUDE_CHANGELOG:-}" = "true" ]; then
|
||||
INCLUDE_CHANGELOG_FLAG="--with-changelog"
|
||||
fi
|
||||
python3 .github/scripts/telegram_release_notify.py \
|
||||
--apk "$APK" \
|
||||
"${FILES_ARGS[@]}" \
|
||||
--version "$VER" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--changelog "docs/changelog/v${VER}.md" \
|
||||
$INCLUDE_CHANGELOG_FLAG
|
||||
--changelog "docs/changelog/v${VER}.md"
|
||||
|
||||
Reference in New Issue
Block a user