108ac59cb1
The Library crammed five text nav-links into a non-wrapping row with no bottom navigation — unusable on phones. Restructure the primary screens into a bottom tab bar: - Add src/app/(tabs)/_layout.tsx: <Tabs> with 5 tabs (Library, Search, Study, Ask, Settings) with emoji icons and the accent active tint. - Move index/search/study/ask/settings into the (tabs) route group; the parens keep URLs unchanged (/, /search, /study, /ask, /settings). - Root _layout becomes a Stack hosting (tabs) (headerless) plus the secondary pushed screens (record, transcript/[id], courses, quiz, bibliography), each with a back-button header. - Drop the per-screen <Stack.Screen> headers from the moved tabs (titles now come from Tabs); relocate Study's "Export Anki" action into the body. - Library: remove the cramped header link-row and duplicate title; add a "nothing uploaded" subheader and a Courses link. Verified at 375px: tab bar pinned to the bottom, 5 even tabs, navigation between tabs works. tsc clean, web export OK, 279 tests pass. CI: build-apk now runs automatically on every push to master (after deploy-web), so the APK at /wisp.apk tracks master instead of being frozen at a stale tag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
5.2 KiB
YAML
118 lines
5.2 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. We then extract the APK
|
|
# with docker create/cp and copy it into the running `wisp` web container so
|
|
# it is downloadable at https://wisp.briggen.dev/wisp.apk.
|
|
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 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 APK + publish to the host (served at /wisp.apk via the compose volume)
|
|
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
|
|
# 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' < ./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: app-release.apk
|