Add Forgejo Actions release workflow for Obtainium APK updates.
Build a signed Flutter APK on desktop-release and publish versioned assets so private-repo Obtainium can track frosty-arm64-v8a-v*.apk. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1aae7438bb
commit
cdbccc23a8
2 changed files with 349 additions and 0 deletions
304
.forgejo/workflows/release.yml
Normal file
304
.forgejo/workflows/release.yml
Normal file
|
|
@ -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}" <<BODY_EOF
|
||||
Frosty fork (Twitch client) — native player + TwitchAdSolutions adblock.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Version | ${VERSION_NAME} (${VERSION_CODE}) |
|
||||
| Commit | ${SHORT_SHA} |
|
||||
| Package | \`com.tommychow.frosty\` |
|
||||
|
||||
**Obtainium:** Forgejo source → \`…/copilot/frosty/releases\`, APK filter \`frosty-arm64-v8a-v.*\\.apk\`, trim \`v?([0-9]+\\.[0-9]+\\.[0-9]+)\` group \`1\`.
|
||||
|
||||
Stock Play Frosty uses a different signing key — uninstall it before installing this fork, or Obtainium cannot update in place.
|
||||
BODY_EOF
|
||||
|
||||
# Create or update release for TAG
|
||||
existing="$(curl -sS -H "$AUTH" "${API}/releases/tags/${TAG}" || true)"
|
||||
release_id="$(printf '%s' "$existing" | jq -r '.id // empty')"
|
||||
if [ -z "$release_id" ]; then
|
||||
payload="$(jq -n \
|
||||
--arg tag "$TAG" \
|
||||
--arg name "Frosty ${VERSION_NAME} (${SHORT_SHA})" \
|
||||
--rawfile body "$BODY_FILE" \
|
||||
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false}')"
|
||||
resp="$(curl -sS -X POST -H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d "$payload" "${API}/releases")"
|
||||
release_id="$(printf '%s' "$resp" | jq -r '.id')"
|
||||
else
|
||||
payload="$(jq -n \
|
||||
--arg name "Frosty ${VERSION_NAME} (${SHORT_SHA})" \
|
||||
--rawfile body "$BODY_FILE" \
|
||||
'{name:$name, body:$body, draft:false, prerelease:false}')"
|
||||
curl -sS -X PATCH -H "$AUTH" -H 'Content-Type: application/json' \
|
||||
-d "$payload" "${API}/releases/${release_id}" >/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"
|
||||
45
docs/OBTAINIUM.md
Normal file
45
docs/OBTAINIUM.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue