name: Forgejo Release Builder # NOTE: keep Forgejo action refs node20-compatible for runner v3.5.1. on: push: tags: - v* permissions: contents: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: 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 --tags --prune --depth=1 origin "${GITHUB_SHA}" git checkout --force FETCH_HEAD - 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 echo "Need root or sudo to install packages." exit 1 fi } if command -v java >/dev/null 2>&1; then java -version else echo "PATH=$PATH" echo "pkg managers: dnf=$(command -v dnf || true) apt=$(command -v apt-get || true) apk=$(command -v apk || true) pacman=$(command -v pacman || true)" if [ -x /usr/bin/dnf ] || command -v dnf >/dev/null 2>&1; then pkg_install dnf install -y java-17-openjdk-devel elif [ -x /usr/bin/apt-get ] || command -v apt-get >/dev/null 2>&1; then pkg_install apt-get update pkg_install apt-get install -y openjdk-17-jdk elif [ -x /sbin/apk ] || [ -x /usr/sbin/apk ] || command -v apk >/dev/null 2>&1; then pkg_install apk add --no-cache openjdk17 elif [ -x /usr/bin/pacman ] || command -v pacman >/dev/null 2>&1; then pkg_install pacman -Sy --noconfirm jdk17-openjdk else echo "No supported package manager found for JDK installation." exit 1 fi java -version fi if command -v javac >/dev/null 2>&1; then javac -version java_home="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" echo "JAVA_HOME=$java_home" >> "$GITHUB_ENV" fi - name: Prepare release metadata id: prepare shell: bash run: | set -euo pipefail version_tag="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}" version_display="$(printf '%s' "$version_tag" | sed -E 's/^v//' )" previous_tag="$(git tag --list 'v*' --sort=-version:refname | grep -Fxv "$version_tag" | head -n 1 || true)" echo "VERSION_TAG=$version_tag" >> "$GITHUB_OUTPUT" echo "VERSION_DISPLAY=$version_display" >> "$GITHUB_OUTPUT" echo "PREV_TAG_NAME=$previous_tag" >> "$GITHUB_OUTPUT" if [ -n "$previous_tag" ]; then commit_logs="$(git log --pretty=format:'- %s (@%an)' "${previous_tag}..HEAD")" else commit_logs='- Initial release' fi { echo 'COMMIT_LOGS<> "$GITHUB_OUTPUT" - name: Set up Gradle shell: bash run: | set -euo pipefail sdk_candidates=( "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}" "/opt/android-sdk-linux" "/home/yuna/Android/Sdk" "/home/runner/Android/Sdk" "/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. Checked:" for candidate in "${sdk_candidates[@]}"; do [ -n "$candidate" ] && echo " - $candidate" done exit 1 fi export ANDROID_HOME="$sdk_dir" export ANDROID_SDK_ROOT="$sdk_dir" echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV" echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV" sdkmanager_bin="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" if [ -x "$sdkmanager_bin" ]; then set +o pipefail yes | "$sdkmanager_bin" --licenses >/dev/null || true yes | "$sdkmanager_bin" "platform-tools" "build-tools;35.0.0" set -o pipefail else echo "sdkmanager not found at $sdkmanager_bin" fi if [ ! -x "$ANDROID_HOME/build-tools/35.0.0/aidl" ]; then echo "Expected executable missing: $ANDROID_HOME/build-tools/35.0.0/aidl" ls -la "$ANDROID_HOME/build-tools" || true exit 1 fi printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties chmod +x ./gradlew export GRADLE_USER_HOME="${PWD}/.gradle-ci" ./gradlew --no-daemon --max-workers=1 \ "-Dorg.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=640m -XX:ReservedCodeCacheSize=192m -Dfile.encoding=UTF-8" \ -Dorg.gradle.parallel=false \ -Dorg.gradle.configureondemand=false \ -Dorg.gradle.vfs.watch=false \ -Dkotlin.daemon.enabled=false \ "-Dkotlin.daemon.jvmargs=-Xmx512m,-XX:MaxMetaspaceSize=256m,-XX:ReservedCodeCacheSize=128m" \ -Dkotlin.compiler.execution.strategy=in-process \ --version - name: Build app run: | if [ -n "${ANDROID_HOME:-}" ]; then printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties fi export GRADLE_USER_HOME="${PWD}/.gradle-ci" ./gradlew --no-daemon --max-workers=1 \ "-Dorg.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=640m -XX:ReservedCodeCacheSize=192m -Dfile.encoding=UTF-8" \ -Dorg.gradle.parallel=false \ -Dorg.gradle.configureondemand=false \ -Dorg.gradle.vfs.watch=false \ -Dkotlin.daemon.enabled=false \ "-Dkotlin.daemon.jvmargs=-Xmx512m,-XX:MaxMetaspaceSize=256m,-XX:ReservedCodeCacheSize=128m" \ -Dkotlin.compiler.execution.strategy=in-process \ --stacktrace \ assembleRelease -Penable-updater -Pdisable-code-shrink - name: Rename release artifacts shell: bash run: | set -euo pipefail version_tag="${{ steps.prepare.outputs.VERSION_TAG }}" mv app/build/outputs/apk/release/app-universal-release-unsigned.apk "Komikku-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk "Komikku-arm64-v8a-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk "Komikku-armeabi-v7a-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-x86-release-unsigned.apk "Komikku-x86-${version_tag}-unsigned.apk" mv app/build/outputs/apk/release/app-x86_64-release-unsigned.apk "Komikku-x86_64-${version_tag}-unsigned.apk" if [ -d app/build/outputs/mapping/release ]; then zip -qr "Komikku-mapping-${version_tag}.zip" app/build/outputs/mapping/release fi - name: Sign APKs (optional) shell: bash env: SIGNING_KEYSTORE_B64: ${{ secrets.SIGNING_KEYSTORE_B64 }} SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} run: | set -euo pipefail if [ -z "${SIGNING_KEYSTORE_B64}" ] || [ -z "${SIGNING_KEYSTORE_PASSWORD}" ] || [ -z "${SIGNING_KEY_ALIAS}" ] || [ -z "${SIGNING_KEY_PASSWORD}" ]; then echo "Signing secrets not fully set; keeping unsigned APKs." exit 0 fi keystore_file="$(mktemp)" printf '%s' "${SIGNING_KEYSTORE_B64}" | base64 -d > "${keystore_file}" apksigner_bin="" if command -v apksigner >/dev/null 2>&1; then apksigner_bin="$(command -v apksigner)" elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/build-tools" ]; then apksigner_bin="$(ls -1 "${ANDROID_HOME}"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -n 1 || true)" fi if [ -z "${apksigner_bin}" ]; then echo "apksigner not found in PATH or ANDROID_HOME/build-tools." exit 1 fi for unsigned_apk in Komikku-*-unsigned.apk; do [ -e "${unsigned_apk}" ] || continue signed_apk="${unsigned_apk/-unsigned.apk/.apk}" "${apksigner_bin}" sign \ --ks "${keystore_file}" \ --ks-key-alias "${SIGNING_KEY_ALIAS}" \ --ks-pass "pass:${SIGNING_KEYSTORE_PASSWORD}" \ --key-pass "pass:${SIGNING_KEY_PASSWORD}" \ --out "${signed_apk}" \ "${unsigned_apk}" "${apksigner_bin}" verify "${signed_apk}" done - name: Create Obtainium-friendly aliases shell: bash run: | set -euo pipefail alias_asset() { src="$1" dst="$2" if [ -e "$src" ]; then cp -f "$src" "$dst" fi } # Prefer signed APKs; fallback to unsigned when signing is not configured. alias_asset "$(ls -1t Komikku-arm64-v8a-*.apk 2>/dev/null | head -n1)" "Komikku-arm64-v8a.apk" alias_asset "$(ls -1t Komikku-arm64-v8a-*-unsigned.apk 2>/dev/null | head -n1)" "Komikku-arm64-v8a.apk" for candidate in Komikku-*.apk; do case "$candidate" in *arm64-v8a*|*armeabi-v7a*|*x86_64*|*x86*) ;; *) alias_asset "$candidate" "Komikku-universal.apk" break ;; esac done - name: Create or update release id: release shell: bash env: FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION_TAG: ${{ steps.prepare.outputs.VERSION_TAG }} VERSION_DISPLAY: ${{ steps.prepare.outputs.VERSION_DISPLAY }} PREV_TAG_NAME: ${{ steps.prepare.outputs.PREV_TAG_NAME }} COMMIT_LOGS: ${{ steps.prepare.outputs.COMMIT_LOGS }} REPO_URL: ${{ github.server_url }}/${{ github.repository }} REPOSITORY_NAME: ${{ github.repository }} run: | set -euo pipefail api_base="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" payload_file="$(mktemp)" response_file="$(mktemp)" python3 - <<'PY' > "$payload_file" import json import os import textwrap version_tag = os.environ["VERSION_TAG"] version_display = os.environ.get("VERSION_DISPLAY", version_tag) previous_tag = os.environ.get("PREV_TAG_NAME", "").strip() commit_logs = os.environ.get("COMMIT_LOGS", "").strip() or "- Initial release" repo_url = os.environ["REPO_URL"] repository_name = os.environ["REPOSITORY_NAME"] if previous_tag: full_changelog = ( f"**Full Changelog**: " f"[{repository_name}@{previous_tag}...{version_tag}]" f"({repo_url}/compare/{previous_tag}...{version_tag})" ) else: full_changelog = "" body = textwrap.dedent( f"""\ #### What's Changed ##### New ##### Improve ##### Fix {commit_logs} ##### Based on {full_changelog} > [!TIP] > > ### If you are unsure which version to download then go with `Komikku-universal.apk` """, ) print( json.dumps( { "tag_name": version_tag, "name": f"Komikku {version_display}", "body": body, "draft": True, "prerelease": False, }, ), ) PY status="$(curl -sS -o "$response_file" -w '%{http_code}' \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${api_base}/tags/${VERSION_TAG}")" if [ "$status" = "200" ]; then release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' < "$response_file")" curl -fsS \ -X PATCH \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H 'Content-Type: application/json' \ --data @"$payload_file" \ "${api_base}/${release_id}" \ > "$response_file" elif [ "$status" = "404" ]; then curl -fsS \ -X POST \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H 'Content-Type: application/json' \ --data @"$payload_file" \ "${api_base}" \ > "$response_file" release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' < "$response_file")" else cat "$response_file" exit 1 fi echo "RELEASE_ID=$release_id" >> "$GITHUB_OUTPUT" - name: Upload release assets shell: bash env: FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_ID: ${{ steps.release.outputs.RELEASE_ID }} run: | set -euo pipefail release_api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" assets_file="$(mktemp)" curl -fsS \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${release_api}/assets" \ > "$assets_file" for file in Komikku-*.apk Komikku-mapping-*.zip; do [ -e "$file" ] || continue if [ "${file#*-unsigned.apk}" != "${file}" ]; then signed_candidate="${file/-unsigned.apk/.apk}" if [ -e "${signed_candidate}" ]; then continue fi fi asset_name="$(basename "$file")" asset_id="$( python3 -c 'import json, sys; assets = json.load(open(sys.argv[1], encoding="utf-8")); name = sys.argv[2]; print(next((str(asset["id"]) for asset in assets if asset.get("name") == name), ""))' \ "$assets_file" \ "$asset_name" )" if [ -n "$asset_id" ]; then curl -fsS \ -X DELETE \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${release_api}/assets/${asset_id}" \ > /dev/null fi encoded_name="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$asset_name")" curl -fsS \ -X POST \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H 'Content-Type: application/octet-stream' \ --data-binary @"$file" \ "${release_api}/assets?name=${encoded_name}" \ > /dev/null done