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
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).
173 lines
7.8 KiB
YAML
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
|