# Build a SIGNED release APK and attach it to a Gitea release (PLAN.md §10, §12). # # Trigger: pushing a semver tag `v*` (e.g. `v1.0.0`). Cutting an APK is a # deliberate act — we do NOT release on every merge to main — so the tag is the # source of truth for the version. `workflow_dispatch` builds the signed APK too # but skips publishing (a dry run to smoke-test signing without cutting a release). # # Version: the tag drives versionName (`v1.2.3` -> `1.2.3`); the workflow run # number is the monotonic versionCode. Both are passed to Gradle as -P overrides. # # Signing (Settings -> Actions -> Secrets on RunicGateway/Android-app). The # keystore never lives in the repo — it is a base64 secret decoded at build time. # Secret names deliberately avoid the reserved GITEA_/GITHUB_ prefixes: # ANDROID_KEYSTORE_BASE64 — base64 of the release .jks (single line) # ANDROID_KEYSTORE_PASSWORD — keystore password # ANDROID_KEY_ALIAS — key alias (e.g. runicgateway) # ANDROID_KEY_PASSWORD — key password (equals the store password for a # PKCS12 keystore) # The release itself is created with the runner's built-in ${{ github.token }}, # so no extra API token secret is required. # # Runner notes are identical to pr-checks.yml (self-hosted `ubuntu-latest`): the # container lacks git/curl/unzip and can't reach api.adoptium.net, so we apt-install # the base tools + JDK 17 rather than using actions/setup-java, install the exact # SDK packages, and `chmod +x ./gradlew` in-step (checkout drops the exec bit). name: Release APK on: push: tags: ['v*'] workflow_dispatch: {} concurrency: group: release-apk-${{ github.ref }} cancel-in-progress: false env: GITEA_HOST: gitea.whitlocktech.com REPO: RunicGateway/Android-app jobs: release-apk: runs-on: ubuntu-latest steps: - name: Install base tools + JDK 17 run: | apt-get update apt-get install -y git curl unzip jq openjdk-17-jdk-headless echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV" - uses: actions/checkout@v4 - name: Set up Android SDK uses: android-actions/setup-android@v3 - name: Install Android SDK packages run: | set +o pipefail yes | sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" - name: Cache Gradle uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }} restore-keys: | gradle-${{ runner.os }}- # Derive versionName from the tag (dispatch runs get a 0.0.0-dev placeholder, # since they don't publish) and a monotonic versionCode from the run number. - name: Resolve version id: ver run: | set -euo pipefail if [ "${{ github.ref_type }}" = "tag" ]; then VN="${GITHUB_REF_NAME#v}" else VN="0.0.0-dev" fi echo "versionName=${VN}" >> "$GITHUB_OUTPUT" echo "versionCode=${{ github.run_number }}" >> "$GITHUB_OUTPUT" echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" echo "==> versionName=${VN} versionCode=${{ github.run_number }} ref=${GITHUB_REF_NAME}" # Decode the keystore secret to a file the build reads via ANDROID_KEYSTORE_FILE. # `base64 -d` tolerates the trailing newline a pasted secret may carry. - name: Decode signing keystore env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} run: | set -euo pipefail if [ -z "${ANDROID_KEYSTORE_BASE64:-}" ]; then echo "::error::ANDROID_KEYSTORE_BASE64 secret is not set — cannot build a signed release." exit 1 fi printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "${RUNNER_TEMP}/release.jks" echo "ANDROID_KEYSTORE_FILE=${RUNNER_TEMP}/release.jks" >> "$GITHUB_ENV" - name: Build signed release APK env: ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | set -euo pipefail chmod +x ./gradlew ./gradlew --no-daemon :app:assembleRelease \ -PversionName="${{ steps.ver.outputs.versionName }}" \ -PversionCode="${{ steps.ver.outputs.versionCode }}" - name: Stage APK id: stage run: | set -euo pipefail SRC="app/build/outputs/apk/release/app-release.apk" test -f "$SRC" || { echo "::error::release APK not found at $SRC"; exit 1; } mkdir -p dist OUT="dist/runic-gateway-${{ steps.ver.outputs.versionName }}.apk" cp "$SRC" "$OUT" ( cd dist && sha256sum "$(basename "$OUT")" > SHA256SUMS ) echo "apk=${OUT}" >> "$GITHUB_OUTPUT" ls -l dist && cat dist/SHA256SUMS # Publish only for a real tag push; a manual dispatch stops after the signed # build above (dry run). - name: Create Gitea release and upload APK if: ${{ github.ref_type == 'tag' }} env: RELEASE_TOKEN: ${{ github.token }} run: | set -euo pipefail TAG="${{ steps.ver.outputs.tag }}" API="https://${GITEA_HOST}/api/v1/repos/${REPO}" TOKEN="$(printf '%s' "${RELEASE_TOKEN}" | tr -d '\r\n')" REL_ID="$(curl -sSf -X POST "${API}/releases" \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg tag "$TAG" \ '{tag_name:$tag, name:$tag, body:("Signed release APK for " + $tag + ". Sideload on Android 10+ (§10); the app self-configures its shard site on first run."), draft:false, prerelease:false}')" \ | jq -r '.id')" echo "Created release ${TAG} (id=${REL_ID})" for f in "$(basename "${{ steps.stage.outputs.apk }}")" SHA256SUMS; do curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \ -H "Authorization: token ${TOKEN}" \ -F "attachment=@dist/${f}" >/dev/null echo " uploaded ${f}" done