140 lines
5.1 KiB
Bash
Executable File
140 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
ENV_FILE="${ROOT_DIR}/credentials/.env"
|
|
|
|
if [[ -f "${ENV_FILE}" ]]; then
|
|
# shellcheck disable=SC1090
|
|
source "${ENV_FILE}"
|
|
fi
|
|
|
|
ARTIFACT=""
|
|
WP_ROOT="${REMOTE_WP_PATH:-/var/www/wordpress}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--artifact)
|
|
ARTIFACT="${2:-}"
|
|
shift 2
|
|
;;
|
|
--wp-root)
|
|
WP_ROOT="${2:-}"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
cat <<'USAGE'
|
|
Deploy a plugin artifact to remote WordPress.
|
|
|
|
Usage:
|
|
scripts/deploy_remote.sh [--artifact /abs/or/relative/path.zip] [--wp-root /var/www/wordpress]
|
|
|
|
Defaults:
|
|
- Artifact: latest ./package/calendar-plugin-*.zip
|
|
- Remote host settings from credentials/.env
|
|
|
|
This script enforces ownership:
|
|
- chown -R www-data:www-data <remote plugin dir>
|
|
USAGE
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "[deploy] unknown argument: $1" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${ARTIFACT}" ]]; then
|
|
ARTIFACT="$(ls -1 "${ROOT_DIR}/package/calendar-plugin-"*.zip 2>/dev/null | sort -V | tail -n1 || true)"
|
|
fi
|
|
|
|
if [[ -z "${ARTIFACT}" ]]; then
|
|
echo "[deploy] no artifact found; run scripts/package_plugin.sh first" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${ARTIFACT}" != /* ]]; then
|
|
ARTIFACT="${ROOT_DIR}/${ARTIFACT#./}"
|
|
fi
|
|
|
|
if [[ ! -f "${ARTIFACT}" ]]; then
|
|
echo "[deploy] artifact not found: ${ARTIFACT}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
MANIFEST="${ARTIFACT%.zip}.manifest.sha256"
|
|
if [[ ! -f "${MANIFEST}" ]]; then
|
|
echo "[deploy] warning: manifest not found beside artifact (${MANIFEST}); continuing" >&2
|
|
fi
|
|
|
|
required=(REMOTE_HOST REMOTE_PORT REMOTE_USER REMOTE_SSH_KEY_PATH REMOTE_APP_DIR REMOTE_WP_CLI WP_URL)
|
|
for key in "${required[@]}"; do
|
|
if [[ -z "${!key:-}" ]]; then
|
|
echo "[deploy] missing required env var: ${key}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
SSH_KEY_PATH="${REMOTE_SSH_KEY_PATH}"
|
|
if [[ "${SSH_KEY_PATH}" != /* ]]; then
|
|
SSH_KEY_PATH="${ROOT_DIR}/${SSH_KEY_PATH}"
|
|
fi
|
|
|
|
if [[ ! -f "${SSH_KEY_PATH}" ]]; then
|
|
echo "[deploy] ssh key not found: ${SSH_KEY_PATH}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
artifact_base="$(basename -- "${ARTIFACT}")"
|
|
stage_name="${artifact_base%.zip}"
|
|
STAGE_DIR="/tmp/${stage_name}"
|
|
|
|
SSH=(ssh -F /dev/null -i "${SSH_KEY_PATH}" -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new "${REMOTE_USER}@${REMOTE_HOST}")
|
|
RSYNC=(rsync -avz --delete -e "ssh -F /dev/null -i ${SSH_KEY_PATH} -p ${REMOTE_PORT} -o StrictHostKeyChecking=accept-new")
|
|
|
|
echo "[deploy] staging on remote: ${STAGE_DIR}"
|
|
"${SSH[@]}" "mkdir -p '${STAGE_DIR}'"
|
|
|
|
if [[ -f "${MANIFEST}" ]]; then
|
|
"${RSYNC[@]}" "${ARTIFACT}" "${MANIFEST}" "${REMOTE_USER}@${REMOTE_HOST}:${STAGE_DIR}/"
|
|
else
|
|
"${RSYNC[@]}" "${ARTIFACT}" "${REMOTE_USER}@${REMOTE_HOST}:${STAGE_DIR}/"
|
|
fi
|
|
|
|
"${SSH[@]}" "set -euo pipefail; rm -rf '${STAGE_DIR}/extracted'; mkdir -p '${STAGE_DIR}/extracted'; unzip -q '${STAGE_DIR}/${artifact_base}' -d '${STAGE_DIR}/extracted'; test -f '${STAGE_DIR}/extracted/calendar-plugin/calendar-plugin.php'"
|
|
"${SSH[@]}" "set -euo pipefail; rsync -a --delete '${STAGE_DIR}/extracted/calendar-plugin/' '${REMOTE_APP_DIR}/'"
|
|
"${SSH[@]}" "set -euo pipefail; chown -R www-data:www-data '${REMOTE_APP_DIR}'"
|
|
# Always cycle plugin activation so activation-hook migrations run on every deploy.
|
|
"${SSH[@]}" "set -euo pipefail; '${REMOTE_WP_CLI}' --path='${WP_ROOT}' plugin deactivate calendar-plugin --allow-root >/dev/null 2>&1 || true; '${REMOTE_WP_CLI}' --path='${WP_ROOT}' plugin activate calendar-plugin --allow-root >/dev/null"
|
|
|
|
# Exact-match style checksum dry-run check for content drift.
|
|
# Ignore directory metadata-only differences, which are expected after chown/remote extraction.
|
|
"${SSH[@]}" "set -euo pipefail; rsync -rcn --delete --omit-dir-times --no-perms --no-owner --no-group --itemize-changes '${STAGE_DIR}/extracted/calendar-plugin/' '${REMOTE_APP_DIR}/' >/tmp/codex_rsync_check.out; if grep -Eq '^(>f|\\*deleting|cd|cL|cD|cS)' /tmp/codex_rsync_check.out; then cat /tmp/codex_rsync_check.out; exit 1; fi"
|
|
|
|
# Ownership sanity check: must be zero mismatches
|
|
non_owned="$("${SSH[@]}" "set -euo pipefail; find '${REMOTE_APP_DIR}' \( ! -user www-data -o ! -group www-data \) | wc -l")"
|
|
if [[ "${non_owned}" != "0" ]]; then
|
|
echo "[deploy] ownership check failed: ${non_owned} path(s) not owned by www-data:www-data" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Resolve slug for endpoint checks
|
|
url_slug="$("${SSH[@]}" "'${REMOTE_WP_CLI}' --path='${WP_ROOT}' option get calendar_plugin_url_slug --allow-root 2>/dev/null || true" | tr -d '\r' | tail -n1)"
|
|
if [[ -n "${url_slug}" ]]; then
|
|
ICS_URL="${WP_URL%/}/${url_slug}/calendar.ics"
|
|
CALDAV_URL="${WP_URL%/}/${url_slug}/caldav/"
|
|
else
|
|
ICS_URL="${WP_URL%/}/calendar.ics"
|
|
CALDAV_URL="${WP_URL%/}/caldav/"
|
|
fi
|
|
|
|
health_code="$(curl -sS -o /tmp/codex_health.out -w '%{http_code}' "${WP_URL%/}/wp-json/calendar/v1/health" || true)"
|
|
ics_code="$(curl -sS -o /tmp/codex_ics.out -w '%{http_code}' "${ICS_URL}" || true)"
|
|
caldav_code="$(curl -sS -o /tmp/codex_caldav.out -w '%{http_code}' "${CALDAV_URL}" || true)"
|
|
|
|
echo "[deploy] plugin deployed: ${artifact_base}"
|
|
echo "[deploy] ownership: www-data:www-data (verified)"
|
|
echo "[deploy] checks: health=${health_code} ics=${ics_code} caldav_unauth=${caldav_code}"
|
|
echo "[deploy] urls: ${ICS_URL} ${CALDAV_URL}"
|