From 6cc262ea5aaffa7784575b26ebdb8a43c0171259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Thu, 6 Aug 2026 21:00:27 +0200 Subject: [PATCH] An update script that stopped where nobody could see it Found by running it rather than by reading it. Under `set -euo pipefail` the tag resolution added in the last commit dies when no release tag exists -- grep exits 1 when nothing matches, and `head -1` closing the pipe early can hand it a SIGPIPE besides. That is every host until the first release is tagged, which is every host today. It printed "== fetching ==" and stopped: fetched, not reset, not restarted, and exit status swallowed by the pipe it was being read through. The fallback comment two lines above claimed to handle exactly this case. And the consequence of moving to SSH: install.sh takes REPO_URL from the running checkout's origin, so whoever pushes over SSH now hands the deployment a URL the service user cannot use -- it has no key and should not have one, being a credential that can push to the repository sitting on a box to do a read-only job. The clone would have failed loudly, with "Permission denied (publickey)" from an account nobody was thinking about. It is refused up front with the fix named instead. Co-Authored-By: Claude Opus 5 (1M context) --- deploy/README.md | 9 ++++++++- deploy/install.sh | 17 +++++++++++++++++ deploy/update.sh | 7 ++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 33c5da7..a544dc4 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -44,11 +44,18 @@ Everything is overridable from the environment: | `SERVICE_USER` | `lembas` | system account to run as | | `HOME_DIR` | `/home/lembas` | that account's home | | `PREFIX` | `/srv/lembas` | install root (bind mount of `HOME_DIR`) | -| `REPO_URL` | this checkout's `origin` | so a fork deploys itself | +| `REPO_URL` | this checkout's `origin` | so a fork deploys itself. **Must be https** — see below | | `LEMBAS_BRANCH` | `main` | branch to fetch, and what the `edge` channel follows | | `LEMBAS_CHANNEL` | `stable` | `stable` follows release tags, `edge` follows the branch tip | | `INSTALL_UPDATE_HELPER` | `0` | `1` lets the web interface deploy that branch as root | +**The deployment fetches over HTTPS, on purpose.** The service user has no SSH +key and should not have one: a credential that can push to the repository, +sitting on a box, to do a read-only job. If you push over SSH your checkout's +`origin` is an `ssh://` URL, which is the one thing that cannot work here — so +the installer refuses it and names the fix rather than letting the clone fail +with `Permission denied (publickey)` from an account you were not thinking about. + ## Channels | | follows | for | diff --git a/deploy/install.sh b/deploy/install.sh index 956de11..04f9b73 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -48,6 +48,23 @@ if [[ -z "$REPO_URL" ]]; then exit 1 fi +# The deployment clones as the service user, which has no SSH key and should not +# have one: a credential that can push to the repository, sitting on a box, to +# do a read-only job. Whoever runs this usually has an ssh:// origin because +# *they* push over SSH, so the default inherited from their checkout is the one +# thing that cannot work here. +# +# The clone would fail loudly anyway. Saying so first turns "Permission denied +# (publickey)" from the service user into a sentence that names the fix. +if [[ "$REPO_URL" == ssh://* || "$REPO_URL" == git@* ]]; then + echo "== repository ==" >&2 + echo " $REPO_URL is an SSH URL, and $SERVICE_USER has no key." >&2 + echo " Set an https URL, which is what a deployment should fetch over:" >&2 + echo " REPO_URL=https://host/owner/repo.git $0" >&2 + echo " (Or give $SERVICE_USER a read-only deploy key and re-run.)" >&2 + exit 1 +fi + echo "== plan ==" echo " host : https://$SITE_HOST -> 127.0.0.1:$APP_PORT" echo " user : $SERVICE_USER ($HOME_DIR)" diff --git a/deploy/update.sh b/deploy/update.sh index cb8728d..38a8d08 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -41,8 +41,13 @@ git_as fetch --quiet --tags --force origin "$BRANCH" # onto a release candidate on the strength of a hyphen. target="origin/$BRANCH" if [[ "$CHANNEL" == "stable" ]]; then + # `|| true` is load-bearing under `set -euo pipefail`, and for two reasons: + # grep exits 1 when nothing matches -- which is every host until the first + # release is tagged -- and `head -1` closing the pipe early can hand grep a + # SIGPIPE. Either kills the script mid-update, after the fetch and before the + # reset, leaving the checkout fetched and unmoved with no error printed. newest=$(git_as tag --list --sort=-v:refname \ - | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -1) + | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true) if [[ -n "$newest" ]]; then target="$newest" else