Create stable universal and arm64 APK alias names and improve release display version so repo URL import works more reliably in Obtainium.
364 lines
13 KiB
YAML
Executable file
364 lines
13 KiB
YAML
Executable file
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: forgejo-release
|
|
steps:
|
|
- name: Clone repository
|
|
shell: bash
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
rm -rf .git
|
|
git init .
|
|
git remote add origin "http://copilot:${FORGEJO_TOKEN}@127.0.0.1:3030/${GITHUB_REPOSITORY}.git"
|
|
git 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
|
|
|
|
- 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<<EOF'
|
|
printf '%s\n' "$commit_logs"
|
|
echo 'EOF'
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Set up Gradle
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -d /opt/android-sdk-linux ]; then
|
|
export ANDROID_HOME=/opt/android-sdk-linux
|
|
export ANDROID_SDK_ROOT=/opt/android-sdk-linux
|
|
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
|
|
echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
|
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
|
|
fi
|
|
chmod +x ./gradlew
|
|
GRADLE_OPTS="-Xmx2g -XX:MaxMetaspaceSize=768m -Dkotlin.daemon.enabled=false -Dkotlin.compiler.execution.strategy=in-process" \
|
|
./gradlew --no-daemon --max-workers=1 --version
|
|
|
|
- name: Check code format
|
|
run: |
|
|
GRADLE_OPTS="-Xmx2g -XX:MaxMetaspaceSize=768m -Dkotlin.daemon.enabled=false -Dkotlin.compiler.execution.strategy=in-process" \
|
|
./gradlew --no-daemon --max-workers=1 spotlessCheck
|
|
|
|
- name: Build app
|
|
run: |
|
|
GRADLE_OPTS="-Xmx2g -XX:MaxMetaspaceSize=768m -Dkotlin.daemon.enabled=false -Dkotlin.compiler.execution.strategy=in-process" \
|
|
./gradlew --no-daemon --max-workers=1 assembleRelease -Penable-updater -Pdisable-code-shrink
|
|
|
|
- name: Run unit tests
|
|
run: |
|
|
GRADLE_OPTS="-Xmx2g -XX:MaxMetaspaceSize=768m -Dkotlin.daemon.enabled=false -Dkotlin.compiler.execution.strategy=in-process" \
|
|
./gradlew --no-daemon --max-workers=1 testReleaseUnitTest
|
|
|
|
- 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
|