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 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)" upstream_repo="komikku-app/komikku" upstream_ref="" upstream_base="" for ref in master main; do candidate="$(git ls-remote "https://github.com/${upstream_repo}.git" "refs/heads/${ref}" | awk '{print $1}' || true)" if [ -n "$candidate" ]; then upstream_ref="$ref" upstream_base="$candidate" break fi done echo "VERSION_TAG=$version_tag" >> "$GITHUB_OUTPUT" echo "VERSION_DISPLAY=$version_display" >> "$GITHUB_OUTPUT" echo "PREV_TAG_NAME=$previous_tag" >> "$GITHUB_OUTPUT" echo "UPSTREAM_REPO=$upstream_repo" >> "$GITHUB_OUTPUT" echo "UPSTREAM_REF=$upstream_ref" >> "$GITHUB_OUTPUT" echo "UPSTREAM_BASE=$upstream_base" >> "$GITHUB_OUTPUT" if [ -n "$upstream_base" ]; then git fetch --no-tags --depth=1 "https://github.com/${upstream_repo}.git" "$upstream_base" || true fi if [ -n "$upstream_base" ] && git cat-file -e "${upstream_base}^{commit}" 2>/dev/null; then commit_logs="$(git log --pretty=format:'- %s (@%an)' "${upstream_base}..HEAD")" elif [ -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 }}" # Strip CI/build metadata suffixes (e.g. -forgejo.29) for stable asset names. version_core="$(printf '%s' "$version_tag" | sed -E 's/^v//' | sed -E 's/-forgejo\.[0-9]+$//')" mv app/build/outputs/apk/release/app-universal-release-unsigned.apk "Komikku-v${version_core}-universal-unsigned.apk" mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk "Komikku-v${version_core}-arm64-v8a-unsigned.apk" mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk "Komikku-v${version_core}-armeabi-v7a-unsigned.apk" mv app/build/outputs/apk/release/app-x86-release-unsigned.apk "Komikku-v${version_core}-x86-unsigned.apk" mv app/build/outputs/apk/release/app-x86_64-release-unsigned.apk "Komikku-v${version_core}-x86_64-unsigned.apk" if [ -d app/build/outputs/mapping/release ]; then zip -qr "Komikku-v${version_core}-mapping.zip" app/build/outputs/mapping/release fi - name: Sign APKs 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 are required for installable release APKs." echo "Set SIGNING_KEYSTORE_B64, SIGNING_KEYSTORE_PASSWORD, SIGNING_KEY_ALIAS, SIGNING_KEY_PASSWORD." exit 1 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 } # Only publish signed APK aliases. alias_asset "$(ls -1t Komikku-v*-arm64-v8a.apk 2>/dev/null | head -n1)" "Komikku-arm64-v8a.apk" alias_asset "$(ls -1t Komikku-v*-armeabi-v7a.apk 2>/dev/null | head -n1)" "Komikku-armeabi-v7a.apk" alias_asset "$(ls -1t Komikku-v*-x86.apk 2>/dev/null | head -n1)" "Komikku-x86.apk" alias_asset "$(ls -1t Komikku-v*-x86_64.apk 2>/dev/null | head -n1)" "Komikku-x86_64.apk" alias_asset "$(ls -1t Komikku-v*-universal.apk 2>/dev/null | head -n1)" "Komikku-universal.apk" - 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 }} UPSTREAM_REPO: ${{ steps.prepare.outputs.UPSTREAM_REPO }} UPSTREAM_REF: ${{ steps.prepare.outputs.UPSTREAM_REF }} UPSTREAM_BASE: ${{ steps.prepare.outputs.UPSTREAM_BASE }} COMMIT_LOGS: ${{ steps.prepare.outputs.COMMIT_LOGS }} REPO_URL: ${{ github.server_url }}/${{ github.repository }} REPOSITORY_NAME: ${{ github.repository }} FORGEJO_CURL_INSECURE: "true" run: | set -euo pipefail export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" discover_jq() { if command -v jq >/dev/null 2>&1; then JQ_BIN="$(command -v jq)" export JQ_BIN return 0 fi for cand in /usr/bin/jq /usr/local/bin/jq /bin/jq; do if [ -x "$cand" ]; then JQ_BIN="$cand" export JQ_BIN return 0 fi done return 1 } ensure_jq() { if discover_jq; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y jq elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y jq elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache jq elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm jq fi discover_jq } ensure_curl() { if command -v curl >/dev/null 2>&1; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y curl elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y curl elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache curl elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm curl fi command -v curl >/dev/null 2>&1 } ensure_jq || { echo "jq not found; install jq on the runner (dnf install -y jq)."; exit 1; } ensure_curl || { echo "curl not found; install curl on the runner (dnf install -y curl)."; exit 1; } curl_tls_args=() if [ "${FORGEJO_CURL_INSECURE:-false}" = "true" ]; then curl_tls_args+=(--insecure) fi api_base="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases" payload_file="$(mktemp)" response_file="$(mktemp)" commit_logs="${COMMIT_LOGS:-}" commit_logs="$(printf '%s' "$commit_logs" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [ -z "$commit_logs" ]; then commit_logs="- Initial release" fi full_changelog="" if [ -n "${PREV_TAG_NAME:-}" ]; then full_changelog="**Full Changelog**: [${REPOSITORY_NAME}@${PREV_TAG_NAME}...${VERSION_TAG}](${REPO_URL}/compare/${PREV_TAG_NAME}...${VERSION_TAG})" fi fork_delta_label="" if [ -n "${UPSTREAM_BASE:-}" ] && [ -n "${UPSTREAM_REF:-}" ]; then fork_delta_label="Fork delta vs ${UPSTREAM_REPO}@${UPSTREAM_REF} (${UPSTREAM_BASE})" fi release_body="$(printf '%s\n' \ "## Komikku ${VERSION_DISPLAY}" \ "" \ "### Download Guide" \ "- **Most users:** \`Komikku-universal.apk\`" \ "- **arm64 devices:** \`Komikku-arm64-v8a.apk\`" \ "- **armeabi-v7a devices:** \`Komikku-armeabi-v7a.apk\`" \ "- **x86 devices:** \`Komikku-x86.apk\`" \ "- **x86_64 devices:** \`Komikku-x86_64.apk\`" \ "" \ "### Changes" \ "${commit_logs}" \ "" \ "### Release Notes" \ "- Assets use stable names for Obtainium compatibility." \ "- Versioned files follow: \`Komikku-v-.apk\`." \ "" \ "${fork_delta_label}" \ "" \ "${full_changelog}" \ "" \ "" \ "> [!TIP]" \ ">" \ "> ### If you are unsure which version to download then go with \`Komikku-universal.apk\`")" "${JQ_BIN}" -n \ --arg tag "${VERSION_TAG}" \ --arg title "Komikku ${VERSION_DISPLAY}" \ --arg body "${release_body}" \ '{tag_name: $tag, name: $title, body: $body, draft: true, prerelease: false}' > "${payload_file}" status="$(curl "${curl_tls_args[@]}" -sS -o "$response_file" -w '%{http_code}' \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${api_base}/tags/${VERSION_TAG}")" if [ "$status" = "200" ]; then release_id="$("${JQ_BIN}" -r '.id' < "$response_file")" curl "${curl_tls_args[@]}" -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 "${curl_tls_args[@]}" -fsS \ -X POST \ -H "Authorization: token ${FORGEJO_TOKEN}" \ -H 'Content-Type: application/json' \ --data @"$payload_file" \ "${api_base}" \ > "$response_file" release_id="$("${JQ_BIN}" -r '.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 }} FORGEJO_CURL_INSECURE: "true" run: | set -euo pipefail export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" discover_jq() { if command -v jq >/dev/null 2>&1; then JQ_BIN="$(command -v jq)" export JQ_BIN return 0 fi for cand in /usr/bin/jq /usr/local/bin/jq /bin/jq; do if [ -x "$cand" ]; then JQ_BIN="$cand" export JQ_BIN return 0 fi done return 1 } ensure_jq() { if discover_jq; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y jq elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y jq elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache jq elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm jq fi discover_jq } ensure_curl() { if command -v curl >/dev/null 2>&1; then return 0 fi run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else return 1 fi } if command -v dnf >/dev/null 2>&1; then run_as_root dnf install -y curl elif command -v apt-get >/dev/null 2>&1; then run_as_root apt-get update run_as_root apt-get install -y curl elif command -v apk >/dev/null 2>&1; then run_as_root apk add --no-cache curl elif command -v pacman >/dev/null 2>&1; then run_as_root pacman -Sy --noconfirm curl fi command -v curl >/dev/null 2>&1 } ensure_jq || { echo "jq not found; install jq on the runner (dnf install -y jq)."; exit 1; } ensure_curl || { echo "curl not found; install curl on the runner (dnf install -y curl)."; exit 1; } curl_tls_args=() if [ "${FORGEJO_CURL_INSECURE:-false}" = "true" ]; then curl_tls_args+=(--insecure) fi release_api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" assets_file="$(mktemp)" curl "${curl_tls_args[@]}" -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 continue fi asset_name="$(basename "$file")" asset_id="$("${JQ_BIN}" -r --arg n "${asset_name}" '(.[] | select(.name == $n) | .id) // empty' "$assets_file")" if [ -n "$asset_id" ] && [ "$asset_id" != "null" ]; then curl "${curl_tls_args[@]}" -fsS \ -X DELETE \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "${release_api}/assets/${asset_id}" \ > /dev/null fi encoded_name="$("${JQ_BIN}" -nr --arg s "${asset_name}" '$s | @uri')" curl "${curl_tls_args[@]}" -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