1
0
mirror of git://f0xx.org/ac/ac-deploy synced 2026-07-29 03:38:07 +03:00

cluster0: lab seeds, Gitea mirrors, compose mail, verify tooling

Freeze lab-seeds backend for COMPOSE_SKIP_MONOLITH; fix Gitea mirror scripts
for 1.25 API; add secrets.lab.env template, verify-mail-lab, LAB_PUBLIC_ORIGIN,
and credentials pointers for mirror token recovery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-06-23 17:07:46 +02:00
parent 98f30786b7
commit c554dc761d
209 changed files with 31249 additions and 6 deletions

View File

@@ -7,7 +7,15 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
load_cluster_env
WS="${AC_WORKSPACE_ROOT:-/var/www/ac/workspace}"
LEGACY="${APP_ROOT}/android_cast/examples/crash_reporter/backend"
LAB_SEED="${ROOT}/lab-seeds/backend"
MONOLITH_BACKEND="${APP_ROOT}/android_cast/examples/crash_reporter/backend"
if [ -d "${LAB_SEED}/public" ]; then
LEGACY="$LAB_SEED"
elif [ "${COMPOSE_SKIP_MONOLITH:-0}" = "1" ]; then
die "lab-seeds missing at ${LAB_SEED} and COMPOSE_SKIP_MONOLITH=1"
else
LEGACY="$MONOLITH_BACKEND"
fi
COMPOSED="${COMPOSE_BACKEND:-/var/www/ac/composed/backend}"
PLATFORM_ROOT="${AC_PLATFORM_ROOT:-/var/www/ac/platform}"
APP_PASS="$(read_cred mariadb_app password)"
@@ -45,8 +53,8 @@ overlay_views() {
}
if [ ! -f "${COMPOSED}/public/index.php" ] || [ "${FORCE_COMPOSE_SEED:-0}" = "1" ]; then
[ -d "$LEGACY/public" ] || die "legacy backend missing at $LEGACY — run deploy-app.sh first"
log "seed composed backend from legacy ${LEGACY}"
[ -d "$LEGACY/public" ] || die "backend seed missing at $LEGACY (lab-seeds or deploy-app.sh monolith)"
log "seed composed backend from ${LEGACY}"
mkdir -p "$(dirname "$COMPOSED")"
rm -rf "$COMPOSED"
mkdir -p "$COMPOSED"
@@ -93,6 +101,17 @@ overlay_views ac-be-remote-access
overlay_views ac-be-access
ISSUES_BASE="${APP_BASE_PATH:-/app/androidcast_project/issues}"
PUBLIC_ORIGIN_CFG="${LAB_PUBLIC_ORIGIN:-https://acl0.f0xx.org}"
load_secrets_lab
AUTH_KEY="${AUTH_ENCRYPTION_KEY:-lab-cluster-dev-32-char-min-secret!!}"
MAIL_TRANSPORT="${MAIL_TRANSPORT:-smtp}"
MAIL_FROM="${MAIL_FROM:-Android Cast Lab <noreply@acl0.f0xx.org>}"
MAIL_REPLY="${MAIL_REPLY_TO:-info@apps.f0xx.org}"
SMTP_HOST="${SMTP_HOST:-127.0.0.1}"
SMTP_PORT="${SMTP_PORT:-587}"
SMTP_ENC="${SMTP_ENCRYPTION:-tls}"
SMTP_USER="${SMTP_USER:-}"
SMTP_PASS="${SMTP_PASS:-}"
mkdir -p "${COMPOSED}/config"
log "write composed config.php base_path=${ISSUES_BASE}"
cat > "${COMPOSED}/config/config.php" <<PHP
@@ -100,6 +119,7 @@ cat > "${COMPOSED}/config/config.php" <<PHP
return [
'app_name' => 'Android Cast Issues',
'base_path' => '${ISSUES_BASE}',
'public_origin' => '${PUBLIC_ORIGIN_CFG}',
'db' => [
'driver' => 'mariadb',
'sqlite_path' => __DIR__ . '/../data/crashes.sqlite',
@@ -125,10 +145,22 @@ return [
'require_wg_tools' => false,
],
'auth' => [
'encryption_key' => 'lab-cluster-dev-32-char-min-secret!!',
'encryption_key' => '${AUTH_KEY}',
'max_attempts' => 8,
'lockout_window_minutes' => 15,
],
'mail' => [
'transport' => '${MAIL_TRANSPORT}',
'from' => '${MAIL_FROM}',
'reply_to' => '${MAIL_REPLY}',
'smtp' => [
'host' => '${SMTP_HOST}',
'port' => ${SMTP_PORT},
'encryption' => '${SMTP_ENC}',
'username' => '${SMTP_USER}',
'password' => '${SMTP_PASS}',
],
],
'url_shortener' => [
'enabled' => true,
'public_base' => '${SHORT_LINKS_PUBLIC_BASE}',

View File

@@ -6,6 +6,11 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
. "$ROOT/scripts/lib/common.sh"
ensure_shared_mounted
if [ "${COMPOSE_SKIP_MONOLITH:-0}" = "1" ]; then
log "COMPOSE_SKIP_MONOLITH=1 — skip monolith git pull (use lab-seeds + compose-lab-backend.sh)"
exit 0
fi
APP_PASS="$(read_cred mariadb_app password)"
DB_HOST="$PRIMARY_DB_HOST"
if is_primary_node; then

View File

@@ -0,0 +1,128 @@
#!/bin/sh
# Install and start Gitea on cluster primary (cast01) — config only, no repo mirrors yet.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=/dev/null
. "$ROOT/scripts/lib/common.sh"
load_cluster_env
GITEA_DIR="${ROOT}/gitea"
TEMPLATE="${GITEA_DIR}/app.ini.template"
INI="/etc/gitea/app.ini"
GITEA_APP_DATA="${GITEA_APP_DATA:-/var/lib/gitea}"
GITEA_HTTP_PORT="${GITEA_HTTP_PORT:-3000}"
GITEA_DOMAIN="${GITEA_PUBLIC_HOST:-${ACL0_CNAME:-acl0.f0xx.org}}"
GITEA_ROOT_URL="${GITEA_ROOT_URL:-https://${GITEA_DOMAIN}/app/androidcast_project/git/}"
GITEA_DB_USER="${GITEA_DB_USER:-androidcast}"
GITEA_DB_PASS="$(read_cred mariadb_app password)"
GITEA_SECRETS="${GITEA_APP_DATA}/.lab-secrets.env"
if [ "${GITEA_ENABLE:-0}" != "1" ]; then
log "GITEA_ENABLE!=1 — skip deploy-gitea"
exit 0
fi
if ! is_primary_node; then
log "deploy-gitea: skip on replica $(host_short) (Gitea runs on ${CAST01_HOST} only)"
exit 0
fi
gitea_stop_clean() {
rc-service gitea stop 2>/dev/null || true
sleep 2
pkill -u gitea -f '/usr/bin/gitea web' 2>/dev/null || true
sleep 1
rm -f "${GITEA_APP_DATA}/gitea.db-shm" "${GITEA_APP_DATA}/gitea.db-wal" 2>/dev/null || true
}
gitea_wait_http() {
_port="${1:-$GITEA_HTTP_PORT}"
_i=0
while [ "$_i" -lt 180 ]; do
if curl -fsS -m 2 -o /dev/null "http://127.0.0.1:${_port}/" 2>/dev/null; then
return 0
fi
_i=$((_i + 1))
sleep 2
done
die "timeout waiting for gitea http://127.0.0.1:${_port}/"
}
[ -f "$TEMPLATE" ] || die "missing $TEMPLATE"
apk add --no-cache gitea 2>/dev/null || apk add --no-cache gitea
mkdir -p "$GITEA_APP_DATA/data/gitea-repositories" "$GITEA_APP_DATA/lfs" /var/log/gitea /etc/gitea
chown -R gitea:www-data "$GITEA_APP_DATA" /var/log/gitea 2>/dev/null || true
# Optional MariaDB backend (set GITEA_DB_TYPE=mysql in cluster.env). Default: sqlite3 for lab.
if [ "${GITEA_DB_TYPE:-sqlite3}" = "mysql" ]; then
mariadb -u root -e "CREATE DATABASE IF NOT EXISTS gitea CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null \
|| mariadb -e "CREATE DATABASE IF NOT EXISTS gitea CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb -u root -e "GRANT ALL PRIVILEGES ON gitea.* TO '${GITEA_DB_USER}'@'localhost'; FLUSH PRIVILEGES;" 2>/dev/null \
|| mariadb -e "GRANT ALL PRIVILEGES ON gitea.* TO '${GITEA_DB_USER}'@'localhost'; FLUSH PRIVILEGES;"
fi
gitea_rand_secret() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 48 | tr -d '/+=' | head -c 64
return 0
fi
head -c 48 /dev/urandom | base64 | tr -d '/+=' | head -c 64
}
if [ -f "$GITEA_SECRETS" ]; then
# shellcheck disable=SC1090
. "$GITEA_SECRETS"
else
GITEA_INTERNAL_TOKEN="$(gitea_rand_secret)"
GITEA_OAUTH_JWT="$(gitea_rand_secret)"
GITEA_LFS_JWT="$(gitea_rand_secret)"
umask 077
cat > "$GITEA_SECRETS" <<EOF
GITEA_INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN}
GITEA_OAUTH_JWT=${GITEA_OAUTH_JWT}
GITEA_LFS_JWT=${GITEA_LFS_JWT}
EOF
chown gitea:gitea "$GITEA_SECRETS" 2>/dev/null || true
fi
if [ ! -f "$INI" ] || [ "${FORCE_GITEA_CONFIG:-0}" = "1" ] || ! grep -q 'AndroidCast lab' "$INI" 2>/dev/null; then
log "render $INI from template (ROOT_URL=$GITEA_ROOT_URL)"
_tmp="$(mktemp)"
sed \
-e "s|@GITEA_APP_DATA@|${GITEA_APP_DATA}|g" \
-e "s|@GITEA_DOMAIN@|${GITEA_DOMAIN}|g" \
-e "s|@GITEA_ROOT_URL@|${GITEA_ROOT_URL}|g" \
-e "s|@GITEA_HTTP_PORT@|${GITEA_HTTP_PORT}|g" \
-e "s|@GITEA_INTERNAL_TOKEN@|${GITEA_INTERNAL_TOKEN}|g" \
-e "s|@GITEA_OAUTH_JWT@|${GITEA_OAUTH_JWT}|g" \
-e "s|@GITEA_LFS_JWT@|${GITEA_LFS_JWT}|g" \
"$TEMPLATE" > "$_tmp"
install -m 640 -o gitea -g www-data "$_tmp" "$INI"
rm -f "$_tmp"
if [ -d "${GITEA_APP_DATA}/custom/conf" ]; then
install -D -m 640 -o gitea -g www-data "$INI" "${GITEA_APP_DATA}/custom/conf/app.ini"
fi
fi
gitea_stop_clean
rc-update add gitea default 2>/dev/null || true
rc-service gitea start 2>/dev/null || rc-service gitea restart 2>/dev/null || true
gitea_wait_http "$GITEA_HTTP_PORT"
# First admin (lab) — idempotent.
GITEA_ADMIN_USER="${GITEA_ADMIN_USER:-admin}"
GITEA_ADMIN_PASS="$(read_cred gitea_admin password)"
if [ -z "$GITEA_ADMIN_PASS" ]; then
GITEA_ADMIN_PASS="$(read_cred mariadb_app password)"
fi
if ! su -s /bin/sh gitea -c "gitea -c '$INI' admin user list" 2>/dev/null | awk 'NR>1 && NF>=2 {found=1} END{exit !found}'; then
log "create Gitea admin user ${GITEA_ADMIN_USER}"
su -s /bin/sh gitea -c "gitea -c '$INI' admin user create \
--username '${GITEA_ADMIN_USER}' \
--password '${GITEA_ADMIN_PASS}' \
--email 'admin@${GITEA_DOMAIN}' \
--admin" 2>/dev/null || log "WARN: admin user create skipped (may already exist)"
fi
log "deploy-gitea_ok $(host_short) ROOT_URL=${GITEA_ROOT_URL} port=${GITEA_HTTP_PORT}"

View File

@@ -185,6 +185,16 @@ read_cred() {
' "$_file"
}
# Optional NFS secrets (not in git): secrets.lab.env
load_secrets_lab() {
_file="${CAST_CLUSTER_ROOT}/secrets.lab.env"
[ -f "$_file" ] || return 0
set -a
# shellcheck source=/dev/null
. "$_file"
set +a
}
ssh_node() {
_host="$1"
shift

View File

@@ -0,0 +1,91 @@
#!/bin/sh
# Mirror git://f0xx.org/ac/* (and legacy android_cast) into lab Gitea org ac on cast01.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=/dev/null
. "$ROOT/scripts/lib/common.sh"
load_cluster_env
GITEA_SCRIPTS="${ROOT}/lab-seeds/backend/scripts/gitea"
REPOS_FILE="${ROOT}/gitea/ac-repos.list"
TOKEN_FILE="${GITEA_APP_DATA:-/var/lib/gitea}/.mirror-admin-token"
GITEA_ORG="${GITEA_ORG:-ac}"
AC_GIT_BASE="${AC_GIT_BASE:-git://f0xx.org/ac}"
if [ "${GITEA_ENABLE:-0}" != "1" ]; then
log "GITEA_ENABLE!=1 — skip mirror-gitea-ac-org"
exit 0
fi
if ! is_primary_node; then
log "mirror-gitea-ac-org: skip on $(host_short) (Gitea on ${CAST01_HOST})"
exit 0
fi
[ -d "$GITEA_SCRIPTS" ] || die "missing $GITEA_SCRIPTS"
[ -f "$REPOS_FILE" ] || die "missing $REPOS_FILE"
ensure_admin_token() {
if [ -n "${GITEA_ADMIN_TOKEN:-}" ]; then
return 0
fi
if [ -r "$TOKEN_FILE" ]; then
GITEA_ADMIN_TOKEN="$(cat "$TOKEN_FILE")"
export GITEA_ADMIN_TOKEN
return 0
fi
log "generate Gitea admin API token"
GITEA_ADMIN_TOKEN="$(su -s /bin/sh gitea -c \
"gitea -c /etc/gitea/app.ini admin user generate-access-token --username admin --token-name mirror-ac --scopes all --raw" \
| tail -1)"
[ -n "$GITEA_ADMIN_TOKEN" ] || die "failed to generate GITEA_ADMIN_TOKEN"
umask 077
printf '%s' "$GITEA_ADMIN_TOKEN" > "$TOKEN_FILE"
chown gitea:www-data "$TOKEN_FILE" 2>/dev/null || true
export GITEA_ADMIN_TOKEN
}
ensure_admin_token
export GITEA_ORG GITEA_OWNER="$GITEA_ORG" GITEA_ORG_OWNERS="${GITEA_ORG_OWNERS:-admin}"
export GITEA_ORG_FULL_NAME="${GITEA_ORG_FULL_NAME:-Android Cast (ac)}"
export GITEA_TRANSFER_FROM="${GITEA_TRANSFER_FROM:-admin}"
export GITEA_MIRROR_INTERVAL="${GITEA_MIRROR_INTERVAL:-10m}"
export GITEA_PRIVATE="${GITEA_PRIVATE:-false}"
log "ensure Gitea org ${GITEA_ORG}"
sh "$GITEA_SCRIPTS/gitea_migrate_org_setup.sh" || die "org setup failed"
OK=0
SKIP=0
FAIL=0
while IFS= read -r _line || [ -n "$_line" ]; do
_line="$(printf '%s' "$_line" | sed 's/#.*//;s/^[ \t]*//;s/[ \t]*$//')"
[ -n "$_line" ] || continue
_repo="$_line"
_url=""
case "$_line" in
*'|'*)
_repo="${_line%%|*}"
_url="${_line#*|}"
;;
*)
_url="${AC_GIT_BASE}/${_repo}"
;;
esac
log "mirror ${_repo} <= ${_url}"
if command -v sqlite3 >/dev/null 2>&1 && \
sqlite3 /var/lib/gitea/gitea.db "SELECT 1 FROM repository r JOIN user u ON r.owner_id=u.id WHERE u.lower_name='$(printf '%s' "$GITEA_ORG" | tr '[:upper:]' '[:lower:]')' AND r.lower_name='$(printf '%s' "$_repo" | tr '[:upper:]' '[:lower:]')' LIMIT 1;" 2>/dev/null | grep -q 1; then
log "skip existing $_repo"
SKIP=$((SKIP + 1))
continue
fi
if GITEA_OWNER="$GITEA_ORG" GITEA_REPO="$_repo" GITEA_MIRROR_URL="$_url" \
sh "$GITEA_SCRIPTS/gitea_attach_remote.sh" "$_url"; then
OK=$((OK + 1))
else
FAIL=$((FAIL + 1))
log "WARN: mirror failed for $_repo"
fi
done < "$REPOS_FILE"
log "mirror-gitea-ac-org summary ok=$OK skip=$SKIP fail=$FAIL org=$GITEA_ORG"
[ "$FAIL" -eq 0 ] || exit 1

