#!/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 \ --release-id \ --package-url \ --sha256 \ --health-url \ --unit-name \ --deploy-root 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}"