Files
cameleer-website/.gitea/workflows/deploy.yml
hsiegeln cb21be71f0
Some checks failed
ci / build-test (push) Successful in 3m55s
deploy / build (push) Successful in 28s
deploy / deploy (push) Failing after 12s
Merge branch 'feat/initial-build' into main
Fix lftp auth (explicit -u USER, + unindented heredoc body).
2026-04-24 20:08:29 +02:00

146 lines
5.6 KiB
YAML

# -----------------------------------------------------------------------------
# cameleer-website — Deploy to Hetzner Webhosting L
#
# Runs ONLY on pushes to `main` and on manual dispatch from the Gitea UI.
# Does NOT run Lighthouse CI (that's in ci.yml — assume any commit that reached
# main already passed the full gate). Rebuilds fresh, runs the TBD guard, and
# rsyncs `dist/` to the origin over SSH with host-key pinning.
#
# Runner: self-hosted arm64 Gitea runner. Adjust `runs-on` if your runner's
# labels differ. Deploy target is Hetzner amd64 — arch mismatch is a non-issue
# because the bundle is static HTML/CSS/JS.
#
# Required secrets (repo settings → Actions → Secrets):
# SFTP_HOST, SFTP_USER, SFTP_PATH, SFTP_KEY, SFTP_KNOWN_HOSTS
# Required variables (repo settings → Actions → Variables):
# PUBLIC_AUTH_SIGNIN_URL, PUBLIC_AUTH_SIGNUP_URL, PUBLIC_SALES_EMAIL
# -----------------------------------------------------------------------------
name: deploy
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
PUBLIC_AUTH_SIGNIN_URL: ${{ secrets.PUBLIC_AUTH_SIGNIN_URL }}
PUBLIC_AUTH_SIGNUP_URL: ${{ secrets.PUBLIC_AUTH_SIGNUP_URL }}
PUBLIC_SALES_EMAIL: ${{ secrets.PUBLIC_SALES_EMAIL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests (sanity check)
run: npm test
- name: Build site
run: npm run build
- name: Guard — no TBD markers may ship in built HTML
run: |
if grep -rlE '(TBD):' dist 2>/dev/null | grep -E '\.(html|svg)$'; then
echo "Built output contains unfilled <TBD:...>) markers."
echo "Fill in imprint.astro and privacy.astro operator fields before merging to main."
exit 1
fi
# Pin to v3 — Gitea Actions implements the v3 artifact protocol.
# upload/download-artifact@v4 talk to a github.com-only backend and
# fail with GHESNotSupportedError on Gitea / Forgejo / GHES.
- name: Upload dist artifact
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Download dist artifact
uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Configure SSH
env:
SFTP_KEY: ${{ secrets.SFTP_KEY }}
SFTP_KNOWN_HOSTS: ${{ secrets.SFTP_KNOWN_HOSTS }}
run: |
set -e
: "${SFTP_KEY:?SFTP_KEY secret must be set}"
: "${SFTP_KNOWN_HOSTS:?SFTP_KNOWN_HOSTS secret must be set}"
mkdir -p ~/.ssh
printf '%s\n' "$SFTP_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$SFTP_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
# 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 lftp openssh-client
fi
- 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 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}"
# `-u USER,` (with trailing comma = empty password) tells lftp not
# to prompt for a password; auth happens via the key passed to ssh
# by sftp:connect-program. Heredoc body is unindented so lftp's
# parser doesn't mistake leading whitespace for a continuation.
# `debug 3` prints the ssh command lftp invokes — useful if this
# ever breaks again.
lftp <<LFTP
debug 3
set cmd:fail-exit yes
set net:max-retries 1
set sftp:connect-program 'ssh -a -x -i $HOME/.ssh/id_ed25519 -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts'
open -u $SFTP_USER, sftp://$SFTP_HOST
mirror --reverse --delete --verbose --parallel=4 dist/ $SFTP_PATH/
bye
LFTP
- name: Post-deploy smoke test
run: |
set -e
echo "Checking security headers on www.cameleer.io..."
HEADERS=$(curl -sI https://www.cameleer.io/ || echo "")
echo "$HEADERS" | grep -i '^strict-transport-security:' || { echo "HSTS missing"; exit 1; }
echo "$HEADERS" | grep -i '^content-security-policy:' || { echo "CSP missing"; exit 1; }
echo "$HEADERS" | grep -i '^x-frame-options:' || { echo "XFO missing"; exit 1; }
echo "All required headers present on the live origin."