Add Gitea Actions CI/CD to build, push, and deploy on main.
CI / test (push) Failing after 12m47s
CI / build-and-push (push) Skipped
CI / deploy (push) Skipped

Mirrors the airports runner flow: test, publish to the Gitea registry, then pull APP_IMAGE into the z00 compose stack.
This commit is contained in:
ben
2026-08-12 22:56:46 +02:00
parent 4898d5cfde
commit 5dd0a3aeac
7 changed files with 371 additions and 6 deletions
+2
View File
@@ -3,6 +3,8 @@ APP_ENV=local
APP_KEY=
APP_DEBUG=true
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_FALLBACK_LOCALE=en
+132
View File
@@ -0,0 +1,132 @@
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: Build frontend assets
run: |
set -euo pipefail
docker run --rm \
-u "$(id -u):$(id -g)" \
-e HOME=/tmp \
-e VITE_APP_NAME=AndyTranscribe \
-e VITE_REVERB_APP_KEY=andytranscribe-key \
-e "VITE_REVERB_HOST=${VITE_REVERB_HOST}" \
-e "VITE_REVERB_PORT=${VITE_REVERB_PORT}" \
-e "VITE_REVERB_SCHEME=${VITE_REVERB_SCHEME}" \
-v "$PWD:/app" \
-w /app \
node:22-alpine \
sh -c "npm ci && npm run build"
- 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 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 }}
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
+22
View File
@@ -154,6 +154,27 @@ Then a normal `docker compose up -d` enables:
- `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`.
## CI/CD (Gitea Actions)
On push to `main`, Gitea Actions (host runner on z00):
1. Runs PHPUnit
2. Builds and pushes `gitea.z00.nu/ben/andytranscribe:<sha>` (+ `:latest`)
3. Deploys by pulling that image into `~/andyTranscibe` (`docker-compose.yml` + `compose.z00.yaml`)
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
| Service | Host port | Role |
@@ -181,6 +202,7 @@ Edit `.env` before `docker compose up` when you need different ports or models:
| Variable | Purpose | Default |
| --- | --- | --- |
| `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_HOST_PORT` | Host port for the web app | `8080` |
| `REVERB_HOST_PORT` | Host port for WebSockets | `8081` |
+41
View File
@@ -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
+9 -6
View File
@@ -39,8 +39,14 @@ x-app-env: &app-env
SEED_USER_EMAIL: ${SEED_USER_EMAIL:-demo@example.com}
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:
app:
<<: *app-image
build:
context: .
dockerfile: Dockerfile
@@ -50,7 +56,6 @@ services:
VITE_REVERB_HOST: ${VITE_REVERB_HOST:-localhost}
VITE_REVERB_PORT: ${REVERB_HOST_PORT:-8081}
VITE_REVERB_SCHEME: ${VITE_REVERB_SCHEME:-http}
image: andytranscribe-app:latest
container_name: andytranscribe-app
ports:
- "${APP_HOST_PORT:-8080}:80"
@@ -72,11 +77,10 @@ services:
condition: service_started
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.
queue:
image: andytranscribe-app:latest
pull_policy: never
<<: *app-image
container_name: andytranscribe-queue
command:
- php
@@ -103,8 +107,7 @@ services:
restart: unless-stopped
reverb:
image: andytranscribe-app:latest
pull_policy: never
<<: *app-image
container_name: andytranscribe-reverb
command:
- php
+85
View File
@@ -0,0 +1,85 @@
#!/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)
GIT_PULL When 1 (default), run git pull --ff-only in each directory first
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}"
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_PULL}" == "1" ]] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git pull --ff-only
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
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
)
done
echo "Deploy complete: ${IMAGE}"
+80
View File
@@ -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."