Compare commits
7
Commits
v1.0.0
..
2b126ee4e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b126ee4e6 | ||
|
|
ab14f5e452 | ||
|
|
761f1a1f78 | ||
|
|
5dd0a3aeac | ||
|
|
4898d5cfde | ||
|
|
f65c816464 | ||
|
|
d60851bb53 |
+4
-2
@@ -3,6 +3,8 @@ APP_ENV=local
|
|||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost:8080
|
APP_URL=http://localhost:8080
|
||||||
|
# Production/CI: set to the Gitea registry image (local Compose builds andytranscribe-app:latest).
|
||||||
|
# APP_IMAGE=gitea.z00.nu/ben/andytranscribe:latest
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
@@ -27,7 +29,7 @@ DB_CONNECTION=sqlite
|
|||||||
# DB_USERNAME=root
|
# DB_USERNAME=root
|
||||||
# DB_PASSWORD=
|
# DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=file
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
@@ -37,7 +39,7 @@ BROADCAST_CONNECTION=reverb
|
|||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=database
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
CACHE_STORE=database
|
CACHE_STORE=file
|
||||||
# CACHE_PREFIX=
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.z00.nu
|
||||||
|
# Baked into the Vite client bundle for production WebSockets.
|
||||||
|
VITE_REVERB_HOST: reverb.transcribe.z00.nu
|
||||||
|
VITE_REVERB_PORT: "443"
|
||||||
|
VITE_REVERB_SCHEME: https
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
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: Install PHP dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate --force --no-interaction
|
||||||
|
php -d memory_limit=512M artisan test --compact
|
||||||
|
|
||||||
|
- name: Validate compose
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
APP_IMAGE="${REGISTRY}/$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]'):test" \
|
||||||
|
APP_KEY="base64:dGVzdC1hcHAta2V5LWZvci1jaS1jb21wb3NlLXZhbGlkYXRpb24=" \
|
||||||
|
docker compose -f docker-compose.yml -f compose.z00.yaml config --quiet
|
||||||
|
|
||||||
|
build-and-push:
|
||||||
|
if: gitea.event_name != 'pull_request'
|
||||||
|
needs: test
|
||||||
|
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: Build and push image
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
REPO_LC="$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
TAG_SHA="${REGISTRY}/${REPO_LC}:${{ gitea.sha }}"
|
||||||
|
TAG_LATEST="${REGISTRY}/${REPO_LC}:latest"
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${REGISTRY}" -u "${{ gitea.actor }}" --password-stdin
|
||||||
|
DOCKER_BUILDKIT=1 docker build \
|
||||||
|
--build-arg VITE_APP_NAME=AndyTranscribe \
|
||||||
|
--build-arg VITE_REVERB_APP_KEY=andytranscribe-key \
|
||||||
|
--build-arg "VITE_REVERB_HOST=${VITE_REVERB_HOST}" \
|
||||||
|
--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}"
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: gitea.ref == 'refs/heads/main' && 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 production
|
||||||
|
env:
|
||||||
|
DEPLOY_PATHS: ${{ secrets.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 "DEPLOY_PATHS secret is not set; skipping deploy."
|
||||||
|
echo "Built image: ${APP_IMAGE}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
./scripts/deploy-production.sh
|
||||||
@@ -154,6 +154,29 @@ Then a normal `docker compose up -d` enables:
|
|||||||
- `queue:listen` so worker code picks up changes between jobs
|
- `queue:listen` so worker code picks up changes between jobs
|
||||||
|
|
||||||
Open [http://localhost:8080](http://localhost:8080) as usual. After changing Composer packages, run `docker compose exec app composer install`.
|
Open [http://localhost:8080](http://localhost:8080) as usual. After changing Composer packages, run `docker compose exec app composer install`.
|
||||||
|
|
||||||
|
## CI/CD (Gitea Actions)
|
||||||
|
|
||||||
|
On push to `main`, Gitea Actions (host runner on z00):
|
||||||
|
|
||||||
|
1. Runs PHPUnit (+ compose config check)
|
||||||
|
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`)
|
||||||
|
|
||||||
|
Do not hot-patch production containers or the deploy checkout. Fix in git and push to `main` so CI deploys.
|
||||||
|
|
||||||
|
One-time server bootstrap (secrets + registry login):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/setup-gitea-ci.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual deploy of an already-built tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_IMAGE=gitea.z00.nu/ben/andytranscribe:<sha> DEPLOY_PATHS=$HOME/andyTranscibe ./scripts/deploy-production.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Services and ports
|
## Services and ports
|
||||||
|
|
||||||
| Service | Host port | Role |
|
| Service | Host port | Role |
|
||||||
@@ -181,6 +204,7 @@ Edit `.env` before `docker compose up` when you need different ports or models:
|
|||||||
| Variable | Purpose | Default |
|
| Variable | Purpose | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `APP_KEY` | Required Laravel encryption key | — |
|
| `APP_KEY` | Required Laravel encryption key | — |
|
||||||
|
| `APP_IMAGE` | Pre-built image for CI/prod deploys (omit locally) | `andytranscribe-app:latest` |
|
||||||
| `APP_URL` | Public app URL | `http://localhost:8080` |
|
| `APP_URL` | Public app URL | `http://localhost:8080` |
|
||||||
| `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` |
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
// CPM/Caddy terminates TLS; trust forwarded proto/host so asset() URLs stay https.
|
||||||
|
$middleware->trustProxies(
|
||||||
|
at: '*',
|
||||||
|
headers: Request::HEADER_X_FORWARDED_FOR
|
||||||
|
| Request::HEADER_X_FORWARDED_HOST
|
||||||
|
| Request::HEADER_X_FORWARDED_PORT
|
||||||
|
| Request::HEADER_X_FORWARDED_PROTO
|
||||||
|
| Request::HEADER_X_FORWARDED_PREFIX,
|
||||||
|
);
|
||||||
|
|
||||||
$middleware->redirectGuestsTo(fn () => route('login'));
|
$middleware->redirectGuestsTo(fn () => route('login'));
|
||||||
$middleware->redirectUsersTo(fn () => route('recordings.index'));
|
$middleware->redirectUsersTo(fn () => route('recordings.index'));
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# z00 production overlay for AndyTranscribe (behind Caddy Proxy Manager)
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- caddy
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
APP_URL: https://transcribe.z00.nu
|
||||||
|
TRUSTED_PROXIES: "*"
|
||||||
|
REVERB_HOST: reverb
|
||||||
|
REVERB_PORT: "8080"
|
||||||
|
REVERB_SCHEME: http
|
||||||
|
reverb:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- caddy
|
||||||
|
environment:
|
||||||
|
APP_URL: https://transcribe.z00.nu
|
||||||
|
REVERB_HOST: reverb.transcribe.z00.nu
|
||||||
|
REVERB_PORT: "443"
|
||||||
|
REVERB_SCHEME: https
|
||||||
|
queue:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
APP_URL: https://transcribe.z00.nu
|
||||||
|
REVERB_HOST: reverb
|
||||||
|
REVERB_PORT: "8080"
|
||||||
|
REVERB_SCHEME: http
|
||||||
|
whisper:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
external: true
|
||||||
|
name: caddy-proxy-manager-test_caddy-test-network
|
||||||
+2
-1
@@ -38,7 +38,8 @@ return [
|
|||||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
'busy_timeout' => 5000,
|
// Wait longer under concurrent writers (queue + web on one SQLite file).
|
||||||
|
'busy_timeout' => (int) env('DB_BUSY_TIMEOUT', 30000),
|
||||||
'journal_mode' => 'WAL',
|
'journal_mode' => 'WAL',
|
||||||
'synchronous' => 'NORMAL',
|
'synchronous' => 'NORMAL',
|
||||||
'transaction_mode' => 'DEFERRED',
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
|||||||
+13
-8
@@ -14,9 +14,11 @@ x-app-env: &app-env
|
|||||||
LOG_CHANNEL: stderr
|
LOG_CHANNEL: stderr
|
||||||
DB_CONNECTION: sqlite
|
DB_CONNECTION: sqlite
|
||||||
DB_DATABASE: /app/database/database.sqlite
|
DB_DATABASE: /app/database/database.sqlite
|
||||||
SESSION_DRIVER: database
|
# File drivers avoid SQLite lock storms: Livewire polls + database queue +
|
||||||
|
# session/cache all writing the same sqlite file caused "database is locked".
|
||||||
|
SESSION_DRIVER: file
|
||||||
QUEUE_CONNECTION: database
|
QUEUE_CONNECTION: database
|
||||||
CACHE_STORE: database
|
CACHE_STORE: file
|
||||||
BROADCAST_CONNECTION: reverb
|
BROADCAST_CONNECTION: reverb
|
||||||
FILESYSTEM_DISK: local
|
FILESYSTEM_DISK: local
|
||||||
REVERB_APP_ID: ${REVERB_APP_ID:-andytranscribe}
|
REVERB_APP_ID: ${REVERB_APP_ID:-andytranscribe}
|
||||||
@@ -37,8 +39,14 @@ x-app-env: &app-env
|
|||||||
SEED_USER_EMAIL: ${SEED_USER_EMAIL:-demo@example.com}
|
SEED_USER_EMAIL: ${SEED_USER_EMAIL:-demo@example.com}
|
||||||
SEED_USER_PASSWORD: ${SEED_USER_PASSWORD:-password}
|
SEED_USER_PASSWORD: ${SEED_USER_PASSWORD:-password}
|
||||||
|
|
||||||
|
# Local: omit APP_IMAGE (builds andytranscribe-app:latest).
|
||||||
|
# CI/prod: set APP_IMAGE=gitea.z00.nu/ben/andytranscribe:<sha> and pull.
|
||||||
|
x-app-image: &app-image
|
||||||
|
image: ${APP_IMAGE:-andytranscribe-app:latest}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
|
<<: *app-image
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -48,7 +56,6 @@ 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}
|
||||||
image: andytranscribe-app:latest
|
|
||||||
container_name: andytranscribe-app
|
container_name: andytranscribe-app
|
||||||
ports:
|
ports:
|
||||||
- "${APP_HOST_PORT:-8080}:80"
|
- "${APP_HOST_PORT:-8080}:80"
|
||||||
@@ -70,11 +77,10 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Shares andytranscribe-app:latest — do not declare build: here (avoids rebuilding 3×).
|
# Shares the app image — do not declare build: here (avoids rebuilding 3×).
|
||||||
# `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:
|
||||||
image: andytranscribe-app:latest
|
<<: *app-image
|
||||||
pull_policy: never
|
|
||||||
container_name: andytranscribe-queue
|
container_name: andytranscribe-queue
|
||||||
command:
|
command:
|
||||||
- php
|
- php
|
||||||
@@ -101,8 +107,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
reverb:
|
reverb:
|
||||||
image: andytranscribe-app:latest
|
<<: *app-image
|
||||||
pull_policy: never
|
|
||||||
container_name: andytranscribe-reverb
|
container_name: andytranscribe-reverb
|
||||||
command:
|
command:
|
||||||
- php
|
- php
|
||||||
|
|||||||
@@ -11,4 +11,36 @@
|
|||||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.bunny.net/css?family=inter:400,500,600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
@fluxAppearance
|
{{-- Flux appearance with dark as the default (Flux ships with "system"). --}}
|
||||||
|
<style>
|
||||||
|
:root.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
window.Flux = {
|
||||||
|
applyAppearance (appearance) {
|
||||||
|
let applyDark = () => document.documentElement.classList.add('dark')
|
||||||
|
let applyLight = () => document.documentElement.classList.remove('dark')
|
||||||
|
|
||||||
|
if (appearance === 'system') {
|
||||||
|
let media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
|
// Persist "system" explicitly so a missing key can mean "use app default (dark)".
|
||||||
|
window.localStorage.setItem('flux.appearance', 'system')
|
||||||
|
|
||||||
|
media.matches ? applyDark() : applyLight()
|
||||||
|
} else if (appearance === 'dark') {
|
||||||
|
window.localStorage.setItem('flux.appearance', 'dark')
|
||||||
|
|
||||||
|
applyDark()
|
||||||
|
} else if (appearance === 'light') {
|
||||||
|
window.localStorage.setItem('flux.appearance', 'light')
|
||||||
|
|
||||||
|
applyLight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Flux.applyAppearance(window.localStorage.getItem('flux.appearance') || 'dark')
|
||||||
|
</script>
|
||||||
|
|||||||
Executable
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Deploy a pre-built APP_IMAGE to one or more AndyTranscribe directories.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
APP_IMAGE Required. e.g. gitea.z00.nu/ben/andytranscribe:abc1234
|
||||||
|
DEPLOY_PATHS Comma-separated instance directories (default: current directory)
|
||||||
|
DEPLOY_SHA Optional git SHA to hard-reset each directory to (CI sets this)
|
||||||
|
GIT_PULL When 1 (default) and DEPLOY_SHA is empty, run git pull --ff-only
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
APP_IMAGE=gitea.z00.nu/ben/andytranscribe:tag DEPLOY_PATHS=$HOME/andyTranscibe ./scripts/deploy-production.sh
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
IMAGE="${APP_IMAGE:-${1:-}}"
|
||||||
|
if [[ -z "${IMAGE}" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PATHS_CSV="${DEPLOY_PATHS:-${2:-.}}"
|
||||||
|
GIT_PULL="${GIT_PULL:-1}"
|
||||||
|
DEPLOY_SHA="${DEPLOY_SHA:-}"
|
||||||
|
IFS=',' read -r -a DIRS <<< "${PATHS_CSV}"
|
||||||
|
|
||||||
|
for dir in "${DIRS[@]}"; do
|
||||||
|
dir="${dir#"${dir%%[![:space:]]*}"}"
|
||||||
|
dir="${dir%"${dir##*[![:space:]]}"}"
|
||||||
|
|
||||||
|
if [[ ! -d "${dir}" ]]; then
|
||||||
|
echo "Missing instance directory: ${dir}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
name="$(basename "${dir}")"
|
||||||
|
echo "==> Deploying ${IMAGE} in ${dir}"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${dir}"
|
||||||
|
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
if [[ -n "${DEPLOY_SHA}" ]]; then
|
||||||
|
git fetch --force origin "${DEPLOY_SHA}"
|
||||||
|
git checkout --force --detach "${DEPLOY_SHA}"
|
||||||
|
git reset --hard "${DEPLOY_SHA}"
|
||||||
|
# Drop local edits/hot-patches; keep runtime data and secrets.
|
||||||
|
git clean -fd \
|
||||||
|
--exclude=database/ \
|
||||||
|
--exclude=storage/ \
|
||||||
|
--exclude=.env \
|
||||||
|
--exclude=.env.*
|
||||||
|
elif [[ "${GIT_PULL}" == "1" ]]; then
|
||||||
|
git pull --ff-only
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export APP_IMAGE="${IMAGE}"
|
||||||
|
export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-${name,,}}"
|
||||||
|
|
||||||
|
compose_files=(-f docker-compose.yml)
|
||||||
|
if [[ -f compose.z00.yaml ]]; then
|
||||||
|
compose_files+=(-f compose.z00.yaml)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '^APP_IMAGE=' .env 2>/dev/null; then
|
||||||
|
sed -i "s|^APP_IMAGE=.*|APP_IMAGE=${IMAGE}|" .env
|
||||||
|
else
|
||||||
|
printf '\nAPP_IMAGE=%s\n' "${IMAGE}" >> .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose "${compose_files[@]}" pull app queue reverb
|
||||||
|
docker compose "${compose_files[@]}" up -d --remove-orphans
|
||||||
|
docker compose "${compose_files[@]}" ps
|
||||||
|
|
||||||
|
app_port="$(awk -F= '/^APP_HOST_PORT=/ {print $2; exit}' .env 2>/dev/null || true)"
|
||||||
|
app_port="${app_port:-18080}"
|
||||||
|
health_url="http://127.0.0.1:${app_port}/up"
|
||||||
|
echo "waiting for ${health_url}"
|
||||||
|
ok=0
|
||||||
|
for _ in $(seq 1 45); do
|
||||||
|
if curl -fsS "${health_url}" >/dev/null 2>&1; then
|
||||||
|
echo "healthy"
|
||||||
|
ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
if [[ "${ok}" -ne 1 ]]; then
|
||||||
|
echo "ERROR: health check failed for ${name}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Hit /login as the public HTTPS edge would (trustProxies needs forwarded proto).
|
||||||
|
app_url="$(awk -F= '/^APP_URL=/ {print $2; exit}' .env 2>/dev/null || true)"
|
||||||
|
app_url="${app_url:-https://transcribe.z00.nu}"
|
||||||
|
public_host="$(python3 - <<PY
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
print(urlparse("${app_url}").hostname or "transcribe.z00.nu")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
asset_scheme="$(
|
||||||
|
curl -fsS \
|
||||||
|
-H "X-Forwarded-Proto: https" \
|
||||||
|
-H "X-Forwarded-Host: ${public_host}" \
|
||||||
|
-H "X-Forwarded-Port: 443" \
|
||||||
|
"http://127.0.0.1:${app_port}/login" \
|
||||||
|
| grep -oE 'https?://[^"'\'' ]+\.css' \
|
||||||
|
| head -1 || true
|
||||||
|
)"
|
||||||
|
if [[ "${asset_scheme}" == http://* ]]; then
|
||||||
|
echo "ERROR: login page still emits http:// asset URLs (${asset_scheme})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "asset check ok: ${asset_scheme:-no absolute css url (relative/ok)}"
|
||||||
|
)
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deploy complete: ${IMAGE}"
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Idempotent bootstrap for AndyTranscribe Gitea Actions secrets on z00.
|
||||||
|
# Requires an already-running Gitea + act_runner (see airports setup).
|
||||||
|
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}"
|
||||||
|
DEPLOY_PATH="${DEPLOY_PATH:-${HOME}/andyTranscibe}"
|
||||||
|
CREDENTIALS_FILE="${GITEA_DIR}/.credentials"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
CI_TOKEN="$(
|
||||||
|
docker exec -u git gitea gitea admin user generate-access-token \
|
||||||
|
-u "${ADMIN_USERNAME}" \
|
||||||
|
-t "ci-${REPO_NAME}-$(date +%Y%m%d%H%M%S)" \
|
||||||
|
--scopes "write:package,read:package,write:repository,read:repository" \
|
||||||
|
--raw
|
||||||
|
)"
|
||||||
|
|
||||||
|
PULL_TOKEN="$(
|
||||||
|
docker exec -u git gitea gitea admin user generate-access-token \
|
||||||
|
-u "${ADMIN_USERNAME}" \
|
||||||
|
-t "pull-${REPO_NAME}-$(date +%Y%m%d%H%M%S)" \
|
||||||
|
--scopes "read:package" \
|
||||||
|
--raw
|
||||||
|
)"
|
||||||
|
|
||||||
|
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 "REGISTRY_TOKEN" "${CI_TOKEN}"
|
||||||
|
set_secret "DEPLOY_PATHS" "${DEPLOY_PATH}"
|
||||||
|
|
||||||
|
printf '%s' "${PULL_TOKEN}" | docker login "${REGISTRY_HOST}" -u "${ADMIN_USERNAME}" --password-stdin
|
||||||
|
|
||||||
|
REPO_LC="$(echo "${REPO_OWNER}/${REPO_NAME}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
if [[ -f "${DEPLOY_PATH}/.env" ]]; then
|
||||||
|
if grep -q '^APP_IMAGE=' "${DEPLOY_PATH}/.env"; then
|
||||||
|
sed -i "s|^APP_IMAGE=.*|APP_IMAGE=${REGISTRY_HOST}/${REPO_LC}:latest|" "${DEPLOY_PATH}/.env"
|
||||||
|
else
|
||||||
|
printf '\nAPP_IMAGE=%s/%s:latest\n' "${REGISTRY_HOST}" "${REPO_LC}" >> "${DEPLOY_PATH}/.env"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure the existing host runner is up (shared with airports).
|
||||||
|
if systemctl --user is-enabled gitea-act-runner.service >/dev/null 2>&1; then
|
||||||
|
systemctl --user restart gitea-act-runner.service || true
|
||||||
|
systemctl --user --no-pager --lines=5 status gitea-act-runner.service || true
|
||||||
|
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."
|
||||||
+7
-1
@@ -6,5 +6,11 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
|||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
abstract class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
//
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Feature tests render Blade without a Vite build in CI.
|
||||||
|
$this->withoutVite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user