diff --git a/scripts/ci/deploy-site.sh b/scripts/ci/deploy-site.sh index b83ff25..79bec6b 100755 --- a/scripts/ci/deploy-site.sh +++ b/scripts/ci/deploy-site.sh @@ -10,8 +10,20 @@ set -euo pipefail # shellcheck source=scripts/ci/lib-ssh.sh . scripts/ci/lib-ssh.sh +# setup_ssh registers the EXIT cleanup trap itself (before writing the key), +# so a mid-setup failure still cleans up — see lib-ssh.sh. setup_ssh -trap teardown_ssh EXIT + +# Guard against deploying a missing/empty build: `rsync --delete` against an +# empty source would wipe the live site. +if [ ! -d web/website/dist ]; then + echo "==> deploy-site: web/website/dist/ does not exist — refusing to deploy" >&2 + exit 1 +fi +if ! find web/website/dist -mindepth 1 -print -quit | grep -q .; then + echo "==> deploy-site: web/website/dist/ is empty — refusing to deploy" >&2 + exit 1 +fi DEPLOY_TARGET="root@${DEPLOY_HOST}:/var/www/pangolin-site/" echo "==> deploy-site: rsync web/website/dist/ -> ${DEPLOY_TARGET}" diff --git a/scripts/ci/lib-ssh.sh b/scripts/ci/lib-ssh.sh index e36ce72..fc7cb9f 100755 --- a/scripts/ci/lib-ssh.sh +++ b/scripts/ci/lib-ssh.sh @@ -3,10 +3,12 @@ # (site / server). `source` this from deploy-*.sh. # # Provides: -# setup_ssh -> writes $DEPLOY_SSH_KEY to a temp private key (mode 600), -# registers the deploy host in known_hosts, and exports -# SSH / RSYNC_SSH (ssh command strings) + SSH_KEY_FILE. -# teardown_ssh -> removes the temp private key. +# setup_ssh -> registers the EXIT cleanup trap first, then writes +# $DEPLOY_SSH_KEY to a temp private key (mode 600), +# registers the deploy host in a dedicated known_hosts +# file, and exports SSH / RSYNC_SSH (ssh command strings) +# + SSH_KEY_FILE. +# teardown_ssh -> removes the temp private key + known_hosts file. # # Requires env: DEPLOY_SSH_KEY (PEM content of the deploy private key, # authorized for root on the target host). @@ -21,9 +23,15 @@ DEPLOY_HOST="${DEPLOY_HOST:-103.119.13.48}" DEPLOY_PORT="${DEPLOY_PORT:-22}" SSH_KEY_FILE="${SSH_KEY_FILE:-/tmp/pangolin_deploy_key.$$}" +SSH_KNOWN_HOSTS_FILE="${SSH_KNOWN_HOSTS_FILE:-/tmp/pangolin_deploy_known_hosts.$$}" # setup_ssh — write the deploy key, register known_hosts, export SSH/RSYNC_SSH. setup_ssh() { + # Register cleanup FIRST: if anything below fails mid-setup (e.g. a + # transient ssh-keyscan error under `set -e`), the private key file must + # still be removed on exit rather than leaking. + trap teardown_ssh EXIT + if [ -z "${DEPLOY_SSH_KEY:-}" ]; then echo "==> setup_ssh: DEPLOY_SSH_KEY is empty" >&2 return 1 @@ -32,22 +40,30 @@ setup_ssh() { mkdir -p ~/.ssh chmod 700 ~/.ssh + # Pre-create the key file with restrictive perms *before* writing any key + # material into it, so there is no window at the default umask between + # file creation and chmod. + install -m 600 /dev/null "${SSH_KEY_FILE}" + # `printf '%s\n'` 末尾补一个换行:Forgejo/Gitea 存 secret 会去掉结尾换行, # 而缺结尾换行的 OpenSSH 格式私钥会被判为 "invalid format" 拒绝加载, # 退化成无密钥 → Permission denied。多补的换行对已含结尾换行的 PEM 无害。 printf '%s\n' "${DEPLOY_SSH_KEY}" > "${SSH_KEY_FILE}" - chmod 600 "${SSH_KEY_FILE}" - ssh-keyscan -p "${DEPLOY_PORT}" -H "${DEPLOY_HOST}" >> ~/.ssh/known_hosts 2>/dev/null + # Populate a dedicated known_hosts file via TOFU keyscan. This is + # belt-and-suspenders: `accept-new` below will pin the host key on first + # real connection regardless, so a transient keyscan failure must not + # abort the deploy. + ssh-keyscan -p "${DEPLOY_PORT}" -H "${DEPLOY_HOST}" >> "${SSH_KNOWN_HOSTS_FILE}" 2>/dev/null || true - SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=no" - RSYNC_SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=no" - export SSH RSYNC_SSH SSH_KEY_FILE DEPLOY_HOST DEPLOY_PORT - echo "==> setup_ssh: key written to ${SSH_KEY_FILE}, known_hosts updated for ${DEPLOY_HOST}:${DEPLOY_PORT}" + SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}" + RSYNC_SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}" + export SSH RSYNC_SSH SSH_KEY_FILE SSH_KNOWN_HOSTS_FILE DEPLOY_HOST DEPLOY_PORT + echo "==> setup_ssh: key written to ${SSH_KEY_FILE}, known_hosts pinned (accept-new) for ${DEPLOY_HOST}:${DEPLOY_PORT}" } -# teardown_ssh — remove the temp private key. +# teardown_ssh — remove the temp private key + known_hosts file. teardown_ssh() { - rm -f "${SSH_KEY_FILE}" + rm -f "${SSH_KEY_FILE}" "${SSH_KNOWN_HOSTS_FILE}" echo "==> teardown_ssh: removed ${SSH_KEY_FILE}" }