2026-04-07 11:48:19 +08:00

168 lines
4.4 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
SERVICE=""
RELEASE_ID=""
PACKAGE_URL=""
SHA256_VALUE=""
HEALTH_URL=""
UNIT_NAME=""
DEPLOY_ROOT=""
READY_TIMEOUT_SECONDS="${READY_TIMEOUT_SECONDS:-180}"
DOWNLOAD_TIMEOUT_SECONDS="${DOWNLOAD_TIMEOUT_SECONDS:-600}"
POLL_INTERVAL_SECONDS="${POLL_INTERVAL_SECONDS:-2}"
usage() {
cat <<'EOF'
Usage:
deploy.sh \
--service <service> \
--release-id <release_id> \
--package-url <url> \
--sha256 <sha256> \
--health-url <url> \
--unit-name <systemd unit> \
--deploy-root <path>
EOF
}
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S %z')" "$*"
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "missing command: $1" >&2
exit 1
}
}
while [[ $# -gt 0 ]]; do
case "$1" in
--service) SERVICE="$2"; shift 2 ;;
--release-id) RELEASE_ID="$2"; shift 2 ;;
--package-url) PACKAGE_URL="$2"; shift 2 ;;
--sha256) SHA256_VALUE="$2"; shift 2 ;;
--health-url) HEALTH_URL="$2"; shift 2 ;;
--unit-name) UNIT_NAME="$2"; shift 2 ;;
--deploy-root) DEPLOY_ROOT="$2"; shift 2 ;;
--ready-timeout-seconds) READY_TIMEOUT_SECONDS="$2"; shift 2 ;;
--download-timeout-seconds) DOWNLOAD_TIMEOUT_SECONDS="$2"; shift 2 ;;
--poll-interval-seconds) POLL_INTERVAL_SECONDS="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown arg: $1" >&2; usage; exit 1 ;;
esac
done
[[ -n "$SERVICE" && -n "$RELEASE_ID" && -n "$PACKAGE_URL" && -n "$SHA256_VALUE" && -n "$HEALTH_URL" && -n "$UNIT_NAME" && -n "$DEPLOY_ROOT" ]] || {
usage
exit 1
}
require_cmd curl
require_cmd sha256sum
require_cmd tar
require_cmd systemctl
release_dir="${DEPLOY_ROOT}/releases/${RELEASE_ID}"
current_link="${DEPLOY_ROOT}/current"
staging_root="/tmp/chatapp-deploy/${SERVICE}/${RELEASE_ID}"
staging_package="${staging_root}/${SERVICE}.tgz"
staging_extract="${staging_root}/extract"
previous_target=""
mkdir -p "${DEPLOY_ROOT}/releases" "${staging_root}" "${staging_extract}"
if [[ -L "${current_link}" ]]; then
previous_target="$(readlink -f "${current_link}" || true)"
fi
cleanup() {
rm -rf "${staging_root}"
}
trap cleanup EXIT
rollback() {
local rollback_reason="$1"
log "rollback: ${rollback_reason}"
if [[ -n "${previous_target}" && -d "${previous_target}" ]]; then
ln -sfn "${previous_target}" "${current_link}.tmp"
mv -Tf "${current_link}.tmp" "${current_link}"
/usr/bin/systemctl reset-failed "${UNIT_NAME}" >/dev/null 2>&1 || true
/usr/bin/systemctl start "${UNIT_NAME}" || true
if ! wait_ready "rollback"; then
log "rollback target is still not ready"
fi
fi
}
wait_ready() {
local phase="$1"
local deadline=$(( $(date +%s) + READY_TIMEOUT_SECONDS ))
while [[ "$(date +%s)" -lt "${deadline}" ]]; do
if curl -fsS --max-time 3 "${HEALTH_URL}" >/dev/null; then
log "${phase}: ready check passed"
return 0
fi
sleep "${POLL_INTERVAL_SECONDS}"
done
return 1
}
stop_service() {
if /usr/bin/systemctl is-active --quiet "${UNIT_NAME}"; then
log "stopping ${UNIT_NAME} with SIGTERM"
# The unit files already set KillSignal=SIGTERM, so stop triggers graceful shutdown.
/usr/bin/systemctl stop "${UNIT_NAME}"
else
log "${UNIT_NAME} already inactive"
fi
}
start_service() {
log "starting ${UNIT_NAME}"
/usr/bin/systemctl reset-failed "${UNIT_NAME}" >/dev/null 2>&1 || true
/usr/bin/systemctl start "${UNIT_NAME}"
}
log "download package ${PACKAGE_URL}"
curl -fL --connect-timeout 10 --max-time "${DOWNLOAD_TIMEOUT_SECONDS}" -o "${staging_package}" "${PACKAGE_URL}"
echo "${SHA256_VALUE} ${staging_package}" | sha256sum -c -
if [[ ! -d "${release_dir}" ]]; then
mkdir -p "${release_dir}"
tar -C "${staging_extract}" -xzf "${staging_package}"
cp -R "${staging_extract}/." "${release_dir}/"
else
log "release dir already exists, reuse ${release_dir}"
fi
if [[ ! -x "${release_dir}/bin/${SERVICE}" ]]; then
echo "release binary missing: ${release_dir}/bin/${SERVICE}" >&2
exit 1
fi
ln -sfn "${release_dir}" "${current_link}.tmp"
mv -Tf "${current_link}.tmp" "${current_link}"
if ! stop_service; then
rollback "systemd stop failed"
echo "systemd stop failed: ${UNIT_NAME}" >&2
exit 1
fi
if ! start_service; then
rollback "systemd start failed"
echo "systemd start failed: ${UNIT_NAME}" >&2
exit 1
fi
if ! wait_ready "deploy"; then
rollback "service failed to become ready after deploy"
echo "service failed to become ready: ${UNIT_NAME}" >&2
exit 1
fi
log "deploy success: ${SERVICE}@${RELEASE_ID}"