From 64aa8f426bbc8e85005e055f1ab81b02f8f05340 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:49:42 +0200 Subject: [PATCH] ci(deploy): switch from rsync to lftp mirror (SFTP-only hosting) Hetzner Webhosting accepts SSH for file transfer but refuses remote command exec, failing rsync with: exec request failed on channel 0 rsync error: error in rsync protocol data stream (code 12) rsync over SSH requires spawning a remote rsync binary, which isn't possible on SFTP-only tiers. Switch the mirror to lftp, which speaks SFTP end-to-end. Same semantics (upload + delete removed files), same key + known_hosts pinning via sftp:connect-program. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitea/workflows/deploy.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 074ae21..97b90aa 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -96,27 +96,35 @@ jobs: chmod 600 ~/.ssh/id_ed25519 printf '%s\n' "$SFTP_KNOWN_HOSTS" > ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts - # Ensure rsync + openssh are present even on a minimal runner image. - if ! command -v rsync >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then + # Hetzner Webhosting accounts are SFTP-only — they accept SSH for file + # transfer but refuse remote command exec ("exec request failed on + # channel 0"). rsync over SSH needs to spawn a remote rsync binary, + # so it cannot work here. Use lftp's mirror instead, which speaks + # SFTP end-to-end with the same key + known_hosts pinning. + if ! command -v lftp >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi $SUDO apt-get update -qq - $SUDO apt-get install -y --no-install-recommends rsync openssh-client + $SUDO apt-get install -y --no-install-recommends lftp openssh-client fi - - name: Deploy via rsync + - name: Deploy via lftp (mirror over SFTP) env: SFTP_USER: ${{ secrets.SFTP_USER }} SFTP_HOST: ${{ secrets.SFTP_HOST }} SFTP_PATH: ${{ secrets.SFTP_PATH }} run: | - # Fail loudly if any secret is missing — otherwise rsync --delete + # Fail loudly if any secret is missing — otherwise mirror --delete # could be directed at the SSH user's home root. : "${SFTP_USER:?SFTP_USER secret must be set}" : "${SFTP_HOST:?SFTP_HOST secret must be set}" : "${SFTP_PATH:?SFTP_PATH secret must be set}" - rsync -avz --delete \ - -e "ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes -o UserKnownHostsFile=~/.ssh/known_hosts" \ - dist/ "$SFTP_USER@$SFTP_HOST:$SFTP_PATH/" + lftp <