Files
ss-tools/build.sh
2026-07-23 18:53:45 +03:00

1076 lines
39 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# #region Tooling.Build [C:2] [TYPE Module]
# @PURPOSE: Unified build script — local docker compose + individual image builds + release bundles
# @COMPLEXITY: 2
#
# Usage: ./build.sh <command> [options]
#
# Commands (local docker):
# up [profile] Build and start local docker compose (default: current)
# down [profile] Stop services
# restart [profile] Rebuild and restart services
# logs [profile] Tail logs
# status Show running containers for all profiles
#
# Commands (individual image build — no export):
# build:backend <tag> Build backend image only (superset-tools-backend:<tag>)
# build:frontend <tag> Build frontend image only (superset-tools-frontend:<tag>)
# build:agent <tag> Build agent image only (superset-tools-agent:<tag>)
#
# Commands (single-image bundle — build + .tar.xz export):
# bundle:backend <tag> Build + export backend .tar.xz only
# bundle:frontend <tag> Build + export frontend .tar.xz only
# bundle:agent <tag> Build + export agent .tar.xz only
#
# Commands (release smoke):
# smoke:bundle <backend-image> <postgres-image>
# Validate clean PostgreSQL migration + backend boot/restart.
#
# Commands (full bundles — backend + frontend + agent + postgres):
# bundle <tag> Slim enterprise bundle (no embeddings agent). Default.
# bundle:embeddings <tag> Enterprise bundle WITH semantic embedding routing (larger agent)
# bundle:light <tag> Lightweight all-in-one bundle (<200 MB, .tar.xz, no Playwright)
#
# help Show this help
#
# Profiles: current (default), master, enterprise-clean
# #endregion Tooling.Build
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
BACKEND_ENV_FILE="$SCRIPT_DIR/backend/.env"
DIST_ROOT="$SCRIPT_DIR/dist/docker"
# ---- Docker Compose detection ----
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
else
echo "Error: docker compose is not available."
exit 1
fi
# ======================================================================
# HELPERS
# ======================================================================
# shellcheck disable=SC2206
setup_compose_profile() {
local profile="$1"
case "$profile" in
master)
PROFILE_ENV_FILE="$SCRIPT_DIR/.env.master"
PROJECT_NAME="superset-tools-master"
COMPOSE_FILE="docker-compose.yml"
;;
current|"")
PROFILE_ENV_FILE="$SCRIPT_DIR/.env.current"
PROJECT_NAME="superset-tools-current"
COMPOSE_FILE="docker-compose.yml"
;;
enterprise-clean)
PROFILE_ENV_FILE="$SCRIPT_DIR/.env.enterprise-clean"
PROJECT_NAME="superset-tools-enterprise"
COMPOSE_FILE="docker-compose.enterprise-clean.yml"
;;
*)
echo "Error: unknown profile '$profile'. Use: current, master, enterprise-clean."
exit 1
;;
esac
COMPOSE_ARGS=(-p "$PROJECT_NAME" -f "$COMPOSE_FILE")
if [[ -f "$PROFILE_ENV_FILE" ]]; then
COMPOSE_ARGS+=(--env-file "$PROFILE_ENV_FILE")
fi
}
ensure_backend_encryption_key() {
if command -v python3 >/dev/null 2>&1; then
python3 - "$BACKEND_ENV_FILE" <<'PY'
import base64
import os
import sys
from pathlib import Path
from typing import List, Optional
def is_valid_fernet_key(raw_value: str) -> bool:
value = raw_value.strip()
if not value:
return False
try:
decoded = base64.urlsafe_b64decode(value.encode())
except Exception:
return False
return len(decoded) == 32
def generate_fernet_key() -> str:
return base64.urlsafe_b64encode(os.urandom(32)).decode()
env_path = Path(sys.argv[1])
env_path.parent.mkdir(parents=True, exist_ok=True)
existing_lines: List[str] = []
existing_key: Optional[str] = None
if env_path.exists():
existing_lines = env_path.read_text(encoding="utf-8").splitlines()
for line in existing_lines:
if line.startswith("ENCRYPTION_KEY="):
candidate = line.partition("=")[2].strip()
if is_valid_fernet_key(candidate):
existing_key = candidate
break
if existing_key is None:
generated_key = generate_fernet_key()
filtered_lines = [line for line in existing_lines if not line.startswith("ENCRYPTION_KEY=")]
filtered_lines.append(f"ENCRYPTION_KEY={generated_key}")
env_path.write_text("\n".join(filtered_lines) + "\n", encoding="utf-8")
print(f"[build] ENCRYPTION_KEY ensured in {env_path}")
else:
print(f"[build] Existing ENCRYPTION_KEY reused from {env_path}")
PY
return
fi
echo "Error: python3 is required to generate backend/.env with ENCRYPTION_KEY."
exit 1
}
get_image_id() {
docker image inspect --format '{{.Id}}' "$1"
}
get_repo_digest() {
local digest
digest="$(docker image inspect --format '{{join .RepoDigests ","}}' "$1" 2>/dev/null || true)"
if [[ -n "${digest}" && "${digest}" != "<no value>" ]]; then
printf '%s' "${digest}"
else
printf '%s' "unavailable"
fi
}
resolve_app_version() {
local tag="$1"
local git_commit
git_commit="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
echo "${tag}+${git_commit}"
}
# ======================================================================
# SINGLE-IMAGE BUILD FUNCTIONS
# ======================================================================
build_backend() {
local tag="$1"
local image="superset-tools-backend:${tag}"
echo "[build] Building backend image ${image}..."
docker build -f docker/backend.Dockerfile -t "${image}" .
echo "[build] ✅ Backend image ${image} built"
}
build_frontend() {
local tag="$1"
local image="superset-tools-frontend:${tag}"
local app_version
app_version="$(resolve_app_version "${tag}")"
echo "[build] Building frontend image ${image} (APP_VERSION=${app_version})..."
docker build --build-arg "APP_VERSION=${app_version}" -f docker/frontend.Dockerfile -t "${image}" .
echo "[build] ✅ Frontend image ${image} built"
}
build_agent() {
local tag="$1"
local image="superset-tools-agent:${tag}"
echo "[build] Building agent image ${image}..."
docker build -f docker/Dockerfile.agent -t "${image}" .
echo "[build] ✅ Agent image ${image} built"
}
build_agent_embeddings() {
local tag="$1"
local image="superset-tools-agent:${tag}"
echo "[build] Building agent image ${image} (WITH_EMBEDDINGS=true)..."
docker build \
--build-arg WITH_EMBEDDINGS=true \
-f docker/Dockerfile.agent \
-t "${image}" .
echo "[build] ✅ Agent image ${image} built (embeddings)"
}
# ======================================================================
# SINGLE-IMAGE EXPORT
# ======================================================================
export_image() {
local image="$1"
local archive="$2"
echo "[bundle] Compressing ${image} -> ${archive}..."
mkdir -p "$(dirname "${archive}")"
docker save "${image}" | xz -T0 -3 > "${archive}"
echo "[bundle] ✅ ${archive} saved"
}
# ======================================================================
# RELEASE SMOKE TESTS
# ======================================================================
run_bundle_smoke() {
local backend_image="$1"
local postgres_image="$2"
local timeout_seconds="${BUNDLE_SMOKE_TIMEOUT_SECONDS:-180}"
local suffix="$(date +%s)-${RANDOM}"
local network="ss-tools-release-smoke-${suffix}"
local db_container="ss-tools-release-smoke-db-${suffix}"
local backend_container="ss-tools-release-smoke-backend-${suffix}"
local database_url="postgresql+psycopg2://postgres:postgres@${db_container}:5432/release_smoke"
(
set -euo pipefail
cleanup_bundle_smoke() {
docker rm -f "${backend_container}" "${db_container}" >/dev/null 2>&1 || true
docker network rm "${network}" >/dev/null 2>&1 || true
}
trap cleanup_bundle_smoke EXIT
wait_for_postgres() {
local elapsed=0
while (( elapsed < timeout_seconds )); do
if docker exec "${db_container}" pg_isready -U postgres -d release_smoke >/dev/null 2>&1; then
return 0
fi
sleep 2
((elapsed += 2))
done
echo "[smoke] ❌ PostgreSQL did not become ready within ${timeout_seconds}s" >&2
docker logs "${db_container}" >&2 || true
return 1
}
wait_for_backend_health() {
local phase="$1"
local elapsed=0
while (( elapsed < timeout_seconds )); do
if docker exec "${backend_container}" curl -fsS http://127.0.0.1:8000/ >/dev/null 2>&1; then
echo "[smoke] ✅ Backend health check passed (${phase})"
return 0
fi
if [[ "$(docker inspect --format '{{.State.Running}}' "${backend_container}" 2>/dev/null || true)" != "true" ]]; then
echo "[smoke] ❌ Backend exited during ${phase}" >&2
docker logs "${backend_container}" >&2 || true
return 1
fi
sleep 2
((elapsed += 2))
done
echo "[smoke] ❌ Backend health check timed out during ${phase}" >&2
docker logs "${backend_container}" >&2 || true
return 1
}
echo "[smoke] Starting clean PostgreSQL release gate..."
docker network create "${network}" >/dev/null
docker run -d --name "${db_container}" --network "${network}" \
--security-opt seccomp=unconfined \
-e POSTGRES_DB=release_smoke \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
"${postgres_image}" >/dev/null
wait_for_postgres
echo "[smoke] Verifying fresh migration, ORM schema initialization, and backend health..."
docker run -d --name "${backend_container}" --network "${network}" \
-e DATABASE_URL="${database_url}" \
-e TASKS_DATABASE_URL="${database_url}" \
-e AUTH_DATABASE_URL="${database_url}" \
-e AUTH_SECRET_KEY=release-smoke-auth-secret \
-e ENCRYPTION_KEY=change-me-generate-a-fernet-key= \
-e SERVICE_JWT=release-smoke-service-token \
-e INITIAL_ADMIN_CREATE=false \
"${backend_image}" >/dev/null
wait_for_backend_health "initial start"
echo "[smoke] Verifying idempotent migration and restart..."
docker restart "${backend_container}" >/dev/null
wait_for_backend_health "restart"
echo "[smoke] ✅ Clean PostgreSQL release gate passed"
)
}
# ======================================================================
# COMPOSE COMMANDS
# ======================================================================
compose_up() {
local profile="${1:-current}"
setup_compose_profile "$profile"
ensure_backend_encryption_key
echo "[build] Profile: $profile (project: $PROJECT_NAME)"
if [[ -f "$PROFILE_ENV_FILE" ]]; then
echo "[build] Env file: $PROFILE_ENV_FILE"
fi
echo "[1/2] Building project images..."
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" build
echo "[2/2] Starting Docker services..."
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" up -d
echo "Done. Services are running."
echo " ${COMPOSE_CMD[*]} ${COMPOSE_ARGS[*]} ps"
echo " ${COMPOSE_CMD[*]} ${COMPOSE_ARGS[*]} logs -f"
}
compose_down() {
local profile="${1:-current}"
setup_compose_profile "$profile"
echo "[build] Stopping $profile (project: $PROJECT_NAME)..."
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" down
echo "[build] Done."
}
compose_restart() {
local profile="${1:-current}"
setup_compose_profile "$profile"
ensure_backend_encryption_key
echo "[build] Restarting $profile (project: $PROJECT_NAME)..."
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" build
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" up -d --force-recreate
echo "[build] Done."
}
compose_logs() {
local profile="${1:-current}"
setup_compose_profile "$profile"
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" logs -f
}
compose_status() {
echo "=== superset-tools docker status ==="
for profile in current master enterprise-clean; do
setup_compose_profile "$profile"
echo "--- $profile (project: $PROJECT_NAME) ---"
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" ps 2>/dev/null || echo "(not running)"
done
}
# ======================================================================
# BUNDLE: full release (backend + frontend + agent .tar.xz's)
# ======================================================================
# ======================================================================
# SINGLE-IMAGE BUNDLE FUNCTIONS
# ======================================================================
bundle_single_backend() {
if [[ $# -lt 1 ]]; then
echo "Error: tag is required."
echo "Usage: ./build.sh bundle:backend <tag>"
exit 1
fi
local tag="$1"
local image="superset-tools-backend:${tag}"
mkdir -p "$DIST_ROOT"
build_backend "${tag}"
export_image "${image}" "${DIST_ROOT}/superset-tools-backend.${tag}.tar.xz"
(cd "${DIST_ROOT}" && sha256sum "superset-tools-backend.${tag}.tar.xz" > "sha256sums-backend.${tag}.txt")
echo "[bundle:backend] ✅ Backend bundle ${tag} created"
echo " xz -dc ${DIST_ROOT}/superset-tools-backend.${tag}.tar.xz | docker load"
}
bundle_single_frontend() {
if [[ $# -lt 1 ]]; then
echo "Error: tag is required."
echo "Usage: ./build.sh bundle:frontend <tag>"
exit 1
fi
local tag="$1"
local image="superset-tools-frontend:${tag}"
mkdir -p "$DIST_ROOT"
build_frontend "${tag}"
export_image "${image}" "${DIST_ROOT}/superset-tools-frontend.${tag}.tar.xz"
(cd "${DIST_ROOT}" && sha256sum "superset-tools-frontend.${tag}.tar.xz" > "sha256sums-frontend.${tag}.txt")
echo "[bundle:frontend] ✅ Frontend bundle ${tag} created"
echo " xz -dc ${DIST_ROOT}/superset-tools-frontend.${tag}.tar.xz | docker load"
}
bundle_single_agent() {
if [[ $# -lt 1 ]]; then
echo "Error: tag is required."
echo "Usage: ./build.sh bundle:agent <tag>"
exit 1
fi
local tag="$1"
local image="superset-tools-agent:${tag}"
mkdir -p "$DIST_ROOT"
build_agent "${tag}"
export_image "${image}" "${DIST_ROOT}/superset-tools-agent.${tag}.tar.xz"
(cd "${DIST_ROOT}" && sha256sum "superset-tools-agent.${tag}.tar.xz" > "sha256sums-agent.${tag}.txt")
echo "[bundle:agent] ✅ Agent bundle ${tag} created"
echo " xz -dc ${DIST_ROOT}/superset-tools-agent.${tag}.tar.xz | docker load"
}
# ======================================================================
# FULL BUNDLE: backend + frontend + agent
# ======================================================================
generate_deploy_compose() {
local tag="$1" backend_tag="$2" frontend_tag="$3" agent_tag="$4" postgres_tag="$5"
cat > "${DIST_ROOT}/docker-compose.enterprise-clean.yml" <<DEPLOY
services:
db:
image: ${postgres_tag}
pull_policy: never
restart: unless-stopped
security_opt:
- seccomp=unconfined
environment:
POSTGRES_DB: \${POSTGRES_DB:-ss_tools}
POSTGRES_USER: \${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
ports:
- "\${POSTGRES_HOST_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-postgres} -d \${POSTGRES_DB:-ss_tools}"]
interval: 10s
timeout: 5s
retries: 10
backend:
image: ${backend_tag}
pull_policy: never
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@db:5432/\${POSTGRES_DB:-ss_tools}
TASKS_DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@db:5432/\${POSTGRES_DB:-ss_tools}
AUTH_DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@db:5432/\${POSTGRES_DB:-ss_tools}
BACKEND_PORT: 8000
AUTH_SECRET_KEY: \${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env}
ENCRYPTION_KEY: \${ENCRYPTION_KEY:?Set ENCRYPTION_KEY in .env}
ENABLE_BELIEF_STATE_LOGGING: \${ENABLE_BELIEF_STATE_LOGGING:-true}
TASK_LOG_LEVEL: \${TASK_LOG_LEVEL:-INFO}
INITIAL_ADMIN_CREATE: \${INITIAL_ADMIN_CREATE:-false}
INITIAL_ADMIN_USERNAME: \${INITIAL_ADMIN_USERNAME:-admin}
INITIAL_ADMIN_PASSWORD: \${INITIAL_ADMIN_PASSWORD:-}
INITIAL_ADMIN_EMAIL: \${INITIAL_ADMIN_EMAIL:-}
OPENAI_API_KEY: \${OPENAI_API_KEY:-}
ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
FEATURES__DATASET_REVIEW: \${FEATURES__DATASET_REVIEW:-true}
FEATURES__HEALTH_MONITOR: \${FEATURES__HEALTH_MONITOR:-true}
CERTS_PATH: /opt/certs
ports:
- "\${BACKEND_HOST_PORT:-8001}:8000"
volumes:
- ./storage:/app/storage
- \${CERTS_PATH:-./certs}:/opt/certs:ro
frontend:
image: ${frontend_tag}
pull_policy: never
restart: unless-stopped
depends_on:
- backend
- agent
environment:
SSL_KEY_PASSPHRASE: \${SSL_KEY_PASSPHRASE:-}
ports:
- "\${FRONTEND_HOST_PORT:-8000}:80"
- "\${FRONTEND_SSL_PORT:-443}:443"
volumes:
- \${CERTS_PATH:-./certs}:/opt/certs:ro
agent:
image: ${agent_tag}
pull_policy: never
restart: unless-stopped
depends_on:
db:
condition: service_healthy
backend:
condition: service_started
environment:
LLM_API_KEY: \${LLM_API_KEY:-}
LLM_BASE_URL: \${LLM_BASE_URL:-https://api.openai.com/v1}
LLM_MODEL: \${LLM_MODEL:-gpt-4o}
FASTAPI_URL: http://backend:8000
AUTH_SECRET_KEY: \${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env}
SERVICE_JWT: \${SERVICE_JWT:?Set SERVICE_JWT in .env}
DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@db:5432/\${POSTGRES_DB:-ss_tools}
GRADIO_SERVER_PORT: 7860
GRADIO_ROOT_PATH: /api/agent/gradio
ports:
- "\${AGENT_HOST_PORT:-7860}:7860"
volumes:
- \${CERTS_PATH:-./certs}:/opt/certs:ro
volumes:
postgres_data:
DEPLOY
}
generate_manifest() {
local tag="$1" backend_tag="$2" frontend_tag="$3" agent_tag="$4" postgres_tag="$5"
local suffix="${6:-}"
local backend_id frontend_id agent_id postgres_id
backend_id="$(get_image_id "${backend_tag}")"
frontend_id="$(get_image_id "${frontend_tag}")"
agent_id="$(get_image_id "${agent_tag}")"
postgres_id="$(get_image_id "${postgres_tag}")"
cat > "${DIST_ROOT}/manifest.${tag}${suffix}.txt" <<EOF
release_tag=${tag}
backend_image=${backend_tag}
backend_image_id=${backend_id}
backend_repo_digest=$(get_repo_digest "${backend_tag}")
backend_archive=superset-tools-backend.${tag}${suffix}.tar.xz
frontend_image=${frontend_tag}
frontend_image_id=${frontend_id}
frontend_repo_digest=$(get_repo_digest "${frontend_tag}")
frontend_archive=superset-tools-frontend.${tag}${suffix}.tar.xz
agent_image=${agent_tag}
agent_image_id=${agent_id}
agent_repo_digest=$(get_repo_digest "${agent_tag}")
agent_archive=superset-tools-agent.${tag}${suffix}.tar.xz
postgres_image=${postgres_tag}
postgres_image_id=${postgres_id}
postgres_repo_digest=$(get_repo_digest "${postgres_tag}")
postgres_archive=superset-tools-postgres.${tag}${suffix}.tar.xz
compose_file=docker-compose.enterprise-clean.yml
env_template=.env.enterprise-clean.example
env_bootstrap_fields=INITIAL_ADMIN_CREATE,INITIAL_ADMIN_USERNAME,INITIAL_ADMIN_PASSWORD,INITIAL_ADMIN_EMAIL
checksums_file=sha256sums.${tag}${suffix}.txt
generated_at_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EOF
cat > "${DIST_ROOT}/manifest.${tag}${suffix}.json" <<EOF
{
"release_tag": "${tag}",
"generated_at_utc": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"images": [
{
"role": "postgres",
"image": "${postgres_tag}",
"image_id": "${postgres_id}",
"repo_digest": "$(get_repo_digest "${postgres_tag}")",
"archive": "superset-tools-postgres.${tag}${suffix}.tar.xz"
},
{
"role": "backend",
"image": "${backend_tag}",
"image_id": "${backend_id}",
"repo_digest": "$(get_repo_digest "${backend_tag}")",
"archive": "superset-tools-backend.${tag}${suffix}.tar.xz"
},
{
"role": "frontend",
"image": "${frontend_tag}",
"image_id": "${frontend_id}",
"repo_digest": "$(get_repo_digest "${frontend_tag}")",
"archive": "superset-tools-frontend.${tag}${suffix}.tar.xz"
},
{
"role": "agent",
"image": "${agent_tag}",
"image_id": "${agent_id}",
"repo_digest": "$(get_repo_digest "${agent_tag}")",
"archive": "superset-tools-agent.${tag}${suffix}.tar.xz"
}
],
"compose_file": "docker-compose.enterprise-clean.yml",
"env_template": ".env.enterprise-clean.example",
"env_bootstrap_fields": [
"INITIAL_ADMIN_CREATE",
"INITIAL_ADMIN_USERNAME",
"INITIAL_ADMIN_PASSWORD",
"INITIAL_ADMIN_EMAIL"
],
"checksums_file": "sha256sums.${tag}${suffix}.txt"
}
EOF
}
# ======================================================================
# GENERATE BUNDLE ENV TEMPLATE (with bundled postgres defaults)
# ======================================================================
generate_bundle_env() {
local tag="$1"
cat > "${DIST_ROOT}/.env.enterprise-clean.example" << 'ENV'
# #region env.enterprise-clean.bundle [C:2] [TYPE Module] [SEMANTICS env,docker,bundle]
# @BRIEF Переменные окружения для docker-compose.enterprise-clean.yml (bundled postgres).
# Все образы предварительно загружены через xz -dc ... | docker load.
# PostgreSQL запускается в контейнере (сервис db) — внешний БД не требуется.
# @LAYER Infrastructure
# #endregion env.enterprise-clean.bundle
# ======================================================================
# PostgreSQL (контейнер) — настройки БД
# ======================================================================
POSTGRES_DB=ss_tools
POSTGRES_USER=postgres
POSTGRES_PASSWORD=change-me
POSTGRES_HOST_PORT=5432
# ======================================================================
# Порты хоста
# ======================================================================
BACKEND_HOST_PORT=8001
FRONTEND_HOST_PORT=8000
FRONTEND_SSL_PORT=443
AGENT_HOST_PORT=7860
# ======================================================================
# Безопасность (ОБЯЗАТЕЛЬНО)
# ======================================================================
# JWT-ключ подписи токенов — единый для backend и agent.
# Сгенерировать: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
AUTH_SECRET_KEY=change-me-to-a-random-secret-32-chars-min
# Fernet-ключ шифрования паролей подключений и API-ключей.
# Сгенерировать: python3 -c "import base64,os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())"
ENCRYPTION_KEY=change-me-generate-a-fernet-key=
# Сервисный токен для agent→backend вызовов.
# Сгенерировать: python3 -c "import secrets; print('svc-' + secrets.token_urlsafe(24))"
SERVICE_JWT=agent-service-secret
# ======================================================================
# Сертификаты (корпоративные, опционально)
# ======================================================================
CERTS_PATH=./certs
SSL_KEY_PASSPHRASE=
# ======================================================================
# LLM / AI провайдеры
# ======================================================================
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
LLM_API_KEY=
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o
# ======================================================================
# Логирование
# ======================================================================
ENABLE_BELIEF_STATE_LOGGING=true
TASK_LOG_LEVEL=INFO
# ======================================================================
# Admin bootstrap (первый запуск)
# ======================================================================
INITIAL_ADMIN_CREATE=false
INITIAL_ADMIN_USERNAME=admin
INITIAL_ADMIN_PASSWORD=change-me
INITIAL_ADMIN_EMAIL=
# ======================================================================
# CORS / Безопасность деплоя
# ======================================================================
ALLOWED_ORIGINS=http://localhost:8000
FORCE_HTTPS=false
APP_TIMEZONE=Europe/Moscow
# ======================================================================
# Features
# ======================================================================
FEATURES__DATASET_REVIEW=true
FEATURES__HEALTH_MONITOR=true
ENV
echo "[bundle] ✅ Generated bundle env template: ${DIST_ROOT}/.env.enterprise-clean.example"
}
bundle_release() {
if [[ $# -lt 1 ]]; then
echo "Error: tag is required."
echo "Usage: ./build.sh bundle <tag> (e.g. v1.0.0)"
exit 1
fi
local tag="$1"
local backend_tag="superset-tools-backend:${tag}"
local frontend_tag="superset-tools-frontend:${tag}"
local agent_tag="superset-tools-agent:${tag}"
local postgres_tag="superset-tools-postgres:${tag}"
local postgres_source="${POSTGRES_IMAGE:-postgres:16-alpine}"
mkdir -p "$DIST_ROOT"
# Build application images
build_backend "${tag}"
build_frontend "${tag}"
build_agent "${tag}"
# Pull and tag postgres image
echo "[bundle] Pulling postgres image ${postgres_source}..."
docker pull "${postgres_source}"
docker tag "${postgres_source}" "${postgres_tag}"
# Release gate: fresh PostgreSQL migration + backend boot + restart must pass.
run_bundle_smoke "${backend_tag}" "${postgres_tag}"
# Export .tar.xz archives
echo "[bundle] Exporting .tar.xz archives..."
export_image "${backend_tag}" "${DIST_ROOT}/superset-tools-backend.${tag}.tar.xz"
export_image "${frontend_tag}" "${DIST_ROOT}/superset-tools-frontend.${tag}.tar.xz"
export_image "${agent_tag}" "${DIST_ROOT}/superset-tools-agent.${tag}.tar.xz"
export_image "${postgres_tag}" "${DIST_ROOT}/superset-tools-postgres.${tag}.tar.xz"
# Checksums
echo "[bundle] Calculating checksums..."
(cd "${DIST_ROOT}" && sha256sum \
"superset-tools-backend.${tag}.tar.xz" \
"superset-tools-frontend.${tag}.tar.xz" \
"superset-tools-agent.${tag}.tar.xz" \
"superset-tools-postgres.${tag}.tar.xz" > "sha256sums.${tag}.txt")
# Generate deploy compose + manifest
generate_deploy_compose "${tag}" "${backend_tag}" "${frontend_tag}" "${agent_tag}" "${postgres_tag}"
generate_manifest "${tag}" "${backend_tag}" "${frontend_tag}" "${agent_tag}" "${postgres_tag}"
# Generate env template for the bundle
generate_bundle_env "${tag}"
echo "[bundle] ✅ Bundle created in ${DIST_ROOT}"
echo " # Load images (decompress stream):"
echo " xz -dc ${DIST_ROOT}/superset-tools-backend.${tag}.tar.xz | docker load"
echo " xz -dc ${DIST_ROOT}/superset-tools-frontend.${tag}.tar.xz | docker load"
echo " xz -dc ${DIST_ROOT}/superset-tools-agent.${tag}.tar.xz | docker load"
echo " xz -dc ${DIST_ROOT}/superset-tools-postgres.${tag}.tar.xz | docker load"
echo " # Edit .env.enterprise-clean.example -> .env.enterprise-clean, then:"
echo " docker compose -f ${DIST_ROOT}/docker-compose.enterprise-clean.yml up -d"
echo ""
echo " NOTE: Bundle includes postgres container (db) — no external PostgreSQL needed."
echo " Corporate certs: mount .crt files via CERTS_PATH=./certs (optional)."
}
# ======================================================================
# BUNDLE: lightweight all-in-one (single image, no Playwright, <200 MB)
# ======================================================================
bundle_light() {
if [[ $# -lt 1 ]]; then
echo "Error: tag is required."
echo "Usage: ./build.sh bundle:light <tag> (e.g. v1.0.0)"
exit 1
fi
local tag="$1"
local image_tag="superset-tools:${tag}"
# Git commit hash for APP_VERSION (e.g. "0.5.2+58d06fb2")
local git_commit
git_commit="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
local app_version="${tag}+${git_commit}"
mkdir -p "$DIST_ROOT"
echo "[bundle:light] Building all-in-one image ${image_tag}..."
docker build --build-arg "APP_VERSION=${app_version}" -f docker/all-in-one.Dockerfile -t "${image_tag}" .
echo "[bundle:light] Exporting .tar.xz..."
docker save "${image_tag}" | xz -T0 -3 > "${DIST_ROOT}/superset-tools.${tag}.tar.xz"
echo "[bundle:light] Calculating checksums..."
(
cd "${DIST_ROOT}"
sha256sum "superset-tools.${tag}.tar.xz" > "sha256sums-light.${tag}.txt"
)
local img_id repo_digest
img_id="$(get_image_id "${image_tag}")"
repo_digest="$(get_repo_digest "${image_tag}")"
cat > "${DIST_ROOT}/manifest-light.${tag}.txt" <<EOF
release_tag=${tag}
image=${image_tag}
image_id=${img_id}
repo_digest=${repo_digest}
archive=superset-tools.${tag}.tar.xz
compose_file=docker-compose.light.yml
checksums_file=sha256sums-light.${tag}.txt
generated_at_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EOF
cat > "${DIST_ROOT}/manifest-light.${tag}.json" <<EOF
{
"release_tag": "${tag}",
"generated_at_utc": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"image": "${image_tag}",
"image_id": "${img_id}",
"repo_digest": "${repo_digest}",
"archive": "superset-tools.${tag}.tar.xz",
"compose_file": "docker-compose.light.yml",
"checksums_file": "sha256sums-light.${tag}.txt"
}
EOF
cat > "${DIST_ROOT}/docker-compose.light.yml" <<'COMPOSE'
services:
db:
image: postgres:16-alpine
pull_policy: never
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-ss_tools}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
ports:
- "${POSTGRES_HOST_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-ss_tools}"]
interval: 10s
timeout: 5s
retries: 10
app:
image: ${IMAGE:-superset-tools:latest}
pull_policy: never
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-ss_tools}
BACKEND_PORT: 8000
INITIAL_ADMIN_CREATE: ${INITIAL_ADMIN_CREATE:-false}
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin}
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-}
INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
FEATURES__DATASET_REVIEW: ${FEATURES__DATASET_REVIEW:-true}
FEATURES__HEALTH_MONITOR: ${FEATURES__HEALTH_MONITOR:-true}
ports:
- "${APP_HOST_PORT:-8000}:8000"
volumes:
- ./storage:/app/storage
volumes:
postgres_data:
COMPOSE
cp "${SCRIPT_DIR}/.env.enterprise-clean.example" "${DIST_ROOT}/.env.light.example"
echo "[bundle:light] ✅ Lightweight bundle created in ${DIST_ROOT}"
echo " # Load image:"
echo " xz -dc ${DIST_ROOT}/superset-tools.${tag}.tar.xz | docker load"
echo " docker compose -f ${DIST_ROOT}/docker-compose.light.yml up"
echo ""
echo " Single container (without compose):"
echo " docker run -p 8000:8000 --env-file backend/.env ${image_tag}"
}
# ======================================================================
# BUNDLE: enterprise with embeddings (agent + sentence-transformers)
# ======================================================================
bundle_embeddings() {
if [[ $# -lt 1 ]]; then
echo "Error: tag is required."
echo "Usage: ./build.sh bundle:embeddings <tag> (e.g. v1.0.0)"
exit 1
fi
local tag="$1"
local suffix="-embeddings"
local backend_tag="superset-tools-backend:${tag}${suffix}"
local frontend_tag="superset-tools-frontend:${tag}${suffix}"
local agent_tag="superset-tools-agent:${tag}${suffix}"
local postgres_tag="superset-tools-postgres:${tag}${suffix}"
local postgres_source="${POSTGRES_IMAGE:-postgres:16-alpine}"
mkdir -p "$DIST_ROOT"
# Build application images
build_backend "${tag}${suffix}"
build_frontend "${tag}${suffix}"
build_agent_embeddings "${tag}${suffix}"
# Pull and tag postgres image
echo "[bundle:embeddings] Pulling postgres image ${postgres_source}..."
docker pull "${postgres_source}"
docker tag "${postgres_source}" "${postgres_tag}"
# Release gate: fresh PostgreSQL migration + backend boot + restart must pass.
run_bundle_smoke "${backend_tag}" "${postgres_tag}"
# Export .tar.xz archives
echo "[bundle:embeddings] Exporting .tar.xz archives..."
export_image "${backend_tag}" "${DIST_ROOT}/superset-tools-backend.${tag}${suffix}.tar.xz"
export_image "${frontend_tag}" "${DIST_ROOT}/superset-tools-frontend.${tag}${suffix}.tar.xz"
export_image "${agent_tag}" "${DIST_ROOT}/superset-tools-agent.${tag}${suffix}.tar.xz"
export_image "${postgres_tag}" "${DIST_ROOT}/superset-tools-postgres.${tag}${suffix}.tar.xz"
# Checksums
echo "[bundle:embeddings] Calculating checksums..."
(cd "${DIST_ROOT}" && sha256sum \
"superset-tools-backend.${tag}${suffix}.tar.xz" \
"superset-tools-frontend.${tag}${suffix}.tar.xz" \
"superset-tools-agent.${tag}${suffix}.tar.xz" \
"superset-tools-postgres.${tag}${suffix}.tar.xz" > "sha256sums.${tag}${suffix}.txt")
# Generate deploy compose + manifest
generate_deploy_compose "${tag}${suffix}" "${backend_tag}" "${frontend_tag}" "${agent_tag}" "${postgres_tag}"
generate_manifest "${tag}" "${backend_tag}" "${frontend_tag}" "${agent_tag}" "${postgres_tag}" "${suffix}"
# Add variant field to manifest for embeddings
cat >> "${DIST_ROOT}/manifest.${tag}${suffix}.txt" <<EOF
agent_embeddings=true
EOF
# Generate env template for the bundle
generate_bundle_env "${tag}${suffix}"
echo "[bundle:embeddings] ✅ Embeddings bundle created in ${DIST_ROOT}"
echo " # Load images:"
echo " xz -dc ${DIST_ROOT}/superset-tools-backend.${tag}${suffix}.tar.xz | docker load"
echo " xz -dc ${DIST_ROOT}/superset-tools-frontend.${tag}${suffix}.tar.xz | docker load"
echo " xz -dc ${DIST_ROOT}/superset-tools-agent.${tag}${suffix}.tar.xz | docker load"
echo " xz -dc ${DIST_ROOT}/superset-tools-postgres.${tag}${suffix}.tar.xz | docker load"
echo " # Edit .env.enterprise-clean.example -> .env.enterprise-clean, then:"
echo " docker compose -f ${DIST_ROOT}/docker-compose.enterprise-clean.yml up -d"
echo ""
echo " NOTE: Agent image includes sentence-transformers+torch (~500 MB+)."
echo " NOTE: Bundle includes postgres container (db) — no external PostgreSQL needed."
echo " Use 'bundle' for the default slim agent (no embedding model)."
}
# ======================================================================
# HELP
# ======================================================================
show_help() {
cat <<'HELP'
Usage: ./build.sh <command> [options]
Commands for local docker:
up [profile] Build and start services (default: current)
down [profile] Stop and remove containers
restart [profile] Rebuild and restart services
logs [profile] Tail logs from all services
status Show running containers for all profiles
Commands for individual image build (docker build only, no export):
build:backend <tag> Build backend image (superset-tools-backend:<tag>)
build:frontend <tag> Build frontend image (superset-tools-frontend:<tag>)
build:agent <tag> Build agent image (superset-tools-agent:<tag>)
Commands for single-image bundle (build + .tar.xz export):
bundle:backend <tag> Build + export backend .tar.xz only
bundle:frontend <tag> Build + export frontend .tar.xz only
bundle:agent <tag> Build + export agent .tar.xz only
Release verification:
smoke:bundle <backend-image> <postgres-image>
Run clean PostgreSQL migration + backend boot/restart gate.
Commands for full bundles (backend + frontend + agent + postgres):
bundle <tag> Default enterprise bundle (4 .tar.xz archives).
Includes: backend + frontend + agent (slim) + postgres (16-alpine).
PostgreSQL работает в докер-контейнере — внешний БД не требуется.
Before export: clean PostgreSQL migration + backend boot/restart smoke gate.
REQUIRED: AUTH_SECRET_KEY, ENCRYPTION_KEY, POSTGRES_PASSWORD, SERVICE_JWT.
Example: ./build.sh bundle v1.0.0
bundle:embeddings <tag>
Enterprise bundle WITH semantic embedding routing.
Agent built WITH sentence-transformers+torch (larger image).
Includes: backend + frontend + agent (embeddings) + postgres.
Before export: clean PostgreSQL migration + backend boot/restart smoke gate.
REQUIRED: AUTH_SECRET_KEY, ENCRYPTION_KEY, POSTGRES_PASSWORD, SERVICE_JWT.
Example: ./build.sh bundle:embeddings v1.0.0
bundle:light <tag> Lightweight all-in-one image (.tar.xz, <200 MB, no Playwright)
Example: ./build.sh bundle:light v1.0.0
Profiles: current (default), master, enterprise-clean
Backward-compatible shortcuts:
./build.sh -> ./build.sh up current
./build.sh master -> ./build.sh up master
./build.sh enterprise-clean -> ./build.sh up enterprise-clean
Output:
Local docker: docker compose -p superset-tools-{profile}
Bundle artifacts: dist/docker/
Notes:
- 'current' and 'master' use docker-compose.yml (dev, with local postgres container).
- 'enterprise-clean' uses docker-compose.enterprise-clean.yml (build from source,
external postgres, corporate certs, optional nginx SSL).
- Individual build commands (build:backend|frontend|agent) produce local docker
images without touching dist/docker/.
- Single-image bundle commands (bundle:backend|frontend|agent) build + export
only one .tar.xz, useful for incremental deployments.
- Bundle commands (bundle, bundle:embeddings) now include postgres:16-alpine
as superset-tools-postgres:<tag> — loaded together with other images
via 'xz -dc ... | docker load'. No external PostgreSQL required.
HELP
}
# ======================================================================
# MAIN DISPATCH
# ======================================================================
main() {
local CMD="${1:-up}"
shift 2>/dev/null || true
case "$CMD" in
up|down|restart|logs|status|help|-h|--help|bundle|bundle:embeddings|bundle:light|smoke:bundle|\
build:backend|build:frontend|build:agent|\
bundle:backend|bundle:frontend|bundle:agent)
# Valid commands — proceed
;;
*)
# Backward compat: treat unknown arg as profile for "up"
local profile="$CMD"
CMD="up"
set -- "$profile" "$@"
;;
esac
case "$CMD" in
up) compose_up "${1:-current}" ;;
down) compose_down "${1:-current}" ;;
restart) compose_restart "${1:-current}" ;;
logs) compose_logs "${1:-current}" ;;
bundle) bundle_release "$@" ;;
bundle:embeddings) bundle_embeddings "$@" ;;
bundle:light) bundle_light "$@" ;;
smoke:bundle) run_bundle_smoke "$@" ;;
build:backend) build_backend "$@" ;;
build:frontend) build_frontend "$@" ;;
build:agent) build_agent "$@" ;;
bundle:backend) bundle_single_backend "$@" ;;
bundle:frontend) bundle_single_frontend "$@" ;;
bundle:agent) bundle_single_agent "$@" ;;
status) compose_status ;;
help|-h|--help) show_help ;;
*)
echo "Error: unknown command '$CMD'. See ./build.sh help"
exit 1
;;
esac
}
main "$@"