F1TV UHD Patch #163
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: F1TV UHD Patch | |
| on: | |
| schedule: | |
| - cron: "0 */3 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| apkm_url: | |
| description: "Direct URL to .apkm file (bypasses auto-download)" | |
| required: false | |
| type: string | |
| force: | |
| description: "Force rebuild even if release already exists" | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| env: | |
| APKTOOL_VERSION: "2.10.0" | |
| APKEEP_VERSION: "0.18.0" | |
| APKEEP_CUSTOM_TAG: "v0.18.1-shield" | |
| PLAYWRIGHT_BROWSERS_PATH: /home/runner/.cache/ms-playwright | |
| jobs: | |
| check: | |
| name: Check for new version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| version_short: ${{ steps.version.outputs.version_short }} | |
| should_build: ${{ steps.decide.outputs.should_build }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Restore apkeep cache | |
| id: apkeep-cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /usr/local/bin/apkeep | |
| key: apkeep-v2-${{ env.APKEEP_VERSION }} | |
| - name: Install apkeep | |
| if: steps.apkeep-cache.outputs.cache-hit != 'true' | |
| run: | | |
| curl -sSL "https://github.com/EFForg/apkeep/releases/download/${APKEEP_VERSION}/apkeep-x86_64-unknown-linux-gnu" -o /usr/local/bin/apkeep | |
| chmod +x /usr/local/bin/apkeep | |
| - name: Check latest TV version from APKPure | |
| id: version | |
| run: | | |
| echo "Listing available versions from APKPure..." | |
| apkeep -l -a com.formulaone.production -d apk-pure 2>&1 | tee /tmp/apkeep_versions.txt | |
| # Find the latest -tv version (last match = most recent) | |
| TV_VERSION=$(grep -oP '[\d.]+-\S*-tv' /tmp/apkeep_versions.txt | tail -1) | |
| if [[ -z "${TV_VERSION}" ]]; then | |
| echo "::warning::No TV variant found in APKPure version list" | |
| echo "version=" >> "$GITHUB_OUTPUT" | |
| echo "version_short=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Short version for the release tag (e.g. "3.0.47.1") | |
| VERSION_SHORT=$(echo "${TV_VERSION}" | grep -oP '^[\d]+(?:\.[\d]+)+') | |
| echo "version=${TV_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "version_short=${VERSION_SHORT}" >> "$GITHUB_OUTPUT" | |
| echo "Latest TV version: ${TV_VERSION} (short: ${VERSION_SHORT})" | |
| - name: Check if release already exists | |
| id: existing | |
| if: steps.version.outputs.version_short != '' | |
| run: | | |
| if gh release view "v${{ steps.version.outputs.version_short }}" &>/dev/null; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Decide whether to build | |
| id: decide | |
| run: | | |
| if [[ -z "${{ steps.version.outputs.version_short }}" ]]; then | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| echo "No TV version found, skipping" | |
| elif [[ "${{ inputs.force }}" == "true" ]]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "Forced build requested" | |
| elif [[ -n "${{ inputs.apkm_url }}" ]]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "Manual APKM URL provided" | |
| elif [[ "${{ steps.existing.outputs.exists }}" == "false" ]]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "New version: v${{ steps.version.outputs.version_short }}" | |
| else | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| echo "Release v${{ steps.version.outputs.version_short }} already exists, skipping" | |
| fi | |
| # Notify on new version detected | |
| - name: Notify via Pushover (new version) | |
| if: steps.decide.outputs.should_build == 'true' && !inputs.force | |
| run: | | |
| curl -s --form-string "token=$PUSHOVER_APP_TOKEN" \ | |
| --form-string "user=$PUSHOVER_USER_KEY" \ | |
| --form-string "title=F1TV UHD: New version detected" \ | |
| --form-string "message=Version ${{ steps.version.outputs.version }} found. Patching started." \ | |
| --form-string "priority=0" \ | |
| --form-string "sound=pushover" \ | |
| https://api.pushover.net/1/messages.json || true | |
| env: | |
| PUSHOVER_APP_TOKEN: ${{ secrets.PUSHOVER_APP_TOKEN }} | |
| PUSHOVER_USER_KEY: ${{ secrets.PUSHOVER_USER_KEY }} | |
| - name: Save apkeep cache | |
| if: always() && steps.apkeep-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /usr/local/bin/apkeep | |
| key: apkeep-v2-${{ env.APKEEP_VERSION }} | |
| patch: | |
| name: Download, patch & release | |
| needs: check | |
| if: needs.check.outputs.should_build == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| - name: Install Python dependencies | |
| run: pip install -r requirements.txt | |
| - name: Get Playwright version | |
| id: pw | |
| run: echo "version=$(playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" | |
| - name: Restore Playwright cache | |
| id: pw-cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: playwright-${{ steps.pw.outputs.version }} | |
| - name: Install Playwright browser | |
| if: steps.pw-cache.outputs.cache-hit != 'true' | |
| run: playwright install chromium --with-deps | |
| - name: Install Playwright OS deps (from cache) | |
| if: steps.pw-cache.outputs.cache-hit == 'true' | |
| run: playwright install-deps chromium | |
| - name: Restore apktool cache | |
| id: apktool-cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /usr/local/bin/apktool* | |
| key: apktool-${{ env.APKTOOL_VERSION }} | |
| - name: Install apktool | |
| if: steps.apktool-cache.outputs.cache-hit != 'true' | |
| run: | | |
| curl -sSL "https://github.com/iBotPeaches/Apktool/releases/download/v${APKTOOL_VERSION}/apktool_${APKTOOL_VERSION}.jar" -o /usr/local/bin/apktool.jar | |
| cat > /usr/local/bin/apktool << 'WRAPPER' | |
| #!/bin/bash | |
| java -jar /usr/local/bin/apktool.jar "$@" | |
| WRAPPER | |
| chmod +x /usr/local/bin/apktool | |
| - name: Set up Android SDK tools | |
| run: | | |
| BT_DIR="$(ls -d ${ANDROID_HOME}/build-tools/*/ 2>/dev/null | sort -V | tail -1)" | |
| if [[ -z "${BT_DIR}" ]]; then | |
| echo "Installing Android build-tools..." | |
| ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0" | |
| BT_DIR="${ANDROID_HOME}/build-tools/35.0.0/" | |
| fi | |
| echo "${BT_DIR}" >> "$GITHUB_PATH" | |
| echo "Using build-tools: ${BT_DIR}" | |
| - name: Restore custom apkeep cache | |
| if: ${{ !inputs.apkm_url }} | |
| id: apkeep-custom-cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /usr/local/bin/apkeep | |
| key: apkeep-custom-${{ env.APKEEP_CUSTOM_TAG }} | |
| - name: Install custom apkeep (with Shield TV profile) | |
| if: ${{ !inputs.apkm_url && steps.apkeep-custom-cache.outputs.cache-hit != 'true' }} | |
| run: | | |
| curl -sSL "https://github.com/Alexvbp/apkeep/releases/download/${APKEEP_CUSTOM_TAG}/apkeep-x86_64-linux.tar.gz" | tar xz -C /usr/local/bin/ | |
| chmod +x /usr/local/bin/apkeep | |
| # ── Option A: Download from Google Play (arm64 via Shield profile) ── | |
| - name: Download from Google Play | |
| if: ${{ !inputs.apkm_url && env.GOOGLE_EMAIL != '' }} | |
| id: download_gplay | |
| run: | | |
| mkdir -p download | |
| echo "Downloading F1TV from Google Play (NVIDIA Shield TV profile)..." | |
| apkeep -d google-play \ | |
| -e "${GOOGLE_EMAIL}" \ | |
| -t "${GOOGLE_AAS_TOKEN}" \ | |
| -o "device=nvidia_shield_tv,split_apk=true" \ | |
| --accept-tos \ | |
| -a "com.formulaone.production" \ | |
| download/ | |
| # split_apk=true creates a directory of APKs | |
| SPLIT_DIR="download/com.formulaone.production" | |
| if [[ -d "${SPLIT_DIR}" ]]; then | |
| echo "Split APKs from Google Play:" | |
| ls -la "${SPLIT_DIR}/" | |
| # Wrap into APKM bundle for patch.sh | |
| (cd "${SPLIT_DIR}" && zip -q ../f1tv-gplay.apkm *.apk) | |
| echo "apkm_path=download/f1tv-gplay.apkm" >> "$GITHUB_OUTPUT" | |
| echo "source=google-play" >> "$GITHUB_OUTPUT" | |
| else | |
| # Single APK fallback | |
| DOWNLOADED=$(find download/ -maxdepth 1 -name 'com.formulaone.production*' -print -quit) | |
| if [[ -n "${DOWNLOADED}" ]]; then | |
| echo "Downloaded: ${DOWNLOADED}" | |
| if [[ "${DOWNLOADED}" == *.apk ]]; then | |
| mkdir -p download/bundle_gp | |
| cp "${DOWNLOADED}" download/bundle_gp/base.apk | |
| (cd download/bundle_gp && zip -q ../f1tv-gplay.apkm base.apk) | |
| echo "apkm_path=download/f1tv-gplay.apkm" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "apkm_path=${DOWNLOADED}" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "source=google-play" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::Google Play download produced no files" | |
| exit 1 | |
| fi | |
| fi | |
| continue-on-error: true | |
| env: | |
| GOOGLE_EMAIL: ${{ secrets.GOOGLE_EMAIL }} | |
| GOOGLE_AAS_TOKEN: ${{ secrets.GOOGLE_AAS_TOKEN }} | |
| # ── Download armeabi-v7a split (for 32-bit device compatibility) ── | |
| - name: Download armeabi-v7a split | |
| if: ${{ steps.download_gplay.outcome == 'success' && env.GOOGLE_EMAIL != '' }} | |
| id: download_armv7 | |
| run: | | |
| echo "Downloading armeabi-v7a split from Google Play (32-bit profile)..." | |
| mkdir -p download_armv7 | |
| apkeep -d google-play \ | |
| -e "${GOOGLE_EMAIL}" \ | |
| -t "${GOOGLE_AAS_TOKEN}" \ | |
| -o "device=generic_armv7_tv,split_apk=true" \ | |
| --accept-tos \ | |
| -a "com.formulaone.production" \ | |
| download_armv7/ | |
| # Google Play names splits as com.formulaone.production.config.<abi>.apk | |
| ARMV7_SPLIT="$(find download_armv7/ -name '*armeabi_v7a*' -o -name '*armeabi-v7a*' | head -1)" | |
| if [[ -z "${ARMV7_SPLIT}" ]]; then | |
| ARMV7_SPLIT="$(find download_armv7/com.formulaone.production/ \( -name '*armeabi_v7a*' -o -name '*armeabi-v7a*' \) -print -quit 2>/dev/null)" | |
| fi | |
| if [[ -n "${ARMV7_SPLIT}" ]]; then | |
| echo "Found armeabi-v7a split: ${ARMV7_SPLIT}" | |
| # Copy into the main bundle directory | |
| MAIN_SPLIT_DIR="download/com.formulaone.production" | |
| if [[ -d "${MAIN_SPLIT_DIR}" ]]; then | |
| cp "${ARMV7_SPLIT}" "${MAIN_SPLIT_DIR}/" | |
| echo "Merged armeabi-v7a split into main bundle" | |
| # Rebuild the APKM with the extra split | |
| (cd "${MAIN_SPLIT_DIR}" && zip -q ../f1tv-gplay.apkm *.apk) | |
| echo "Rebuilt APKM bundle with armeabi-v7a" | |
| else | |
| echo "::warning::Main split directory not found, cannot merge armeabi-v7a split" | |
| fi | |
| else | |
| echo "::warning::armeabi-v7a split not found in 32-bit download" | |
| echo "Download contents:" | |
| find download_armv7/ -name '*.apk' -ls | |
| fi | |
| continue-on-error: true | |
| env: | |
| GOOGLE_EMAIL: ${{ secrets.GOOGLE_EMAIL }} | |
| GOOGLE_AAS_TOKEN: ${{ secrets.GOOGLE_AAS_TOKEN }} | |
| # ── Option B: Download from APKPure via apkeep ── | |
| - name: Download from APKPure | |
| if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' }} | |
| id: download_apkpure | |
| run: | | |
| echo "Google Play unavailable, falling back to APKPure..." | |
| mkdir -p download | |
| TV_VERSION="${{ needs.check.outputs.version }}" | |
| echo "Downloading F1TV TV version: ${TV_VERSION}" | |
| apkeep -a "com.formulaone.production@${TV_VERSION}" -d apk-pure download/ | |
| # Find the downloaded file | |
| echo "Download directory contents:" | |
| ls -la download/ | |
| DOWNLOADED=$(find download/ -maxdepth 1 -name 'com.formulaone.production*' -print -quit) | |
| if [[ -z "${DOWNLOADED}" ]]; then | |
| echo "::error::APKPure download failed" | |
| exit 1 | |
| fi | |
| echo "Downloaded: ${DOWNLOADED}" | |
| # XAPK is a zip of split APKs (same as APKM) | |
| if [[ "${DOWNLOADED}" == *.xapk ]]; then | |
| mv "${DOWNLOADED}" "${DOWNLOADED%.xapk}.apkm" | |
| DOWNLOADED="${DOWNLOADED%.xapk}.apkm" | |
| fi | |
| # Single APK: wrap it into an APKM bundle so patch.sh can handle it | |
| if [[ "${DOWNLOADED}" == *.apk ]]; then | |
| echo "Single APK detected, wrapping as APKM bundle..." | |
| mkdir -p download/bundle | |
| cp "${DOWNLOADED}" download/bundle/base.apk | |
| (cd download/bundle && zip -q ../f1tv-apkpure.apkm base.apk) | |
| DOWNLOADED="download/f1tv-apkpure.apkm" | |
| fi | |
| echo "apkm_path=${DOWNLOADED}" >> "$GITHUB_OUTPUT" | |
| echo "source=apkpure" >> "$GITHUB_OUTPUT" | |
| continue-on-error: true | |
| # ── Option C: Download from APKMirror via Playwright ── | |
| - name: Check latest version from RSS | |
| if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' && steps.download_apkpure.outcome != 'success' }} | |
| id: rss | |
| run: python scripts/check_version.py | |
| continue-on-error: true | |
| - name: Download APKM from APKMirror | |
| if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' && steps.download_apkpure.outcome != 'success' && steps.rss.outputs.release_url != '' }} | |
| id: download_auto | |
| run: | | |
| echo "APKPure download failed, trying APKMirror..." | |
| mkdir -p download | |
| APKM_PATH=$(python scripts/download_apkm.py \ | |
| "${{ steps.rss.outputs.release_url }}" \ | |
| --variant-url "${{ steps.rss.outputs.variant_url }}" \ | |
| -o download) | |
| echo "apkm_path=${APKM_PATH}" >> "$GITHUB_OUTPUT" | |
| echo "source=apkmirror" >> "$GITHUB_OUTPUT" | |
| continue-on-error: true | |
| - name: Upload download debug screenshots | |
| if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' && steps.download_apkpure.outcome != 'success' && steps.download_auto.outcome != 'success' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: download-debug-screenshots | |
| path: download/debug_* | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # ── Option D: Download from direct URL ── | |
| - name: Download APKM from URL | |
| if: ${{ inputs.apkm_url }} | |
| id: download_url | |
| run: | | |
| mkdir -p download | |
| curl -sSL -o download/f1tv.apkm "${{ inputs.apkm_url }}" | |
| echo "apkm_path=download/f1tv.apkm" >> "$GITHUB_OUTPUT" | |
| - name: Resolve APKM path | |
| id: apkm | |
| run: | | |
| if [[ -n "${{ steps.download_url.outputs.apkm_path }}" ]]; then | |
| echo "path=${{ steps.download_url.outputs.apkm_path }}" >> "$GITHUB_OUTPUT" | |
| elif [[ -n "${{ steps.download_gplay.outputs.apkm_path }}" ]]; then | |
| echo "path=${{ steps.download_gplay.outputs.apkm_path }}" >> "$GITHUB_OUTPUT" | |
| echo "source=${{ steps.download_gplay.outputs.source }}" >> "$GITHUB_OUTPUT" | |
| elif [[ -n "${{ steps.download_apkpure.outputs.apkm_path }}" ]]; then | |
| echo "path=${{ steps.download_apkpure.outputs.apkm_path }}" >> "$GITHUB_OUTPUT" | |
| echo "source=${{ steps.download_apkpure.outputs.source }}" >> "$GITHUB_OUTPUT" | |
| elif [[ -n "${{ steps.download_auto.outputs.apkm_path }}" ]]; then | |
| echo "path=${{ steps.download_auto.outputs.apkm_path }}" >> "$GITHUB_OUTPUT" | |
| echo "source=${{ steps.download_auto.outputs.source }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::error::All download methods failed (Google Play + APKPure + APKMirror)." | |
| echo "::error::Trigger the workflow manually with a direct APKM URL." | |
| exit 1 | |
| fi | |
| # ── Decode persistent keystore from secret (optional) ── | |
| - name: Set up keystore | |
| if: ${{ env.KEYSTORE_B64 != '' }} | |
| run: | | |
| echo "${KEYSTORE_B64}" | base64 -d > /tmp/patch.keystore | |
| echo "KEYSTORE_PATH=/tmp/patch.keystore" >> "$GITHUB_ENV" | |
| env: | |
| KEYSTORE_B64: ${{ secrets.KEYSTORE_B64 }} | |
| # ── Patch ── | |
| - name: Patch APKM | |
| run: | | |
| chmod +x scripts/patch.sh | |
| bash scripts/patch.sh "${{ steps.apkm.outputs.path }}" output/ | |
| env: | |
| KEYSTORE_PATH: ${{ env.KEYSTORE_PATH || '' }} | |
| KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS || 'android' }} | |
| KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS || 'f1tvpatch' }} | |
| # ── Upload artifacts (always, even if release creation fails) ── | |
| - name: Upload patched APKs | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: f1tv-uhd-patched-${{ needs.check.outputs.version_short }} | |
| path: output/ | |
| retention-days: 90 | |
| # ── Create GitHub Release ── | |
| - name: Create release | |
| id: release | |
| run: | | |
| TAG="v${{ needs.check.outputs.version_short }}" | |
| TITLE="F1TV UHD Patched - ${{ needs.check.outputs.version }}" | |
| # Delete existing release if force-rebuilding | |
| if [[ "${{ inputs.force }}" == "true" ]]; then | |
| gh release delete "${TAG}" --yes 2>/dev/null || true | |
| git push --delete origin "${TAG}" 2>/dev/null || true | |
| fi | |
| SOURCE="${{ steps.apkm.outputs.source }}" | |
| if [[ "${SOURCE}" == "google-play" ]]; then | |
| if [[ "${{ steps.download_armv7.outcome }}" == "success" ]]; then | |
| SOURCE_NOTE="Source: Google Play (arm64-v8a + armeabi-v7a)" | |
| else | |
| SOURCE_NOTE="Source: Google Play (arm64-v8a native)" | |
| fi | |
| elif [[ "${SOURCE}" == "apkpure" ]]; then | |
| SOURCE_NOTE="Source: APKPure (armeabi-v7a)" | |
| else | |
| SOURCE_NOTE="Source: ${SOURCE:-manual}" | |
| fi | |
| gh release create "${TAG}" \ | |
| --title "${TITLE}" \ | |
| --notes "$(cat <<NOTES | |
| Patched F1TV Android TV app with UHD/4K enabled. | |
| **Version:** \`${{ needs.check.outputs.version }}\` | |
| **${SOURCE_NOTE}** | |
| ## Install | |
| Download \`f1tv-uhd-patched.apkm\` below, then use the install script: | |
| \`\`\`bash | |
| ./scripts/install.sh f1tv-uhd-patched.apkm [device-ip:5555] | |
| \`\`\` | |
| See the [README](https://github.com/${{ github.repository }}#installing-on-your-android-tv) for full setup instructions. | |
| NOTES | |
| )" \ | |
| output/f1tv-uhd-patched.apkm | |
| echo "release_url=https://github.com/${{ github.repository }}/releases/tag/${TAG}" >> "$GITHUB_OUTPUT" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ── Notify: success ── | |
| - name: Notify via Pushover (success) | |
| if: success() | |
| run: | | |
| curl -s --form-string "token=$PUSHOVER_APP_TOKEN" \ | |
| --form-string "user=$PUSHOVER_USER_KEY" \ | |
| --form-string "title=F1TV UHD: Patch ready!" \ | |
| --form-string "message=Version ${{ needs.check.outputs.version }} patched and released." \ | |
| --form-string "url=${{ steps.release.outputs.release_url }}" \ | |
| --form-string "url_title=Download from GitHub" \ | |
| --form-string "priority=0" \ | |
| --form-string "sound=pushover" \ | |
| https://api.pushover.net/1/messages.json || true | |
| env: | |
| PUSHOVER_APP_TOKEN: ${{ secrets.PUSHOVER_APP_TOKEN }} | |
| PUSHOVER_USER_KEY: ${{ secrets.PUSHOVER_USER_KEY }} | |
| # ── Notify: failure ── | |
| - name: Notify via Pushover (failure) | |
| if: failure() | |
| run: | | |
| curl -s --form-string "token=$PUSHOVER_APP_TOKEN" \ | |
| --form-string "user=$PUSHOVER_USER_KEY" \ | |
| --form-string "title=F1TV UHD: Patch failed" \ | |
| --form-string "message=Version ${{ needs.check.outputs.version }} failed to patch. Check workflow logs." \ | |
| --form-string "url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --form-string "url_title=View workflow run" \ | |
| --form-string "priority=0" \ | |
| --form-string "sound=falling" \ | |
| https://api.pushover.net/1/messages.json || true | |
| env: | |
| PUSHOVER_APP_TOKEN: ${{ secrets.PUSHOVER_APP_TOKEN }} | |
| PUSHOVER_USER_KEY: ${{ secrets.PUSHOVER_USER_KEY }} | |
| # ── Save caches (runs even if job fails) ── | |
| - name: Save Playwright cache | |
| if: always() && steps.pw-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: playwright-${{ steps.pw.outputs.version }} | |
| - name: Save apktool cache | |
| if: always() && steps.apktool-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /usr/local/bin/apktool* | |
| key: apktool-${{ env.APKTOOL_VERSION }} | |
| - name: Save custom apkeep cache | |
| if: always() && steps.apkeep-custom-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /usr/local/bin/apkeep | |
| key: apkeep-custom-${{ env.APKEEP_CUSTOM_TAG }} |