mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 06:24:35 +03:00
501d54edec
* ci(release): pin i686-pc-windows-msvc to Rust 1.77.2 for Win7 compat Fixes #318. Rust 1.78 (May 2024) raised the std MSRV for Windows from Win7 to Win10 by switching std::time to GetSystemTimePreciseAsFileTime, a kernel32 export that doesn't exist on Win7 SP1. Building the i686 binary with stable Rust (currently 1.86+) produces an exe that fails to load on Win7 with "the procedure entry point GetSystemTimePreciseAsFile could not be located in the dynamic link library kernel32.dll" — making the whole reason we ship i686 (legacy Win7 32-bit boxes per #272) moot. Add a per-matrix `rust_toolchain` knob; only i686-pc-windows-msvc uses it, pinning to 1.77.2 (last stable that supports Win7). Other targets remain on @stable and pick up regular Rust updates. dtolnay/rust-toolchain switches from `@stable` to `@master` because the per-tag aliases (`@stable`, `@1.77.2`) can't be selected via a matrix variable — `@master` accepts the toolchain string as input. Cache key gains a toolchain suffix so the 1.77.2 cache doesn't collide with the stable cache for the same target, and a future toolchain bump invalidates only the affected slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(release): make i686-pc-windows-msvc continue-on-error Companion to the Rust 1.77.2 pin: if the deps' MSRV ever moves above 1.77, the i686 target will fail to build, but we don't want it to block the rest of the release. Mirror the mipsel-softfloat approach. If/when this triggers, options are dropping i686 entirely or moving to the tier-3 i686-win7-windows-msvc target. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
895 lines
42 KiB
YAML
895 lines
42 KiB
YAML
name: release
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*'
|
||
# Manual re-trigger for the case where one matrix job (e.g. mipsel-softfloat)
|
||
# failed on the original tag push and we've since pushed the build fix to
|
||
# main but can't force-move the immutable tag (tag protection rule). Run
|
||
# this workflow manually with `version` set to the existing release tag —
|
||
# the build matrix runs against the current main, artifacts are uploaded
|
||
# to the matching release page, and the release-notes step is a no-op
|
||
# (release already exists). Pair with `gh variable set
|
||
# TELEGRAM_NOTIFY_ENABLED --body false` before dispatch if you don't want
|
||
# the channel re-pinged for what's effectively the same release.
|
||
workflow_dispatch:
|
||
inputs:
|
||
version:
|
||
description: 'Existing release tag to upload to (without the leading v). Example: 1.4.0'
|
||
required: true
|
||
type: string
|
||
|
||
permissions:
|
||
contents: write
|
||
# `tunnel-docker` job pushes to ghcr.io/therealaleph/mhrv-tunnel-node.
|
||
# `packages: write` is required by docker/login-action when authenticating
|
||
# to GHCR with the workflow's auto-provisioned GITHUB_TOKEN. Granted at
|
||
# the workflow level so the matrix-build job (which doesn't need it) and
|
||
# the release job (which doesn't need it) both still have a single
|
||
# well-scoped permissions block.
|
||
packages: write
|
||
|
||
# Runner strategy:
|
||
# - Linux + Android + mipsel: self-hosted (mhrv-hetzner-*, Hetzner
|
||
# 8-core / 31 GB Ubuntu 24.04 box with
|
||
# Rust, Android SDK+NDK, Docker, all
|
||
# cross-compile toolchains pre-installed).
|
||
# Two runners registered for parallelism.
|
||
# - macOS arm64 + amd64, Windows: GitHub-hosted (we don't self-host those
|
||
# OSes; the free minutes on a public repo
|
||
# are plenty for those two platforms).
|
||
#
|
||
# Why self-hosted: GH-hosted 2-core runners were spending ~13 min cold per
|
||
# release; on the Hetzner box a cold linux-amd64 build is 1m9s, and warm
|
||
# builds with Swatinem/rust-cache are sub-minute. Keeps the toolchain warm,
|
||
# and more importantly keeps target/ warm via the rust-cache action.
|
||
|
||
jobs:
|
||
build:
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
# Pin to Ubuntu 22.04 GLIBC target (GLIBC 2.35) so the glibc builds
|
||
# load on any distro ≥ Ubuntu 22.04 / Debian 12 / Mint 21 / Fedora 36.
|
||
# On self-hosted this is a Rust-side choice (cargo target triple),
|
||
# not an OS-of-the-runner choice — the runner itself is Ubuntu 24.04
|
||
# (GLIBC 2.39), but we link against the 2.35-era glibc via the
|
||
# x86_64-unknown-linux-gnu target triple which pins to the oldest
|
||
# GLIBC symbol version rustc is willing to emit. Users behind tight
|
||
# internet who can't dist-upgrade keep working.
|
||
- target: x86_64-unknown-linux-gnu
|
||
os: [self-hosted, linux, x64, mhrv-build]
|
||
name: mhrv-rs-linux-amd64
|
||
- target: aarch64-unknown-linux-gnu
|
||
os: [self-hosted, linux, x64, mhrv-build]
|
||
name: mhrv-rs-linux-arm64
|
||
- target: arm-unknown-linux-gnueabihf
|
||
os: [self-hosted, linux, x64, mhrv-build]
|
||
name: mhrv-rs-raspbian-armhf
|
||
- target: x86_64-apple-darwin
|
||
os: macos-latest
|
||
name: mhrv-rs-macos-amd64
|
||
- target: aarch64-apple-darwin
|
||
os: macos-latest
|
||
name: mhrv-rs-macos-arm64
|
||
- target: x86_64-pc-windows-gnu
|
||
os: windows-latest
|
||
name: mhrv-rs-windows-amd64
|
||
- target: i686-pc-windows-msvc
|
||
os: windows-latest
|
||
name: mhrv-rs-windows-i686
|
||
# Pin Rust 1.77.2 specifically for this target. Rust 1.78
|
||
# (May 2024) raised the Windows MSRV from Win7 to Win10 by
|
||
# switching std::time to GetSystemTimePreciseAsFileTime, a
|
||
# kernel32 export that doesn't exist on Win7. The whole
|
||
# reason this target ships is to support legacy Win7 32-bit
|
||
# boxes (#272), so a stock-stable build defeats the purpose.
|
||
# 1.77.2 is the last stable that produces a Win7-loadable
|
||
# binary; other targets stay on @stable. (Fixes #318.)
|
||
rust_toolchain: "1.77.2"
|
||
- target: x86_64-unknown-linux-musl
|
||
os: [self-hosted, linux, x64, mhrv-build]
|
||
name: mhrv-rs-linux-musl-amd64
|
||
- target: aarch64-unknown-linux-musl
|
||
os: [self-hosted, linux, x64, mhrv-build]
|
||
name: mhrv-rs-linux-musl-arm64
|
||
# OpenWRT MT7621 (soft-float mipsel 32-bit). Dozens of cheap
|
||
# home routers run this chipset and they *specifically* need
|
||
# the soft-float variant — MT7621 has no hardware FPU and a
|
||
# hard-float binary segfaults on the first fp op. Tier-3 in
|
||
# Rust since 1.72; we build it via messense's musl-cross
|
||
# docker image which still has a mipsel-softfloat toolchain.
|
||
# `continue-on-error: true` so a regression here doesn't block
|
||
# the rest of the release. Issue #26.
|
||
- target: mipsel-unknown-linux-musl
|
||
os: [self-hosted, linux, x64, mhrv-build]
|
||
name: mhrv-rs-openwrt-mipsel-softfloat
|
||
mipsel_softfloat: true
|
||
|
||
runs-on: ${{ matrix.os }}
|
||
# mipsel-softfloat is best-effort: the Rust tier-3 target occasionally
|
||
# regresses. Letting it fail keeps the main release going so
|
||
# desktop/Android users aren't blocked by MT7621 router support.
|
||
# i686-pc-windows-msvc is similarly best-effort — pinned to Rust
|
||
# 1.77.2 for Win7 compat (#318), so a future dep MSRV bump above
|
||
# 1.77 will fail this one target. Letting it skip keeps the rest
|
||
# of the release unblocked; we'd then choose between dropping the
|
||
# target or moving to the tier-3 win7-msvc target with build-std.
|
||
continue-on-error: ${{ matrix.mipsel_softfloat == true || matrix.target == 'i686-pc-windows-msvc' }}
|
||
|
||
steps:
|
||
# Heal any root-owned leftovers from a previous mipsel docker
|
||
# build that failed before its post-step chown could run. The
|
||
# docker container writes target/ as root, and if cargo errors
|
||
# inside the container the outer `sudo chown -R` line never
|
||
# executes (bash -e exits on the docker non-zero), leaving root-
|
||
# owned files that fail every subsequent `actions/checkout@v4`
|
||
# workspace clean with `EACCES: permission denied unlink`. This
|
||
# step is a no-op on a clean runner, so cheap to keep always-on.
|
||
# Self-hosted only; GitHub-hosted runners get a fresh VM each run.
|
||
- name: Pre-checkout — clean root-owned files (self-hosted only)
|
||
if: contains(matrix.os, 'self-hosted')
|
||
run: |
|
||
if [ -d "$GITHUB_WORKSPACE/target" ]; then
|
||
sudo rm -rf "$GITHUB_WORKSPACE/target" || true
|
||
fi
|
||
# Stale .rustc_info.json at the workspace root is the
|
||
# specific file `actions/checkout` errors on; nuke any
|
||
# other root-owned scraps that may be sitting there too.
|
||
sudo find "$GITHUB_WORKSPACE" -maxdepth 2 -uid 0 -exec rm -rf {} + 2>/dev/null || true
|
||
|
||
- uses: actions/checkout@v4
|
||
|
||
# Skip the host-level rustup install for mipsel-softfloat — that
|
||
# target is tier-3 in stable Rust (no prebuilt stdlib available
|
||
# via rustup), and the docker image we use for this build ships
|
||
# its own Rust toolchain + std. Trying to pass
|
||
# `targets: mipsel-unknown-linux-musl` to dtolnay/rust-toolchain
|
||
# errors out with "error: component 'rust-std' for target
|
||
# 'mipsel-unknown-linux-musl' is unavailable for download", which
|
||
# fails the job before the docker step ever runs.
|
||
#
|
||
# On self-hosted this action is mostly a no-op: rustup is already
|
||
# installed and the standard target triples are pre-added. It
|
||
# still verifies the target is present and is cheap enough to keep
|
||
# as a safety net.
|
||
# Per-matrix-entry toolchain selection. Default is `stable` (latest)
|
||
# for every target except where `rust_toolchain` is explicitly pinned
|
||
# — currently just i686-pc-windows-msvc, which needs 1.77.2 to keep
|
||
# the Win7 binary loadable (Rust 1.78+ raised Windows MSRV to Win10).
|
||
- uses: dtolnay/rust-toolchain@master
|
||
if: matrix.mipsel_softfloat != true
|
||
with:
|
||
toolchain: ${{ matrix.rust_toolchain || 'stable' }}
|
||
targets: ${{ matrix.target }}
|
||
|
||
# Cache target/ + cargo registry across runs — this is the big
|
||
# self-hosted speedup. Without it, actions/checkout@v4's default
|
||
# `git clean -ffdx` wipes target/ between runs and every build is
|
||
# cold. With it, warm builds are sub-minute even for the full
|
||
# release profile.
|
||
#
|
||
# cache-bin: false is MANDATORY on our self-hosted runners. With
|
||
# the default (true), rust-cache aggressively prunes $CARGO_HOME/bin
|
||
# of binaries it didn't install via `cargo install`, including the
|
||
# `rustup` binary that cargo/rustc/etc. are symlinked to. The next
|
||
# job then hits "command not found" or a broken-symlink TOML parse
|
||
# error from a stale cargo. We want target/ + registry caching, NOT
|
||
# bin pruning. rustup is pre-installed on the runners anyway.
|
||
- uses: Swatinem/rust-cache@v2
|
||
if: matrix.mipsel_softfloat != true
|
||
with:
|
||
# Include toolchain in the cache key so a pinned-Rust target
|
||
# (i686-pc-windows-msvc on 1.77.2) doesn't collide with
|
||
# stable-Rust caches for other targets, and a future toolchain
|
||
# bump invalidates only the affected slot.
|
||
key: ${{ matrix.target }}-${{ matrix.rust_toolchain || 'stable' }}
|
||
cache-bin: "false"
|
||
|
||
# eframe needs a few system libs on Linux for window management, keyboard,
|
||
# and OpenGL/X11/Wayland. Gated to GitHub-hosted runners only — the
|
||
# self-hosted runners pre-install all of these once at setup time, and
|
||
# letting multiple parallel matrix jobs race on `sudo apt-get install`
|
||
# fights over /var/lib/apt/lists/lock and fails them all.
|
||
- name: Install Linux eframe system deps
|
||
if: runner.os == 'Linux' && runner.environment == 'github-hosted'
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y \
|
||
libxkbcommon-dev \
|
||
libwayland-dev \
|
||
libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
|
||
libx11-dev \
|
||
libgl1-mesa-dev libglib2.0-dev libgtk-3-dev
|
||
|
||
# Cross-compile toolchains. Same story as above — gated to hosted
|
||
# runners; self-hosted has gcc-aarch64-linux-gnu + gcc-arm-linux-gnueabihf
|
||
# pre-installed, and the linker entries live in
|
||
# /home/ghrunner/cargo-{01,02}/config.toml (seeded once at runner
|
||
# setup time, picked up via CARGO_HOME env).
|
||
- name: Install aarch64 cross-compile toolchain (Linux only)
|
||
if: matrix.target == 'aarch64-unknown-linux-gnu' && runner.environment == 'github-hosted'
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||
echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml
|
||
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml
|
||
|
||
- name: Install armhf cross-compile toolchain (Linux only)
|
||
if: matrix.target == 'arm-unknown-linux-gnueabihf' && runner.environment == 'github-hosted'
|
||
run: |
|
||
sudo apt-get update
|
||
sudo apt-get install -y gcc-arm-linux-gnueabihf
|
||
echo '[target.arm-unknown-linux-gnueabihf]' >> ~/.cargo/config.toml
|
||
echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config.toml
|
||
|
||
- name: Install Windows MinGW toolchain
|
||
if: matrix.target == 'x86_64-pc-windows-gnu'
|
||
id: msys2
|
||
uses: msys2/setup-msys2@v2
|
||
with:
|
||
msystem: MINGW64
|
||
update: true
|
||
install: mingw-w64-x86_64-gcc
|
||
|
||
- name: Configure Windows GNU linker
|
||
if: matrix.target == 'x86_64-pc-windows-gnu'
|
||
shell: pwsh
|
||
run: |
|
||
$gcc = "${{ steps.msys2.outputs.msys2-location }}\mingw64\bin\gcc.exe" -replace '\\','/'
|
||
New-Item -ItemType Directory -Force -Path $env:USERPROFILE/.cargo | Out-Null
|
||
Add-Content -Path $env:USERPROFILE/.cargo/config.toml -Value '[target.x86_64-pc-windows-gnu]'
|
||
Add-Content -Path $env:USERPROFILE/.cargo/config.toml -Value "linker = '$gcc'"
|
||
|
||
- name: Build CLI
|
||
if: "!endsWith(matrix.target, '-linux-musl')"
|
||
run: cargo build --release --target ${{ matrix.target }} --bin mhrv-rs
|
||
|
||
# Fully-static musl builds for OpenWRT / Alpine / libc-less systems.
|
||
# messense/rust-musl-cross ships a pre-built musl toolchain so `ring`
|
||
# (rustls' crypto backend) cross-compiles cleanly on both archs.
|
||
- name: Build CLI (musl via docker)
|
||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||
run: |
|
||
docker run --rm -v "$PWD":/src -w /src \
|
||
messense/rust-musl-cross:x86_64-musl \
|
||
cargo build --release --target x86_64-unknown-linux-musl --bin mhrv-rs
|
||
sudo chown -R "$(id -u):$(id -g)" target
|
||
|
||
- name: Build CLI (musl via docker, arm64)
|
||
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||
run: |
|
||
docker run --rm -v "$PWD":/src -w /src \
|
||
messense/rust-musl-cross:aarch64-musl \
|
||
cargo build --release --target aarch64-unknown-linux-musl --bin mhrv-rs
|
||
sudo chown -R "$(id -u):$(id -g)" target
|
||
|
||
# OpenWRT MT7621 / mipsel-softfloat. messense doesn't publish a
|
||
# `:mipsel-musl-softfloat` tag — the mipsel-musl image is
|
||
# hardfloat. We build soft-float anyway via
|
||
# `RUSTFLAGS=-C target-feature=+soft-float` + `-Z build-std` so
|
||
# libstd itself is recompiled to emit soft-float code. The
|
||
# gcc/musl shipping in the image is hardfloat but we never link
|
||
# anything more than libc (`ring` is pure asm for the crypto
|
||
# that matters), so musl's lack of softfloat libm doesn't bite.
|
||
# Requires nightly Rust since mipsel is Rust tier 3 in the
|
||
# stable channel — no prebuilt std.
|
||
- name: Build CLI (mipsel-softfloat via docker)
|
||
if: matrix.target == 'mipsel-unknown-linux-musl' && matrix.mipsel_softfloat == true
|
||
# The inner script is single-quoted so the `#` lines stay as
|
||
# real comments. An earlier version of this step used
|
||
# `sh -c "... \` (backslash-continuation inside a
|
||
# double-quoted YAML folded string) which collapsed into one
|
||
# line — the first `#` then commented out everything after it,
|
||
# reducing the whole docker payload to `set -eux;` and failing
|
||
# silently at the post-docker chown. Heredoc-style single
|
||
# quotes preserve newlines verbatim; no comment collapse.
|
||
run: |
|
||
# Always chown back, even if docker exits non-zero. The previous
|
||
# form (`docker run …; sudo chown …`) ran chown only on success
|
||
# because bash -e short-circuits on the docker failure; that
|
||
# left target/ root-owned and broke `actions/checkout@v4` on
|
||
# every subsequent self-hosted run with EACCES on
|
||
# target/.rustc_info.json. The `trap … EXIT` runs the chown
|
||
# whether docker succeeded or failed, so a transient mipsel
|
||
# compile regression never poisons the runner workspace.
|
||
set +e
|
||
trap 'sudo chown -R "$(id -u):$(id -g)" target 2>/dev/null || true' EXIT
|
||
docker run --rm -v "$PWD":/src -w /src \
|
||
-e RUSTFLAGS='-C target-feature=+soft-float' \
|
||
messense/rust-musl-cross:mipsel-musl \
|
||
bash -c '
|
||
set -eux
|
||
# The image ships a pre-installed nightly that rustup
|
||
# cannot upgrade in place — `clippy-preview/share/doc/clippy/README.md`
|
||
# is missing from the pre-bake, and rustup errors with
|
||
# "failure removing component clippy-preview". Nuke it
|
||
# first, then install fresh.
|
||
rustup toolchain uninstall nightly 2>/dev/null || true
|
||
rustup toolchain install nightly --profile minimal
|
||
rustup component add rust-src --toolchain nightly
|
||
cargo +nightly build --release \
|
||
-Z build-std=std,panic_abort \
|
||
--target mipsel-unknown-linux-musl \
|
||
--bin mhrv-rs
|
||
'
|
||
rc=$?
|
||
# `trap … EXIT` will fire the chown on shell exit — the explicit
|
||
# exit here just propagates the docker exit code as the step
|
||
# status (success vs continue-on-error path).
|
||
exit $rc
|
||
|
||
# UI build: we try to build the UI binary on every platform. If it fails
|
||
# on cross-compile for linux-arm64 (missing arm64 system libs cross),
|
||
# we still ship the CLI. We also skip the UI on musl targets (OpenWRT etc.
|
||
# are headless, bundling X11 makes no sense).
|
||
- name: Build UI
|
||
if: matrix.target != 'aarch64-unknown-linux-gnu' && matrix.target != 'arm-unknown-linux-gnueabihf' && !endsWith(matrix.target, '-linux-musl')
|
||
run: cargo build --release --target ${{ matrix.target }} --features ui --bin mhrv-rs-ui
|
||
|
||
- name: Package (unix)
|
||
if: runner.os != 'Windows'
|
||
run: |
|
||
mkdir -p dist
|
||
cp target/${{ matrix.target }}/release/mhrv-rs dist/mhrv-rs
|
||
chmod +x dist/mhrv-rs
|
||
if [ -f target/${{ matrix.target }}/release/mhrv-rs-ui ]; then
|
||
cp target/${{ matrix.target }}/release/mhrv-rs-ui dist/mhrv-rs-ui
|
||
chmod +x dist/mhrv-rs-ui
|
||
fi
|
||
# OpenWRT / musl archives get the procd init script instead of run.sh,
|
||
# since routers don't have a CA to install and run headless via procd.
|
||
case "${{ matrix.target }}" in
|
||
*-linux-musl)
|
||
cp assets/openwrt/mhrv-rs.init dist/mhrv-rs.init
|
||
chmod +x dist/mhrv-rs.init
|
||
;;
|
||
*)
|
||
cp assets/launchers/run.sh dist/run.sh
|
||
chmod +x dist/run.sh
|
||
if [ "${{ runner.os }}" = "macOS" ]; then
|
||
cp assets/launchers/run.command dist/run.command
|
||
chmod +x dist/run.command
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
- name: Build macOS .app bundle
|
||
if: runner.os == 'macOS'
|
||
run: |
|
||
# Tag push: $GITHUB_REF == "refs/tags/v1.4.0", strip "refs/tags/v".
|
||
# workflow_dispatch: inputs.version comes in as e.g. "1.4.0".
|
||
# Fall back to ref_name (the bare branch/tag name) and strip a
|
||
# possible leading "v" so both paths produce the bare version.
|
||
VER="${{ inputs.version || github.ref_name }}"
|
||
VER="${VER#v}"
|
||
./assets/macos/build-app.sh dist/mhrv-rs-ui "$VER" dist
|
||
# Make a clean zip of just the .app for the release
|
||
cd dist
|
||
zip -qry "${{ matrix.name }}-app.zip" mhrv-rs.app
|
||
|
||
- name: Package (windows)
|
||
if: runner.os == 'Windows'
|
||
shell: pwsh
|
||
run: |
|
||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||
Copy-Item target/${{ matrix.target }}/release/mhrv-rs.exe dist/mhrv-rs.exe
|
||
if (Test-Path target/${{ matrix.target }}/release/mhrv-rs-ui.exe) {
|
||
Copy-Item target/${{ matrix.target }}/release/mhrv-rs-ui.exe dist/mhrv-rs-ui.exe
|
||
}
|
||
Copy-Item assets/launchers/run.bat dist/run.bat
|
||
|
||
- name: Make archive
|
||
shell: bash
|
||
run: |
|
||
cd dist
|
||
case "${{ matrix.target }}" in
|
||
*-pc-windows-*)
|
||
7z a -tzip "${{ matrix.name }}.zip" mhrv-rs.exe mhrv-rs-ui.exe run.bat
|
||
;;
|
||
*-apple-darwin)
|
||
tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh run.command
|
||
;;
|
||
*-linux-musl)
|
||
tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs.init
|
||
;;
|
||
*)
|
||
tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh 2>/dev/null || tar czf "${{ matrix.name }}.tar.gz" mhrv-rs run.sh
|
||
;;
|
||
esac
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: ${{ matrix.name }}
|
||
path: |
|
||
dist/${{ matrix.name }}.tar.gz
|
||
dist/${{ matrix.name }}.zip
|
||
dist/${{ matrix.name }}-app.zip
|
||
if-no-files-found: ignore
|
||
|
||
# Android build — separate job so it doesn't inflate the matrix. The
|
||
# Rust side here cross-compiles to FOUR ABIs (arm64-v8a, armeabi-v7a,
|
||
# x86_64, x86) via cargo-ndk and drops the .so files into the Gradle
|
||
# project's jniLibs/ tree, which then packages them into a single
|
||
# universal APK. Users pick it once, no per-ABI split.
|
||
#
|
||
# Runs on self-hosted. The runner has Android SDK + NDK r26c + cargo-ndk
|
||
# pre-installed under /opt/android-sdk; the env block below points Gradle
|
||
# at those paths so we don't re-download ~1 GB of SDK per release.
|
||
android:
|
||
runs-on: [self-hosted, linux, x64, mhrv-build]
|
||
env:
|
||
ANDROID_SDK_ROOT: /opt/android-sdk
|
||
ANDROID_HOME: /opt/android-sdk
|
||
ANDROID_NDK_HOME: /opt/android-sdk/ndk/26.2.11394342
|
||
ANDROID_NDK_ROOT: /opt/android-sdk/ndk/26.2.11394342
|
||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
# Rust toolchain: idempotent on self-hosted (targets already present),
|
||
# kept here so the workflow still works if we ever run it on a GH-hosted
|
||
# fallback.
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android
|
||
|
||
# Cache cargo + target/ across Android release builds. Four cargo-ndk
|
||
# release builds back-to-back with LTO is where the cold cost comes
|
||
# from; rust-cache brings warm runs down to ~3–4 min from ~9 min cold.
|
||
# cache-bin: false — see the rationale on the matrix build job above.
|
||
# On top of that, `cargo-ndk` lives in /usr/local/bin/ on our runners
|
||
# (not $CARGO_HOME/bin), specifically so rust-cache's default bin
|
||
# pruning can't delete it.
|
||
- uses: Swatinem/rust-cache@v2
|
||
with:
|
||
key: android-universal
|
||
cache-bin: "false"
|
||
# cargo-ndk writes into `target/<android-triple>/release/`, all
|
||
# four of which we want to cache.
|
||
workspaces: |
|
||
. -> target
|
||
|
||
# `./gradlew :app:assembleRelease` triggers cargoBuildRelease first
|
||
# which invokes cargo-ndk with all four targets, then Gradle packages
|
||
# the APK (release buildType signed with the committed release.jks —
|
||
# see android/app/build.gradle.kts comment explaining why).
|
||
- name: Build release APK
|
||
working-directory: android
|
||
run: |
|
||
chmod +x ./gradlew
|
||
./gradlew :app:assembleRelease --no-daemon --stacktrace
|
||
|
||
- name: Rename APKs with version
|
||
working-directory: android
|
||
run: |
|
||
VER="${{ inputs.version || github.ref_name }}"
|
||
VER="${VER#v}"
|
||
mkdir -p ../dist
|
||
|
||
# With splits.abi enabled in build.gradle.kts (issue #136), AGP
|
||
# emits:
|
||
# app-universal-release.apk — all 4 ABIs bundled (~50 MB)
|
||
# app-arm64-v8a-release.apk — modern 64-bit ARM (~15 MB)
|
||
# app-armeabi-v7a-release.apk — older 32-bit ARM
|
||
# app-x86_64-release.apk — emulator on Intel Macs / Chromebook
|
||
# app-x86-release.apk — legacy 32-bit Intel emulator
|
||
#
|
||
# We publish all of them so users behind narrow / flaky
|
||
# censorship tunnels can grab the per-ABI APK that matches
|
||
# their device (~15 MB) instead of the ~50 MB universal.
|
||
# Universal stays named `mhrv-rs-android-universal-v*.apk` so
|
||
# existing download links and Telegram mirrors keep working.
|
||
declare -A ABI_TO_OUTNAME=(
|
||
["universal"]="mhrv-rs-android-universal-v${VER}.apk"
|
||
["arm64-v8a"]="mhrv-rs-android-arm64-v8a-v${VER}.apk"
|
||
["armeabi-v7a"]="mhrv-rs-android-armeabi-v7a-v${VER}.apk"
|
||
["x86_64"]="mhrv-rs-android-x86_64-v${VER}.apk"
|
||
["x86"]="mhrv-rs-android-x86-v${VER}.apk"
|
||
)
|
||
|
||
missing=0
|
||
for abi in "${!ABI_TO_OUTNAME[@]}"; do
|
||
SRC="app/build/outputs/apk/release/app-${abi}-release.apk"
|
||
if [ -f "$SRC" ]; then
|
||
cp "$SRC" "../dist/${ABI_TO_OUTNAME[$abi]}"
|
||
ls -la "../dist/${ABI_TO_OUTNAME[$abi]}"
|
||
else
|
||
echo "::warning::missing expected APK: $SRC"
|
||
missing=$((missing + 1))
|
||
fi
|
||
done
|
||
|
||
# Require at least the universal — if that's missing something
|
||
# is genuinely broken and we should fail loud rather than ship
|
||
# a partial release.
|
||
if [ ! -f "../dist/mhrv-rs-android-universal-v${VER}.apk" ]; then
|
||
echo "::error::universal APK missing; actual outputs:"
|
||
find app/build/outputs/apk -type f -name '*.apk' -print
|
||
exit 1
|
||
fi
|
||
if [ "$missing" -gt 0 ]; then
|
||
echo "::warning::$missing per-ABI APK(s) missing; continuing with universal + whatever built"
|
||
fi
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: mhrv-rs-android-universal
|
||
path: dist/*.apk
|
||
if-no-files-found: error
|
||
|
||
# Build + publish the tunnel-node Docker image to GHCR. Issue: every
|
||
# full-mode user has to set up tunnel-node on a VPS, and "rustup +
|
||
# cargo build --release" on a 1GB VPS is non-trivial — fails on memory,
|
||
# takes 8+ minutes if it works, blocks anyone without Rust experience.
|
||
# A prebuilt multi-arch image makes deployment a one-liner:
|
||
# docker run -d -p 8080:8080 -e TUNNEL_AUTH_KEY=... \
|
||
# ghcr.io/therealaleph/mhrv-tunnel-node:latest
|
||
#
|
||
# Tags published per release:
|
||
# v1.5.0 — exact version pin
|
||
# 1.5 — auto-following minor
|
||
# latest — most recent release (skipped on workflow_dispatch
|
||
# re-publishes; see `latest` condition below)
|
||
#
|
||
# Build platforms: linux/amd64 and linux/arm64. Most VPS providers
|
||
# (DigitalOcean, Hetzner, Oracle Free Tier) offer arm64 instances at
|
||
# half price, and the binary works on both.
|
||
tunnel-docker:
|
||
needs: build
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
# Compute the version string the same way the rest of the workflow
|
||
# does: tag pushes get it from github.ref_name (e.g. "v1.5.0"),
|
||
# workflow_dispatch from the explicit `inputs.version` (e.g.
|
||
# "1.5.0"). Strip a possible leading "v" so the docker tag namespace
|
||
# is consistent: `:1.5.0`, not `:v1.5.0`.
|
||
- name: Compute version
|
||
id: ver
|
||
run: |
|
||
VER="${{ inputs.version || github.ref_name }}"
|
||
VER="${VER#v}"
|
||
MINOR="${VER%.*}"
|
||
echo "version=${VER}" >> "$GITHUB_OUTPUT"
|
||
echo "minor=${MINOR}" >> "$GITHUB_OUTPUT"
|
||
echo "Building docker for v${VER} (minor: ${MINOR})"
|
||
|
||
- uses: docker/setup-qemu-action@v3
|
||
- uses: docker/setup-buildx-action@v3
|
||
|
||
- name: Log in to GHCR
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
# Build for both amd64 and arm64. `:latest` is only updated on
|
||
# actual tag pushes — workflow_dispatch re-runs on an existing
|
||
# version (e.g. for the v1.4.0 mipsel republish) shouldn't move
|
||
# the latest pointer.
|
||
- name: Build and push image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
context: ./tunnel-node
|
||
file: ./tunnel-node/Dockerfile
|
||
platforms: linux/amd64,linux/arm64
|
||
push: true
|
||
tags: |
|
||
ghcr.io/${{ github.repository_owner }}/mhrv-tunnel-node:${{ steps.ver.outputs.version }}
|
||
ghcr.io/${{ github.repository_owner }}/mhrv-tunnel-node:${{ steps.ver.outputs.minor }}
|
||
${{ github.event_name == 'push' && format('ghcr.io/{0}/mhrv-tunnel-node:latest', github.repository_owner) || '' }}
|
||
cache-from: type=gha
|
||
cache-to: type=gha,mode=max
|
||
|
||
# release + telegram: lightweight aggregation jobs kept on GH-hosted
|
||
# ubuntu-latest. They only download artifacts and call APIs — no build
|
||
# tooling needed, no benefit from moving to self-hosted, and keeping them
|
||
# off the self-hosted runners avoids contention with Linux build jobs from
|
||
# the next tag if two releases overlap.
|
||
release:
|
||
needs: [build, android]
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- uses: actions/download-artifact@v4
|
||
with:
|
||
path: dist
|
||
merge-multiple: true
|
||
|
||
# Compose the GitHub release body from `docs/changelog/v<ver>.md`
|
||
# so the Releases page tells humans what actually changed —
|
||
# `generate_release_notes: true` alone produces "Full Changelog:
|
||
# …compare/v1.x.0...v1.x.1" which is empty when no PRs landed
|
||
# between tags (e.g. for fix-forward releases like v1.4.1). The
|
||
# changelog file already exists for every release in our format
|
||
# (Persian section, then `---`, then English section); we wrap it
|
||
# with a header and append the auto-generated commit list at the
|
||
# bottom by NOT setting body_path and instead setting body
|
||
# directly to changelog_content + (the existing
|
||
# generate_release_notes flag handles the trailing comparison
|
||
# link automatically).
|
||
- name: Compose release body
|
||
id: relbody
|
||
run: |
|
||
VER="${{ inputs.version || github.ref_name }}"
|
||
VER="${VER#v}"
|
||
CHANGELOG="docs/changelog/v${VER}.md"
|
||
if [ ! -f "$CHANGELOG" ]; then
|
||
echo "::warning::no changelog at $CHANGELOG; release body will fall back to generate_release_notes only"
|
||
echo "has_changelog=false" >> "$GITHUB_OUTPUT"
|
||
exit 0
|
||
fi
|
||
{
|
||
echo 'body<<__RELEASE_BODY_EOF__'
|
||
# Strip leading HTML comment blocks (single-line OR multi-line)
|
||
# using the SAME regex as
|
||
# .github/scripts/telegram_release_notify.py:parse_changelog,
|
||
# so the GitHub Release page and the Telegram post agree on
|
||
# exactly what counts as "the leading comment block." Both
|
||
# also strip any leading whitespace/blank lines that follow.
|
||
#
|
||
# Quoted heredoc (`<<'PY'`) so backticks/$ in the python
|
||
# snippet aren't shell-interpolated; CHANGELOG is passed in
|
||
# as an env var on the python invocation rather than via
|
||
# `$CHANGELOG` interpolation inside the heredoc.
|
||
CHANGELOG_PATH="$CHANGELOG" python3 - <<'PY'
|
||
import os, re, pathlib
|
||
body = pathlib.Path(os.environ["CHANGELOG_PATH"]).read_text(encoding="utf-8")
|
||
print(re.sub(r"^\s*(?:<!--.*?-->\s*)+", "", body, count=1, flags=re.S), end="")
|
||
PY
|
||
echo
|
||
echo '__RELEASE_BODY_EOF__'
|
||
} >> "$GITHUB_OUTPUT"
|
||
echo "has_changelog=true" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Release
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
# On tag push, action-gh-release defaults tag_name to the
|
||
# current ref. On workflow_dispatch the ref is `main` (or
|
||
# whichever branch we dispatched from), which would create a
|
||
# bogus release named "main"; force the tag explicitly so
|
||
# artifacts upload to the existing release identified by
|
||
# `inputs.version`. The leading `v` is preserved (release
|
||
# tags are `v1.4.0`, not `1.4.0`).
|
||
tag_name: ${{ inputs.version && format('v{0}', inputs.version) || github.ref_name }}
|
||
files: dist/*
|
||
# Append auto-generated comparison link AFTER our changelog
|
||
# body — `append_body: true` puts our body first, then the
|
||
# auto notes. If no changelog file existed, body is empty and
|
||
# the auto notes carry the whole release-page content (same
|
||
# behavior as before this change).
|
||
body: ${{ steps.relbody.outputs.body }}
|
||
append_body: true
|
||
generate_release_notes: true
|
||
|
||
# Refresh the in-repo `releases/` folder with the latest pre-built
|
||
# artifacts so users behind GitHub-Releases-page filtering (the IR
|
||
# state network filters the dynamic /releases/ URL but not the static
|
||
# `Code → Download ZIP` of the source tree) can still download.
|
||
# Practice was started pre-v1.1.0, dropped, then resumed at user
|
||
# request after a Telegram-channel suggestion: "فقط داخل پوشه ریلیز
|
||
# پروژه اپلود بکن — مشکل دانلود حل میشه — راحت میشه از گیتهاب دانلود
|
||
# کرد." The folder holds ONLY the latest version (replace, not
|
||
# archive); each tag refresh overwrites the previous artifacts. The
|
||
# existing release-page workflow keeps versioned artifacts behind
|
||
# `https://github.com/.../releases/tag/v...` for users who can reach
|
||
# that URL — this in-repo folder is the fallback for users who can't.
|
||
commit-releases:
|
||
needs: [build, android, release]
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
steps:
|
||
# Always check out main, not the tag — we're committing back to
|
||
# the moving branch. fetch-depth 0 so `git push origin HEAD:main`
|
||
# has the lineage to fast-forward.
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
ref: main
|
||
fetch-depth: 0
|
||
|
||
# Pull artifacts from the GitHub Release page (which the `release`
|
||
# job populated a few seconds earlier) rather than the workflow
|
||
# artifacts API. The artifacts API path —
|
||
# `actions/download-artifact@v4` with `merge-multiple: true` —
|
||
# has been failing with "artifact download failed after 5
|
||
# retries" on one of the ~13 artifacts on multiple consecutive
|
||
# runs (v1.7.5 retrigger, v1.7.6). The 10 fast downloads that
|
||
# complete first all succeed; the 11th-13th hit the error.
|
||
# `gh release download` reads from GitHub's Release-page CDN,
|
||
# which is independent of the artifacts blob store and has a
|
||
# different retry / rate-limit profile. Same files, more
|
||
# reliable surface.
|
||
- name: Download artifacts from the GitHub Release page
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
VER="${{ inputs.version || github.ref_name }}"
|
||
# Strip leading `v` to normalize, then re-add — the Release
|
||
# tag is `vX.Y.Z`, but for the rest of the workflow we use
|
||
# bare `X.Y.Z`. Mirror the same pattern here so a downstream
|
||
# readme update can use the bare version.
|
||
VER="${VER#v}"
|
||
mkdir -p artifacts
|
||
gh release download "v${VER}" \
|
||
--repo "${{ github.repository }}" \
|
||
--dir artifacts \
|
||
--pattern '*.tar.gz' \
|
||
--pattern '*.zip' \
|
||
--pattern '*.apk'
|
||
echo "--- artifacts/ contents ---"
|
||
ls -la artifacts/
|
||
|
||
- name: Refresh releases/ folder
|
||
run: |
|
||
set -euo pipefail
|
||
VER="${{ inputs.version || github.ref_name }}"
|
||
VER="${VER#v}"
|
||
|
||
mkdir -p releases
|
||
|
||
# Wipe old binary artifacts (.apk, .tar.gz, .zip) but keep
|
||
# README.md and .gitattributes — those are folder-level docs
|
||
# that stay constant across versions and shouldn't be
|
||
# regenerated on every release.
|
||
find releases -maxdepth 1 -type f \
|
||
\( -name '*.apk' -o -name '*.tar.gz' -o -name '*.zip' \) \
|
||
-delete
|
||
|
||
# Copy desktop archives. Their names already include the
|
||
# platform identifier (mhrv-rs-linux-amd64.tar.gz, etc.) and
|
||
# are version-stable — no rename needed.
|
||
for f in artifacts/*.tar.gz artifacts/*.zip; do
|
||
[ -f "$f" ] || continue
|
||
cp "$f" "releases/$(basename "$f")"
|
||
done
|
||
|
||
# Android APKs come with the version baked into the name
|
||
# (mhrv-rs-android-universal-v1.7.5.apk). Copy all of them so
|
||
# users on slow connections can grab a per-ABI APK (~37 MB)
|
||
# instead of the universal (~110 MB).
|
||
for f in artifacts/mhrv-rs-android-*.apk; do
|
||
[ -f "$f" ] || continue
|
||
cp "$f" "releases/$(basename "$f")"
|
||
done
|
||
|
||
# Update the "Current version" line in releases/README.md
|
||
# (both English and Persian copies) and APK filename refs so
|
||
# the doc stays accurate. `sed -i` BSD/GNU compatibility is
|
||
# handled by passing an empty extension explicitly — runner
|
||
# is Linux so `-i` alone works, but the empty-string form
|
||
# also works on macOS for anyone running this locally.
|
||
if [ -f releases/README.md ]; then
|
||
sed -i.bak \
|
||
-e "s/Current version: \*\*v[0-9][0-9.]*\*\*/Current version: **v${VER}**/" \
|
||
-e "s/نسخهٔ فعلی: \*\*v[0-9][0-9.]*\*\*/نسخهٔ فعلی: **v${VER}**/" \
|
||
-e "s/mhrv-rs-android-universal-v[0-9][0-9.]*\.apk/mhrv-rs-android-universal-v${VER}.apk/g" \
|
||
releases/README.md
|
||
rm -f releases/README.md.bak
|
||
fi
|
||
|
||
echo "--- releases/ contents after refresh ---"
|
||
ls -la releases/
|
||
|
||
- name: Commit + push to main
|
||
run: |
|
||
set -euo pipefail
|
||
VER="${{ inputs.version || github.ref_name }}"
|
||
VER="${VER#v}"
|
||
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||
|
||
git add releases
|
||
if git diff --cached --quiet; then
|
||
echo "No releases/ changes to commit (artifacts identical to current HEAD?)."
|
||
exit 0
|
||
fi
|
||
|
||
git commit -m "chore(releases): refresh prebuilt binaries for v${VER}" \
|
||
-m "Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP)."
|
||
|
||
# Push to main. The release workflow runs on the tag commit,
|
||
# which is reachable from main as a fast-forward — push is
|
||
# straightforward, no force needed. Tag protection rules
|
||
# apply to refs/tags/* not refs/heads/main, so this push
|
||
# isn't gated by the same protection.
|
||
git push origin HEAD:main
|
||
|
||
# Notify the Persian-speaking Telegram channel with the CI-built
|
||
# Android APK + its sha256 + the per-version 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.
|
||
#
|
||
# Needs two repo secrets:
|
||
# TELEGRAM_BOT_TOKEN — bot the channel admits as poster
|
||
# TELEGRAM_CHAT_ID — numeric chat id (starts with -100...)
|
||
# Missing either => the whole job is skipped (not failed) so a
|
||
# forker who hasn't set up a Telegram channel gets a clean release.
|
||
telegram:
|
||
needs: [android, release]
|
||
runs-on: ubuntu-latest
|
||
# Gated on the repo variable `TELEGRAM_NOTIFY_ENABLED`. Default is
|
||
# OFF — the job skips silently unless the variable is set to the
|
||
# literal string "true". Toggle via:
|
||
#
|
||
# gh variable set TELEGRAM_NOTIFY_ENABLED --body true
|
||
# gh variable set TELEGRAM_NOTIFY_ENABLED --body false
|
||
#
|
||
# Keeping the machinery (script + secrets) in place so flipping
|
||
# the switch back on is a one-liner, not a workflow edit.
|
||
if: ${{ vars.TELEGRAM_NOTIFY_ENABLED == 'true' && needs.android.result == 'success' }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/download-artifact@v4
|
||
with:
|
||
name: mhrv-rs-android-universal
|
||
path: apk
|
||
|
||
- 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.
|
||
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/
|
||
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" \
|
||
--version "$VER" \
|
||
--repo "$GITHUB_REPOSITORY" \
|
||
--changelog "docs/changelog/v${VER}.md" \
|
||
$INCLUDE_CHANGELOG_FLAG
|