Files
scrabble-game/.gitea/workflows/android-build.yaml
T
Ilia Denisov a067a2fd01
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m14s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
chore(android): de-anchor ANDROID_PLAN.md from the docs, re-enable the APK workflow
Reword every code comment and doc that referenced ANDROID_PLAN.md or its stage anchors (§E, G-step-0, O1) so the living docs no longer depend on the plan file: .claude/CLAUDE.md, deploy/README.md, docs/ARCHITECTURE.md, docs/TESTING.md, ui/README.md, ui/android/.gitignore, ui/e2e/native.spec.ts, ui/src/lib/netstate.ts.

Re-enable the manual signed-APK workflow (android-build.yaml.disabled -> android-build.yaml; the CI host already has the Android SDK; workflow_dispatch-only, so it does not auto-run on this PR). Add docs/ICONS.md — the single-master icon/logo format reference (web, PWA, Android, future iOS, store listings).
2026-07-13 14:05:43 +02:00

173 lines
7.8 KiB
YAML

# Manual signed-APK build for the standalone Android app (RuStore). Runs ONLY from master, ONLY on
# workflow_dispatch with confirm=build — the same deliberate-manual shape as prod-deploy.yaml, never on
# a PR. It builds the native-flavoured SPA, bundles the offline dictionaries into the APK assets, and
# assembles a release APK, uploaded as a run artifact (RuStore upload stays manual for the MVP).
#
# Signing degrades gracefully: with the ANDROID_KEYSTORE_* secrets present the APK is signed; without
# them build.gradle produces an UNSIGNED release APK (so a dry run still proves the whole pipeline).
# The keystore is a publication prerequisite — see deploy/README.md (Android build/release runbook).
# The toolchain is self-provisioned here (JDK 21 + a cached Android SDK), so the runner host needs
# nothing pre-installed.
name: android-build
run-name: "android build ${{ github.sha }}"
on:
workflow_dispatch:
inputs:
confirm:
description: 'Type "build" to confirm an APK build from master.'
required: true
default: ""
permissions:
contents: read
env:
NO_COLOR: "1"
# The dictionary release, one source of truth (same Gitea variable the backend image + CI use). It
# both fetches the DAWGs and labels the bundled files (VITE_DICT_VERSION must equal __DICT_VERSION__).
DICT_VERSION: ${{ vars.DICT_VERSION }}
VITE_DICT_VERSION: ${{ vars.DICT_VERSION }}
# Hide in-app purchases in the RuStore MVP (RuStore, not Google Play — VITE_GP_BUILD stays unset).
VITE_PAYMENTS_DISABLED: "1"
# The update overlay's store target; empty until publication (the button no-ops, and the version gate
# is dormant in the MVP so it never fires). Set the ANDROID_RUSTORE_URL variable when the app is live.
VITE_RUSTORE_URL: ${{ vars.ANDROID_RUSTORE_URL }}
# Host-executor runner: the Android SDK is pre-installed on the host. Override ANDROID_SDK_DIR if it
# lives elsewhere (the default matches deploy/README.md's install path).
ANDROID_HOME: ${{ vars.ANDROID_SDK_DIR || '/opt/android-sdk' }}
ANDROID_SDK_ROOT: ${{ vars.ANDROID_SDK_DIR || '/opt/android-sdk' }}
jobs:
build:
if: ${{ github.ref == 'refs/heads/master' && inputs.confirm == 'build' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Host-executor runner: the Android SDK is pre-installed on the host (JDK 21 comes from setup-java
# below). Fail fast + legibly if the runner user cannot read/execute it or a needed package is
# missing — this doubles as the runner-access check (deploy/README.md).
- name: Verify the host Android SDK
run: |
sm="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
if [ ! -x "$sm" ]; then
echo "::error::sdkmanager not found/executable at $sm — set the ANDROID_SDK_DIR variable if the SDK lives elsewhere, or grant the runner user read+exec: sudo chmod -R a+rX \"$ANDROID_HOME\""; exit 1
fi
for pkg in "platforms/android-36" "build-tools"; do
if [ ! -d "$ANDROID_HOME/$pkg" ]; then
echo "::error::missing $ANDROID_HOME/$pkg — run: \"$sm\" 'platforms;android-36' 'build-tools;36.0.0'"; exit 1
fi
done
echo "Android SDK OK at $ANDROID_HOME"; "$sm" --version
- name: Compute version + native build env
id: prep
env:
PUBLIC_BASE_URL: ${{ vars.PROD_PUBLIC_BASE_URL }}
run: |
# A store release must sit on an exact vMAJOR.MINOR.PATCH tag (G tags before dispatch) so the
# versionCode is deterministic and strictly increasing across uploads. Refuse anything else
# rather than derive a versionCode from a "-N-gSHA" describe.
desc="$(git describe --tags --exact-match 2>/dev/null || true)"
case "$desc" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "::error::HEAD is not on a clean vX.Y.Z tag (git describe --exact-match = '${desc:-none}'); tag the release first"; exit 1 ;;
esac
v="${desc#v}"
IFS=. read -r MA MI PA <<< "$v"
# 10# forces base-10 so a zero-padded part is never read as octal.
code=$(( 10#$MA * 1000000 + 10#$MI * 1000 + 10#$PA ))
# The native SPA talks to the production origin (reuse the prod public base URL); strip any
# trailing slash so the Connect endpoint never doubles it.
gateway="${PUBLIC_BASE_URL%/}"
{
echo "tag=$desc"
echo "name=$v"
echo "code=$code"
echo "gateway=$gateway"
} >> "$GITHUB_OUTPUT"
echo "release $desc -> versionName $v versionCode $code, gateway $gateway"
# Same release + fetch as the Go/UI jobs in ci.yaml — the bundled dicts come from this tarball,
# NOT the scrabble-solver sibling (ui is a Node project outside go.work).
- name: Fetch dictionary DAWGs
run: |
mkdir -p "${GITHUB_WORKSPACE}/dawg"
curl -fsSL -o /tmp/dawg.tar.gz "https://gitea.iliadenisov.ru/developer/scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz"
tar xzf /tmp/dawg.tar.gz -C "${GITHUB_WORKSPACE}/dawg"
ls -la "${GITHUB_WORKSPACE}/dawg"
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install pnpm
run: npm install -g pnpm@11.0.9
- name: Install deps
working-directory: ui
run: pnpm install --frozen-lockfile
- name: Build the SPA (native flavour)
working-directory: ui
env:
VITE_GATEWAY_URL: ${{ steps.prep.outputs.gateway }}
VITE_APP_VERSION: ${{ steps.prep.outputs.tag }}
run: pnpm run build
# Copy the release DAWGs into dist/dict/<variant>@<version>.dawg for the offline-first bundled tier
# (after the build, before cap sync copies dist/ into the native assets).
- name: Bundle the dictionaries
working-directory: ui
env:
DICT_DIR: ${{ github.workspace }}/dawg
run: node scripts/bundle-dicts.mjs
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
# Local cap binary (dodges the corepack pre-flight flake); syncs dist/ (incl. dict/) + native deps.
- name: Sync the native project
working-directory: ui
run: node_modules/.bin/cap sync android
- name: Decode the release keystore
id: keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "${GITHUB_WORKSPACE}/release.jks"
echo "file=${GITHUB_WORKSPACE}/release.jks" >> "$GITHUB_OUTPUT"
echo "keystore decoded -> signed release build"
else
echo "file=" >> "$GITHUB_OUTPUT"
echo "::warning::ANDROID_KEYSTORE_BASE64 is not set — building an UNSIGNED release APK (not installable/publishable)"
fi
- name: Assemble the release APK
working-directory: ui/android
env:
ANDROID_KEYSTORE_FILE: ${{ steps.keystore.outputs.file }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: ./gradlew assembleRelease -PversionCode=${{ steps.prep.outputs.code }} -PversionName=${{ steps.prep.outputs.name }} --console=plain
- name: Upload the APK artifact
uses: actions/upload-artifact@v4
with:
name: erudit-${{ steps.prep.outputs.name }}-apk
path: ui/android/app/build/outputs/apk/release/*.apk
if-no-files-found: error