373 lines
14 KiB
YAML
Executable file
373 lines
14 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: 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
|
|
|
|
- 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
|
|
export GRADLE_USER_HOME="${PWD}/.gradle-ci"
|
|
./gradlew --no-daemon --max-workers=1 \
|
|
"-Dorg.gradle.jvmargs=-Xmx640m -XX:MaxMetaspaceSize=384m -XX:ReservedCodeCacheSize=96m -Dfile.encoding=UTF-8" \
|
|
-Dorg.gradle.parallel=false \
|
|
-Dorg.gradle.configureondemand=false \
|
|
-Dorg.gradle.vfs.watch=false \
|
|
-Dkotlin.daemon.enabled=false \
|
|
"-Dkotlin.daemon.jvmargs=-Xmx256m,-XX:MaxMetaspaceSize=128m,-XX:ReservedCodeCacheSize=96m" \
|
|
-Dkotlin.compiler.execution.strategy=in-process \
|
|
--version
|
|
|
|
- name: Build app
|
|
run: |
|
|
export GRADLE_USER_HOME="${PWD}/.gradle-ci"
|
|
./gradlew --no-daemon --max-workers=1 \
|
|
"-Dorg.gradle.jvmargs=-Xmx640m -XX:MaxMetaspaceSize=384m -XX:ReservedCodeCacheSize=96m -Dfile.encoding=UTF-8" \
|
|
-Dorg.gradle.parallel=false \
|
|
-Dorg.gradle.configureondemand=false \
|
|
-Dorg.gradle.vfs.watch=false \
|
|
-Dkotlin.daemon.enabled=false \
|
|
"-Dkotlin.daemon.jvmargs=-Xmx256m,-XX:MaxMetaspaceSize=128m,-XX:ReservedCodeCacheSize=96m" \
|
|
-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
|