Files
wisp/.gitea/workflows/ci.yml
T
NilsBriggen e256508c44
CI / test (push) Successful in 23s
CI / deploy-web (push) Successful in 30s
CI / build-apk (push) Successful in 27m38s
APK build: persistent ccache + Gradle cache + export-only target
The Android CI job recompiled all native C++ (whisper.cpp, reanimated,
worklets, gesture-handler) from scratch every push — ~41 min on the
shared runner — because nothing survived the `COPY . .` layer
invalidation, and `--load` exported the whole ~15GB image just to copy
one APK out.

Dockerfile (docker/android.Dockerfile):
- Install ccache and patch the NDK CMake toolchain file so every module's
  externalNativeBuild routes C/C++ compiles + links through ccache (the
  third-party modules don't honor ccache on their own). ccache dir is a
  persistent BuildKit cache mount, so objects survive across builds.
- Mount GRADLE_USER_HOME as a cache and pass --build-cache --parallel, so
  Kotlin/Java/resource/dex tasks and resolved deps persist too.
- Cache-mount the bun install dir; move the nodejs install into the cached
  toolchain layer (was reinstalling on every source change).
- New `apk` stage (FROM scratch) holding just the signed APK, for
  --output type=local extraction without loading the 15GB image.

CI (.gitea/workflows/ci.yml):
- build-apk now builds `--target apk --output type=local,dest=./out` and
  streams ./out/app-release.apk to /srv/wisp/wisp.apk — no --load, no
  docker create/cp, and the build image no longer piles up on the host.

Validated locally (arm64, real sign+assemble path):
  cold gradle 5m5s -> warm gradle 57s; 61/61 C++ compiles served from
  ccache (100% hit); 226/578 Gradle tasks from cache; export-only build
  60s wall, APK valid + v2/v3 signed. The first server build after this
  is still cold (~41 min) to populate the caches; pushes after that are
  much faster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:09:34 +02:00

123 lines
5.6 KiB
YAML

# .gitea/workflows/ci.yml — Wisp CI/CD (Gitea Actions, GitHub-Actions syntax)
# ---------------------------------------------------------------------------
# Runner model: a single self-hosted act_runner ON the briggen.dev server, with
# the host Docker socket mounted into job containers (see /root/act_runner/
# config.yaml). Because the runner is co-located with the host Docker daemon +
# the Traefik `proxy` network, deploy needs NO ssh keys and NO image registry —
# `docker compose` simply builds and (re)creates the `wisp` container on the host.
#
# Triggers:
# push to master -> test, then deploy-web, then build-apk
# pull_request -> test only
#
# Required repo secrets (Settings -> Actions -> Secrets) — used by the build-apk
# job: ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS,
# ANDROID_KEY_PASSWORD.
# ---------------------------------------------------------------------------
name: CI
on:
push:
branches: [master]
tags: ['v*']
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# ---- test: typecheck + unit tests ----------------------------------------
# Runs on the default runner image (has Node, which actions/checkout needs);
# Bun is installed in a step rather than via a Node-less bun container.
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Bun
run: |
curl -fsSL https://bun.sh/install | bash
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
- run: bun install --frozen-lockfile
- run: bun run typecheck
- run: bun run test
# ---- deploy-web: build + (re)start the nginx web container on the host ----
# Runs on the server runner; the mounted host Docker socket means `docker
# compose` builds the image and recreates the `wisp` service in place, joined
# to the existing Traefik `proxy` network. No SSH, no registry.
deploy-web:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build + (re)start wisp on the host
run: docker compose -f deploy/wisp.compose.yml --project-directory . up -d --build --remove-orphans
- name: Health check
run: |
for i in $(seq 1 10); do
if docker exec wisp wget -qO- http://localhost:8080/healthz | grep -q ok; then
echo "healthy"; exit 0
fi
sleep 2
done
echo "wisp container did not become healthy" >&2; exit 1
# ---- build-apk: signed release APK, served at /wisp.apk ------------------
# Socket-safe: the whole Android build runs inside `docker buildx build`
# (context streamed to the host BuildKit — no host bind-mounts), signed via
# BuildKit --secret from the ANDROID_* repo secrets. The `apk` target +
# `--output type=local` write just the APK to ./out (no image load), which we
# stream into /srv/wisp/wisp.apk so it's downloadable at
# https://wisp.briggen.dev/wisp.apk. Speed comes from persistent BuildKit
# cache mounts (ccache for C++, Gradle build cache) declared in the Dockerfile.
build-apk:
# Auto-build the APK on every push to master (arm64-only keeps it ~minutes).
# Runs after deploy-web so the web updates promptly, then the APK follows and
# is published to /srv/wisp/wisp.apk (served at /wisp.apk via the compose mount).
needs: [test, deploy-web]
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
timeout-minutes: 120
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 + extract the signed APK (export-only target, no image load)
run: |
# `--target apk --output type=local` writes ONLY app-release.apk to
# ./out — no `--load` of the ~15GB build image (saves the multi-minute
# export/unpack each run and stops that image filling the host disk).
# The persistent ccache + Gradle build cache (BuildKit cache mounts in
# the Dockerfile) make C++ recompiles near-instant on warm runs.
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" \
--target apk --output type=local,dest=./out \
-f docker/android.Dockerfile .
ls -lh ./out/app-release.apk
- name: Publish APK to the host (served at /wisp.apk via the compose volume)
run: |
# Stream the APK into a host-mounted dir so it survives web container
# recreation (the wisp container mounts /srv/wisp/wisp.apk read-only).
docker run --rm -i -v /srv/wisp:/out alpine:3 sh -c 'mkdir -p /out && cat > /out/wisp.apk' < ./out/app-release.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: ./out/app-release.apk