mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change. Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
264 lines
6.6 KiB
Bash
Executable File
264 lines
6.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# 智能部署脚本 - 自动检测代码变化并重建
|
|
#
|
|
# 用法:
|
|
# 部署/更新: ./deploy.sh
|
|
# 强制全部重建: ./deploy.sh --force
|
|
|
|
set -euo pipefail
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
cd -- "${SCRIPT_DIR}"
|
|
|
|
LOCAL_APP_IMAGE="${LOCAL_APP_IMAGE:-aether-app:latest}"
|
|
export LOCAL_APP_IMAGE
|
|
|
|
detect_build_version() {
|
|
if command -v git >/dev/null 2>&1; then
|
|
local version
|
|
if version=$(git describe --tags --match 'v[0-9]*' --always --dirty 2>/dev/null); then
|
|
if [ -n "$version" ]; then
|
|
printf '%s\n' "$version"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
printf 'local-%s\n' "$(date -u +%Y%m%d%H%M%S)"
|
|
}
|
|
|
|
AETHER_BUILD_VERSION="${AETHER_BUILD_VERSION:-$(detect_build_version)}"
|
|
export AETHER_BUILD_VERSION
|
|
|
|
# 兼容 docker-compose 和 docker compose
|
|
if command -v docker-compose &> /dev/null; then
|
|
DC=(docker-compose -f docker-compose.yml -f docker-compose.local.yml)
|
|
USE_LEGACY_COMPOSE=true
|
|
else
|
|
DC=(docker compose -f docker-compose.yml -f docker-compose.local.yml)
|
|
USE_LEGACY_COMPOSE=false
|
|
fi
|
|
|
|
compose_up() {
|
|
if [ "$USE_LEGACY_COMPOSE" = true ]; then
|
|
"${DC[@]}" up -d --no-build "$@"
|
|
else
|
|
"${DC[@]}" up -d --no-build --pull missing "$@"
|
|
fi
|
|
}
|
|
|
|
# 缓存文件
|
|
CODE_HASH_FILE=".code-hash"
|
|
|
|
validate_code_hash_file() {
|
|
if [ -L "${CODE_HASH_FILE}" ]; then
|
|
echo "Refusing symbolic-link build state: ${CODE_HASH_FILE}" >&2
|
|
return 1
|
|
fi
|
|
if [ -e "${CODE_HASH_FILE}" ] && [ ! -f "${CODE_HASH_FILE}" ]; then
|
|
echo "Build state is not a regular file: ${CODE_HASH_FILE}" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: ./deploy.sh [options]
|
|
|
|
Options:
|
|
--force, -f 强制重建并重启
|
|
-h, --help 显示帮助
|
|
|
|
Environment:
|
|
LOCAL_APP_IMAGE 本地构建镜像名,默认 aether-app:latest
|
|
AETHER_BUILD_VERSION 应用显示版本,默认 git describe --tags --match 'v[0-9]*' --always --dirty
|
|
EOF
|
|
}
|
|
|
|
FORCE_REBUILD_ALL=false
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--force|-f)
|
|
FORCE_REBUILD_ALL=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
require_file() {
|
|
if [ ! -f "$1" ]; then
|
|
echo "Required file not found: $1"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
hash_stream() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum | cut -d' ' -f1
|
|
else
|
|
shasum -a 256 | cut -d' ' -f1
|
|
fi
|
|
}
|
|
|
|
emit_file_for_hash() {
|
|
local path="$1"
|
|
[ -f "$path" ] || return 0
|
|
printf '\n>>> %s\n' "$path"
|
|
cat "$path"
|
|
}
|
|
|
|
emit_tree_for_hash() {
|
|
local root="$1"
|
|
[ -d "$root" ] || return 0
|
|
find "$root" -type f \
|
|
! -path '*/node_modules/*' \
|
|
! -path '*/target/*' \
|
|
! -path '*/dist/*' \
|
|
! -path '*/.mypy_cache/*' \
|
|
! -path '*/.vite/*' \
|
|
2>/dev/null | sort | while IFS= read -r path; do
|
|
emit_file_for_hash "$path"
|
|
done
|
|
}
|
|
|
|
# 计算代码文件的哈希值
|
|
calc_code_hash() {
|
|
{
|
|
printf '\n>>> AETHER_BUILD_VERSION\n%s\n' "$AETHER_BUILD_VERSION"
|
|
|
|
for file in \
|
|
Dockerfile.app.local \
|
|
docker-compose.yml \
|
|
docker-compose.local.yml \
|
|
.dockerignore \
|
|
Cargo.toml \
|
|
Cargo.lock \
|
|
rust-toolchain.toml \
|
|
frontend/package.json \
|
|
frontend/package-lock.json \
|
|
frontend/index.html \
|
|
frontend/vite.config.ts \
|
|
frontend/tsconfig.json \
|
|
frontend/tsconfig.app.json \
|
|
frontend/tsconfig.node.json \
|
|
frontend/postcss.config.js \
|
|
frontend/tailwind.config.js; do
|
|
emit_file_for_hash "$file"
|
|
done
|
|
|
|
for dir in frontend/src frontend/public apps crates; do
|
|
emit_tree_for_hash "$dir"
|
|
done
|
|
} | hash_stream
|
|
}
|
|
|
|
# 检查代码是否变化
|
|
check_code_changed() {
|
|
local current_hash
|
|
current_hash=$(calc_code_hash)
|
|
validate_code_hash_file || exit 1
|
|
if [ -f "$CODE_HASH_FILE" ]; then
|
|
local saved_hash
|
|
saved_hash=$(<"$CODE_HASH_FILE")
|
|
if [ "$current_hash" = "$saved_hash" ]; then
|
|
return 1
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
save_code_hash() {
|
|
local staged
|
|
staged="$(mktemp "./.code-hash.tmp.XXXXXXXX")" \
|
|
|| { echo "Could not create temporary build state" >&2; return 1; }
|
|
if ! calc_code_hash >"${staged}"; then
|
|
rm -f -- "${staged}"
|
|
return 1
|
|
fi
|
|
chmod 0600 "${staged}"
|
|
if ! validate_code_hash_file; then
|
|
rm -f -- "${staged}"
|
|
return 1
|
|
fi
|
|
if ! mv -f -- "${staged}" "${CODE_HASH_FILE}"; then
|
|
rm -f -- "${staged}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# 构建应用镜像
|
|
build_app() {
|
|
require_file Dockerfile.app.local
|
|
echo ">>> Building app image: $LOCAL_APP_IMAGE"
|
|
echo ">>> Build version: $AETHER_BUILD_VERSION"
|
|
DOCKER_BUILDKIT="${DOCKER_BUILDKIT:-1}" docker build \
|
|
--pull=false \
|
|
--build-arg "AETHER_BUILD_VERSION=$AETHER_BUILD_VERSION" \
|
|
-f Dockerfile.app.local \
|
|
-t "$LOCAL_APP_IMAGE" \
|
|
.
|
|
save_code_hash
|
|
}
|
|
|
|
# 强制全部重建
|
|
if [ "$FORCE_REBUILD_ALL" = true ]; then
|
|
echo ">>> Force rebuilding everything..."
|
|
build_app
|
|
compose_up --force-recreate
|
|
docker image prune -f
|
|
echo ">>> Done!"
|
|
"${DC[@]}" ps
|
|
exit 0
|
|
fi
|
|
|
|
# 标记是否需要重启
|
|
NEED_RESTART=false
|
|
|
|
# 检查代码是否变化
|
|
if ! docker image inspect "$LOCAL_APP_IMAGE" >/dev/null 2>&1; then
|
|
echo ">>> App image not found, building..."
|
|
build_app
|
|
NEED_RESTART=true
|
|
elif check_code_changed; then
|
|
echo ">>> Code changed, rebuilding app image..."
|
|
build_app
|
|
NEED_RESTART=true
|
|
else
|
|
echo ">>> Code unchanged."
|
|
fi
|
|
|
|
# 检查容器是否在运行
|
|
CONTAINERS_RUNNING=true
|
|
if [ -z "$("${DC[@]}" ps -q 2>/dev/null)" ]; then
|
|
CONTAINERS_RUNNING=false
|
|
fi
|
|
|
|
# 有变化时重启,或容器未运行时启动
|
|
if [ "$NEED_RESTART" = true ]; then
|
|
echo ">>> Restarting services..."
|
|
compose_up
|
|
elif [ "$CONTAINERS_RUNNING" = false ]; then
|
|
echo ">>> Containers not running, starting services..."
|
|
compose_up
|
|
else
|
|
echo ">>> No changes detected, skipping restart."
|
|
fi
|
|
|
|
# 清理
|
|
docker image prune -f >/dev/null 2>&1 || true
|
|
|
|
echo ">>> Done!"
|
|
echo ">>> Note: empty databases auto-bootstrap on first start."
|
|
echo ">>> Note: database schema and data preparation run automatically before app startup."
|
|
echo ">>> Note: set AETHER_GATEWAY_DATABASE_MODE=verify-only to require a separate database prepare step."
|
|
"${DC[@]}" ps
|