diff --git a/CHANGELOG.md b/CHANGELOG.md index aa88e6a..43cff21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.8.0] - 2026-06-23 + +### Added +- **`infra/vps/` PRAB generation orchestrator**: systemd timer + service + bash + script that SSHes from the Coolify KVM8 VPS to Hostinger and drives + `wp prautoblogger generate --sync` in a held-open SSH session. Sidesteps + Hostinger's ~10-min background-PHP-process kill that was blocking full + generation runs. Components: + - `orchestrate-generation.sh`: main script with DRY_RUN mode, structured + logging, reaper pre-sweep, failure email alert, and shellcheck-clean quoting. + - `prab-generation.service` / `prab-generation.timer`: systemd oneshot service + (2h timeout, no auto-restart) + daily 06:00 UTC timer with 5-min jitter. + - `install-orchestrator.sh`: one-time setup script; installs units but does NOT + enable the timer (requires explicit CEO greenlight post-QA). + - Security: `StrictHostKeyChecking=accept-new` with pre-seeded known_hosts for + the Hostinger IP; no hardcoded secrets; SSH key referenced by env-var path. + ## [1.7.2] - 2026-06-17 ### Fixed diff --git a/infra/vps/install-orchestrator.sh b/infra/vps/install-orchestrator.sh index 98f33cb..2146ae8 100755 --- a/infra/vps/install-orchestrator.sh +++ b/infra/vps/install-orchestrator.sh @@ -45,6 +45,21 @@ mkdir -p "${INSTALL_DIR}" install -m 0750 "${ORCHESTRATE_SH}" "${INSTALL_DIR}/orchestrate-generation.sh" echo " OK: ${INSTALL_DIR}/orchestrate-generation.sh" +# Pre-seed known_hosts with Hostinger's host key so ssh(1) can verify +# the server identity without StrictHostKeyChecking=no. +# ssh-keyscan output is pinned here; re-run if Hostinger rotates its key. +KNOWN_HOSTS_FILE="${INSTALL_DIR}/known_hosts" +if [[ ! -f "${KNOWN_HOSTS_FILE}" ]]; then + echo "Pre-seeding known_hosts from Hostinger (${SSH_HOST:-145.223.107.228}:${SSH_PORT:-65002})..." + ssh-keyscan -p "${SSH_PORT:-65002}" "${SSH_HOST:-145.223.107.228}" \ + > "${KNOWN_HOSTS_FILE}" 2>/dev/null \ + && chmod 0600 "${KNOWN_HOSTS_FILE}" \ + && echo " OK: ${KNOWN_HOSTS_FILE}" \ + || echo " WARN: ssh-keyscan failed — known_hosts not seeded; first SSH will add key via accept-new" +else + echo " OK: ${KNOWN_HOSTS_FILE} (already exists, not overwritten)" +fi + echo "Installing systemd units..." install -m 0644 "${SERVICE_FILE}" "${SYSTEMD_DIR}/prab-generation.service" install -m 0644 "${TIMER_FILE}" "${SYSTEMD_DIR}/prab-generation.timer" diff --git a/infra/vps/orchestrate-generation.sh b/infra/vps/orchestrate-generation.sh index e71f566..1827879 100755 --- a/infra/vps/orchestrate-generation.sh +++ b/infra/vps/orchestrate-generation.sh @@ -36,6 +36,9 @@ SSH_HOST="${SSH_HOST:-145.223.107.228}" SSH_PORT="${SSH_PORT:-65002}" SSH_USER="${SSH_USER:-u117248512}" WP_PATH="${WP_PATH:-/home/u117248512/domains/peptiderepo.com/public_html}" +# Known-hosts file pre-seeded by install-orchestrator.sh with Hostinger's host key. +# Pins the server identity; StrictHostKeyChecking=accept-new adds on first connect. +KNOWN_HOSTS="${KNOWN_HOSTS:-/opt/prab-orchestrator/known_hosts}" # --------------------------------------------------------------------------- # Logging helpers @@ -66,7 +69,7 @@ fi # --------------------------------------------------------------------------- if [[ "${DRY_RUN}" == "1" ]]; then log "DRY_RUN=1 — echoing commands only, no generation" - log "WOULD RUN: ssh -i ${SSH_KEY} -p ${SSH_PORT} -o StrictHostKeyChecking=no -o ConnectTimeout=15 ${SSH_USER}@${SSH_HOST} \\" + log "WOULD RUN: ssh -i ${SSH_KEY} -p ${SSH_PORT} -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 ${SSH_USER}@${SSH_HOST} \\" log " 'wp prautoblogger generate --sync --path=${WP_PATH} 2>&1'" log "DRY_RUN complete" exit 0 @@ -90,7 +93,8 @@ REAP_OUT="" # shellcheck disable=SC2029 REAP_OUT="$(ssh -i "${SSH_KEY}" \ -p "${SSH_PORT}" \ - -o StrictHostKeyChecking=no \ + -o StrictHostKeyChecking=accept-new \ + -o UserKnownHostsFile="${KNOWN_HOSTS}" \ -o ConnectTimeout=15 \ -o ServerAliveInterval=60 \ -o ServerAliveCountMax=30 \ @@ -112,7 +116,8 @@ EXIT_CODE=0 # shellcheck disable=SC2029 RESULT_OUTPUT="$(ssh -i "${SSH_KEY}" \ -p "${SSH_PORT}" \ - -o StrictHostKeyChecking=no \ + -o StrictHostKeyChecking=accept-new \ + -o UserKnownHostsFile="${KNOWN_HOSTS}" \ -o ConnectTimeout=15 \ -o ServerAliveInterval=60 \ -o ServerAliveCountMax=30 \ diff --git a/infra/vps/prab-generation.service b/infra/vps/prab-generation.service index a215f5c..5015be6 100644 --- a/infra/vps/prab-generation.service +++ b/infra/vps/prab-generation.service @@ -7,6 +7,9 @@ After=network.target [Service] Type=oneshot +# root owns /root/.ssh/hostinger_prod_ed25519 and /opt/prab-orchestrator/known_hosts; +# a dedicated service user would need its own keypair + sudo-less SSH access. +# Acceptable on this dedicated lab VPS (not shared); revisit if VPS is multi-tenant. User=root ExecStart=/opt/prab-orchestrator/orchestrate-generation.sh StandardOutput=journal diff --git a/package.json b/package.json index ee10ec4..447a511 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peptide-e2e", - "version": "1.7.2", + "version": "1.8.0", "description": "Playwright smoke tests for peptiderepo.com (staging + production)", "private": true, "scripts": {