CI: Gitea Actions web auto-deploy (runner-on-server) + signing prep
- .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:
+41
-187
@@ -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
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ expo-env.d.ts
|
|||||||
.kotlin/
|
.kotlin/
|
||||||
*.orig.*
|
*.orig.*
|
||||||
*.jks
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
wisp-release.keystore
|
||||||
*.p8
|
*.p8
|
||||||
*.p12
|
*.p12
|
||||||
*.key
|
*.key
|
||||||
|
|||||||
Reference in New Issue
Block a user