14918cc94f
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 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s
Land the offline-model redesign (ANDROID_PLAN.md O1-O7): replace the explicit offline toggle with a single detected net-state machine, unify the lobby, and add a two-tier client-version gate. Contour-safe: both version vars empty => dormant, the wire change is additive, so web/pwa/vk/tg behaviour is unchanged unless the gate is deliberately configured. O1 net-state reducer (test-first). O2 store + wiring (connection/offline shims; +@capacitor/network). O3 remove the offline toggle + migrate the pref. O4 two-tier gate: hard update_required degrades to an offline Update/Play-offline notice (not terminal); soft GATEWAY_RECOMMENDED_CLIENT_VERSION -> X-Update-Recommended nudge. O5 unified lobby (device-local + greyed-from-cache server games; self-set identity; closes G-step-0). O6 create flows (with-friends online/offline segment + offline dict guard). O7 docs. Deploy/CI: wire the version gate through the deploy (GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION as plain unprefixed vars via compose + ci.yaml + prod-deploy.yaml + write-prod-env.sh + .env.example) so the gate is live when set (test-contour commit-hash version fails open; empty => dormant). Disable the manual android-build CI workflow for now (rename .disabled). Bump the app bundle budget 127->130 KB for the added always-loaded wiring. Fix the UI Docker stage (gateway/Dockerfile): install --ignore-scripts so the Alpine/musl SPA build no longer tries to native-build sharp (a local android:assets tool, unused in the image); esbuild's binary is an optional dep so Vite still builds. Tests: gateway go green (two-tier gate over a real HTTP handler); docker build of the landing + gateway targets green locally; compose --no-interpolate confirms the gateway env; svelte-check 0/0, vitest 617, e2e 122/122 (chromium + webkit).
173 lines
7.8 KiB
Plaintext
173 lines
7.8 KiB
Plaintext
# 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) and
|
|
# ANDROID_PLAN.md §E. 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
|