name: Build RustDesk Android on: workflow_dispatch: env: # Change only this value to build another RustDesk tag/release. RUSTDESK_VERSION: "1.4.9" # Target constants, not software versions. ANDROID_ABI: arm64-v8a RUST_TARGET: aarch64-linux-android FLUTTER_TARGET: android-arm64 jobs: build-android: name: Build RustDesk Android ARM64 runs-on: ci-linux-builder defaults: run: shell: bash env: NEXUS_CARGO_URL: ${{ secrets.NEXUS_CARGO_URL }} NEXUS_MAVEN_URL: ${{ secrets.NEXUS_MAVEN_URL }} steps: - name: Checkout CI repository uses: actions/checkout@v4 - name: Prepare RAM CI environment uses: ./.gitea/actions/prepare-ram-ci # ----------------------------------------------------------------------- # RustDesk source # ----------------------------------------------------------------------- - name: Clone RustDesk run: | set -euxo pipefail rm -rf rustdesk git clone \ --branch "${RUSTDESK_VERSION}" \ --depth 1 \ --recurse-submodules \ --shallow-submodules \ https://github.com/rustdesk/rustdesk.git \ rustdesk cd rustdesk echo "RustDesk commit:" git rev-parse HEAD echo echo "RustDesk tag:" git describe --tags --always echo echo "Submodules:" git submodule status # ----------------------------------------------------------------------- # Resolve versions from the selected upstream RustDesk tag. # ----------------------------------------------------------------------- - name: Resolve upstream build configuration run: | set -euxo pipefail python3 <<'PY' > build-config.env import re from pathlib import Path repo = Path("rustdesk") flutter = ( repo / ".github/workflows/flutter-build.yml" ).read_text(encoding="utf-8") bridge = ( repo / ".github/workflows/bridge.yml" ).read_text(encoding="utf-8") def env_value(text, name, required=True): match = re.search( rf'(?m)^\s*{re.escape(name)}:\s*["\']?([^"\'#\s]+)', text, ) if match: return match.group(1).strip() if required: raise SystemExit( f"Unable to resolve upstream variable: {name}" ) return "" android_flutter = ( env_value( flutter, "ANDROID_FLUTTER_VERSION", required=False, ) or env_value(flutter, "FLUTTER_VERSION") ) # Select the default bridge artifact used by Android. bridge_flutter = "" for block in re.findall(r"\{(.*?)\}", bridge, flags=re.S): artifact = re.search( r'artifact-name:\s*["\']([^"\']+)["\']', block, ) version = re.search( r'flutter-version:\s*["\']([^"\']+)["\']', block, ) if ( artifact and artifact.group(1) == "bridge-artifact" and version ): bridge_flutter = version.group(1) break if not bridge_flutter: raise SystemExit( "Unable to resolve default bridge Flutter version" ) values = { "RUST_VERSION": env_value(flutter, "RUST_VERSION"), "CARGO_NDK_VERSION": env_value(flutter, "CARGO_NDK_VERSION"), "ANDROID_FLUTTER_VERSION": android_flutter, "NDK_VERSION": env_value(flutter, "NDK_VERSION"), "VCPKG_COMMIT_ID": env_value(flutter, "VCPKG_COMMIT_ID"), "BRIDGE_RUST_VERSION": env_value(bridge, "RUST_VERSION"), "BRIDGE_FLUTTER_VERSION": bridge_flutter, "CARGO_EXPAND_VERSION": env_value(bridge, "CARGO_EXPAND_VERSION"), "FLUTTER_RUST_BRIDGE_VERSION": env_value( bridge, "FLUTTER_RUST_BRIDGE_VERSION", ), } for key, value in values.items(): print(f"{key}={value}") PY echo "Resolved upstream configuration:" cat build-config.env while IFS='=' read -r key value; do [ -z "${key}" ] && continue echo "${key}=${value}" >> "${GITHUB_ENV}" done < build-config.env # ----------------------------------------------------------------------- # Prepare exact toolchain requested by the selected tag. # Prefer the prebuilt builder image where it already matches. # ----------------------------------------------------------------------- - name: Prepare builder environment run: | set -euxo pipefail echo "=== Rust ===" rustup toolchain install \ "${RUST_VERSION}" \ --profile minimal \ --component rustfmt rustup target add \ --toolchain "${RUST_VERSION}" \ "${RUST_TARGET}" export RUSTUP_TOOLCHAIN="${RUST_VERSION}" current_cargo_ndk="$( cargo ndk --version 2>/dev/null | awk '{print $2}' || true )" if [ "${current_cargo_ndk}" != "${CARGO_NDK_VERSION}" ]; then cargo install \ cargo-ndk \ --version "${CARGO_NDK_VERSION}" \ --locked \ --force fi echo "RUSTUP_TOOLCHAIN=${RUST_VERSION}" \ >> "${GITHUB_ENV}" rustc --version cargo --version cargo ndk --version echo echo "=== Flutter SDKs ===" flutter_version() { local root="$1" if [ ! -x "${root}/bin/flutter" ]; then return 1 fi git config --global \ --add safe.directory "${root}" \ >/dev/null 2>&1 \ || true git \ -C "${root}" \ describe \ --tags \ --exact-match \ HEAD \ 2>/dev/null \ || true } prepare_flutter() { local requested="$1" local preferred="$2" local fallback="$3" local root="" if ( [ -n "${preferred}" ] && [ "$(flutter_version "${preferred}")" = "${requested}" ] ); then # Flutter writes into its SDK cache and receives source patches. # Copy the image SDK into RAM before using it. root="${fallback}" if [ ! -d "${root}" ]; then mkdir -p "$(dirname "${root}")" cp -a "${preferred}" "${root}" fi else root="${fallback}" if [ "$(flutter_version "${root}")" != "${requested}" ]; then rm -rf "${root}" mkdir -p "$(dirname "${root}")" git clone \ --branch "${requested}" \ --depth 1 \ https://github.com/flutter/flutter.git \ "${root}" fi fi git config --global \ --add safe.directory "${root}" \ >/dev/null 2>&1 \ || true printf '%s\n' "${root}" } BRIDGE_FLUTTER_ROOT="$( prepare_flutter \ "${BRIDGE_FLUTTER_VERSION}" \ "${FLUTTER_BRIDGE_HOME:-}" \ "${HOME}/.cache/rustdesk/flutter-${BRIDGE_FLUTTER_VERSION}" )" ANDROID_FLUTTER_ROOT="$( prepare_flutter \ "${ANDROID_FLUTTER_VERSION}" \ "${FLUTTER_ANDROID_HOME:-}" \ "${HOME}/.cache/rustdesk/flutter-${ANDROID_FLUTTER_VERSION}" )" echo "BRIDGE_FLUTTER_ROOT=${BRIDGE_FLUTTER_ROOT}" \ >> "${GITHUB_ENV}" echo "ANDROID_FLUTTER_ROOT=${ANDROID_FLUTTER_ROOT}" \ >> "${GITHUB_ENV}" "${BRIDGE_FLUTTER_ROOT}/bin/flutter" \ config \ --no-analytics \ >/dev/null "${ANDROID_FLUTTER_ROOT}/bin/flutter" \ config \ --no-analytics \ >/dev/null echo "Bridge Flutter:" "${BRIDGE_FLUTTER_ROOT}/bin/flutter" --version echo echo "Android Flutter:" "${ANDROID_FLUTTER_ROOT}/bin/flutter" --version echo echo "=== Android NDK ===" # Convert r28c -> expected Pkg.Revision prefix 28.2. NDK_EXPECTED_PREFIX="$( python3 <<'PY' import os import re version = os.environ["NDK_VERSION"] match = re.fullmatch(r"r(\d+)([a-z]?)", version) if not match: raise SystemExit( f"Unsupported NDK version format: {version}" ) major = int(match.group(1)) letter = match.group(2) if letter: minor = ord(letter) - ord("a") print(f"{major}.{minor}.") else: print(f"{major}.") PY )" current_ndk_ok=0 if ( [ -n "${ANDROID_NDK_HOME:-}" ] && [ -f "${ANDROID_NDK_HOME}/source.properties" ] ); then CURRENT_NDK_REVISION="$( sed -n \ 's/^Pkg\.Revision[[:space:]]*=[[:space:]]*//p' \ "${ANDROID_NDK_HOME}/source.properties" )" if [[ "${CURRENT_NDK_REVISION}" == "${NDK_EXPECTED_PREFIX}"* ]]; then current_ndk_ok=1 fi fi if [ "${current_ndk_ok}" -eq 0 ]; then ANDROID_NDK_HOME="${HOME}/.cache/rustdesk/android-ndk-${NDK_VERSION}" if [ ! -f "${ANDROID_NDK_HOME}/source.properties" ]; then rm -rf "${ANDROID_NDK_HOME}" tmp_zip="${HOME}/.cache/rustdesk/android-ndk-${NDK_VERSION}.zip" tmp_extract="${HOME}/.cache/rustdesk/ndk-extract-${NDK_VERSION}" mkdir -p "${HOME}/.cache/rustdesk" rm -rf "${tmp_extract}" curl -fL \ "https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip" \ -o "${tmp_zip}" python3 -m zipfile \ -e \ "${tmp_zip}" \ "${tmp_extract}" mv \ "${tmp_extract}/android-ndk-${NDK_VERSION}" \ "${ANDROID_NDK_HOME}" rm -rf "${tmp_extract}" fi fi test -f "${ANDROID_NDK_HOME}/source.properties" echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME}" \ >> "${GITHUB_ENV}" echo "ANDROID_NDK_ROOT=${ANDROID_NDK_HOME}" \ >> "${GITHUB_ENV}" cat "${ANDROID_NDK_HOME}/source.properties" echo echo "=== vcpkg ===" # Reuse the image checkout and prebuilt libraries in RAM when present. image_vcpkg="${VCPKG_ROOT:-}" export VCPKG_ROOT="${CI_ROOT}/vcpkg" if [ ! -d "${VCPKG_ROOT}/.git" ]; then if [ -n "${image_vcpkg}" ] && [ -d "${image_vcpkg}/.git" ]; then cp -a "${image_vcpkg}" "${VCPKG_ROOT}" else git clone https://github.com/microsoft/vcpkg.git "${VCPKG_ROOT}" fi fi echo "VCPKG_ROOT=${VCPKG_ROOT}" >> "${GITHUB_ENV}" current_vcpkg="$( git -C "${VCPKG_ROOT}" rev-parse HEAD )" if [ "${current_vcpkg}" != "${VCPKG_COMMIT_ID}" ]; then git -C "${VCPKG_ROOT}" fetch \ origin \ "${VCPKG_COMMIT_ID}" git -C "${VCPKG_ROOT}" checkout \ --force \ "${VCPKG_COMMIT_ID}" rm -rf \ "${VCPKG_ROOT}/installed/arm64-android" \ "${VCPKG_ROOT}/buildtrees" \ "${VCPKG_ROOT}/packages" fi "${VCPKG_ROOT}/bootstrap-vcpkg.sh" \ -disableMetrics "${VCPKG_ROOT}/vcpkg" version echo echo "Builder environment prepared" # ----------------------------------------------------------------------- # Generate Flutter/Rust bridge in THE SAME checkout, like the original # working workflow. Use version-specific preparation commands from the # selected tag's upstream bridge.yml. # ----------------------------------------------------------------------- - name: Generate Flutter Rust bridge run: | set -euxo pipefail export RUSTUP_TOOLCHAIN="${BRIDGE_RUST_VERSION}" export PATH="${BRIDGE_FLUTTER_ROOT}/bin:${CARGO_HOME}/bin:${PATH}" rustup toolchain install \ "${BRIDGE_RUST_VERSION}" \ --profile minimal \ --component rustfmt cd rustdesk python3 <<'PY' import re from pathlib import Path text = Path( ".github/workflows/bridge.yml" ).read_text(encoding="utf-8") def run_blocks(text): lines = text.splitlines() blocks = [] for index, line in enumerate(lines): match = re.match(r"^(\s*)run:\s*\|\s*$", line) if not match: continue base_indent = len(match.group(1)) body = [] for child in lines[index + 1:]: if child.strip(): indent = len(child) - len(child.lstrip()) if indent <= base_indent: break cut = min( len(child), base_indent + 2, ) body.append(child[cut:]) blocks.append("\n".join(body) + "\n") return blocks blocks = run_blocks(text) prepare = next( ( block for block in blocks if "cargo install cargo-expand" in block and "flutter_rust_bridge_codegen" in block ), None, ) generate = next( ( block for block in blocks if "flutter_rust_bridge_codegen --rust-input" in block ), None, ) if not prepare: raise SystemExit( "Unable to find upstream bridge preparation block" ) if not generate: raise SystemExit( "Unable to find upstream bridge generation block" ) def replace_expression( body, expression, replacement, ): pattern = ( r"\$\{\{\s*" + re.escape(expression) + r"\s*\}\}" ) return re.sub( pattern, lambda _match: replacement, body, ) replacements = ( ( "env.CARGO_EXPAND_VERSION", "${CARGO_EXPAND_VERSION}", ), ( "env.FLUTTER_RUST_BRIDGE_VERSION", "${FLUTTER_RUST_BRIDGE_VERSION}", ), ( "matrix.job.flutter-version", "${BRIDGE_FLUTTER_VERSION}", ), ) for expression, replacement in replacements: prepare = replace_expression( prepare, expression, replacement, ) generate = replace_expression( generate, expression, replacement, ) # Upstream bridge.yml may call the codegen binary through # ~/.cargo/bin/... . Our builder intentionally uses a custom # CARGO_HOME (for example /opt/rust/cargo), so a hard-coded HOME # cargo path is not portable. The selected tool version has already # been installed above; resolve it through PATH instead. cargo_bin_patterns = ( r"~/.cargo/bin/flutter_rust_bridge_codegen", r"\$HOME/.cargo/bin/flutter_rust_bridge_codegen", r"\$\{HOME\}/.cargo/bin/flutter_rust_bridge_codegen", r"/root/.cargo/bin/flutter_rust_bridge_codegen", ) for pattern in cargo_bin_patterns: generate = re.sub( pattern, "flutter_rust_bridge_codegen", generate, ) if re.search(r"\$\{\{", prepare + generate): raise SystemExit( "Unsupported upstream expression remains " "in bridge commands" ) Path("bridge-prepare.sh").write_text( "#!/usr/bin/env bash\n" "set -euxo pipefail\n" + prepare, encoding="utf-8", ) Path("bridge-generate.sh").write_text( "#!/usr/bin/env bash\n" "set -euxo pipefail\n" + generate, encoding="utf-8", ) PY chmod +x \ bridge-prepare.sh \ bridge-generate.sh bash -n bridge-prepare.sh bash -n bridge-generate.sh echo echo "Bridge codegen binary:" command -v flutter_rust_bridge_codegen flutter_rust_bridge_codegen --version echo echo "Generated bridge command:" grep -n "flutter_rust_bridge_codegen" \ bridge-generate.sh ./bridge-prepare.sh ./bridge-generate.sh test -f src/bridge_generated.rs test -f src/bridge_generated.io.rs test -f flutter/lib/generated_bridge.dart test -f flutter/lib/generated_bridge.freezed.dart echo echo "Generated bridge:" ls -lh \ src/bridge_generated.rs \ src/bridge_generated.io.rs \ flutter/lib/generated_bridge.dart \ flutter/lib/generated_bridge.freezed.dart # Restore only tracked changes made by bridge preparation. # Generated bridge files are untracked and remain in place. rm -f \ bridge-prepare.sh \ bridge-generate.sh git restore \ --worktree \ --source=HEAD \ -- \ flutter/pubspec.yaml \ flutter/pubspec.lock \ 2>/dev/null \ || true rm -rf flutter/.dart_tool test -f src/bridge_generated.rs test -f src/bridge_generated.io.rs echo echo "Flutter Rust bridge generated successfully" # ----------------------------------------------------------------------- # Custom RustDesk ID server + public key. # Relay is NOT modified. # ----------------------------------------------------------------------- - name: Configure custom ID server and key env: CUSTOM_ID_SERVER: ${{ secrets.RUSTDESK_ID_SERVER }} CUSTOM_SERVER_KEY: ${{ secrets.RUSTDESK_KEY }} run: | set -euo pipefail if [ -z "${CUSTOM_ID_SERVER}" ]; then echo "ERROR: RUSTDESK_ID_SERVER secret is empty" exit 1 fi if [ -z "${CUSTOM_SERVER_KEY}" ]; then echo "ERROR: RUSTDESK_KEY secret is empty" exit 1 fi cd rustdesk python3 <<'PY' import json import os import re from pathlib import Path path = Path("libs/hbb_common/src/config.rs") if not path.exists(): raise SystemExit( "libs/hbb_common/src/config.rs does not exist. " "Check that submodules were cloned correctly." ) text = path.read_text(encoding="utf-8") id_server = os.environ["CUSTOM_ID_SERVER"].strip() server_key = os.environ["CUSTOM_SERVER_KEY"].strip() if not id_server: raise SystemExit("RUSTDESK_ID_SERVER is empty") if not server_key: raise SystemExit("RUSTDESK_KEY is empty") text, id_count = re.subn( r"pub const RENDEZVOUS_SERVERS\s*:\s*[^=]+?=" r"\s*&\[(?:.|\n)*?\];", "pub const RENDEZVOUS_SERVERS: &[&str] = &[" + json.dumps(id_server) + "];", text, count=1, ) text, key_count = re.subn( r"pub const RS_PUB_KEY\s*:\s*[^=]+?=" r"\s*.*?;", "pub const RS_PUB_KEY: &str = " + json.dumps(server_key) + ";", text, count=1, flags=re.S, ) if id_count != 1: raise SystemExit( "Failed to patch RENDEZVOUS_SERVERS. " "RustDesk source layout may have changed." ) if key_count != 1: raise SystemExit( "Failed to patch RS_PUB_KEY. " "RustDesk source layout may have changed." ) path.write_text(text, encoding="utf-8") print(f"ID server configured: {id_server}") print("Server public key configured successfully") PY grep -n \ "RENDEZVOUS_SERVERS" \ libs/hbb_common/src/config.rs echo "Relay configuration untouched: yes" # ----------------------------------------------------------------------- # Apply Flutter compatibility patches supplied by the selected tag. # Only patches that cleanly apply to the selected Android Flutter SDK # are applied. # ----------------------------------------------------------------------- - name: Patch Android Flutter SDK run: | set -euxo pipefail if [ ! -d "rustdesk/.github/patches" ]; then exit 0 fi shopt -s nullglob for patch in rustdesk/.github/patches/flutter_*.diff; do if git \ -C "${ANDROID_FLUTTER_ROOT}" \ apply \ --check \ "${PWD}/${patch}" \ >/dev/null 2>&1; then echo "Applying Flutter patch: ${patch}" git \ -C "${ANDROID_FLUTTER_ROOT}" \ apply \ "${PWD}/${patch}" fi done # ----------------------------------------------------------------------- # Android native dependencies. # ----------------------------------------------------------------------- - name: Verify Android native dependencies run: | set -euxo pipefail cd rustdesk echo "VCPKG_ROOT=${VCPKG_ROOT}" echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME}" if ! ./flutter/build_android_deps.sh "${ANDROID_ABI}"; then echo echo "Android dependency verification/build failed." echo "Dumping vcpkg logs:" echo find "${VCPKG_ROOT}" \ -name "*.log" \ -type f \ | while read -r logfile; do echo "===== ${logfile} =====" cat "${logfile}" || true echo done exit 1 fi test -d \ "${VCPKG_ROOT}/installed/arm64-android" echo echo "Android native dependencies OK" # ----------------------------------------------------------------------- # Build Rust native Android library. # # Important: copy the .so into jniLibs in THIS SAME step, matching # upstream RustDesk Android CI. The next Flutter step no longer depends # on target/ surviving as a build-output directory. # ----------------------------------------------------------------------- - name: Build RustDesk native library run: | set -euxo pipefail export RUSTUP_TOOLCHAIN="${RUST_VERSION}" cd rustdesk rustc --version cargo --version cargo ndk --version test -f src/bridge_generated.rs test -f src/bridge_generated.io.rs if ! rustup \ target list \ --installed \ --toolchain "${RUST_VERSION}" \ | grep -qx "${RUST_TARGET}"; then echo "ERROR: ${RUST_TARGET} Rust target is missing" exit 1 fi ./flutter/ndk_arm64.sh NATIVE_LIB="target/${RUST_TARGET}/release/liblibrustdesk.so" test -f "${NATIVE_LIB}" mkdir -p \ "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}" cp \ "${NATIVE_LIB}" \ "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so" test -f \ "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so" echo echo "Native library:" ls -lh \ "${NATIVE_LIB}" \ "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so" # ----------------------------------------------------------------------- # Permanent Android release signing. # ----------------------------------------------------------------------- - name: Configure Android release signing env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} run: | set -euo pipefail if [ -z "${ANDROID_KEYSTORE_BASE64}" ]; then echo "ERROR: ANDROID_KEYSTORE_BASE64 secret is empty" exit 1 fi if [ -z "${ANDROID_KEYSTORE_PASSWORD}" ]; then echo "ERROR: ANDROID_KEYSTORE_PASSWORD secret is empty" exit 1 fi if [ -z "${ANDROID_KEY_ALIAS}" ]; then echo "ERROR: ANDROID_KEY_ALIAS secret is empty" exit 1 fi cd rustdesk/flutter/android printf '%s' "${ANDROID_KEYSTORE_BASE64}" \ | base64 --decode \ > app/rustdesk-release.jks chmod 600 app/rustdesk-release.jks cat > key.properties </dev/null echo "Android release signing configured successfully." # ----------------------------------------------------------------------- # Build signed ARM64 APK. # ----------------------------------------------------------------------- - name: Build signed APK run: | set -euxo pipefail export RUSTUP_TOOLCHAIN="${RUST_VERSION}" export PATH="${ANDROID_FLUTTER_ROOT}/bin:${CARGO_HOME}/bin:${PATH}" flutter --version cd rustdesk JNI_DIR="flutter/android/app/src/main/jniLibs/${ANDROID_ABI}" test -f "${JNI_DIR}/librustdesk.so" cp \ "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so" \ "${JNI_DIR}/libc++_shared.so" sed -i \ 's/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g' \ flutter/android/gradle.properties echo echo "JNI libraries:" ls -lh "${JNI_DIR}/" echo echo "Signing files:" ls -lh \ flutter/android/key.properties \ flutter/android/app/rustdesk-release.jks pushd flutter flutter clean flutter pub get flutter build apk \ --release \ --target-platform "${FLUTTER_TARGET}" \ --split-per-abi popd APK_SOURCE="flutter/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk" test -f "${APK_SOURCE}" mkdir -p output cp \ "${APK_SOURCE}" \ "output/rustdesk-${RUSTDESK_VERSION}-custom-arm64.apk" echo echo "Built APK:" ls -lh \ "output/rustdesk-${RUSTDESK_VERSION}-custom-arm64.apk" # ----------------------------------------------------------------------- # Verify APK release signature. # ----------------------------------------------------------------------- - name: Verify APK signature run: | set -euxo pipefail APK="rustdesk/output/rustdesk-${RUSTDESK_VERSION}-custom-arm64.apk" APKSIGNER="$( find "${ANDROID_SDK_ROOT}/build-tools" \ -type f \ -name apksigner \ -print \ | sort -V \ | tail -n 1 )" if [ -z "${APKSIGNER}" ]; then echo "ERROR: apksigner was not found" exit 1 fi if [ ! -f "${APK}" ]; then echo "ERROR: APK not found: ${APK}" exit 1 fi echo "Using apksigner: ${APKSIGNER}" "${APKSIGNER}" verify \ --verbose \ --print-certs \ "${APK}" echo "APK signature verification successful." # ----------------------------------------------------------------------- # APK checksum. # ----------------------------------------------------------------------- - name: Create checksum run: | set -euxo pipefail cd rustdesk/output sha256sum \ "rustdesk-${RUSTDESK_VERSION}-custom-arm64.apk" \ > "rustdesk-${RUSTDESK_VERSION}-custom-arm64.apk.sha256" cat \ "rustdesk-${RUSTDESK_VERSION}-custom-arm64.apk.sha256" # ----------------------------------------------------------------------- # Upload Gitea Actions artifact. # Keep the same action family as the original known-working workflow. # ----------------------------------------------------------------------- - name: Upload APK uses: actions/upload-artifact@v4.6.2 with: name: rustdesk-${{ env.RUSTDESK_VERSION }}-custom-arm64 path: | rustdesk/output/rustdesk-${{ env.RUSTDESK_VERSION }}-custom-arm64.apk rustdesk/output/rustdesk-${{ env.RUSTDESK_VERSION }}-custom-arm64.apk.sha256 if-no-files-found: error