From 8d5dfffbef0e410f3b4c11a82c546a4f2c690add Mon Sep 17 00:00:00 2001 From: bachnxuan Date: Mon, 13 Jul 2026 11:10:33 +0700 Subject: [PATCH] feat(tg): group build artifacts in one message Signed-off-by: bachnxuan --- README.md | 2 +- ci/package.sh | 47 +++++++++++++++++++++----------- ci/utils.sh | 6 +++++ py/src/tools/cli.py | 7 ++++- py/src/tools/models.py | 12 +++++++++ py/src/tools/tg.py | 61 +++++++++++++++++++++++++++++++++++++++++- 6 files changed, 116 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 48c51e9..1b55333 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ notes: - `LXC` only works with `BUILD_TARGET=xaga` - `TG_NOTIFY=true` needs `TG_BOT_TOKEN` and `TG_CHAT_ID` - `GH_TOKEN` is optional, but helps when fetching latest release assets +- generic telegram success notifications upload one grouped message containing the AnyKernel3 zip plus a boot-image zip ## output @@ -190,4 +191,3 @@ notes: | build.log | build log | where `` has the format `${KERNEL_NAME}-${KERNEL_VERSION}-${VARIANT}` (with `-${KERNEL_COMMIT}` appended when `IS_RELEASE` is not `true`). - diff --git a/ci/package.sh b/ci/package.sh index 2125fca..1f17532 100644 --- a/ci/package.sh +++ b/ci/package.sh @@ -106,21 +106,17 @@ write_metadata() { "$KERNEL_COMMIT" } -notify_success() { - local final_package="$1" - local build_time="$2" - # For indicating package type (boot image, anykernel3) - local additional_tag="$3" - +build_result_caption() { + local build_time="$1" + local additional_tag="$2" + local note="${3:-}" local kernel_commit_url kernel_commit_url="$(repo_spec "$KERNEL_REPO" github-commit-url "$KERNEL_COMMIT")" local minutes=$((build_time / 60)) local seconds=$((build_time % 60)) - local result_caption - result_caption=$( - cat << EOF + cat << EOF ✅ *$(escape_md_v2 "$KERNEL_NAME Build Successfully!")* 🏷️ \#$(escape_md_v2 "$BUILD_TAG") \#$(escape_md_v2 "$additional_tag") @@ -131,9 +127,18 @@ $(tg_run_line) *Commit:* [$(escape_md_v2 "$KERNEL_COMMIT")]($(escape_md_v2 "$kernel_commit_url")) *Compiler:* $(escape_md_v2 "$COMPILER_STRING") *Features:* KSU $(parse_bool "$KSU"), SuSFS $(is_true "$SUSFS" && escape_md_v2 "$SUSFS_VERSION" || echo "Disabled"), LXC $(parse_bool "$LXC"), Stock config $(parse_bool "$STOCK_CONFIG") +$(if [[ -n $note ]]; then printf '*Note:* %s\n' "$(escape_md_v2 "$note")"; fi) EOF - ) +} +notify_success() { + local final_package="$1" + local build_time="$2" + # For indicating package type (boot image, anykernel3) + local additional_tag="$3" + + local result_caption + result_caption="$(build_result_caption "$build_time" "$additional_tag")" telegram_upload_file "$final_package" "$result_caption" } @@ -143,16 +148,26 @@ telegram_notify() { # AnyKernel3 local ak3_package="$OUT_DIR/$package_name-AnyKernel3.zip" - notify_success "$ak3_package" "$build_time" "anykernel3" - - # Boot image if [[ "$BUILD_TARGET" == "xaga" ]]; then + notify_success "$ak3_package" "$build_time" "anykernel3" return fi + + local boot_package="$OUT_DIR/$package_name-boot.zip" pushd "$OUT_DIR" > /dev/null - zip -9q -T "$package_name-boot.zip" "$package_name"-boot*.img + zip -9q -T "$(basename "$boot_package")" "$package_name"-boot*.img popd > /dev/null - notify_success "$OUT_DIR/$package_name-boot.zip" "$build_time" "boot_image" - rm -f "$OUT_DIR/$package_name-boot.zip" + local result_caption + result_caption="$( + build_result_caption \ + "$build_time" \ + "generic" \ + "Boot image zip contains raw, gzip, and lz4 variants." + )" + telegram_upload_gallery \ + "$result_caption" \ + "$ak3_package" \ + "$boot_package" + rm -f "$boot_package" } diff --git a/ci/utils.sh b/ci/utils.sh index f4722ca..9a8ec2c 100644 --- a/ci/utils.sh +++ b/ci/utils.sh @@ -50,6 +50,12 @@ telegram_upload_file() { printf '%s' "$*" | py_cli tg doc "$file" } +telegram_upload_gallery() { + is_true "${TG_NOTIFY:-false}" || return 0 + + printf '%s' "$1" | py_cli tg gallery "${@:2}" +} + init_logging() { # Clean logfile before writing : > "$LOGFILE" diff --git a/py/src/tools/cli.py b/py/src/tools/cli.py index b033991..e1972e7 100644 --- a/py/src/tools/cli.py +++ b/py/src/tools/cli.py @@ -6,7 +6,7 @@ from .common import escape_md_v2, stdin_text from .github_release import asset_url_api, asset_url_json, next_tag from .meta import write_metadata -from .tg import send_document, send_message +from .tg import send_document, send_document_gallery, send_message app = typer.Typer(no_args_is_help=True) release_app = typer.Typer(no_args_is_help=True) @@ -76,6 +76,11 @@ def tg_doc(file: Path) -> None: send_document(file, stdin_text()) +@tg_app.command("gallery") +def tg_gallery(files: list[Path]) -> None: + send_document_gallery(files, stdin_text()) + + @util_app.command("escape-md-v2") def util_escape_md_v2(text: Annotated[str, typer.Argument()]) -> None: typer.echo(escape_md_v2(text), nl=False) diff --git a/py/src/tools/models.py b/py/src/tools/models.py index 69f86ae..ece73c9 100644 --- a/py/src/tools/models.py +++ b/py/src/tools/models.py @@ -47,3 +47,15 @@ class TelegramDocumentRequest(BaseModel): parse_mode: str = "MarkdownV2" disable_web_page_preview: str = "true" caption: str + + +class TelegramMediaDocument(BaseModel): + type: str = "document" + media: str + caption: str | None = None + parse_mode: str | None = None + + +class TelegramMediaGroupRequest(BaseModel): + chat_id: str + media: list[TelegramMediaDocument] diff --git a/py/src/tools/tg.py b/py/src/tools/tg.py index a675004..8af2e58 100644 --- a/py/src/tools/tg.py +++ b/py/src/tools/tg.py @@ -1,10 +1,18 @@ +import json +from io import BufferedReader from pathlib import Path import requests from pydantic import ValidationError from .common import die, env -from .models import TelegramDocumentRequest, TelegramMessageRequest, TelegramResponse +from .models import ( + TelegramDocumentRequest, + TelegramMediaDocument, + TelegramMediaGroupRequest, + TelegramMessageRequest, + TelegramResponse, +) def tg_api_url(method: str) -> str: @@ -54,3 +62,54 @@ def send_document(file_path: Path, caption: str) -> None: die(f"sendDocument returned invalid JSON: {e}") if not data.ok: die(f"sendDocument failed: {data.description or 'Unknown error'}") + + +def send_document_gallery(file_paths: list[Path], caption: str) -> None: + if not file_paths: + die("No files provided for document gallery upload") + + missing_files = [str(file_path) for file_path in file_paths if not file_path.exists()] + if missing_files: + die(f"File not found: {', '.join(missing_files)}") + + media = [] + files: dict[str, tuple[str, BufferedReader]] = {} + handles: list[BufferedReader] = [] + try: + for index, file_path in enumerate(file_paths): + attachment_name = f"file{index}" + media.append( + TelegramMediaDocument( + media=f"attach://{attachment_name}", + caption=caption if index == 0 else None, + parse_mode="MarkdownV2" if index == 0 else None, + ) + ) + + handle = file_path.open("rb") + handles.append(handle) + files[attachment_name] = (file_path.name, handle) + + payload = TelegramMediaGroupRequest(chat_id=env("TG_CHAT_ID"), media=media) + response = requests.post( + tg_api_url("sendMediaGroup"), + data={ + "chat_id": payload.chat_id, + "media": json.dumps([item.model_dump(mode="json", exclude_none=True) for item in payload.media]), + }, + files=files, + timeout=180, + ) + response.raise_for_status() + except requests.RequestException as e: + die(f"sendMediaGroup request failed: {e}") + finally: + for handle in handles: + handle.close() + + try: + data = TelegramResponse.model_validate_json(response.text) + except ValidationError as e: + die(f"sendMediaGroup returned invalid JSON: {e}") + if not data.ok: + die(f"sendMediaGroup failed: {data.description or 'Unknown error'}")