diff --git a/.env.example b/.env.example index 22939c3..52f1522 100644 --- a/.env.example +++ b/.env.example @@ -66,7 +66,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" -# Laravel Reverb (WebSockets). Browser uses VITE_*; server publish uses REVERB_HOST. +# Laravel Reverb (WebSockets). Browser uses Blade meta (reverb.client) with VITE_* fallback. REVERB_APP_ID=andytranscribe REVERB_APP_KEY=andytranscribe-key REVERB_APP_SECRET=andytranscribe-secret @@ -75,11 +75,19 @@ REVERB_PORT=8080 REVERB_SCHEME=http REVERB_SERVER_HOST=0.0.0.0 REVERB_SERVER_PORT=8080 +# Public Echo target (z00 stage/prod set these in the instance .env). +# REVERB_PUBLIC_HOST=reverb.transcribe.z00.nu +# REVERB_PUBLIC_PORT=443 +# REVERB_PUBLIC_SCHEME=https # Host ports for docker compose (FrankenPHP app, Reverb WS, Whisper) APP_HOST_PORT=8080 REVERB_HOST_PORT=8081 WHISPER_HOST_PORT=8090 +# Unique per Compose instance on a shared Docker host (prod default keeps existing names). +# CONTAINER_PREFIX=andytranscribe +# CADDY_NETWORK=caddy-proxy-manager-test_caddy-test-network +# PUBLIC_APP_URL=https://transcribe.z00.nu VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_HOST=localhost diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 994533f..a62eb15 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -4,12 +4,13 @@ on: push: branches: - main + - stage pull_request: workflow_dispatch: env: REGISTRY: gitea.z00.nu - # Baked into the Vite client bundle for production WebSockets. + # Fallback baked into the Vite bundle; public pages prefer Blade meta tags. VITE_REVERB_HOST: reverb.transcribe.z00.nu VITE_REVERB_PORT: "443" VITE_REVERB_SCHEME: https @@ -47,6 +48,18 @@ jobs: APP_KEY="base64:dGVzdC1hcHAta2V5LWZvci1jaS1jb21wb3NlLXZhbGlkYXRpb24=" \ docker compose -f docker-compose.yml -f compose.z00.yaml config --quiet + - name: Validate stage compose + run: | + set -euo pipefail + APP_IMAGE="${REGISTRY}/$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]'):test" \ + APP_KEY="base64:dGVzdC1hcHAta2V5LWZvci1jaS1jb21wb3NlLXZhbGlkYXRpb24=" \ + PUBLIC_APP_URL="https://stage.transcribe.z00.nu" \ + APP_URL="https://stage.transcribe.z00.nu" \ + REVERB_PUBLIC_HOST="reverb.stage.transcribe.z00.nu" \ + CONTAINER_PREFIX="andytranscribe-stage" \ + CADDY_NETWORK="andytranscribe-stage-caddy" \ + docker compose -f docker-compose.yml -f compose.z00.yaml config --quiet + build-and-push: if: gitea.event_name != 'pull_request' needs: test @@ -70,6 +83,7 @@ jobs: REPO_LC="$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')" TAG_SHA="${REGISTRY}/${REPO_LC}:${{ gitea.sha }}" TAG_LATEST="${REGISTRY}/${REPO_LC}:latest" + TAG_STAGE="${REGISTRY}/${REPO_LC}:stage" echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${REGISTRY}" -u "${{ gitea.actor }}" --password-stdin DOCKER_BUILDKIT=1 docker build \ --build-arg VITE_APP_NAME=AndyTranscribe \ @@ -78,10 +92,15 @@ jobs: --build-arg "VITE_REVERB_PORT=${VITE_REVERB_PORT}" \ --build-arg "VITE_REVERB_SCHEME=${VITE_REVERB_SCHEME}" \ -t "${TAG_SHA}" \ - -t "${TAG_LATEST}" \ . docker push "${TAG_SHA}" - docker push "${TAG_LATEST}" + if [ "${{ gitea.ref }}" = "refs/heads/main" ]; then + docker tag "${TAG_SHA}" "${TAG_LATEST}" + docker push "${TAG_LATEST}" + elif [ "${{ gitea.ref }}" = "refs/heads/stage" ]; then + docker tag "${TAG_SHA}" "${TAG_STAGE}" + docker push "${TAG_STAGE}" + fi deploy: if: gitea.ref == 'refs/heads/main' && gitea.event_name != 'pull_request' @@ -115,3 +134,36 @@ jobs: exit 0 fi ./scripts/deploy-production.sh + + deploy-stage: + if: gitea.ref == 'refs/heads/stage' && gitea.event_name != 'pull_request' + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + set -euo pipefail + HOST="${{ gitea.server_url }}" + HOST="${HOST#https://}" + HOST="${HOST#http://}" + git clone --depth 1 \ + "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@${HOST}/${{ gitea.repository }}.git" \ + . + git fetch --depth 1 origin "${{ gitea.sha }}" + git checkout --force "${{ gitea.sha }}" + + - name: Deploy stage + env: + DEPLOY_PATHS: ${{ secrets.STAGE_DEPLOY_PATHS }} + DEPLOY_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + REPO_LC="$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')" + export APP_IMAGE="${REGISTRY}/${REPO_LC}:${{ gitea.sha }}" + chmod +x scripts/deploy-production.sh + if [ -z "${DEPLOY_PATHS:-}" ]; then + echo "STAGE_DEPLOY_PATHS secret is not set; skipping deploy." + echo "Built image: ${APP_IMAGE}" + exit 0 + fi + ./scripts/deploy-production.sh diff --git a/README.md b/README.md index f16d388..22d598e 100644 --- a/README.md +++ b/README.md @@ -157,24 +157,47 @@ Open [http://localhost:8080](http://localhost:8080) as usual. After changing Com ## CI/CD (Gitea Actions) -On push to `main`, Gitea Actions (host runner on z00): +Gitea Actions (host runner on z00) on push: -1. Runs PHPUnit (+ compose config check) -2. Builds and pushes `gitea.z00.nu/ben/andytranscribe:` (+ `:latest`) — Vite assets are baked in the image build -3. Deploys by hard-resetting `~/andyTranscibe` to that SHA and pulling the image (`docker-compose.yml` + `compose.z00.yaml`) +| Branch | Image tags | Deploy target | +| --- | --- | --- | +| `main` | `` and `:latest` | `~/andyTranscibe` → https://transcribe.z00.nu | +| `stage` | `` and `:stage` | `~/andyTranscibe-stage` → https://stage.transcribe.z00.nu | -Do not hot-patch production containers or the deploy checkout. Fix in git and push to `main` so CI deploys. +Both runs: + +1. PHPUnit (+ compose config check for prod and stage env) +2. Build and push `gitea.z00.nu/ben/andytranscribe:` — Vite assets are baked in; browser Reverb host/key come from Blade meta at runtime +3. Hard-reset the matching checkout and pull the image (`docker-compose.yml` + `compose.z00.yaml`) + +Stage is a separate Compose project: own SQLite, uploads, `APP_KEY`, Reverb credentials, container names (`andytranscribe-stage-*`), host ports, and Caddy Docker network (`andytranscribe-stage-caddy`). Do not copy prod `.env` or data into the stage directory. + +Do not hot-patch production or staging containers or their deploy checkouts. Fix in git; push `stage` to preview, then `main` to ship. One-time server bootstrap (secrets + registry login): ```bash ./scripts/setup-gitea-ci.sh +./scripts/setup-stage.sh +``` + +`setup-stage.sh` clones `$HOME/andyTranscibe-stage`, writes a unique `.env`, creates `andytranscribe-stage-caddy`, and attaches Caddy Proxy Manager to that network. You still need DNS for `stage.transcribe.z00.nu` and `reverb.stage.transcribe.z00.nu`, plus Proxy Manager hosts: + +- `stage.transcribe.z00.nu` → `andytranscribe-stage-app:80` +- `reverb.stage.transcribe.z00.nu` → `andytranscribe-stage-reverb:8080` + +Create and push the branch after the workflow file is on the default branch: + +```bash +git checkout -b stage +git push -u origin stage ``` Manual deploy of an already-built tag: ```bash APP_IMAGE=gitea.z00.nu/ben/andytranscribe: DEPLOY_PATHS=$HOME/andyTranscibe ./scripts/deploy-production.sh +APP_IMAGE=gitea.z00.nu/ben/andytranscribe: DEPLOY_PATHS=$HOME/andyTranscibe-stage ./scripts/deploy-production.sh ``` ## Services and ports @@ -209,17 +232,17 @@ Edit `.env` before `docker compose up` when you need different ports or models: | `APP_HOST_PORT` | Host port for the web app | `8080` | | `REVERB_HOST_PORT` | Host port for WebSockets | `8081` | | `WHISPER_HOST_PORT` | Host port for Whisper | `8090` | +| `REVERB_PUBLIC_HOST` | Browser WebSocket host (Blade meta) | `REVERB_HOST` | +| `CONTAINER_PREFIX` | Docker `container_name` prefix | `andytranscribe` | +| `CADDY_NETWORK` | External Caddy network (z00 overlay) | `caddy-proxy-manager-test_caddy-test-network` | +| `PUBLIC_APP_URL` | Public URL injected by the z00 overlay (unset = prod) | `https://transcribe.z00.nu` | | `LOCAL_WHISPER_MODEL` | Whisper model id | `Systran/faster-whisper-base` | | `TRANSCRIPTION_TIMEOUT` | Job/HTTP timeout (seconds). Hung Whisper calls fail the job; UI can restart. | `600` | | `DB_QUEUE_RETRY_AFTER` | Must exceed `TRANSCRIPTION_TIMEOUT` | `660` | -Inside Compose, Laravel talks to Whisper at `http://whisper:8000/v1` and publishes broadcasts to the `reverb` service. The browser connects to Reverb on `localhost:8081`. +Inside Compose, Laravel talks to Whisper at `http://whisper:8000/v1` and publishes broadcasts to the `reverb` service. The browser connects using Reverb settings from the HTML meta tags (with Vite `VITE_REVERB_*` as a local fallback). -If you change `REVERB_APP_KEY` or browser-facing Reverb host/port settings, rebuild so Vite embeds the new values: - -```bash -docker compose up --build -d -``` +If you change `REVERB_APP_KEY` or browser-facing Reverb host/port settings, restart the app container so Blade picks up the new values. An image rebuild is only needed when Vite-baked fallbacks must change. ### GPU Whisper (optional) diff --git a/compose.z00.yaml b/compose.z00.yaml index 02344ca..e0dac1e 100644 --- a/compose.z00.yaml +++ b/compose.z00.yaml @@ -1,4 +1,5 @@ -# z00 production overlay for AndyTranscribe (behind Caddy Proxy Manager) +# z00 overlay for AndyTranscribe (behind Caddy Proxy Manager). +# Instance .env supplies PUBLIC_APP_URL, REVERB_PUBLIC_HOST, CADDY_NETWORK, CONTAINER_PREFIX. services: app: networks: @@ -7,30 +8,39 @@ services: environment: APP_ENV: production APP_DEBUG: "false" - APP_URL: https://transcribe.z00.nu + APP_URL: ${PUBLIC_APP_URL:-https://transcribe.z00.nu} TRUSTED_PROXIES: "*" REVERB_HOST: reverb REVERB_PORT: "8080" REVERB_SCHEME: http + REVERB_PUBLIC_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu} + REVERB_PUBLIC_PORT: "443" + REVERB_PUBLIC_SCHEME: https reverb: networks: - default - caddy environment: - APP_URL: https://transcribe.z00.nu - REVERB_HOST: reverb.transcribe.z00.nu + APP_URL: ${PUBLIC_APP_URL:-https://transcribe.z00.nu} + REVERB_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu} REVERB_PORT: "443" REVERB_SCHEME: https + REVERB_PUBLIC_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu} + REVERB_PUBLIC_PORT: "443" + REVERB_PUBLIC_SCHEME: https queue: networks: - default environment: APP_ENV: production APP_DEBUG: "false" - APP_URL: https://transcribe.z00.nu + APP_URL: ${PUBLIC_APP_URL:-https://transcribe.z00.nu} REVERB_HOST: reverb REVERB_PORT: "8080" REVERB_SCHEME: http + REVERB_PUBLIC_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu} + REVERB_PUBLIC_PORT: "443" + REVERB_PUBLIC_SCHEME: https whisper: networks: - default @@ -38,4 +48,4 @@ services: networks: caddy: external: true - name: caddy-proxy-manager-test_caddy-test-network + name: ${CADDY_NETWORK:-caddy-proxy-manager-test_caddy-test-network} diff --git a/config/reverb.php b/config/reverb.php index 91f3880..3e4d4da 100644 --- a/config/reverb.php +++ b/config/reverb.php @@ -15,6 +15,24 @@ return [ 'default' => env('REVERB_SERVER', 'reverb'), + /* + |-------------------------------------------------------------------------- + | Browser (Echo) connection + |-------------------------------------------------------------------------- + | + | Public WebSocket host/key rendered into HTML so one image can serve + | production and staging. Server-side publish still uses REVERB_HOST + | (Docker DNS "reverb" behind the z00 overlay). + | + */ + + 'client' => [ + 'key' => env('REVERB_APP_KEY'), + 'host' => env('REVERB_PUBLIC_HOST', env('REVERB_HOST')), + 'port' => env('REVERB_PUBLIC_PORT', env('REVERB_PORT', 443)), + 'scheme' => env('REVERB_PUBLIC_SCHEME', env('REVERB_SCHEME', 'https')), + ], + /* |-------------------------------------------------------------------------- | Reverb Servers diff --git a/docker-compose.yml b/docker-compose.yml index 6057588..4e6657d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,10 @@ x-app-env: &app-env REVERB_SCHEME: http REVERB_SERVER_HOST: 0.0.0.0 REVERB_SERVER_PORT: 8080 + # Browser-facing Reverb (Blade meta). Overlay sets public host for z00. + REVERB_PUBLIC_HOST: ${REVERB_PUBLIC_HOST:-localhost} + REVERB_PUBLIC_PORT: ${REVERB_PUBLIC_PORT:-8081} + REVERB_PUBLIC_SCHEME: ${REVERB_PUBLIC_SCHEME:-http} LOCAL_WHISPER_URL: http://whisper:8000/v1 LOCAL_WHISPER_API_KEY: ${LOCAL_WHISPER_API_KEY:-not-needed} LOCAL_WHISPER_MODEL: ${LOCAL_WHISPER_MODEL:-Systran/faster-whisper-base} @@ -56,7 +60,7 @@ services: VITE_REVERB_HOST: ${VITE_REVERB_HOST:-localhost} VITE_REVERB_PORT: ${REVERB_HOST_PORT:-8081} VITE_REVERB_SCHEME: ${VITE_REVERB_SCHEME:-http} - container_name: andytranscribe-app + container_name: ${CONTAINER_PREFIX:-andytranscribe}-app ports: - "${APP_HOST_PORT:-8080}:80" environment: @@ -81,7 +85,7 @@ services: # `docker compose up --build` builds `app` first, then starts these with the tagged image. queue: <<: *app-image - container_name: andytranscribe-queue + container_name: ${CONTAINER_PREFIX:-andytranscribe}-queue command: - php - artisan @@ -108,7 +112,7 @@ services: reverb: <<: *app-image - container_name: andytranscribe-reverb + container_name: ${CONTAINER_PREFIX:-andytranscribe}-reverb command: - php - artisan @@ -132,7 +136,7 @@ services: whisper: image: fedirz/faster-whisper-server:latest-cpu - container_name: andytranscribe-whisper + container_name: ${CONTAINER_PREFIX:-andytranscribe}-whisper ports: # Host 8090 avoids clashing with the FrankenPHP app on 8080 - "${WHISPER_HOST_PORT:-8090}:8000" @@ -153,7 +157,7 @@ services: whisper-gpu: profiles: ["gpu"] image: fedirz/faster-whisper-server:latest-cuda - container_name: andytranscribe-whisper-gpu + container_name: ${CONTAINER_PREFIX:-andytranscribe}-whisper-gpu ports: - "${WHISPER_HOST_PORT:-8090}:8000" volumes: diff --git a/phpunit.xml b/phpunit.xml index e7f0a48..628b887 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -32,5 +32,9 @@ + + + + diff --git a/resources/js/echo.js b/resources/js/echo.js index 65b8ad9..c749ac0 100644 --- a/resources/js/echo.js +++ b/resources/js/echo.js @@ -3,13 +3,36 @@ import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; +function metaContent(name) { + return document.querySelector(`meta[name="${name}"]`)?.getAttribute('content')?.trim() || ''; +} + +const isLocalHost = ['localhost', '127.0.0.1'].includes(window.location.hostname); +const internalHosts = new Set(['', 'reverb', 'localhost', '127.0.0.1']); + +const key = metaContent('reverb-key') || import.meta.env.VITE_REVERB_APP_KEY; + +let host = metaContent('reverb-host') || import.meta.env.VITE_REVERB_HOST || ''; +let port = metaContent('reverb-port') || import.meta.env.VITE_REVERB_PORT || ''; +let scheme = metaContent('reverb-scheme') || import.meta.env.VITE_REVERB_SCHEME || 'https'; + +if (!isLocalHost && internalHosts.has(host)) { + host = `reverb.${window.location.hostname}`; + port = '443'; + scheme = 'https'; +} else if (isLocalHost && host === 'reverb') { + host = import.meta.env.VITE_REVERB_HOST || 'localhost'; + port = import.meta.env.VITE_REVERB_PORT || '8081'; + scheme = import.meta.env.VITE_REVERB_SCHEME || 'http'; +} + window.Echo = new Echo({ broadcaster: 'reverb', - key: import.meta.env.VITE_REVERB_APP_KEY, - wsHost: import.meta.env.VITE_REVERB_HOST, - wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, - wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, - forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', + key, + wsHost: host, + wsPort: port || 80, + wssPort: port || 443, + forceTLS: scheme === 'https', enabledTransports: ['ws', 'wss'], authEndpoint: '/broadcasting/auth', auth: { diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index 456e5a1..b97d181 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -1,6 +1,10 @@ + + + + {{ filled($title ?? null) ? $title.' — '.config('app.name', 'AndyTranscribe') : config('app.name', 'AndyTranscribe') }} diff --git a/scripts/setup-gitea-ci.sh b/scripts/setup-gitea-ci.sh index 1d8ded0..e1cf65e 100755 --- a/scripts/setup-gitea-ci.sh +++ b/scripts/setup-gitea-ci.sh @@ -8,6 +8,7 @@ REPO_OWNER="${REPO_OWNER:-ben}" REPO_NAME="${REPO_NAME:-AndyTranscribe}" REGISTRY_HOST="${REGISTRY_HOST:-gitea.z00.nu}" DEPLOY_PATH="${DEPLOY_PATH:-${HOME}/andyTranscibe}" +STAGE_DEPLOY_PATH="${STAGE_DEPLOY_PATH:-${HOME}/andyTranscibe-stage}" CREDENTIALS_FILE="${GITEA_DIR}/.credentials" if [[ ! -f "${CREDENTIALS_FILE}" ]]; then @@ -57,6 +58,7 @@ set_secret() { set_secret "REGISTRY_TOKEN" "${CI_TOKEN}" set_secret "DEPLOY_PATHS" "${DEPLOY_PATH}" +set_secret "STAGE_DEPLOY_PATHS" "${STAGE_DEPLOY_PATH}" printf '%s' "${PULL_TOKEN}" | docker login "${REGISTRY_HOST}" -u "${ADMIN_USERNAME}" --password-stdin @@ -77,4 +79,6 @@ fi echo "Gitea CI secrets configured for ${REGISTRY_HOST}/${REPO_OWNER}/${REPO_NAME}" echo "Deploy path: ${DEPLOY_PATH}" -echo "Push to main to build, push the image, and deploy." +echo "Stage deploy path: ${STAGE_DEPLOY_PATH}" +echo "Push to main to deploy production; push to stage to deploy https://stage.transcribe.z00.nu" +echo "First-time stage instance: ./scripts/setup-stage.sh" diff --git a/scripts/setup-stage.sh b/scripts/setup-stage.sh new file mode 100755 index 0000000..bcbfca2 --- /dev/null +++ b/scripts/setup-stage.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# One-time bootstrap for the AndyTranscribe staging instance on z00. +# Creates ~/andyTranscibe-stage, a unique .env, the stage Caddy Docker network, +# and the Gitea STAGE_DEPLOY_PATHS secret. +set -euo pipefail + +GITEA_DIR="${GITEA_DIR:-${HOME}/gitea}" +REPO_OWNER="${REPO_OWNER:-ben}" +REPO_NAME="${REPO_NAME:-AndyTranscribe}" +REGISTRY_HOST="${REGISTRY_HOST:-gitea.z00.nu}" +PROD_PATH="${PROD_PATH:-${HOME}/andyTranscibe}" +STAGE_PATH="${STAGE_PATH:-${HOME}/andyTranscibe-stage}" +STAGE_URL="${STAGE_URL:-https://stage.transcribe.z00.nu}" +REVERB_PUBLIC_HOST="${REVERB_PUBLIC_HOST:-reverb.stage.transcribe.z00.nu}" +CONTAINER_PREFIX="${CONTAINER_PREFIX:-andytranscribe-stage}" +CADDY_NETWORK="${CADDY_NETWORK:-andytranscribe-stage-caddy}" +PROD_CADDY_NETWORK="${PROD_CADDY_NETWORK:-caddy-proxy-manager-test_caddy-test-network}" +CADDY_CONTAINER="${CADDY_CONTAINER:-}" +CREDENTIALS_FILE="${GITEA_DIR}/.credentials" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +upsert_env() { + local file="$1" + local key="$2" + local value="$3" + python3 - "$file" "$key" "$value" <<'PY' +import re +import sys + +path, key, value = sys.argv[1], sys.argv[2], sys.argv[3] +text = open(path).read() +pattern = re.compile(r"^" + re.escape(key) + r"=.*$", re.M) +replacement = f"{key}={value}" +if pattern.search(text): + text = pattern.sub(replacement, text, count=1) +else: + text = text.rstrip("\n") + "\n" + replacement + "\n" +open(path, "w").write(text) +PY +} + +if [[ ! -f "${CREDENTIALS_FILE}" ]]; then + echo "Missing ${CREDENTIALS_FILE}" >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +source "${CREDENTIALS_FILE}" + +API="https://${REGISTRY_HOST}/api/v1" +AUTH=(-u "${ADMIN_USERNAME}:${ADMIN_PASSWORD}") + +if ! curl -fsS "${AUTH[@]}" "${API}/repos/${REPO_OWNER}/${REPO_NAME}" >/dev/null 2>&1; then + echo "Repository ${REPO_OWNER}/${REPO_NAME} not found on ${REGISTRY_HOST}" >&2 + exit 1 +fi + +if [[ ! -d "${STAGE_PATH}/.git" ]]; then + mkdir -p "$(dirname "${STAGE_PATH}")" + if [[ -d "${PROD_PATH}/.git" ]]; then + git clone "${PROD_PATH}" "${STAGE_PATH}" + else + git clone "https://${REGISTRY_HOST}/${REPO_OWNER}/${REPO_NAME}.git" "${STAGE_PATH}" + fi +fi + +( + cd "${STAGE_PATH}" + git fetch origin || true + if git rev-parse --verify origin/stage >/dev/null 2>&1; then + git checkout stage + git pull --ff-only origin stage || true + else + echo "Remote branch origin/stage not found yet; leaving $(git branch --show-current)." + fi +) + +mkdir -p \ + "${STAGE_PATH}/database" \ + "${STAGE_PATH}/storage/app/private/recordings" \ + "${STAGE_PATH}/storage/app/public" \ + "${STAGE_PATH}/storage/framework/cache" \ + "${STAGE_PATH}/storage/framework/sessions" \ + "${STAGE_PATH}/storage/framework/views" \ + "${STAGE_PATH}/storage/logs" \ + "${STAGE_PATH}/bootstrap/cache" + +if [[ ! -f "${STAGE_PATH}/database/database.sqlite" ]]; then + touch "${STAGE_PATH}/database/database.sqlite" +fi + +ENV_SRC="${STAGE_PATH}/.env.example" +if [[ ! -f "${ENV_SRC}" ]]; then + ENV_SRC="${REPO_ROOT}/.env.example" +fi + +if [[ ! -f "${STAGE_PATH}/.env" ]]; then + cp "${ENV_SRC}" "${STAGE_PATH}/.env" + upsert_env "${STAGE_PATH}/.env" "APP_ENV" "production" + upsert_env "${STAGE_PATH}/.env" "APP_DEBUG" "false" + upsert_env "${STAGE_PATH}/.env" "APP_KEY" "base64:$(openssl rand -base64 32)" + upsert_env "${STAGE_PATH}/.env" "APP_URL" "${STAGE_URL}" + upsert_env "${STAGE_PATH}/.env" "PUBLIC_APP_URL" "${STAGE_URL}" + upsert_env "${STAGE_PATH}/.env" "APP_HOST_PORT" "18180" + upsert_env "${STAGE_PATH}/.env" "REVERB_HOST_PORT" "18181" + upsert_env "${STAGE_PATH}/.env" "WHISPER_HOST_PORT" "18190" + upsert_env "${STAGE_PATH}/.env" "REVERB_APP_ID" "$(openssl rand -hex 8)" + upsert_env "${STAGE_PATH}/.env" "REVERB_APP_KEY" "$(openssl rand -hex 16)" + upsert_env "${STAGE_PATH}/.env" "REVERB_APP_SECRET" "$(openssl rand -hex 20)" + upsert_env "${STAGE_PATH}/.env" "REVERB_PUBLIC_HOST" "${REVERB_PUBLIC_HOST}" + upsert_env "${STAGE_PATH}/.env" "REVERB_PUBLIC_PORT" "443" + upsert_env "${STAGE_PATH}/.env" "REVERB_PUBLIC_SCHEME" "https" + upsert_env "${STAGE_PATH}/.env" "CONTAINER_PREFIX" "${CONTAINER_PREFIX}" + upsert_env "${STAGE_PATH}/.env" "CADDY_NETWORK" "${CADDY_NETWORK}" + echo "Wrote ${STAGE_PATH}/.env" +else + echo "Keeping existing ${STAGE_PATH}/.env" +fi + +if ! docker network inspect "${CADDY_NETWORK}" >/dev/null 2>&1; then + docker network create "${CADDY_NETWORK}" + echo "Created Docker network ${CADDY_NETWORK}" +fi + +if [[ -z "${CADDY_CONTAINER}" ]]; then + CADDY_CONTAINER="$( + docker network inspect "${PROD_CADDY_NETWORK}" \ + --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}' \ + | grep -i caddy \ + | head -1 || true + )" +fi + +if [[ -z "${CADDY_CONTAINER}" ]]; then + echo "Could not find a Caddy container on ${PROD_CADDY_NETWORK}." >&2 + echo "Set CADDY_CONTAINER and re-run: docker network connect ${CADDY_NETWORK} <caddy-container>" >&2 + exit 1 +fi + +if docker network inspect "${CADDY_NETWORK}" --format '{{range .Containers}}{{.Name}}{{"\n"}}{{end}}' | grep -qx "${CADDY_CONTAINER}"; then + echo "Caddy container ${CADDY_CONTAINER} already on ${CADDY_NETWORK}" +else + docker network connect "${CADDY_NETWORK}" "${CADDY_CONTAINER}" + echo "Attached ${CADDY_CONTAINER} to ${CADDY_NETWORK}" +fi + +set_secret() { + local name="$1" + local value="$2" + local tmp + tmp="$(mktemp)" + python3 -c 'import json,sys; json.dump({"data": sys.argv[1]}, open(sys.argv[2], "w"))' "${value}" "${tmp}" + curl -fsS "${AUTH[@]}" -X PUT \ + "${API}/repos/${REPO_OWNER}/${REPO_NAME}/actions/secrets/${name}" \ + -H "Content-Type: application/json" \ + --data-binary @"${tmp}" >/dev/null + rm -f "${tmp}" +} + +set_secret "STAGE_DEPLOY_PATHS" "${STAGE_PATH}" + +echo "Stage instance ready at ${STAGE_PATH}" +echo "Public URL: ${STAGE_URL}" +echo "Reverb host: ${REVERB_PUBLIC_HOST}" +echo "Caddy network: ${CADDY_NETWORK} (container ${CADDY_CONTAINER})" +echo "Gitea secret STAGE_DEPLOY_PATHS=${STAGE_PATH}" +echo +echo "Still required: DNS for stage.transcribe.z00.nu and ${REVERB_PUBLIC_HOST}," +echo "plus Caddy Proxy Manager hosts pointing at ${CONTAINER_PREFIX}-app:80" +echo "and ${CONTAINER_PREFIX}-reverb:8080." +echo "Then: git checkout -b stage && git push -u origin stage" diff --git a/tests/Feature/ReverbClientConfigTest.php b/tests/Feature/ReverbClientConfigTest.php new file mode 100644 index 0000000..5896627 --- /dev/null +++ b/tests/Feature/ReverbClientConfigTest.php @@ -0,0 +1,33 @@ +<?php + +namespace Tests\Feature; + +use Tests\TestCase; + +class ReverbClientConfigTest extends TestCase +{ + public function test_login_page_exposes_environment_reverb_client_settings(): void + { + $this->get(route('login')) + ->assertOk() + ->assertSee('<meta name="reverb-key" content="testing-reverb-key">', false) + ->assertSee('<meta name="reverb-host" content="reverb.testing.example">', false) + ->assertSee('<meta name="reverb-port" content="443">', false) + ->assertSee('<meta name="reverb-scheme" content="https">', false); + } + + public function test_login_page_reflects_runtime_reverb_client_config(): void + { + config([ + 'reverb.client.key' => 'stage-reverb-key', + 'reverb.client.host' => 'reverb.stage.transcribe.z00.nu', + 'reverb.client.port' => '443', + 'reverb.client.scheme' => 'https', + ]); + + $this->get(route('login')) + ->assertOk() + ->assertSee('<meta name="reverb-key" content="stage-reverb-key">', false) + ->assertSee('<meta name="reverb-host" content="reverb.stage.transcribe.z00.nu">', false); + } +}