diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 00000000..8e8ac8fd --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,304 @@ +# Builds a signed Frosty release APK and publishes a Forgejo release for Obtainium. +# Pattern: finamp-fork / jellyfin-android-fork on host runner `desktop-release`. +# +# Required repository secrets: +# RELEASE_KEYSTORE_BASE64, STORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD +# CLIENT_ID, SECRET (Twitch Helix app credentials — --dart-define) +# +# Obtainium: +# Source: Forgejo +# URL: https://forgejo.fsociety00.cc/api/v1/repos/copilot/frosty/releases +# APK filter: frosty-arm64-v8a-v.*\.apk +# Trim version: v?([0-9]+\.[0-9]+\.[0-9]+) +# Match group: 1 +# Private repo → Forgejo API token in Obtainium. + +name: Release APK + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: frosty-release + cancel-in-progress: true + +jobs: + build-and-release: + name: Build & publish release + runs-on: desktop-release + + steps: + - name: Clone repository + shell: bash + env: + FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + rm -rf .git + git init . + origin_url="${GITHUB_SERVER_URL#https://}" + origin_url="${origin_url#http://}" + git remote add origin "https://copilot:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git" + git -c http.sslVerify=false fetch --depth=1 origin "${GITHUB_SHA}" + git checkout --force FETCH_HEAD + test -f pubspec.yaml + test -f android/app/build.gradle.kts + echo "Checked out ${GITHUB_REPOSITORY}@${GITHUB_SHA}" + + - name: Set up JDK + shell: bash + run: | + set -euo pipefail + pkg_install() { + if [ "$(id -u)" -eq 0 ]; then "$@" + elif command -v sudo >/dev/null 2>&1; then sudo "$@" + else "$@"; fi + } + if ! command -v java >/dev/null 2>&1; then + if command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y java-21-openjdk-devel + elif command -v apt-get >/dev/null 2>&1; then + pkg_install apt-get update + pkg_install apt-get install -y openjdk-21-jdk + elif command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache openjdk21 + fi + fi + java -version + if command -v javac >/dev/null 2>&1; then + java_home="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" + echo "JAVA_HOME=$java_home" >> "$GITHUB_ENV" + fi + + - name: Set up Android SDK + shell: bash + run: | + set -euo pipefail + sdk_candidates=( + "${ANDROID_HOME:-}" + "${ANDROID_SDK_ROOT:-}" + "/home/yuna/Android/Sdk" + "${HOME}/Android/Sdk" + "/opt/android-sdk-linux" + "/root/Android/Sdk" + ) + sdk_dir="" + for candidate in "${sdk_candidates[@]}"; do + if [ -n "$candidate" ] && [ -d "$candidate/platforms" ] && [ -d "$candidate/build-tools" ]; then + sdk_dir="$candidate" + break + fi + done + if [ -z "$sdk_dir" ]; then + echo "Android SDK not found on desktop-release host." + exit 1 + fi + echo "ANDROID_HOME=$sdk_dir" >> "$GITHUB_ENV" + echo "ANDROID_SDK_ROOT=$sdk_dir" >> "$GITHUB_ENV" + printf 'sdk.dir=%s\n' "$sdk_dir" > android/local.properties + echo "Using Android SDK: $sdk_dir" + + - name: Set up Flutter + shell: bash + run: | + set -euo pipefail + pkg_install() { + if [ "$(id -u)" -eq 0 ]; then "$@" + elif command -v sudo >/dev/null 2>&1; then sudo "$@" + else "$@"; fi + } + if ! command -v curl >/dev/null 2>&1; then + if command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache curl bash git unzip xz + elif command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y curl git unzip + elif command -v apt-get >/dev/null 2>&1; then + pkg_install apt-get update + pkg_install apt-get install -y curl git unzip + fi + fi + # Prefer pinned SDK already on the desktop-release host. + flutter_version="$(awk '/^[[:space:]]*flutter:/{print $2; exit}' pubspec.yaml)" + flutter_version="${flutter_version:-3.47.0}" + candidates=( + "/home/yuna/.local/flutter-${flutter_version}" + "${HOME}/.cache/frosty-ci-flutter-${flutter_version}" + "/home/yuna/.local/flutter" + ) + flutter_home="" + for c in "${candidates[@]}"; do + if [ -x "${c}/bin/flutter" ]; then + flutter_home="$c" + break + fi + done + if [ -z "$flutter_home" ]; then + flutter_home="${HOME}/.cache/frosty-ci-flutter-${flutter_version}" + rm -rf "${flutter_home}" + git clone --depth 1 --branch "${flutter_version}" https://github.com/flutter/flutter.git "${flutter_home}" + fi + echo "${flutter_home}/bin" >> "$GITHUB_PATH" + echo "PATH=${flutter_home}/bin:${PATH}" >> "$GITHUB_ENV" + export PATH="${flutter_home}/bin:${PATH}" + flutter --version + flutter precache --android + + - name: Decode keystore + shell: bash + env: + RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }} + STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + run: | + set -euo pipefail + if [ -z "${RELEASE_KEYSTORE_BASE64:-}" ] || [ -z "${STORE_PASSWORD:-}" ] || [ -z "${KEY_ALIAS:-}" ] || [ -z "${KEY_PASSWORD:-}" ]; then + echo "Missing signing secrets: RELEASE_KEYSTORE_BASE64, STORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD" + exit 1 + fi + printf '%s' "$RELEASE_KEYSTORE_BASE64" | base64 -d > android/upload-keystore.jks + test -s android/upload-keystore.jks + { + echo "storePassword=${STORE_PASSWORD}" + echo "keyPassword=${KEY_PASSWORD}" + echo "keyAlias=${KEY_ALIAS}" + echo "storeFile=${GITHUB_WORKSPACE}/android/upload-keystore.jks" + } > android/key.properties + echo "Keystore ready ($(wc -c < android/upload-keystore.jks) bytes)." + + - name: Resolve version metadata + id: version + shell: bash + run: | + set -euo pipefail + raw="$(grep -E '^version:' pubspec.yaml | head -1 | awk '{print $2}')" + VERSION_NAME="${raw%%+*}" + VERSION_CODE="${raw##*+}" + [ "$VERSION_CODE" != "$raw" ] || VERSION_CODE=1 + SHORT_SHA="${GITHUB_SHA::7}" + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + TAG="${GITHUB_REF#refs/tags/}" + else + TAG="build-${VERSION_NAME}-${VERSION_CODE}-${SHORT_SHA}" + fi + echo "version_name=${VERSION_NAME}" >> "$GITHUB_OUTPUT" + echo "version_code=${VERSION_CODE}" >> "$GITHUB_OUTPUT" + echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG} version=${VERSION_NAME}+${VERSION_CODE}" + + - name: Build signed release APK + id: build + shell: bash + env: + CLIENT_ID: ${{ secrets.CLIENT_ID }} + SECRET: ${{ secrets.SECRET }} + VERSION_NAME: ${{ steps.version.outputs.version_name }} + VERSION_CODE: ${{ steps.version.outputs.version_code }} + run: | + set -euo pipefail + if [ -z "${CLIENT_ID:-}" ] || [ -z "${SECRET:-}" ]; then + echo "Missing Twitch secrets CLIENT_ID / SECRET (Helix OAuth app)." + exit 1 + fi + flutter pub get + flutter build apk --release \ + --build-name="${VERSION_NAME}" \ + --build-number="${VERSION_CODE}" \ + --dart-define="CLIENT_ID=${CLIENT_ID}" \ + --dart-define="SECRET=${SECRET}" + mkdir -p dist + cp build/app/outputs/flutter-apk/app-release.apk \ + "dist/frosty-v${VERSION_NAME}.apk" + flutter build apk --release --split-per-abi \ + --build-name="${VERSION_NAME}" \ + --build-number="${VERSION_CODE}" \ + --dart-define="CLIENT_ID=${CLIENT_ID}" \ + --dart-define="SECRET=${SECRET}" || true + for abi in arm64-v8a armeabi-v7a x86_64; do + src="build/app/outputs/flutter-apk/app-${abi}-release.apk" + if [ -f "$src" ]; then + cp "$src" "dist/frosty-${abi}-v${VERSION_NAME}.apk" + fi + done + ls -lh dist/ + + - name: Publish Forgejo release + shell: bash + env: + FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + VERSION_NAME: ${{ steps.version.outputs.version_name }} + VERSION_CODE: ${{ steps.version.outputs.version_code }} + SHORT_SHA: ${{ steps.version.outputs.short_sha }} + run: | + set -euo pipefail + pkg_install() { + if [ "$(id -u)" -eq 0 ]; then "$@" + elif command -v sudo >/dev/null 2>&1; then sudo "$@" + else return 1; fi + } + for pkg in curl jq; do + command -v "$pkg" >/dev/null 2>&1 && continue + if command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache "$pkg" + elif command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y "$pkg" + elif command -v apt-get >/dev/null 2>&1; then + pkg_install apt-get update + pkg_install apt-get install -y "$pkg" + fi + done + + API="${GITHUB_API_URL:-${GITHUB_SERVER_URL}/api/v1}/repos/${GITHUB_REPOSITORY}" + AUTH="Authorization: token ${FORGEJO_TOKEN}" + + BODY_FILE="$(mktemp)" + cat > "${BODY_FILE}" </dev/null + fi + test -n "$release_id" && test "$release_id" != "null" + + for apk in dist/*.apk; do + [ -f "$apk" ] || continue + name="$(basename "$apk")" + echo "Uploading ${name}…" + curl -sS -X POST -H "$AUTH" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"${apk}" \ + "${API}/releases/${release_id}/assets?name=${name}" \ + | jq -r '.name // .message' + done + echo "Published ${TAG} as release ${release_id}" + rm -f "$BODY_FILE" diff --git a/docs/OBTAINIUM.md b/docs/OBTAINIUM.md new file mode 100644 index 00000000..1a342ef0 --- /dev/null +++ b/docs/OBTAINIUM.md @@ -0,0 +1,45 @@ +# Obtainium (Android) + +Forgejo fork: [copilot/frosty](https://forgejo.fsociety00.cc/copilot/frosty) (**private**). + +Releases: `.forgejo/workflows/release.yml` on runner `desktop-release` (tag `v*` or workflow_dispatch). + +## Obtainium settings + +| Setting | Value | +|---|---| +| Source | Forgejo | +| URL | `https://forgejo.fsociety00.cc/api/v1/repos/copilot/frosty/releases` | +| APK filter | `frosty-arm64-v8a-v.*\.apk` | +| Trim version | `v?([0-9]+\.[0-9]+\.[0-9]+)` | +| Match group | `1` | + +Repo is private → add a Forgejo API token in Obtainium (same PAT as other Newas apps). + +Quick add: + +```text +obtainium://add/https://forgejo.fsociety00.cc/api/v1/repos/copilot/frosty/releases +``` + +## Signing / Play Store conflict + +Package id stays `com.tommychow.frosty`. Play Store Frosty and this fork use **different signing keys**, so Obtainium cannot update stock → fork in place. Uninstall the Play build first, then install from Obtainium. + +## CI secrets (Forgejo repo) + +| Secret | Purpose | +|---|---| +| `RELEASE_KEYSTORE_BASE64` | Same pattern as Flow / Finamp / Jellyfin forks | +| `STORE_PASSWORD` | Keystore password | +| `KEY_ALIAS` | Key alias | +| `KEY_PASSWORD` | Key password | +| `CLIENT_ID` | Twitch Helix app client id (`--dart-define`) | +| `SECRET` | Twitch Helix app secret (`--dart-define`) | + +## Cut a release + +1. Bump `version:` in `pubspec.yaml` (`x.y.z+build`) if needed. +2. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z` + or run **Actions → Release APK → Run workflow**. +3. Install / update via Obtainium once the release asset appears.