237 lines
7.1 KiB
Bash
237 lines
7.1 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
action="${1:-}"
|
|
app_name="${2:-}"
|
|
archive="${3:-}"
|
|
mode="${4:-auto}"
|
|
deploy_root="${5:-/home/jsxq/apps}"
|
|
health_url="${6:-}"
|
|
skip_health="${7:-0}"
|
|
publish_port="${8:-0}"
|
|
container_port="${9:-0}"
|
|
|
|
if [[ ! "$app_name" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
|
|
echo "Invalid application name" >&2
|
|
exit 2
|
|
fi
|
|
|
|
expected_prefix="/home/${USER}/apps"
|
|
if [[ "$deploy_root" != "$expected_prefix" ]]; then
|
|
echo "Refusing deployment outside $expected_prefix" >&2
|
|
exit 2
|
|
fi
|
|
|
|
app_name="${app_name,,}"
|
|
project_name="codex-${app_name}"
|
|
app_root="${deploy_root}/${app_name}"
|
|
releases_dir="${app_root}/releases"
|
|
current_link="${app_root}/current"
|
|
previous_link="${app_root}/previous"
|
|
|
|
find_compose_file() {
|
|
local directory="$1"
|
|
local candidate
|
|
for candidate in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
|
if [[ -f "${directory}/${candidate}" ]]; then
|
|
printf '%s\n' "${directory}/${candidate}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
compose_up() {
|
|
local directory="$1"
|
|
local compose_file
|
|
compose_file="$(find_compose_file "$directory")"
|
|
(cd "$directory" && docker compose -p "$project_name" -f "$compose_file" up -d --build --remove-orphans)
|
|
}
|
|
|
|
compose_down() {
|
|
local directory="$1"
|
|
local compose_file
|
|
compose_file="$(find_compose_file "$directory" || true)"
|
|
if [[ -z "$compose_file" ]]; then
|
|
return 0
|
|
fi
|
|
(cd "$directory" && docker compose -p "$project_name" -f "$compose_file" down --remove-orphans) || true
|
|
}
|
|
|
|
swap_release_links() {
|
|
local from="$1"
|
|
local to="$2"
|
|
ln -sfn "$to" "$current_link"
|
|
ln -sfn "$from" "$previous_link"
|
|
}
|
|
|
|
rollback_release() {
|
|
if [[ ! -L "$previous_link" ]]; then
|
|
echo "No previous release is available" >&2
|
|
return 1
|
|
fi
|
|
|
|
local current_release previous_release current_mode previous_mode
|
|
current_release="$(readlink -f "$current_link" 2>/dev/null || true)"
|
|
previous_release="$(readlink -f "$previous_link")"
|
|
current_mode=""
|
|
if [[ -n "$current_release" && -f "${current_release}/.codex-deploy-mode" ]]; then
|
|
current_mode="$(cat "${current_release}/.codex-deploy-mode")"
|
|
fi
|
|
previous_mode="$(cat "${previous_release}/.codex-deploy-mode")"
|
|
|
|
if [[ "$current_mode" == "compose" && -n "$current_release" ]]; then
|
|
compose_down "$current_release"
|
|
elif [[ "$current_mode" == "dockerfile" ]]; then
|
|
docker rm -f "${project_name}-app" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
if [[ "$previous_mode" == "compose" ]]; then
|
|
compose_up "$previous_release"
|
|
elif [[ "$previous_mode" == "dockerfile" ]]; then
|
|
if docker container inspect "${project_name}-previous" >/dev/null 2>&1; then
|
|
docker rename "${project_name}-previous" "${project_name}-app"
|
|
docker start "${project_name}-app" >/dev/null
|
|
else
|
|
echo "Previous Dockerfile container is unavailable" >&2
|
|
return 1
|
|
fi
|
|
else
|
|
echo "Unknown previous deployment mode: $previous_mode" >&2
|
|
return 1
|
|
fi
|
|
|
|
swap_release_links "$current_release" "$previous_release"
|
|
echo "ROLLBACK_STATUS=success"
|
|
echo "CURRENT_RELEASE=$previous_release"
|
|
}
|
|
|
|
if [[ "$action" == "rollback" ]]; then
|
|
rollback_release
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$action" != "deploy" ]]; then
|
|
echo "Expected deploy or rollback action" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ ! -f "$archive" ]]; then
|
|
echo "Deployment archive not found: $archive" >&2
|
|
exit 2
|
|
fi
|
|
|
|
command -v docker >/dev/null
|
|
docker compose version >/dev/null
|
|
|
|
mkdir -p "$releases_dir"
|
|
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
release_dir="${releases_dir}/${timestamp}"
|
|
if [[ -e "$release_dir" ]]; then
|
|
release_dir="${release_dir}-$RANDOM"
|
|
fi
|
|
mkdir -p "$release_dir"
|
|
tar -xzf "$archive" -C "$release_dir"
|
|
printf '%s\n' "$mode" > "${release_dir}/.codex-deploy-mode"
|
|
|
|
old_release="$(readlink -f "$current_link" 2>/dev/null || true)"
|
|
old_mode=""
|
|
if [[ -n "$old_release" ]]; then
|
|
if [[ -f "${old_release}/.codex-deploy-mode" ]]; then
|
|
old_mode="$(cat "${old_release}/.codex-deploy-mode")"
|
|
fi
|
|
ln -sfn "$old_release" "$previous_link"
|
|
fi
|
|
ln -sfn "$release_dir" "$current_link"
|
|
|
|
deploy_failed=0
|
|
if [[ "$mode" == "compose" ]]; then
|
|
if [[ "$old_mode" == "dockerfile" ]] && docker container inspect "${project_name}-app" >/dev/null 2>&1; then
|
|
docker rm -f "${project_name}-previous" >/dev/null 2>&1 || true
|
|
docker stop "${project_name}-app" >/dev/null || true
|
|
docker rename "${project_name}-app" "${project_name}-previous"
|
|
fi
|
|
if ! find_compose_file "$release_dir" >/dev/null; then
|
|
echo "Compose file not found in release" >&2
|
|
deploy_failed=1
|
|
elif ! compose_up "$release_dir"; then
|
|
deploy_failed=1
|
|
fi
|
|
elif [[ "$mode" == "dockerfile" ]]; then
|
|
if [[ "$old_mode" == "compose" && -n "$old_release" ]]; then
|
|
compose_down "$old_release"
|
|
fi
|
|
if [[ ! -f "${release_dir}/Dockerfile" ]]; then
|
|
echo "Dockerfile not found in release" >&2
|
|
deploy_failed=1
|
|
else
|
|
image_name="${project_name}:${timestamp}"
|
|
if ! (cd "$release_dir" && docker build -t "$image_name" .); then
|
|
deploy_failed=1
|
|
else
|
|
docker rm -f "${project_name}-previous" >/dev/null 2>&1 || true
|
|
if docker container inspect "${project_name}-app" >/dev/null 2>&1; then
|
|
docker stop "${project_name}-app" >/dev/null || true
|
|
docker rename "${project_name}-app" "${project_name}-previous"
|
|
fi
|
|
run_args=(--detach --name "${project_name}-app" --restart unless-stopped)
|
|
if [[ "$publish_port" != "0" && "$container_port" != "0" ]]; then
|
|
run_args+=(--publish "${publish_port}:${container_port}")
|
|
fi
|
|
if [[ -f "${release_dir}/.env" ]]; then
|
|
run_args+=(--env-file "${release_dir}/.env")
|
|
fi
|
|
if ! docker run "${run_args[@]}" "$image_name"; then
|
|
deploy_failed=1
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
echo "Unsupported deployment mode: $mode" >&2
|
|
deploy_failed=1
|
|
fi
|
|
|
|
if [[ "$deploy_failed" == "0" && "$skip_health" != "1" ]]; then
|
|
sleep 3
|
|
if [[ -n "$health_url" ]]; then
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl --fail --silent --show-error --max-time 15 "$health_url" >/dev/null || deploy_failed=1
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
wget --quiet --timeout=15 --spider "$health_url" || deploy_failed=1
|
|
else
|
|
echo "No curl or wget is available for health checks" >&2
|
|
deploy_failed=1
|
|
fi
|
|
elif [[ "$mode" == "compose" ]]; then
|
|
compose_file="$(find_compose_file "$release_dir")"
|
|
(cd "$release_dir" && docker compose -p "$project_name" -f "$compose_file" ps --status running -q | grep -q .) || deploy_failed=1
|
|
else
|
|
[[ "$(docker inspect -f '{{.State.Running}}' "${project_name}-app" 2>/dev/null || true)" == "true" ]] || deploy_failed=1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$deploy_failed" != "0" ]]; then
|
|
echo "Deployment or health check failed" >&2
|
|
if [[ -n "$old_release" && -L "$previous_link" ]]; then
|
|
rollback_release || true
|
|
echo "AUTO_ROLLBACK=attempted" >&2
|
|
else
|
|
if [[ "$mode" == "compose" ]]; then
|
|
compose_down "$release_dir"
|
|
else
|
|
docker rm -f "${project_name}-app" >/dev/null 2>&1 || true
|
|
fi
|
|
rm -f "$current_link"
|
|
echo "AUTO_ROLLBACK=unavailable" >&2
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
echo "DEPLOY_STATUS=success"
|
|
echo "APP_NAME=$app_name"
|
|
echo "MODE=$mode"
|
|
echo "CURRENT_RELEASE=$release_dir"
|
|
if [[ -n "$health_url" ]]; then
|
|
echo "HEALTH_URL=$health_url"
|
|
fi
|