CI: Gitea Actions web auto-deploy (runner-on-server) + signing prep
CI / test (push) Failing after 3s
CI / deploy-web (push) Has been skipped

- .gitea/workflows/ci.yml rewritten for a self-hosted runner co-located with
  the host Docker daemon + Traefik: push to master -> test -> deploy-web
  (docker compose up --build in place; no SSH, no registry). APK job documented
  as the next iteration (socket-safe build+extract; signing secrets already set).
- .gitignore: never commit the release keystore (*.keystore + wisp-release.keystore).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 18:27:26 +02:00
parent 9f42ee2460
commit 84e7a56b13
2 changed files with 43 additions and 187 deletions
+41 -187
View File
@@ -1,28 +1,18 @@
# .gitea/workflows/ci.yml # .gitea/workflows/ci.yml — Wisp CI/CD (Gitea Actions, GitHub-Actions syntax)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Wisp CI/CD for Gitea Actions (GitHub-Actions-compatible 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: # Triggers:
# - push to master -> test, then (on success) deploy-web + build-apk # push to master -> test, then deploy-web
# - pull_request -> test only (no deploys) # pull_request -> test only
# #
# Jobs: # Required repo secrets (Settings -> Actions -> Secrets) — used by the APK job
# test Lint-gate: typecheck + vitest in the Bun container. # once it is enabled (see TODO at the bottom): ANDROID_KEYSTORE_BASE64,
# deploy-web Build the nginx web image and ship it to briggen.dev WITHOUT a # ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD.
# registry (docker save | gzip | scp | docker load | compose up).
# build-apk Build the Android toolchain image, prebuild + sign + assemble a
# release APK, scp it to the server as /srv/wisp/web/wisp.apk, and
# upload it as a CI artifact.
#
# Required repo secrets (Settings -> Actions -> Secrets):
# SSH_HOST, SSH_USER, SSH_KEY (deploy + apk upload)
# ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD,
# ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD (apk signing)
# See docs/DEPLOY.md and scripts/gen-keystore.sh.
#
# Runner assumptions: a Gitea Actions runner with Docker available on the host
# (the deploy/apk jobs run docker build/save/load and ssh/scp). The `test` job
# runs inside the oven/bun:1 container.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
name: CI name: CI
@@ -32,189 +22,53 @@ on:
branches: [master] branches: [master]
pull_request: pull_request:
# Cancel superseded runs on the same ref to save runner time.
concurrency: concurrency:
group: ci-${{ github.ref }} group: ci-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# ========================================================================= # ---- test: typecheck + unit tests, in the Bun image ----------------------
# test — typecheck + unit tests. Gates both deploy jobs.
# =========================================================================
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Run the test job directly inside the Bun image so bun is preinstalled and
# the environment matches the web build's builder stage.
container: container:
image: oven/bun:1 image: oven/bun:1
steps: steps:
# Check out the repository at the triggering commit. - uses: actions/checkout@v4
- name: Checkout - run: bun install --frozen-lockfile
uses: actions/checkout@v4 - run: bun run typecheck
- run: bun run test
# Install dependencies exactly as locked (fails on lockfile drift). # ---- deploy-web: build + (re)start the nginx web container on the host ----
- name: Install dependencies # Runs on the server runner; the mounted host Docker socket means `docker
run: bun install --frozen-lockfile # compose` builds the image and recreates the `wisp` service in place, joined
# to the existing Traefik `proxy` network. No SSH, no registry.
# TypeScript strict typecheck (tsc --noEmit).
- name: Typecheck
run: bun run typecheck
# Vitest unit/property tests (vitest run).
- name: Test
run: bun run test
# =========================================================================
# deploy-web — build image, transfer registry-free, compose up on server.
# Runs only on push to master, after `test` passes.
# =========================================================================
deploy-web: deploy-web:
needs: test needs: test
# Guard: only deploy for pushes to master (never on PRs or other branches).
if: github.event_name == 'push' && github.ref == 'refs/heads/master' if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@v4
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
# Build the production web image on the runner. Build context is the repo - name: Health check
# root so the Dockerfile can install deps and run the Expo export.
- name: Build web image
run: docker build -f docker/web.Dockerfile -t wisp-web:latest .
# Make the server's host key known so the subsequent ssh/scp don't fail on
# host-key verification and we are not vulnerable to a blind MITM. We key
# off SSH_HOST; this writes the server's public host keys into known_hosts.
- name: Add server to known_hosts
run: | run: |
mkdir -p ~/.ssh for i in $(seq 1 10); do
chmod 700 ~/.ssh if docker exec wisp wget -qO- http://localhost:8080/healthz | grep -q ok; then
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts echo "healthy"; exit 0
chmod 644 ~/.ssh/known_hosts fi
sleep 2
# Install the deploy SSH private key from the secret into the agent. done
- name: Configure SSH key echo "wisp container did not become healthy" >&2; exit 1
run: |
install -m 600 /dev/null ~/.ssh/id_deploy
printf '%s\n' "${{ secrets.SSH_KEY }}" > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
# Registry-free image transfer: stream `docker save | gzip` straight over
# ssh to `gunzip | docker load` on the server. Avoids writing a big tarball
# to disk and avoids needing a Docker registry entirely.
- name: Ship image to server (save | gzip | ssh docker load)
run: |
docker save wisp-web:latest | gzip \
| ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
'gunzip | docker load'
# Recreate the service from the freshly loaded image. The compose file must
# already exist at /srv/wisp/docker-compose.yml (one-time server setup).
# `pull_policy: never` in the compose file keeps it from trying a registry pull.
- name: Restart service via docker compose
run: |
ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
'docker compose -f /srv/wisp/docker-compose.yml up -d --remove-orphans'
# Optional: prune dangling images left behind by repeated loads so the
# server disk doesn't fill up with old wisp-web layers.
- name: Prune dangling images on server
run: |
ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
'docker image prune -f'
# ========================================================================= # =========================================================================
# build-apk — reproducible signed release APK, shipped to server + artifact. # TODO (next iteration, once the runner is validated): build-apk.
# Runs only on push to master, after `test` passes. # The Android build is genuinely risky (whisper.rn C++/NDK on RN 0.85) and
# untested locally (no Android SDK on the dev box). It must be socket-safe:
# a FULL `docker build` of a build-stage in docker/android.Dockerfile (repo
# COPYed in, prebuild + gradlew assembleRelease, signing via BuildKit
# --secret using the ANDROID_* secrets), then extract the APK with
# `docker create`/`docker cp` (NO host bind-mounts — they don't resolve over
# the mounted socket) and `docker cp` it into the running `wisp` container at
# /usr/share/nginx/html/wisp.apk (served at https://wisp.briggen.dev/wisp.apk),
# plus upload it as a CI artifact. Keystore secrets are already configured.
# ========================================================================= # =========================================================================
build-apk:
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Build the Android toolchain image (JDK 17 + Android SDK + Bun). This is
# the reproducible environment described in docker/android.Dockerfile.
- name: Build Android builder image
run: docker build -f docker/android.Dockerfile -t wisp-android:latest .
# Run the full Android build INSIDE the toolchain container with the repo
# bind-mounted at /workspace. All secrets are passed via -e so they live
# only in the container's environment (not on the command line / git).
#
# Steps inside the container:
# 1. bun install (frozen) — JS deps for prebuild + autolinking.
# 2. expo prebuild --platform android --no-install — generate android/.
# 3. scripts/ci-android-sign.sh — decode keystore + patch build.gradle.
# 4. ./gradlew assembleRelease — produce the signed release APK.
#
# The bind mount means the generated android/ + APK are visible on the
# runner afterwards (for scp + artifact upload below).
- name: Build signed release APK
run: |
docker run --rm \
-v "${{ github.workspace }}:/workspace" \
-w /workspace \
-e ANDROID_KEYSTORE_BASE64="${{ secrets.ANDROID_KEYSTORE_BASE64 }}" \
-e ANDROID_KEYSTORE_PASSWORD="${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \
-e ANDROID_KEY_ALIAS="${{ secrets.ANDROID_KEY_ALIAS }}" \
-e ANDROID_KEY_PASSWORD="${{ secrets.ANDROID_KEY_PASSWORD }}" \
wisp-android:latest \
bash -lc '
set -euo pipefail
bun install --frozen-lockfile
bunx expo prebuild --platform android --no-install
bash scripts/ci-android-sign.sh
cd android
./gradlew assembleRelease --no-daemon
'
# Locate the assembled APK. The default Gradle output for the release variant
# is android/app/build/outputs/apk/release/app-release.apk. Capture the path
# for the next steps and fail loudly if it's missing.
- name: Locate APK
id: apk
run: |
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
if [[ ! -f "$APK_PATH" ]]; then
echo "APK not found at $APK_PATH" >&2
find android/app/build/outputs -name '*.apk' -print >&2 || true
exit 1
fi
echo "path=$APK_PATH" >> "$GITHUB_OUTPUT"
# Known_hosts + SSH key for the scp upload (same pattern as deploy-web).
- name: Add server to known_hosts
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Configure SSH key
run: |
install -m 600 /dev/null ~/.ssh/id_deploy
printf '%s\n' "${{ secrets.SSH_KEY }}" > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
# Copy the signed APK to the server where nginx serves it at /wisp.apk.
# docker-compose mounts /srv/wisp/web/wisp.apk into the container's web root.
# Ensure the target dir exists, then scp the APK into place.
- name: Upload APK to server (/srv/wisp/web/wisp.apk)
run: |
ssh -i ~/.ssh/id_deploy "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
'mkdir -p /srv/wisp/web'
scp -i ~/.ssh/id_deploy \
"${{ steps.apk.outputs.path }}" \
"${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/srv/wisp/web/wisp.apk"
# Also publish the APK as a CI artifact so it can be downloaded from the
# run's summary page even without server access.
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: wisp-release-apk
path: ${{ steps.apk.outputs.path }}
if-no-files-found: error
+2
View File
@@ -13,6 +13,8 @@ expo-env.d.ts
.kotlin/ .kotlin/
*.orig.* *.orig.*
*.jks *.jks
*.keystore
wisp-release.keystore
*.p8 *.p8
*.p12 *.p12
*.key *.key