All checks were successful
Forgejo Release Builder / release (push) Successful in 22m39s
Decode keystore from secrets, sign unsigned APK artifacts with apksigner when configured, and prefer uploading signed APKs.
332 lines
12 KiB
YAML
Executable file
332 lines
12 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/}}"
|
|
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 "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 or update release
|
|
id: release
|
|
shell: bash
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION_TAG: ${{ steps.prepare.outputs.VERSION_TAG }}
|
|
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"]
|
|
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-{version_tag}.apk`
|
|
""",
|
|
)
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"tag_name": version_tag,
|
|
"name": f"Komikku {version_tag}",
|
|
"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
|