Add a stage branch deploy beside production on z00.
CI / test (push) Failing after 23s
CI / build-and-push (push) Skipped
CI / deploy (push) Skipped
CI / deploy-stage (push) Skipped

Pushing stage runs CI and deploys to stage.transcribe.z00.nu with its own data, Reverb credentials, and Caddy network.
This commit is contained in:
ben
2026-08-13 18:19:47 +02:00
parent c6856c04d5
commit 5e76bfdec1
12 changed files with 388 additions and 32 deletions
+9 -1
View File
@@ -66,7 +66,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" 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_ID=andytranscribe
REVERB_APP_KEY=andytranscribe-key REVERB_APP_KEY=andytranscribe-key
REVERB_APP_SECRET=andytranscribe-secret REVERB_APP_SECRET=andytranscribe-secret
@@ -75,11 +75,19 @@ REVERB_PORT=8080
REVERB_SCHEME=http REVERB_SCHEME=http
REVERB_SERVER_HOST=0.0.0.0 REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080 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) # Host ports for docker compose (FrankenPHP app, Reverb WS, Whisper)
APP_HOST_PORT=8080 APP_HOST_PORT=8080
REVERB_HOST_PORT=8081 REVERB_HOST_PORT=8081
WHISPER_HOST_PORT=8090 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_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST=localhost VITE_REVERB_HOST=localhost
+54 -2
View File
@@ -4,12 +4,13 @@ on:
push: push:
branches: branches:
- main - main
- stage
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
env: env:
REGISTRY: gitea.z00.nu 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_HOST: reverb.transcribe.z00.nu
VITE_REVERB_PORT: "443" VITE_REVERB_PORT: "443"
VITE_REVERB_SCHEME: https VITE_REVERB_SCHEME: https
@@ -47,6 +48,18 @@ jobs:
APP_KEY="base64:dGVzdC1hcHAta2V5LWZvci1jaS1jb21wb3NlLXZhbGlkYXRpb24=" \ APP_KEY="base64:dGVzdC1hcHAta2V5LWZvci1jaS1jb21wb3NlLXZhbGlkYXRpb24=" \
docker compose -f docker-compose.yml -f compose.z00.yaml config --quiet 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: build-and-push:
if: gitea.event_name != 'pull_request' if: gitea.event_name != 'pull_request'
needs: test needs: test
@@ -70,6 +83,7 @@ jobs:
REPO_LC="$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')" REPO_LC="$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')"
TAG_SHA="${REGISTRY}/${REPO_LC}:${{ gitea.sha }}" TAG_SHA="${REGISTRY}/${REPO_LC}:${{ gitea.sha }}"
TAG_LATEST="${REGISTRY}/${REPO_LC}:latest" TAG_LATEST="${REGISTRY}/${REPO_LC}:latest"
TAG_STAGE="${REGISTRY}/${REPO_LC}:stage"
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${REGISTRY}" -u "${{ gitea.actor }}" --password-stdin echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${REGISTRY}" -u "${{ gitea.actor }}" --password-stdin
DOCKER_BUILDKIT=1 docker build \ DOCKER_BUILDKIT=1 docker build \
--build-arg VITE_APP_NAME=AndyTranscribe \ --build-arg VITE_APP_NAME=AndyTranscribe \
@@ -78,10 +92,15 @@ jobs:
--build-arg "VITE_REVERB_PORT=${VITE_REVERB_PORT}" \ --build-arg "VITE_REVERB_PORT=${VITE_REVERB_PORT}" \
--build-arg "VITE_REVERB_SCHEME=${VITE_REVERB_SCHEME}" \ --build-arg "VITE_REVERB_SCHEME=${VITE_REVERB_SCHEME}" \
-t "${TAG_SHA}" \ -t "${TAG_SHA}" \
-t "${TAG_LATEST}" \
. .
docker push "${TAG_SHA}" docker push "${TAG_SHA}"
if [ "${{ gitea.ref }}" = "refs/heads/main" ]; then
docker tag "${TAG_SHA}" "${TAG_LATEST}"
docker push "${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: deploy:
if: gitea.ref == 'refs/heads/main' && gitea.event_name != 'pull_request' if: gitea.ref == 'refs/heads/main' && gitea.event_name != 'pull_request'
@@ -115,3 +134,36 @@ jobs:
exit 0 exit 0
fi fi
./scripts/deploy-production.sh ./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
+34 -11
View File
@@ -157,24 +157,47 @@ Open [http://localhost:8080](http://localhost:8080) as usual. After changing Com
## CI/CD (Gitea Actions) ## 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) | Branch | Image tags | Deploy target |
2. Builds and pushes `gitea.z00.nu/ben/andytranscribe:<sha>` (+ `: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`) | `main` | `<sha>` and `:latest` | `~/andyTranscibe` → https://transcribe.z00.nu |
| `stage` | `<sha>` 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:<sha>` — 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): One-time server bootstrap (secrets + registry login):
```bash ```bash
./scripts/setup-gitea-ci.sh ./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: Manual deploy of an already-built tag:
```bash ```bash
APP_IMAGE=gitea.z00.nu/ben/andytranscribe:<sha> DEPLOY_PATHS=$HOME/andyTranscibe ./scripts/deploy-production.sh APP_IMAGE=gitea.z00.nu/ben/andytranscribe:<sha> DEPLOY_PATHS=$HOME/andyTranscibe ./scripts/deploy-production.sh
APP_IMAGE=gitea.z00.nu/ben/andytranscribe:<sha> DEPLOY_PATHS=$HOME/andyTranscibe-stage ./scripts/deploy-production.sh
``` ```
## Services and ports ## 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` | | `APP_HOST_PORT` | Host port for the web app | `8080` |
| `REVERB_HOST_PORT` | Host port for WebSockets | `8081` | | `REVERB_HOST_PORT` | Host port for WebSockets | `8081` |
| `WHISPER_HOST_PORT` | Host port for Whisper | `8090` | | `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` | | `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` | | `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` | | `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: 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.
```bash
docker compose up --build -d
```
### GPU Whisper (optional) ### GPU Whisper (optional)
+16 -6
View File
@@ -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: services:
app: app:
networks: networks:
@@ -7,30 +8,39 @@ services:
environment: environment:
APP_ENV: production APP_ENV: production
APP_DEBUG: "false" APP_DEBUG: "false"
APP_URL: https://transcribe.z00.nu APP_URL: ${PUBLIC_APP_URL:-https://transcribe.z00.nu}
TRUSTED_PROXIES: "*" TRUSTED_PROXIES: "*"
REVERB_HOST: reverb REVERB_HOST: reverb
REVERB_PORT: "8080" REVERB_PORT: "8080"
REVERB_SCHEME: http REVERB_SCHEME: http
REVERB_PUBLIC_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu}
REVERB_PUBLIC_PORT: "443"
REVERB_PUBLIC_SCHEME: https
reverb: reverb:
networks: networks:
- default - default
- caddy - caddy
environment: environment:
APP_URL: https://transcribe.z00.nu APP_URL: ${PUBLIC_APP_URL:-https://transcribe.z00.nu}
REVERB_HOST: reverb.transcribe.z00.nu REVERB_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu}
REVERB_PORT: "443" REVERB_PORT: "443"
REVERB_SCHEME: https REVERB_SCHEME: https
REVERB_PUBLIC_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu}
REVERB_PUBLIC_PORT: "443"
REVERB_PUBLIC_SCHEME: https
queue: queue:
networks: networks:
- default - default
environment: environment:
APP_ENV: production APP_ENV: production
APP_DEBUG: "false" APP_DEBUG: "false"
APP_URL: https://transcribe.z00.nu APP_URL: ${PUBLIC_APP_URL:-https://transcribe.z00.nu}
REVERB_HOST: reverb REVERB_HOST: reverb
REVERB_PORT: "8080" REVERB_PORT: "8080"
REVERB_SCHEME: http REVERB_SCHEME: http
REVERB_PUBLIC_HOST: ${REVERB_PUBLIC_HOST:-reverb.transcribe.z00.nu}
REVERB_PUBLIC_PORT: "443"
REVERB_PUBLIC_SCHEME: https
whisper: whisper:
networks: networks:
- default - default
@@ -38,4 +48,4 @@ services:
networks: networks:
caddy: caddy:
external: true external: true
name: caddy-proxy-manager-test_caddy-test-network name: ${CADDY_NETWORK:-caddy-proxy-manager-test_caddy-test-network}
+18
View File
@@ -15,6 +15,24 @@ return [
'default' => env('REVERB_SERVER', 'reverb'), '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 | Reverb Servers
+9 -5
View File
@@ -30,6 +30,10 @@ x-app-env: &app-env
REVERB_SCHEME: http REVERB_SCHEME: http
REVERB_SERVER_HOST: 0.0.0.0 REVERB_SERVER_HOST: 0.0.0.0
REVERB_SERVER_PORT: 8080 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_URL: http://whisper:8000/v1
LOCAL_WHISPER_API_KEY: ${LOCAL_WHISPER_API_KEY:-not-needed} LOCAL_WHISPER_API_KEY: ${LOCAL_WHISPER_API_KEY:-not-needed}
LOCAL_WHISPER_MODEL: ${LOCAL_WHISPER_MODEL:-Systran/faster-whisper-base} LOCAL_WHISPER_MODEL: ${LOCAL_WHISPER_MODEL:-Systran/faster-whisper-base}
@@ -56,7 +60,7 @@ services:
VITE_REVERB_HOST: ${VITE_REVERB_HOST:-localhost} VITE_REVERB_HOST: ${VITE_REVERB_HOST:-localhost}
VITE_REVERB_PORT: ${REVERB_HOST_PORT:-8081} VITE_REVERB_PORT: ${REVERB_HOST_PORT:-8081}
VITE_REVERB_SCHEME: ${VITE_REVERB_SCHEME:-http} VITE_REVERB_SCHEME: ${VITE_REVERB_SCHEME:-http}
container_name: andytranscribe-app container_name: ${CONTAINER_PREFIX:-andytranscribe}-app
ports: ports:
- "${APP_HOST_PORT:-8080}:80" - "${APP_HOST_PORT:-8080}:80"
environment: environment:
@@ -81,7 +85,7 @@ services:
# `docker compose up --build` builds `app` first, then starts these with the tagged image. # `docker compose up --build` builds `app` first, then starts these with the tagged image.
queue: queue:
<<: *app-image <<: *app-image
container_name: andytranscribe-queue container_name: ${CONTAINER_PREFIX:-andytranscribe}-queue
command: command:
- php - php
- artisan - artisan
@@ -108,7 +112,7 @@ services:
reverb: reverb:
<<: *app-image <<: *app-image
container_name: andytranscribe-reverb container_name: ${CONTAINER_PREFIX:-andytranscribe}-reverb
command: command:
- php - php
- artisan - artisan
@@ -132,7 +136,7 @@ services:
whisper: whisper:
image: fedirz/faster-whisper-server:latest-cpu image: fedirz/faster-whisper-server:latest-cpu
container_name: andytranscribe-whisper container_name: ${CONTAINER_PREFIX:-andytranscribe}-whisper
ports: ports:
# Host 8090 avoids clashing with the FrankenPHP app on 8080 # Host 8090 avoids clashing with the FrankenPHP app on 8080
- "${WHISPER_HOST_PORT:-8090}:8000" - "${WHISPER_HOST_PORT:-8090}:8000"
@@ -153,7 +157,7 @@ services:
whisper-gpu: whisper-gpu:
profiles: ["gpu"] profiles: ["gpu"]
image: fedirz/faster-whisper-server:latest-cuda image: fedirz/faster-whisper-server:latest-cuda
container_name: andytranscribe-whisper-gpu container_name: ${CONTAINER_PREFIX:-andytranscribe}-whisper-gpu
ports: ports:
- "${WHISPER_HOST_PORT:-8090}:8000" - "${WHISPER_HOST_PORT:-8090}:8000"
volumes: volumes:
+4
View File
@@ -32,5 +32,9 @@
<env name="PULSE_ENABLED" value="false"/> <env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/> <env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/> <env name="NIGHTWATCH_ENABLED" value="false"/>
<env name="REVERB_APP_KEY" value="testing-reverb-key" force="true"/>
<env name="REVERB_PUBLIC_HOST" value="reverb.testing.example" force="true"/>
<env name="REVERB_PUBLIC_PORT" value="443" force="true"/>
<env name="REVERB_PUBLIC_SCHEME" value="https" force="true"/>
</php> </php>
</phpunit> </phpunit>
+28 -5
View File
@@ -3,13 +3,36 @@ import Echo from 'laravel-echo';
import Pusher from 'pusher-js'; import Pusher from 'pusher-js';
window.Pusher = Pusher; 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({ window.Echo = new Echo({
broadcaster: 'reverb', broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY, key,
wsHost: import.meta.env.VITE_REVERB_HOST, wsHost: host,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, wsPort: port || 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, wssPort: port || 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', forceTLS: scheme === 'https',
enabledTransports: ['ws', 'wss'], enabledTransports: ['ws', 'wss'],
authEndpoint: '/broadcasting/auth', authEndpoint: '/broadcasting/auth',
auth: { auth: {
+4
View File
@@ -1,6 +1,10 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="reverb-key" content="{{ config('reverb.client.key') }}">
<meta name="reverb-host" content="{{ config('reverb.client.host') }}">
<meta name="reverb-port" content="{{ config('reverb.client.port') }}">
<meta name="reverb-scheme" content="{{ config('reverb.client.scheme') }}">
<title> <title>
{{ filled($title ?? null) ? $title.' — '.config('app.name', 'AndyTranscribe') : config('app.name', 'AndyTranscribe') }} {{ filled($title ?? null) ? $title.' — '.config('app.name', 'AndyTranscribe') : config('app.name', 'AndyTranscribe') }}
+5 -1
View File
@@ -8,6 +8,7 @@ REPO_OWNER="${REPO_OWNER:-ben}"
REPO_NAME="${REPO_NAME:-AndyTranscribe}" REPO_NAME="${REPO_NAME:-AndyTranscribe}"
REGISTRY_HOST="${REGISTRY_HOST:-gitea.z00.nu}" REGISTRY_HOST="${REGISTRY_HOST:-gitea.z00.nu}"
DEPLOY_PATH="${DEPLOY_PATH:-${HOME}/andyTranscibe}" DEPLOY_PATH="${DEPLOY_PATH:-${HOME}/andyTranscibe}"
STAGE_DEPLOY_PATH="${STAGE_DEPLOY_PATH:-${HOME}/andyTranscibe-stage}"
CREDENTIALS_FILE="${GITEA_DIR}/.credentials" CREDENTIALS_FILE="${GITEA_DIR}/.credentials"
if [[ ! -f "${CREDENTIALS_FILE}" ]]; then if [[ ! -f "${CREDENTIALS_FILE}" ]]; then
@@ -57,6 +58,7 @@ set_secret() {
set_secret "REGISTRY_TOKEN" "${CI_TOKEN}" set_secret "REGISTRY_TOKEN" "${CI_TOKEN}"
set_secret "DEPLOY_PATHS" "${DEPLOY_PATH}" 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 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 "Gitea CI secrets configured for ${REGISTRY_HOST}/${REPO_OWNER}/${REPO_NAME}"
echo "Deploy path: ${DEPLOY_PATH}" 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"
+173
View File
@@ -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"
+33
View File
@@ -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);
}
}