diff --git a/.forgejo/workflows/build_release.yml b/.forgejo/workflows/build_release.yml new file mode 100755 index 000000000..b64bf5ed3 --- /dev/null +++ b/.forgejo/workflows/build_release.yml @@ -0,0 +1,250 @@ +name: Forgejo Release Builder + +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 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: 17 + distribution: temurin + + - name: Write google-services.json + uses: DamianReeves/write-file-action@6929a9a6d1807689191dcc8bbe62b54d70a32b42 # v1.3 + with: + path: app/google-services.json + contents: ${{ secrets.GOOGLE_SERVICES_JSON }} + write-mode: overwrite + + - name: Write client_secrets.json + uses: DamianReeves/write-file-action@6929a9a6d1807689191dcc8bbe62b54d70a32b42 # v1.3 + with: + path: app/src/main/assets/client_secrets.json + contents: ${{ secrets.GOOGLE_CLIENT_SECRETS_JSON }} + write-mode: overwrite + + - 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<> "$GITHUB_OUTPUT" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5 + + - name: Check code format + run: ./gradlew spotlessCheck + + - name: Build app + run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater + + - name: Run unit tests + run: ./gradlew testReleaseUnitTest + + - name: Sign APK + uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478 + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + alias: ${{ secrets.ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + env: + BUILD_TOOLS_VERSION: '35.0.1' + + - 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-signed.apk "Komikku-${version_tag}.apk" + mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk "Komikku-arm64-v8a-${version_tag}.apk" + mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk "Komikku-armeabi-v7a-${version_tag}.apk" + mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk "Komikku-x86-${version_tag}.apk" + mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk "Komikku-x86_64-${version_tag}.apk" + + zip -qr "Komikku-mapping-${version_tag}.zip" app/build/outputs/mapping/release + + - 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 + + 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 diff --git a/docs/forgejo-release-runner.md b/docs/forgejo-release-runner.md new file mode 100755 index 000000000..4602a9539 --- /dev/null +++ b/docs/forgejo-release-runner.md @@ -0,0 +1,78 @@ +# Forgejo release runner + +This repository now includes a Forgejo-native release workflow at `.forgejo/workflows/build_release.yml`. + +## What it does + +- Triggers on tags matching `v*` +- Builds the release APKs +- Signs the APKs +- Creates or updates a draft release through the Forgejo API +- Uploads the APKs and a zipped ProGuard mapping as release assets + +The workflow intentionally avoids GitHub-only artifact and release actions so it can run on Forgejo with a self-hosted runner. + +## Required repository secrets + +Set these secrets in the Forgejo repository settings: + +- `GOOGLE_SERVICES_JSON` +- `GOOGLE_CLIENT_SECRETS_JSON` +- `SIGNING_KEY` +- `ALIAS` +- `KEY_STORE_PASSWORD` +- `KEY_PASSWORD` + +`secrets.GITHUB_TOKEN` is used as the Forgejo-provided built-in token for the release API calls. + +## Runner label + +The workflow uses: + +```yaml +runs-on: forgejo-release +``` + +Register a runner with a matching label. Host mode is the simplest option for Android builds: + +```sh +forgejo-runner register \ + --instance https://192.168.1.155:3030 \ + --token \ + --name komikku-release \ + --labels "forgejo-release:host" +``` + +## Runner prerequisites + +Install these on the runner host before enabling the workflow: + +- Git +- curl +- zip +- Python 3 +- JDK 17 +- Android SDK command-line tools +- Android platform `android-35` +- Android build-tools `35.0.1` + +Recommended environment variables on the runner host: + +```sh +export ANDROID_HOME=/opt/android-sdk +export ANDROID_SDK_ROOT=/opt/android-sdk +export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH" +``` + +## Suggested setup flow + +1. Enable Actions on the Forgejo instance and repository. +2. Register the `forgejo-release` runner. +3. Add the required repository secrets. +4. Push a test tag such as `v0.0.0-forgejo.1`. +5. Open the draft release in Forgejo and verify the uploaded APKs. + +## Notes + +- The existing GitHub workflows under `.github/workflows/` remain unchanged. +- The Forgejo release notes are generated from git history, so they do not depend on the GitHub compare API.