#!/bin/sh set -eu COMMAND="up" if [ "$#" -gt 0 ] && [ "${1#-}" = "$1" ]; then COMMAND="$1" shift fi SKILLHUB_REF="${SKILLHUB_REF:-main}" SKILLHUB_HOME_DEFAULT="${TMPDIR:-/tmp}/skillhub-runtime" SKILLHUB_HOME="${SKILLHUB_HOME:-$SKILLHUB_HOME_DEFAULT}" SKILLHUB_VERSION_VALUE="${SKILLHUB_VERSION:-}" SKILLHUB_PUBLIC_BASE_URL_VALUE="${SKILLHUB_PUBLIC_BASE_URL:-}" SKILLHUB_ALIYUN_REGISTRY="${SKILLHUB_ALIYUN_REGISTRY:-crpi-ptu2rqimrigtq0qx.cn-hangzhou.personal.cr.aliyuncs.com}" SKILLHUB_ALIYUN_NAMESPACE="${SKILLHUB_ALIYUN_NAMESPACE:-skill_hub}" SKILLHUB_MIRROR_REGISTRY_VALUE="${SKILLHUB_MIRROR_REGISTRY:-}" SKILLHUB_SERVER_IMAGE_VALUE="${SKILLHUB_SERVER_IMAGE:-}" SKILLHUB_WEB_IMAGE_VALUE="${SKILLHUB_WEB_IMAGE:-}" SKILLHUB_SCANNER_IMAGE_VALUE="${SKILLHUB_SCANNER_IMAGE:-}" POSTGRES_IMAGE_VALUE="${POSTGRES_IMAGE:-}" REDIS_IMAGE_VALUE="${REDIS_IMAGE:-}" DISABLE_SCANNER=false USE_ALIYUN=false EXPLICIT_REF=false WEBSITE_ID="" ANALYTICS_UPSTREAM="" ANALYTICS_ADAPTER="umami" ANALYTICS_APP_ID="" BUSINESS_LOGS_ENDPOINT="" BUSINESS_LOGS_NETWORK="" while [ "$#" -gt 0 ]; do case "$1" in --version) [ "$#" -ge 2 ] || { echo "Missing value for --version" >&2; exit 1; } SKILLHUB_VERSION_VALUE="$2" shift 2 ;; --aliyun) if [ -z "$SKILLHUB_ALIYUN_REGISTRY" ] || [ -z "$SKILLHUB_ALIYUN_NAMESPACE" ]; then echo "SKILLHUB_ALIYUN_REGISTRY and SKILLHUB_ALIYUN_NAMESPACE must be configured for --aliyun" >&2 exit 1 fi SKILLHUB_MIRROR_REGISTRY_VALUE="${SKILLHUB_ALIYUN_REGISTRY%/}/${SKILLHUB_ALIYUN_NAMESPACE}" USE_ALIYUN=true shift ;; --mirror-registry) [ "$#" -ge 2 ] || { echo "Missing value for --mirror-registry" >&2; exit 1; } SKILLHUB_MIRROR_REGISTRY_VALUE="$2" shift 2 ;; --home) [ "$#" -ge 2 ] || { echo "Missing value for --home" >&2; exit 1; } SKILLHUB_HOME="$2" shift 2 ;; --ref) [ "$#" -ge 2 ] || { echo "Missing value for --ref" >&2; exit 1; } SKILLHUB_REF="$2" EXPLICIT_REF=true shift 2 ;; --server-image) [ "$#" -ge 2 ] || { echo "Missing value for --server-image" >&2; exit 1; } SKILLHUB_SERVER_IMAGE_VALUE="$2" shift 2 ;; --web-image) [ "$#" -ge 2 ] || { echo "Missing value for --web-image" >&2; exit 1; } SKILLHUB_WEB_IMAGE_VALUE="$2" shift 2 ;; --postgres-image) [ "$#" -ge 2 ] || { echo "Missing value for --postgres-image" >&2; exit 1; } POSTGRES_IMAGE_VALUE="$2" shift 2 ;; --redis-image) [ "$#" -ge 2 ] || { echo "Missing value for --redis-image" >&2; exit 1; } REDIS_IMAGE_VALUE="$2" shift 2 ;; --scanner-image) [ "$#" -ge 2 ] || { echo "Missing value for --scanner-image" >&2; exit 1; } SKILLHUB_SCANNER_IMAGE_VALUE="$2" shift 2 ;; --no-scanner) DISABLE_SCANNER=true shift ;; --public-url) [ "$#" -ge 2 ] || { echo "Missing value for --public-url" >&2; exit 1; } SKILLHUB_PUBLIC_BASE_URL_VALUE="$2" shift 2 ;; --website-id) [ "$#" -ge 2 ] || { echo "Missing value for --website-id" >&2; exit 1; } WEBSITE_ID="$2" shift 2 ;; --upstream) [ "$#" -ge 2 ] || { echo "Missing value for --upstream" >&2; exit 1; } ANALYTICS_UPSTREAM="$2" shift 2 ;; --adapter) [ "$#" -ge 2 ] || { echo "Missing value for --adapter" >&2; exit 1; } ANALYTICS_ADAPTER="$2" shift 2 ;; --app-id) [ "$#" -ge 2 ] || { echo "Missing value for --app-id" >&2; exit 1; } ANALYTICS_APP_ID="$2" shift 2 ;; --logs-endpoint) [ "$#" -ge 2 ] || { echo "Missing value for --logs-endpoint" >&2; exit 1; } BUSINESS_LOGS_ENDPOINT="$2" shift 2 ;; --logs-network) [ "$#" -ge 2 ] || { echo "Missing value for --logs-network" >&2; exit 1; } BUSINESS_LOGS_NETWORK="$2" shift 2 ;; --help|-h) cat < umami (default) or datafinder (with --app-id) --app-id Existing DataFinder application ID Business Logs (default off): business-logs-up Prepare/start optional forwarder and enable server collection First run creates business-logs/.env; configure downstream and retry business-logs-enable Apply .env.release to server; --logs-endpoint overrides receiver business-logs-disable Disable collection and recreate server; keep receiver and data business-logs-down Stop only the optional Collector; keep files and network business-logs-ps Show collection state and optional Collector status business-logs-logs Follow optional Collector logs --logs-endpoint Full OTLP/HTTP Logs URL (no credentials/query/fragment) --logs-network Attach server to this external private network Options: --version Use a specific image tag, for example v0.1.0 --aliyun Use the configured Aliyun mirror registry --mirror-registry Use mirrored images from / --home Store runtime files in a specific directory --ref Download runtime files from a specific Git ref --public-url Public access URL (e.g. https://skill.example.com) --server-image Override backend image repository --web-image Override frontend image repository --scanner-image Override scanner image repository --postgres-image Override PostgreSQL image --redis-image Override Redis image --no-scanner Do not start the skill-scanner service EOF exit 0 ;; *) echo "Unsupported argument: $1" >&2 exit 1 ;; esac done if [ "$USE_ALIYUN" = "true" ]; then SKILLHUB_RAW_BASE="${SKILLHUB_RAW_BASE:-https://imageless.oss-cn-beijing.aliyuncs.com}" RUNTIME_SCRIPT_URL="$SKILLHUB_RAW_BASE/runtime.sh" echo "Using Aliyun OSS for runtime files: $SKILLHUB_RAW_BASE" else SKILLHUB_RAW_BASE="${SKILLHUB_RAW_BASE:-https://raw.githubusercontent.com/iflytek/skillhub/$SKILLHUB_REF}" RUNTIME_SCRIPT_URL="$SKILLHUB_RAW_BASE/scripts/runtime.sh" echo "Using GitHub raw for runtime files: $SKILLHUB_RAW_BASE" fi COMPOSE_FILE="$SKILLHUB_HOME/compose.release.yml" OVERLAY_FILE="$SKILLHUB_HOME/compose.release.analytics.yml" ANALYTICS_HOME="$SKILLHUB_HOME/analytics" ANALYTICS_COMPOSE_FILE="$ANALYTICS_HOME/compose.analytics-umami.yml" ANALYTICS_ENV_FILE="$ANALYTICS_HOME/.env" ENV_EXAMPLE_FILE="$SKILLHUB_HOME/.env.release.example" ENV_FILE="$SKILLHUB_HOME/.env.release" BUSINESS_LOGS_HOME="$SKILLHUB_HOME/business-logs" BUSINESS_LOGS_ENV_FILE="$BUSINESS_LOGS_HOME/.env" BUSINESS_LOGS_COMPOSE_FILE="$BUSINESS_LOGS_HOME/compose.business-logs-collector.yml" BUSINESS_LOGS_OVERLAY_FILE="$SKILLHUB_HOME/compose.release.business-logs.yml" find_compose() { if docker compose version >/dev/null 2>&1; then echo "docker compose" return 0 fi if command -v docker-compose >/dev/null 2>&1; then echo "docker-compose" return 0 fi echo "Docker Compose is required." >&2 exit 1 } download_file() { src="$1" dest="$2" tmp="$dest.tmp" curl -fsSL "$src" -o "$tmp" mv "$tmp" "$dest" } set_env_value() { key="$1" value="$2" target_env_file="${3:-$ENV_FILE}" if [ ! -f "$target_env_file" ]; then return 0 fi tmp="$target_env_file.tmp" found=false old_umask="$(umask)" umask 077 { while IFS= read -r line || [ -n "$line" ]; do case "$line" in "$key="*) printf '%s=%s\n' "$key" "$value" found=true ;; *) printf '%s\n' "$line" ;; esac done <"$target_env_file" if [ "$found" = "false" ]; then printf '%s=%s\n' "$key" "$value" fi } >"$tmp" umask "$old_umask" mv "$tmp" "$target_env_file" chmod 600 "$target_env_file" } secure_env_file() { if [ -f "$ENV_FILE" ]; then chmod 600 "$ENV_FILE" fi } # Older examples supplied an empty JavaMail trust list, which rejects SSL SMTP # hosts instead of selecting the JVM trust store. Preserve every nonempty value # and all unrelated dotenv text; never evaluate credentials as shell code. migrate_empty_smtp_trust_override() { case "${SPRING_MAIL_PROPERTIES_MAIL_SMTP_SSL_TRUST:-}" in *[![:space:]]*) ;; *) unset SPRING_MAIL_PROPERTIES_MAIL_SMTP_SSL_TRUST ;; esac migration_umask="$(umask)" umask 077 if migration_tmp="$(mktemp "$ENV_FILE.smtp.XXXXXX")"; then : else migration_status=$? umask "$migration_umask" return "$migration_status" fi if awk ' function empty_trust(line, value, quote, tail, closing, content, rest) { if (line !~ /^[[:space:]]*(export[[:space:]]+)?SPRING_MAIL_PROPERTIES_MAIL_SMTP_SSL_TRUST[[:space:]]*=/) return 0 value = line sub(/^[^=]*=/, "", value) sub(/^[[:space:]]*/, "", value) if (value ~ /^($|#)/) return 1 quote = substr(value, 1, 1) if (quote != "\"" && quote != sprintf("%c", 39)) return 0 tail = substr(value, 2) closing = index(tail, quote) if (!closing) return 0 content = substr(tail, 1, closing - 1) rest = substr(tail, closing + 1) return content ~ /^[[:space:]]*$/ && rest ~ /^[[:space:]]*(#.*)?$/ } empty_trust($0) { removed = 1; next } { print } END { if (!removed) exit 3 } ' "$ENV_FILE" >"$migration_tmp"; then umask "$migration_umask" mv "$migration_tmp" "$ENV_FILE" chmod 600 "$ENV_FILE" echo "Removed empty SMTP SSL trust override; JVM certificate verification remains enabled." else migration_status=$? umask "$migration_umask" rm -f "$migration_tmp" [ "$migration_status" -eq 3 ] || return "$migration_status" fi } get_env_value() { key="$1" default_value="${2:-}" value="$(grep "^$key=" "${3:-$ENV_FILE}" | tail -n 1 | cut -d= -f2- || true)" if [ -n "$value" ]; then printf '%s' "$value" else printf '%s' "$default_value" fi } generate_secret() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 32 return 0 fi if [ -r /dev/urandom ] && command -v od >/dev/null 2>&1; then dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' return 0 fi echo "Unable to generate SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET. Install openssl or configure it manually." >&2 exit 1 } is_placeholder_secret() { case "$1" in ""|change-me-in-production|replace-me|replace-with-random-download-secret-32-bytes|TODO*|todo*|replace*) return 0 ;; *) return 1 ;; esac } ensure_anonymous_download_secret() { secret="$(get_env_value "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET" "")" if ! is_placeholder_secret "$secret" && [ "${#secret}" -ge 32 ]; then return 0 fi set_env_value "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET" "$(generate_secret)" echo "Generated SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET in $ENV_FILE" } wait_for_postgres_ready() { postgres_user="$1" postgres_db="$2" attempt=1 while [ "$attempt" -le 60 ]; do if run_compose exec -T postgres pg_isready -U "$postgres_user" -d "$postgres_db" >/dev/null 2>&1; then return 0 fi attempt=$((attempt + 1)) sleep 2 done echo "PostgreSQL did not become ready in time." >&2 run_compose logs postgres >&2 || true exit 1 } wait_for_redis_ready() { attempt=1 while [ "$attempt" -le 60 ]; do if run_compose exec -T redis redis-cli ping >/dev/null 2>&1; then return 0 fi attempt=$((attempt + 1)) sleep 2 done echo "Redis did not become ready in time." >&2 run_compose logs redis >&2 || true exit 1 } ensure_postgres_password_matches_env() { postgres_user="$(get_env_value "POSTGRES_USER" "skillhub")" postgres_db="$(get_env_value "POSTGRES_DB" "skillhub")" postgres_password="$(get_env_value "POSTGRES_PASSWORD" "skillhub_demo")" if [ -z "$postgres_password" ]; then echo "POSTGRES_PASSWORD must not be empty." >&2 exit 1 fi wait_for_postgres_ready "$postgres_user" "$postgres_db" run_compose exec -T postgres \ psql -U "$postgres_user" -d "$postgres_db" \ -v ON_ERROR_STOP=1 \ -v password="$postgres_password" <<'SQL' >/dev/null SELECT format('ALTER ROLE %I WITH PASSWORD %L', current_user, :'password'); \gexec SQL run_compose exec -T -e "PGPASSWORD=$postgres_password" postgres \ psql -h 127.0.0.1 -U "$postgres_user" -d "$postgres_db" \ -v ON_ERROR_STOP=1 \ -c 'select current_user;' >/dev/null } prepare_runtime_files() { # Migrate legacy identity before creating a replacement release file. Failed # migration must not leave fresh defaults that a later retry could trust. if [ -f "$ANALYTICS_ENV_FILE" ]; then ensure_analytics_identity fi mkdir -p "$SKILLHUB_HOME" download_file "$SKILLHUB_RAW_BASE/compose.release.yml" "$COMPOSE_FILE" # GitHub and OSS both publish this root-level overlay; fetch only when used. if [ "$COMMAND" = "analytics-enable" ] || [ -f "$OVERLAY_FILE" ]; then download_file "$SKILLHUB_RAW_BASE/compose.release.analytics.yml" "$OVERLAY_FILE" fi download_file "$SKILLHUB_RAW_BASE/.env.release.example" "$ENV_EXAMPLE_FILE" if [ ! -f "$ENV_FILE" ]; then old_umask="$(umask)" umask 077 cp "$ENV_EXAMPLE_FILE" "$ENV_FILE" umask "$old_umask" fi secure_env_file if [ "$COMMAND" = "business-logs-enable" ] || [ "$COMMAND" = "business-logs-up" ] || [ "$(get_env_value SKILLHUB_BUSINESS_LOGS_NETWORK_ENABLED false)" = "true" ]; then download_file "$SKILLHUB_RAW_BASE/compose.release.business-logs.yml" "$BUSINESS_LOGS_OVERLAY_FILE" fi migrate_empty_smtp_trust_override if [ -n "$SKILLHUB_MIRROR_REGISTRY_VALUE" ]; then mirror_registry="${SKILLHUB_MIRROR_REGISTRY_VALUE%/}" if [ -z "$POSTGRES_IMAGE_VALUE" ]; then POSTGRES_IMAGE_VALUE="$mirror_registry/postgres:16-alpine" fi if [ -z "$REDIS_IMAGE_VALUE" ]; then REDIS_IMAGE_VALUE="$mirror_registry/redis:7-alpine" fi if [ -z "$SKILLHUB_SERVER_IMAGE_VALUE" ]; then SKILLHUB_SERVER_IMAGE_VALUE="$mirror_registry/skillhub-server" fi if [ -z "$SKILLHUB_WEB_IMAGE_VALUE" ]; then SKILLHUB_WEB_IMAGE_VALUE="$mirror_registry/skillhub-web" fi if [ -z "$SKILLHUB_SCANNER_IMAGE_VALUE" ]; then SKILLHUB_SCANNER_IMAGE_VALUE="$mirror_registry/skillhub-scanner" fi fi if [ -n "$SKILLHUB_VERSION_VALUE" ]; then set_env_value "SKILLHUB_VERSION" "$SKILLHUB_VERSION_VALUE" fi if [ -n "$POSTGRES_IMAGE_VALUE" ]; then set_env_value "POSTGRES_IMAGE" "$POSTGRES_IMAGE_VALUE" fi if [ -n "$REDIS_IMAGE_VALUE" ]; then set_env_value "REDIS_IMAGE" "$REDIS_IMAGE_VALUE" fi if [ -n "$SKILLHUB_SERVER_IMAGE_VALUE" ]; then set_env_value "SKILLHUB_SERVER_IMAGE" "$SKILLHUB_SERVER_IMAGE_VALUE" fi if [ -n "$SKILLHUB_WEB_IMAGE_VALUE" ]; then set_env_value "SKILLHUB_WEB_IMAGE" "$SKILLHUB_WEB_IMAGE_VALUE" fi if [ -n "$SKILLHUB_SCANNER_IMAGE_VALUE" ]; then set_env_value "SKILLHUB_SCANNER_IMAGE" "$SKILLHUB_SCANNER_IMAGE_VALUE" fi if [ -n "$SKILLHUB_PUBLIC_BASE_URL_VALUE" ]; then set_env_value "SKILLHUB_PUBLIC_BASE_URL" "$SKILLHUB_PUBLIC_BASE_URL_VALUE" fi if [ "$DISABLE_SCANNER" = "true" ]; then set_env_value "SKILLHUB_SECURITY_SCANNER_ENABLED" "false" fi ensure_anonymous_download_secret } run_compose() { compose_cmd="$(find_compose)" if [ "$(get_env_value SKILLHUB_ANALYTICS_NETWORK_ENABLED false)" = "true" ]; then ensure_analytics_network set_env_value SKILLHUB_ANALYTICS_NETWORK "$SKILLHUB_ANALYTICS_NETWORK" # Preserve the normal default network and attach only Web to analytics. set -- -f "$OVERLAY_FILE" "$@" fi if [ "$(get_env_value SKILLHUB_BUSINESS_LOGS_NETWORK_ENABLED false)" = "true" ]; then ensure_business_logs_network set -- -f "$BUSINESS_LOGS_OVERLAY_FILE" "$@" fi # shellcheck disable=SC2086 $compose_cmd --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@" } ensure_business_logs_network() { SKILLHUB_BUSINESS_LOGS_NETWORK="$(get_env_value SKILLHUB_BUSINESS_LOGS_NETWORK skillhub-business-logs)" export SKILLHUB_BUSINESS_LOGS_NETWORK if ! docker network inspect "$SKILLHUB_BUSINESS_LOGS_NETWORK" >/dev/null 2>&1; then docker network create "$SKILLHUB_BUSINESS_LOGS_NETWORK" >/dev/null fi } prepare_business_logs_files() { mkdir -p "$BUSINESS_LOGS_HOME" # Root-level names work unchanged on both versioned GitHub raw and flat OSS. download_file "$SKILLHUB_RAW_BASE/compose.business-logs-collector.yml" "$BUSINESS_LOGS_COMPOSE_FILE" download_file "$SKILLHUB_RAW_BASE/business-logs-collector.yaml" "$BUSINESS_LOGS_HOME/business-logs-collector.yaml" if [ ! -f "$BUSINESS_LOGS_ENV_FILE" ]; then old_umask="$(umask)" umask 077 { printf 'SKILLHUB_BUSINESS_LOGS_PROJECT=%s\n' "$(get_env_value SKILLHUB_BUSINESS_LOGS_PROJECT skillhub-business-logs)" printf 'SKILLHUB_BUSINESS_LOGS_NETWORK=%s\n' "${BUSINESS_LOGS_NETWORK:-$(get_env_value SKILLHUB_BUSINESS_LOGS_NETWORK skillhub-business-logs)}" printf 'SKILLHUB_BUSINESS_LOGS_DOWNSTREAM_ENDPOINT=\n' printf "SKILLHUB_BUSINESS_LOGS_DOWNSTREAM_HEADERS='{}'\n" } >"$BUSINESS_LOGS_ENV_FILE" umask "$old_umask" fi chmod 600 "$BUSINESS_LOGS_ENV_FILE" } run_business_logs_compose() { # The independent file owns its identity. Inherited Compose variables cannot redirect it. SKILLHUB_BUSINESS_LOGS_PROJECT="$(get_env_value SKILLHUB_BUSINESS_LOGS_PROJECT skillhub-business-logs "$BUSINESS_LOGS_ENV_FILE")" SKILLHUB_BUSINESS_LOGS_NETWORK="$(get_env_value SKILLHUB_BUSINESS_LOGS_NETWORK skillhub-business-logs "$BUSINESS_LOGS_ENV_FILE")" export SKILLHUB_BUSINESS_LOGS_NETWORK compose_cmd="$(find_compose)" # shellcheck disable=SC2086 $compose_cmd -p "$SKILLHUB_BUSINESS_LOGS_PROJECT" --env-file "$BUSINESS_LOGS_ENV_FILE" -f "$BUSINESS_LOGS_COMPOSE_FILE" "$@" } validate_business_logs_endpoint() { case "$1" in http://*/*|https://*/*) ;; *) echo "Business Logs requires a full HTTP(S) Logs URL" >&2; exit 1 ;; esac case "$1" in *'@'*|*'?'*|*'#'*|*[[:space:]]*|*'\'*) echo "Business Logs URL must not contain credentials, query, fragment or whitespace" >&2; exit 1 ;; esac } enable_business_logs() { receiver_endpoint="${BUSINESS_LOGS_ENDPOINT:-$(get_env_value SKILLHUB_BUSINESS_LOGS_ENDPOINT)}" validate_business_logs_endpoint "$receiver_endpoint" if [ -n "$BUSINESS_LOGS_NETWORK" ]; then case "$BUSINESS_LOGS_NETWORK" in *[!A-Za-z0-9_.-]*|[-.]* ) echo "Invalid Business Logs network name" >&2; exit 1 ;; esac set_env_value SKILLHUB_BUSINESS_LOGS_NETWORK "$BUSINESS_LOGS_NETWORK" set_env_value SKILLHUB_BUSINESS_LOGS_NETWORK_ENABLED true fi set_env_value SKILLHUB_BUSINESS_LOGS_ENDPOINT "$receiver_endpoint" set_env_value SKILLHUB_BUSINESS_LOGS_ENABLED true run_compose up -d --no-deps --force-recreate server echo "Business Logs enabled; produce a new event and query the receiver." } ensure_analytics_identity() { # Once initialized, the independent directory owns both identifiers. Neither # a recreated release file nor inherited Compose variables may redirect it. for identity_key in SKILLHUB_ANALYTICS_PROJECT SKILLHUB_ANALYTICS_NETWORK; do identity_value="$(get_env_value "$identity_key" "" "$ANALYTICS_ENV_FILE")" if [ -z "$identity_value" ]; then if [ "$ORIGINAL_ENV_FILE_EXISTS" != "true" ]; then echo "Missing $identity_key in $ANALYTICS_ENV_FILE. Restore the original .env.release or set the verified original identifier in analytics/.env before retrying; no project discovery is performed." >&2 exit 1 fi identity_value="$(get_env_value "$identity_key" skillhub-analytics)" set_env_value "$identity_key" "$identity_value" "$ANALYTICS_ENV_FILE" fi case "$identity_key" in SKILLHUB_ANALYTICS_PROJECT) SKILLHUB_ANALYTICS_PROJECT="$identity_value" ;; SKILLHUB_ANALYTICS_NETWORK) SKILLHUB_ANALYTICS_NETWORK="$identity_value" ;; esac done chmod 600 "$ANALYTICS_ENV_FILE" # Shell variables take precedence over --env-file in Compose interpolation. export SKILLHUB_ANALYTICS_PROJECT SKILLHUB_ANALYTICS_NETWORK } ensure_analytics_network() { ensure_analytics_identity network_name="$SKILLHUB_ANALYTICS_NETWORK" if ! docker network inspect "$network_name" >/dev/null 2>&1; then docker network create "$network_name" >/dev/null fi } run_analytics_compose() { ensure_analytics_identity compose_cmd="$(find_compose)" # Explicit -p also protects against an inherited COMPOSE_PROJECT_NAME. # shellcheck disable=SC2086 $compose_cmd -p "$SKILLHUB_ANALYTICS_PROJECT" --env-file "$ANALYTICS_ENV_FILE" -f "$ANALYTICS_COMPOSE_FILE" "$@" } prepare_analytics_files() { mkdir -p "$ANALYTICS_HOME" # GitHub raw and the OSS mirror both publish files at the repository root. download_file "$SKILLHUB_RAW_BASE/compose.analytics-umami.yml" "$ANALYTICS_COMPOSE_FILE" if [ ! -f "$ANALYTICS_ENV_FILE" ]; then old_umask="$(umask)" umask 077 { printf 'UMAMI_DB_PASSWORD=%s\n' "$(generate_secret)" printf 'UMAMI_APP_SECRET=%s\n' "$(generate_secret)" printf 'UMAMI_ADMIN_PORT=8300\n' printf 'SKILLHUB_ANALYTICS_PROJECT=%s\n' "$(get_env_value SKILLHUB_ANALYTICS_PROJECT skillhub-analytics)" printf 'SKILLHUB_ANALYTICS_NETWORK=%s\n' "$(get_env_value SKILLHUB_ANALYTICS_NETWORK skillhub-analytics)" } >"$ANALYTICS_ENV_FILE" umask "$old_umask" fi ensure_analytics_identity } validate_analytics_input() { case "$ANALYTICS_UPSTREAM" in http://*|https://*) ;; *) echo "--upstream must be an HTTP(S) origin" >&2; exit 1 ;; esac case "${ANALYTICS_UPSTREAM#*://}" in ""|*[!A-Za-z0-9._:-]*|*..*) echo "--upstream must not contain paths, credentials or whitespace" >&2; exit 1 ;; esac case "$ANALYTICS_ADAPTER" in umami) if ! printf '%s' "$WEBSITE_ID" | grep -Eiq '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then echo "--website-id must be a UUID" >&2; exit 1 fi ;; datafinder) case "$ANALYTICS_APP_ID" in ""|0*|*[!0-9]*) echo "--app-id must be a positive decimal integer" >&2; exit 1 ;; esac [ "$ANALYTICS_APP_ID" -le 2147483647 ] || { echo "--app-id is out of range" >&2; exit 1; } ;; *) echo "Unsupported analytics adapter" >&2; exit 1 ;; esac } shell_quote() { case "$1" in *[!A-Za-z0-9_./:=@%+-]*) printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" ;; *) printf '%s' "$1" ;; esac } append_runtime_flag() { RUNTIME_LIFECYCLE_ARGS="$RUNTIME_LIFECYCLE_ARGS $1" } append_runtime_option() { RUNTIME_LIFECYCLE_ARGS="$RUNTIME_LIFECYCLE_ARGS $1 $(shell_quote "$2")" } build_runtime_lifecycle_args() { RUNTIME_LIFECYCLE_ARGS="" if [ "$USE_ALIYUN" = "true" ]; then append_runtime_flag "--aliyun" elif [ "$EXPLICIT_REF" = "true" ] || [ "$SKILLHUB_REF" != "main" ]; then append_runtime_option "--ref" "$SKILLHUB_REF" fi if [ -n "$SKILLHUB_VERSION_VALUE" ]; then append_runtime_option "--version" "$SKILLHUB_VERSION_VALUE" fi if [ "$SKILLHUB_HOME" != "$SKILLHUB_HOME_DEFAULT" ]; then append_runtime_option "--home" "$SKILLHUB_HOME" fi if [ -n "$SKILLHUB_PUBLIC_BASE_URL_VALUE" ]; then append_runtime_option "--public-url" "$SKILLHUB_PUBLIC_BASE_URL_VALUE" fi if [ -n "$SKILLHUB_MIRROR_REGISTRY_VALUE" ] && [ "$USE_ALIYUN" != "true" ]; then append_runtime_option "--mirror-registry" "$SKILLHUB_MIRROR_REGISTRY_VALUE" fi if [ -n "$SKILLHUB_SERVER_IMAGE_VALUE" ]; then append_runtime_option "--server-image" "$SKILLHUB_SERVER_IMAGE_VALUE" fi if [ -n "$SKILLHUB_WEB_IMAGE_VALUE" ]; then append_runtime_option "--web-image" "$SKILLHUB_WEB_IMAGE_VALUE" fi if [ -n "$SKILLHUB_SCANNER_IMAGE_VALUE" ]; then append_runtime_option "--scanner-image" "$SKILLHUB_SCANNER_IMAGE_VALUE" fi if [ -n "$POSTGRES_IMAGE_VALUE" ]; then append_runtime_option "--postgres-image" "$POSTGRES_IMAGE_VALUE" fi if [ -n "$REDIS_IMAGE_VALUE" ]; then append_runtime_option "--redis-image" "$REDIS_IMAGE_VALUE" fi if [ "$DISABLE_SCANNER" = "true" ]; then append_runtime_flag "--no-scanner" fi } print_runtime_command() { lifecycle_command="$1" printf ' curl -fsSL %s | sh -s -- %s%s\n' \ "$RUNTIME_SCRIPT_URL" \ "$lifecycle_command" \ "$RUNTIME_LIFECYCLE_ARGS" } build_runtime_lifecycle_args # Remember this before preparation can recreate defaults after clean. ORIGINAL_ENV_FILE_EXISTS=false [ ! -f "$ENV_FILE" ] || ORIGINAL_ENV_FILE_EXISTS=true prepare_runtime_files case "$COMMAND" in up) run_compose up -d postgres ensure_postgres_password_matches_env if [ "$DISABLE_SCANNER" = "true" ]; then run_compose up -d redis wait_for_redis_ready SKILLHUB_SECURITY_SCANNER_ENABLED=false run_compose up -d --no-deps --scale skill-scanner=0 server web else run_compose up -d fi PUBLIC_URL="${SKILLHUB_PUBLIC_BASE_URL_VALUE:-http://localhost}" cat <&2 exit 1 fi validate_business_logs_endpoint "${downstream_endpoint%/}/v1/logs" # Read the independent project's stored network; do not overwrite operator secrets. BUSINESS_LOGS_NETWORK="$(get_env_value SKILLHUB_BUSINESS_LOGS_NETWORK skillhub-business-logs "$BUSINESS_LOGS_ENV_FILE")" set_env_value SKILLHUB_BUSINESS_LOGS_NETWORK "$BUSINESS_LOGS_NETWORK" ensure_business_logs_network run_business_logs_compose config --quiet run_business_logs_compose up -d --force-recreate BUSINESS_LOGS_ENDPOINT=http://business-logs-collector:4318/v1/logs enable_business_logs ;; business-logs-enable) enable_business_logs ;; business-logs-disable) set_env_value SKILLHUB_BUSINESS_LOGS_ENABLED false # Attachment intent survives collection being disabled, so enable can reuse it. run_compose up -d --no-deps --force-recreate server echo "Business Logs disabled; receiver data and network retained." ;; business-logs-ps) printf 'Business Logs enabled: %s\n' "$(get_env_value SKILLHUB_BUSINESS_LOGS_ENABLED false)" if [ -f "$BUSINESS_LOGS_ENV_FILE" ]; then run_business_logs_compose ps else echo "No optional Collector; the receiver is managed externally." fi ;; business-logs-down|business-logs-logs) [ -f "$BUSINESS_LOGS_ENV_FILE" ] || { echo "No optional Collector; run business-logs-up first" >&2; exit 1; } case "$COMMAND" in business-logs-down) run_business_logs_compose stop ;; business-logs-logs) run_business_logs_compose logs -f ;; esac ;; analytics-up) prepare_analytics_files ensure_analytics_network run_analytics_compose up -d admin_port="$(grep '^UMAMI_ADMIN_PORT=' "$ANALYTICS_ENV_FILE" | cut -d= -f2-)" printf 'Umami management: http://127.0.0.1:%s\nChange the default admin/umami password, create a website, copy its UUID, then run analytics-enable --website-id .\nAnalytics files: %s\n' "$admin_port" "$ANALYTICS_HOME" ;; analytics-enable) if [ -z "$ANALYTICS_UPSTREAM" ] && [ "$ANALYTICS_ADAPTER" != "umami" ]; then echo "--upstream is required for an existing platform" >&2 exit 1 fi if [ -z "$ANALYTICS_UPSTREAM" ]; then ANALYTICS_UPSTREAM=http://umami:3000 local_analytics=true else local_analytics=false fi validate_analytics_input if [ "$ANALYTICS_ADAPTER" = "umami" ]; then analytics_config="{\"enabled\":true,\"adapter\":\"umami\",\"consent\":\"granted\",\"adapterConfig\":{\"websiteId\":\"$WEBSITE_ID\"}}" else analytics_config="{\"enabled\":true,\"adapter\":\"datafinder\",\"consent\":\"granted\",\"adapterConfig\":{\"appId\":$ANALYTICS_APP_ID}}" fi if [ "$local_analytics" = "true" ]; then [ -f "$ANALYTICS_ENV_FILE" ] || { echo "Run analytics-up first or provide --upstream for an existing platform" >&2; exit 1; } ensure_analytics_network set_env_value SKILLHUB_ANALYTICS_NETWORK_ENABLED true else set_env_value SKILLHUB_ANALYTICS_NETWORK_ENABLED false fi set_env_value SKILLHUB_WEB_ANALYTICS_CONFIG "'$analytics_config'" set_env_value SKILLHUB_WEB_ANALYTICS_UPSTREAM "$ANALYTICS_UPSTREAM" run_compose up -d --no-deps --force-recreate web echo "Web analytics enabled; refresh open browser tabs and verify events in the platform." ;; analytics-disable) set_env_value SKILLHUB_WEB_ANALYTICS_CONFIG "" set_env_value SKILLHUB_WEB_ANALYTICS_UPSTREAM "" set_env_value SKILLHUB_ANALYTICS_NETWORK_ENABLED false run_compose up -d --no-deps --force-recreate web echo "Web analytics disabled; platform and data remain unchanged." ;; analytics-down|analytics-ps|analytics-logs|analytics-pull) [ -f "$ANALYTICS_ENV_FILE" ] || { echo "No local analytics deployment; run analytics-up first" >&2; exit 1; } prepare_analytics_files case "$COMMAND" in analytics-down) run_analytics_compose stop ;; analytics-ps) run_analytics_compose ps ;; analytics-logs) run_analytics_compose logs -f ;; analytics-pull) run_analytics_compose pull ;; esac ;; *) echo "Unsupported command: $COMMAND" >&2 echo "Usage: sh runtime.sh [up|down|clean|ps|logs|pull] [options]" >&2 exit 1 ;; esac