name: Android Release on: push: tags: - "v*" workflow_dispatch: jobs: build-release-apk: name: Build signed release APK runs-on: ubuntu-latest timeout-minutes: 60 permissions: code: read releases: write env: ANDROID_COMPILE_SDK: "36" ANDROID_BUILD_TOOLS: "36.0.0" ANDROID_NDK_VERSION: "30.0.15729638" ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} GITEA_RELEASE_TOKEN: ${{ secrets.GITEA_TOKEN }} steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" cache: npm - name: Set up Java uses: actions/setup-java@v4 with: distribution: temurin java-version: "21" cache: gradle - name: Set up Rust uses: dtolnay/rust-toolchain@1.95.0 with: targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android - name: Cache Rust build uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri -> target - name: Set up Android SDK uses: android-actions/setup-android@v3 - name: Install Android SDK packages run: | yes | sdkmanager --licenses >/dev/null || true sdkmanager \ "platforms;android-${ANDROID_COMPILE_SDK}" \ "build-tools;${ANDROID_BUILD_TOOLS}" \ "ndk;${ANDROID_NDK_VERSION}" echo "NDK_HOME=${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}" >> "${GITEA_ENV}" echo "${ANDROID_HOME}/build-tools/${ANDROID_BUILD_TOOLS}" >> "${GITEA_PATH}" - name: Install JavaScript dependencies run: npm ci - name: Check release tag and app version run: | if [ "${GITHUB_REF_TYPE}" != "tag" ]; then echo "Release builds must run on a tag such as v0.1.0." >&2 exit 1 fi APP_VERSION="$(node -p "require('./src-tauri/tauri.conf.json').version")" if [ "${GITHUB_REF_NAME}" != "v${APP_VERSION}" ]; then echo "Tag ${GITHUB_REF_NAME} does not match app version v${APP_VERSION}." >&2 exit 1 fi - name: Validate signing secrets run: | for SECRET_NAME in \ ANDROID_KEYSTORE_BASE64 \ ANDROID_KEYSTORE_PASSWORD \ ANDROID_KEY_ALIAS \ ANDROID_KEY_PASSWORD \ GITEA_RELEASE_TOKEN do if [ -z "${!SECRET_NAME:-}" ]; then echo "Missing Gitea Actions secret: ${SECRET_NAME}" >&2 exit 1 fi done - name: Restore release keystore run: | SIGNING_DIRECTORY="${RUNNER_TEMP}/wallpaperflow-signing" mkdir -p "${SIGNING_DIRECTORY}" printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${SIGNING_DIRECTORY}/release.jks" chmod 600 "${SIGNING_DIRECTORY}/release.jks" echo "ANDROID_SIGNING_KEYSTORE=${SIGNING_DIRECTORY}/release.jks" >> "${GITEA_ENV}" - name: Build unsigned release APK run: npm run tauri -- android build --apk --ci - name: Align and sign APK run: | UNSIGNED_APK="src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk" RELEASE_DIRECTORY="release" ALIGNED_APK="${RELEASE_DIRECTORY}/WallpaperFlow-release-aligned.apk" SIGNED_APK="${RELEASE_DIRECTORY}/WallpaperFlow-release.apk" test -f "${UNSIGNED_APK}" mkdir -p "${RELEASE_DIRECTORY}" zipalign -p -f 4 "${UNSIGNED_APK}" "${ALIGNED_APK}" apksigner sign \ --ks "${ANDROID_SIGNING_KEYSTORE}" \ --ks-key-alias "${ANDROID_KEY_ALIAS}" \ --ks-pass env:ANDROID_KEYSTORE_PASSWORD \ --key-pass env:ANDROID_KEY_PASSWORD \ --out "${SIGNED_APK}" \ "${ALIGNED_APK}" apksigner verify --verbose --print-certs "${SIGNED_APK}" rm "${ALIGNED_APK}" (cd "${RELEASE_DIRECTORY}" && sha256sum WallpaperFlow-release.apk > WallpaperFlow-release.apk.sha256) - name: Attach APK to Gitea release run: | API_ROOT="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}" RELEASE_JSON="${RUNNER_TEMP}/wallpaperflow-release.json" CREATE_JSON="${RUNNER_TEMP}/wallpaperflow-create-release.json" ASSETS_JSON="${RUNNER_TEMP}/wallpaperflow-release-assets.json" STATUS="$(curl --silent --show-error \ --output "${RELEASE_JSON}" \ --write-out '%{http_code}' \ --header "Authorization: token ${GITEA_RELEASE_TOKEN}" \ "${API_ROOT}/releases/tags/${GITHUB_REF_NAME}")" if [ "${STATUS}" = "404" ]; then node -e ' process.stdout.write(JSON.stringify({ tag_name: process.env.GITHUB_REF_NAME, name: `WallpaperFlow ${process.env.GITHUB_REF_NAME}`, body: "Automatisch erstelltes Android-Release.", draft: false, prerelease: false })) ' > "${CREATE_JSON}" curl --fail --silent --show-error \ --request POST \ --header "Authorization: token ${GITEA_RELEASE_TOKEN}" \ --header "Content-Type: application/json" \ --data-binary "@${CREATE_JSON}" \ --output "${RELEASE_JSON}" \ "${API_ROOT}/releases" elif [ "${STATUS}" != "200" ]; then echo "Could not read Gitea release for ${GITHUB_REF_NAME} (HTTP ${STATUS})." >&2 exit 1 fi RELEASE_ID="$(node -e ' const release = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); if (!release.id) process.exit(1); process.stdout.write(String(release.id)); ' "${RELEASE_JSON}")" curl --fail --silent --show-error \ --header "Authorization: token ${GITEA_RELEASE_TOKEN}" \ --output "${ASSETS_JSON}" \ "${API_ROOT}/releases/${RELEASE_ID}/assets" for FILE in \ release/WallpaperFlow-release.apk \ release/WallpaperFlow-release.apk.sha256 do ASSET_NAME="$(basename "${FILE}")" ASSET_ID="$(ASSET_NAME="${ASSET_NAME}" node -e ' const assets = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); const asset = assets.find(item => item.name === process.env.ASSET_NAME); if (asset) process.stdout.write(String(asset.id)); ' "${ASSETS_JSON}")" if [ -n "${ASSET_ID}" ]; then curl --fail --silent --show-error \ --request DELETE \ --header "Authorization: token ${GITEA_RELEASE_TOKEN}" \ "${API_ROOT}/releases/${RELEASE_ID}/assets/${ASSET_ID}" fi curl --fail --silent --show-error \ --request POST \ --header "Authorization: token ${GITEA_RELEASE_TOKEN}" \ --form "attachment=@${FILE}" \ "${API_ROOT}/releases/${RELEASE_ID}/assets" >/dev/null done