View File

@@ -0,0 +1,55 @@
#!/bin/sh
# Verify lab Gitea org ac mirrors vs ac-repos.list (cast01).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=/dev/null
. "$ROOT/scripts/lib/common.sh"
load_cluster_env
REPOS_FILE="${ROOT}/gitea/ac-repos.list"
DB="${GITEA_APP_DATA:-/var/lib/gitea}/gitea.db"
ORG="${GITEA_ORG:-ac}"
if [ "${GITEA_ENABLE:-0}" != "1" ]; then
log "GITEA_ENABLE!=1 — skip"
exit 0
fi
if ! is_primary_node; then
log "verify-gitea-mirrors: skip on $(host_short)"
exit 0
fi
FAIL=0
rc-service gitea status 2>/dev/null | grep -qi started || { log "FAIL gitea not running"; FAIL=1; }
curl -fsS -m 5 -o /dev/null http://127.0.0.1:3000/ || { log "FAIL gitea http"; FAIL=1; }
GITEA_COUNT="$(sqlite3 "$DB" "SELECT COUNT(*) FROM repository r JOIN user u ON r.owner_id=u.id WHERE u.lower_name='$(printf '%s' "$ORG" | tr '[:upper:]' '[:lower:]')';" 2>/dev/null || echo 0)"
log "gitea_org_${ORG}_repos=${GITEA_COUNT}"
while IFS= read -r _line || [ -n "$_line" ]; do
_line="$(printf '%s' "$_line" | sed 's/#.*//;s/^[ \t]*//;s/[ \t]*$//')"
[ -n "$_line" ] || continue
_repo="${_line%%|*}"
_url="${_line#*|}"
[ "$_url" = "$_repo" ] && _url="${AC_GIT_BASE:-git://f0xx.org/ac}/${_repo}"
if ! git ls-remote "$_url" HEAD >/dev/null 2>&1 && \
! git ls-remote "$_url" refs/heads/next >/dev/null 2>&1 && \
! git ls-remote "$_url" 2>/dev/null | grep -q refs/heads/; then
log "WARN git_missing $_url (not exported on git server)"
FAIL=1
continue
fi
if sqlite3 "$DB" "SELECT 1 FROM repository r JOIN user u ON r.owner_id=u.id WHERE u.lower_name='$(printf '%s' "$ORG" | tr '[:upper:]' '[:lower:]')' AND r.lower_name='$(printf '%s' "$_repo" | tr '[:upper:]' '[:lower:]')' LIMIT 1;" 2>/dev/null | grep -q 1; then
log "OK gitea_mirror ${ORG}/${_repo}"
else
log "FAIL gitea_missing ${ORG}/${_repo} (git remote OK)"
FAIL=1
fi
done < "$REPOS_FILE"
if [ "$FAIL" -eq 0 ]; then
log "verify-gitea-mirrors_ok"
exit 0
fi
log "verify-gitea-mirrors_WARN (see git_missing / gitea_missing above)"
exit 1

