ci: build + serve signed Android APK on push to master
- docker/android.Dockerfile: full socket-safe build (JDK17 + SDK + NDK 27 + CMake + Bun) -> expo prebuild -> sign (BuildKit --secret) -> assembleRelease. - ci.yml build-apk job: buildx build, extract APK via docker create/cp, copy into the running wisp container so it serves at /wisp.apk; artifact upload. - app.json: set android.package = dev.briggen.wisp (required by prebuild). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+44
-12
@@ -64,15 +64,47 @@ jobs:
|
|||||||
done
|
done
|
||||||
echo "wisp container did not become healthy" >&2; exit 1
|
echo "wisp container did not become healthy" >&2; exit 1
|
||||||
|
|
||||||
# =========================================================================
|
# ---- build-apk: signed release APK, served at /wisp.apk ------------------
|
||||||
# TODO (next iteration, once the runner is validated): build-apk.
|
# Socket-safe: the whole Android build runs inside `docker buildx build`
|
||||||
# The Android build is genuinely risky (whisper.rn C++/NDK on RN 0.85) and
|
# (context streamed to the host BuildKit — no host bind-mounts), signed via
|
||||||
# untested locally (no Android SDK on the dev box). It must be socket-safe:
|
# BuildKit --secret from the ANDROID_* repo secrets. We then extract the APK
|
||||||
# a FULL `docker build` of a build-stage in docker/android.Dockerfile (repo
|
# with docker create/cp and copy it into the running `wisp` web container so
|
||||||
# COPYed in, prebuild + gradlew assembleRelease, signing via BuildKit
|
# it is downloadable at https://wisp.briggen.dev/wisp.apk.
|
||||||
# --secret using the ANDROID_* secrets), then extract the APK with
|
build-apk:
|
||||||
# `docker create`/`docker cp` (NO host bind-mounts — they don't resolve over
|
needs: [test, deploy-web]
|
||||||
# the mounted socket) and `docker cp` it into the running `wisp` container at
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||||
# /usr/share/nginx/html/wisp.apk (served at https://wisp.briggen.dev/wisp.apk),
|
runs-on: ubuntu-latest
|
||||||
# plus upload it as a CI artifact. Keystore secrets are already configured.
|
steps:
|
||||||
# =========================================================================
|
- uses: actions/checkout@v4
|
||||||
|
- name: Stage signing secrets as files
|
||||||
|
run: |
|
||||||
|
umask 077
|
||||||
|
mkdir -p "$RUNNER_TEMP/sec"
|
||||||
|
printf '%s' "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" > "$RUNNER_TEMP/sec/ks_b64"
|
||||||
|
printf '%s' "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" > "$RUNNER_TEMP/sec/ks_pass"
|
||||||
|
printf '%s' "${{ secrets.ANDROID_KEY_ALIAS }}" > "$RUNNER_TEMP/sec/ks_alias"
|
||||||
|
printf '%s' "${{ secrets.ANDROID_KEY_PASSWORD }}" > "$RUNNER_TEMP/sec/ks_keypass"
|
||||||
|
- name: Build signed APK image
|
||||||
|
run: |
|
||||||
|
docker buildx build \
|
||||||
|
--secret id=ks_b64,src="$RUNNER_TEMP/sec/ks_b64" \
|
||||||
|
--secret id=ks_pass,src="$RUNNER_TEMP/sec/ks_pass" \
|
||||||
|
--secret id=ks_alias,src="$RUNNER_TEMP/sec/ks_alias" \
|
||||||
|
--secret id=ks_keypass,src="$RUNNER_TEMP/sec/ks_keypass" \
|
||||||
|
-f docker/android.Dockerfile -t wisp-android:ci --load .
|
||||||
|
- name: Extract + publish APK to /wisp.apk
|
||||||
|
run: |
|
||||||
|
cid=$(docker create wisp-android:ci)
|
||||||
|
docker cp "$cid:/workspace/android/app/build/outputs/apk/release/app-release.apk" ./app-release.apk
|
||||||
|
docker rm "$cid"
|
||||||
|
ls -lh app-release.apk
|
||||||
|
docker cp ./app-release.apk wisp:/usr/share/nginx/html/wisp.apk
|
||||||
|
- name: Cleanup staged secrets
|
||||||
|
if: always()
|
||||||
|
run: rm -rf "$RUNNER_TEMP/sec"
|
||||||
|
- name: Upload APK artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: wisp-release-apk
|
||||||
|
path: app-release.apk
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"icon": "./assets/expo.icon"
|
"icon": "./assets/expo.icon"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
"package": "dev.briggen.wisp",
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
||||||
|
|||||||
+69
-80
@@ -1,110 +1,99 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
#
|
#
|
||||||
# docker/android.Dockerfile
|
# docker/android.Dockerfile — full Android build for Wisp -> signed release APK.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Reproducible Android build environment for Wisp (Expo SDK 56 / RN 0.85).
|
# Two stages:
|
||||||
|
# toolchain : JDK17 + Android SDK (platform/build-tools) + NDK + CMake + Bun.
|
||||||
|
# Reusable on its own (docker build --target toolchain).
|
||||||
|
# build : COPYs the repo, installs JS deps, `expo prebuild`, signs, and
|
||||||
|
# runs `gradlew assembleRelease`. The signed APK lands at
|
||||||
|
# /workspace/android/app/build/outputs/apk/release/app-release.apk
|
||||||
#
|
#
|
||||||
# This image contains everything needed to run:
|
# This is socket-safe for CI: the whole build happens inside `docker build`
|
||||||
# bunx expo prebuild --platform android --no-install
|
# (context streamed to the daemon — NO host bind mounts, which don't resolve
|
||||||
# cd android && ./gradlew assembleRelease
|
# over a mounted Docker socket). CI then extracts the APK via docker create/cp.
|
||||||
#
|
#
|
||||||
# It is used both in CI (.gitea/workflows/ci.yml -> build-apk) and locally
|
# Signing secrets are passed as BuildKit secrets (NOT build args / layers):
|
||||||
# (see docs/DEPLOY.md "Build the APK locally") so the APK is built identically
|
# --secret id=ks_b64 -> ANDROID_KEYSTORE_BASE64 (base64 of the .keystore)
|
||||||
# everywhere. The repository itself is mounted in at run time; this image only
|
# --secret id=ks_pass -> ANDROID_KEYSTORE_PASSWORD
|
||||||
# provides the toolchain (JDK + Android SDK + Bun), not the source.
|
# --secret id=ks_alias -> ANDROID_KEY_ALIAS
|
||||||
|
# --secret id=ks_keypass -> ANDROID_KEY_PASSWORD
|
||||||
#
|
#
|
||||||
|
# JDK pinned to 17 (AGP 8.x); versions track RN 0.85 / Expo SDK 56
|
||||||
|
# (compileSdk/targetSdk 36, buildTools 36.0.0, NDK 27.1.12297006).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Pinned tool versions (Expo SDK 56 / React Native 0.85 era).
|
|
||||||
# Sourced from node_modules/react-native/gradle/libs.versions.toml:
|
|
||||||
# compileSdk = 36 targetSdk = 36 minSdk = 24
|
|
||||||
# buildTools = 36.0.0
|
|
||||||
# ndkVersion = 27.1.12297006 (installed lazily by Gradle if a module needs it)
|
|
||||||
# AGP = 8.12.0 Kotlin = 2.1.20 Gradle wrapper = 9.3.1
|
|
||||||
#
|
|
||||||
# IMPORTANT: pin to JDK 17. AGP 8.x targets JDK 17; the dev machine may have a
|
|
||||||
# newer JDK (e.g. JDK 26), but CI/Docker MUST use 17 for reproducible, supported
|
|
||||||
# builds. See docs/DEPLOY.md.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
FROM eclipse-temurin:17-jdk
|
|
||||||
|
|
||||||
# ---- Build-time version arguments (override with --build-arg if needed) ----
|
FROM eclipse-temurin:17-jdk AS toolchain
|
||||||
# Android compile/target SDK platform required by Expo SDK 56 / RN 0.85.
|
|
||||||
ARG ANDROID_PLATFORM_VERSION=36
|
ARG ANDROID_PLATFORM_VERSION=36
|
||||||
# Android build-tools version matching libs.versions.toml (buildTools = 36.0.0).
|
|
||||||
ARG ANDROID_BUILD_TOOLS_VERSION=36.0.0
|
ARG ANDROID_BUILD_TOOLS_VERSION=36.0.0
|
||||||
# Google's commandline-tools package. "latest" via the published zip; pinned to a
|
ARG ANDROID_NDK_VERSION=27.1.12297006
|
||||||
# known build number for reproducibility. Update from:
|
ARG ANDROID_CMAKE_VERSION=3.22.1
|
||||||
# https://developer.android.com/studio#command-line-tools-only
|
|
||||||
ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708
|
ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708
|
||||||
|
|
||||||
# ---- Android SDK locations (both vars set for tool compatibility) ----------
|
ENV ANDROID_HOME=/opt/android-sdk \
|
||||||
ENV ANDROID_HOME=/opt/android-sdk
|
ANDROID_SDK_ROOT=/opt/android-sdk \
|
||||||
ENV ANDROID_SDK_ROOT=/opt/android-sdk
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
# Put the SDK tools on PATH. cmdline-tools live under .../cmdline-tools/latest/bin
|
GRADLE_USER_HOME=/opt/gradle \
|
||||||
# (we deliberately install them into a directory literally named "latest", which
|
BUN_INSTALL=/root/.bun
|
||||||
# is what sdkmanager expects).
|
ENV PATH=/root/.bun/bin:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}
|
||||||
ENV PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools
|
|
||||||
|
|
||||||
# Non-interactive apt for clean CI logs.
|
# OS deps for SDK download + RN/CMake native builds.
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
# ---- OS packages -----------------------------------------------------------
|
|
||||||
# - unzip/curl: fetch + extract the cmdline-tools zip
|
|
||||||
# - git: expo prebuild / some gradle tasks shell out to git
|
|
||||||
# - bash: scripts/ci-android-sign.sh is bash (the default sh would do, but be explicit)
|
|
||||||
# - ca-certificates: TLS for downloads (sdkmanager, gradle, HF, etc.)
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
unzip \
|
unzip curl git bash ca-certificates \
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
bash \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ---- Install Bun (JS package manager / task runner) ------------------------
|
# Bun (Wisp's package manager / task runner).
|
||||||
# Wisp uses Bun, not npm/yarn. The official installer drops Bun into ~/.bun.
|
RUN curl -fsSL https://bun.sh/install | bash && bun --version
|
||||||
# We run as root in this image, so HOME=/root.
|
|
||||||
ENV BUN_INSTALL=/root/.bun
|
|
||||||
ENV PATH=${BUN_INSTALL}/bin:${PATH}
|
|
||||||
RUN curl -fsSL https://bun.sh/install | bash \
|
|
||||||
&& bun --version
|
|
||||||
|
|
||||||
# ---- Install the Android SDK commandline tools -----------------------------
|
# Android commandline-tools into $ANDROID_HOME/cmdline-tools/latest.
|
||||||
# Download the Linux cmdline-tools zip and unpack it into the directory layout
|
|
||||||
# sdkmanager requires: $ANDROID_HOME/cmdline-tools/latest/...
|
|
||||||
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools \
|
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools \
|
||||||
&& curl -fsSL -o /tmp/cmdline-tools.zip \
|
&& curl -fsSL -o /tmp/ct.zip \
|
||||||
"https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" \
|
"https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" \
|
||||||
&& unzip -q /tmp/cmdline-tools.zip -d /tmp/cmdline-tools \
|
&& unzip -q /tmp/ct.zip -d /tmp/ct \
|
||||||
# The zip extracts to a top-level "cmdline-tools" dir; move it to "latest".
|
&& mv /tmp/ct/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \
|
||||||
&& mv /tmp/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \
|
&& rm -rf /tmp/ct.zip /tmp/ct
|
||||||
&& rm -rf /tmp/cmdline-tools.zip /tmp/cmdline-tools
|
|
||||||
|
|
||||||
# ---- Accept licenses and install SDK packages ------------------------------
|
# Licenses + SDK packages. NDK + CMake are required because whisper.rn compiles
|
||||||
# `yes |` auto-accepts every interactive license prompt. We install:
|
# native C++ (whisper.cpp) via externalNativeBuild.
|
||||||
# - platform-tools (adb, etc.)
|
|
||||||
# - platforms;android-36 (compileSdk/targetSdk = 36)
|
|
||||||
# - build-tools;36.0.0 (aapt2, zipalign, apksigner, ...)
|
|
||||||
# The NDK (27.1.12297006) is intentionally NOT pre-installed: most JS-only Expo
|
|
||||||
# apps don't need it, and Gradle will download the exact required NDK on demand.
|
|
||||||
# If your native deps require it at build time, add:
|
|
||||||
# "ndk;27.1.12297006"
|
|
||||||
# to the sdkmanager line below to bake it in (saves time but enlarges the image).
|
|
||||||
RUN yes | sdkmanager --licenses > /dev/null \
|
RUN yes | sdkmanager --licenses > /dev/null \
|
||||||
&& sdkmanager --install \
|
&& sdkmanager --install \
|
||||||
"platform-tools" \
|
"platform-tools" \
|
||||||
"platforms;android-${ANDROID_PLATFORM_VERSION}" \
|
"platforms;android-${ANDROID_PLATFORM_VERSION}" \
|
||||||
"build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
|
"build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
|
||||||
|
"ndk;${ANDROID_NDK_VERSION}" \
|
||||||
|
"cmake;${ANDROID_CMAKE_VERSION}" > /dev/null \
|
||||||
&& yes | sdkmanager --licenses > /dev/null
|
&& yes | sdkmanager --licenses > /dev/null
|
||||||
|
|
||||||
# Gradle home lives outside the mounted repo so the daemon cache can persist via
|
|
||||||
# a Docker volume between builds (see docs/DEPLOY.md for the local-build command).
|
|
||||||
ENV GRADLE_USER_HOME=/opt/gradle
|
|
||||||
|
|
||||||
# The repository is mounted here at run time (e.g. `-v "$PWD":/workspace`).
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
# Default to an interactive shell; CI overrides this with an explicit build
|
# ---------------------------------------------------------------------------
|
||||||
# command (`bash -lc "..."`). Documented usage is in docs/DEPLOY.md.
|
FROM toolchain AS build
|
||||||
CMD ["bash"]
|
|
||||||
|
# Install JS deps first (cached unless package.json/bun.lock change).
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Bring in the rest of the source (.dockerignore excludes node_modules, android/, ios/, dist...).
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Generate the native android/ project (autolinks whisper.rn, reanimated, etc.).
|
||||||
|
RUN bunx expo prebuild --platform android --no-install
|
||||||
|
|
||||||
|
# Decode the keystore + wire release signing, then assemble the signed APK.
|
||||||
|
# Secrets are mounted as files under /run/secrets and exported only for this RUN.
|
||||||
|
RUN --mount=type=secret,id=ks_b64 \
|
||||||
|
--mount=type=secret,id=ks_pass \
|
||||||
|
--mount=type=secret,id=ks_alias \
|
||||||
|
--mount=type=secret,id=ks_keypass \
|
||||||
|
ANDROID_KEYSTORE_BASE64="$(cat /run/secrets/ks_b64)" \
|
||||||
|
ANDROID_KEYSTORE_PASSWORD="$(cat /run/secrets/ks_pass)" \
|
||||||
|
ANDROID_KEY_ALIAS="$(cat /run/secrets/ks_alias)" \
|
||||||
|
ANDROID_KEY_PASSWORD="$(cat /run/secrets/ks_keypass)" \
|
||||||
|
bash scripts/ci-android-sign.sh \
|
||||||
|
&& cd android \
|
||||||
|
&& ./gradlew assembleRelease --no-daemon -x lint --stacktrace
|
||||||
|
|
||||||
|
# The signed APK (handy for `docker create` + `docker cp` extraction in CI).
|
||||||
|
# /workspace/android/app/build/outputs/apk/release/app-release.apk
|
||||||
|
|||||||
Reference in New Issue
Block a user