View File

@@ -0,0 +1,30 @@
#!/bin/sh
# Lab SMTP smoke — requires secrets.lab.env SMTP_* or local relay on cast01.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=/dev/null
. "$ROOT/scripts/lib/common.sh"
load_cluster_env
load_secrets_lab
TO="${1:-}"
[ -n "$TO" ] || die "usage: verify-mail-lab.sh recipient@example.com"
COMPOSED="${COMPOSE_BACKEND:-/var/www/ac/composed/backend}"
[ -f "${COMPOSED}/config/config.php" ] || die "missing composed backend — run compose-lab-backend.sh"
export MAIL_TEST_TO="$TO"
_out="$(php -r '
require "'"${COMPOSED}"'/src/bootstrap.php";
$to = getenv("MAIL_TEST_TO") ?: "";
$origin = rtrim((string) cfg("public_origin", "https://acl0.f0xx.org"), "/");
$base = rtrim((string) cfg("base_path", ""), "/");
$ok = AuthMailer::sendVerifyEmail($to, $origin . $base . "/verify-email?token=lab-smoke");
echo $ok ? "mail_ok" : "mail_fail";
' 2>&1)" || _out="mail_fail"
log "$_out to=$TO"
case "$_out" in
mail_ok) exit 0 ;;
*) exit 1 ;;
esac