name: Deploy test site # Builds site/dist (site/public/ plus demo pages copied out of docs/, see # site/build.sh) and publishes it to https://cnxvinyl.lol/ on the # buildfor.life web server (s1), the same Caddy that serves the blog. # One-time setup (DNS, Caddy vhost, secrets, deploy key): site/README.md. # # Secrets (repo or user level): # VPS_HOST, VPS_USER, VPS_SSH_KEY required; same server as the blog # VPS_PORT optional, default 22 # VPS_KNOWN_HOSTS optional pinned host key line(s); # without it the key is scanned per run on: push: branches: [master] paths: - "site/**" - "docs/**/*.html" - "docs/pitch/**" - ".gitea/workflows/deploy-site.yml" workflow_dispatch: # A newer push supersedes a running deploy. Safe to cancel mid-run: rsync # writes the non-live directory and the rename swap happens last. concurrency: group: deploy-cnxvinyl-lol cancel-in-progress: true jobs: deploy: runs-on: ubuntu-latest env: VPS_HOST: ${{ secrets.VPS_HOST }} VPS_USER: ${{ secrets.VPS_USER }} VPS_PORT: ${{ secrets.VPS_PORT }} SITE_DIR: /var/www/cnxvinyl.lol SITE_URL: https://cnxvinyl.lol steps: - name: Checkout uses: actions/checkout@v4 - name: Build site/dist run: bash site/build.sh - name: Publish to the package registry (bkk01) # bkk01 serves cnxvinyl.lol from releases in the B4L package registry # (cnx-network-clan, docs/src/s1-migration.md): the package event # makes it fetch this version within seconds. Until DNS moves to # bkk01, s1 below stays the live server and gets the same build. # First, so a failing s1 step cannot hold back bkk01's release. # REGISTRY_USER / REGISTRY_TOKEN: organization secrets, an account # with a write:package token in a B4L team that may write packages. env: REGISTRY_USER: ${{ secrets.REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_TOKEN" ]; then echo "::error::REGISTRY_USER and REGISTRY_TOKEN must be set as organization secrets" exit 1 fi version="$(date -u +%Y%m%dT%H%M%S)-${GITHUB_SHA::12}" tar -czf site.tar.gz -C site/dist . curl -fsS --user "$REGISTRY_USER:$REGISTRY_TOKEN" --upload-file site.tar.gz \ "${{ github.server_url }}/api/packages/${{ github.repository_owner }}/generic/cnxvinyl.lol/$version/site.tar.gz" echo "published cnxvinyl.lol $version ($(du -h site.tar.gz | cut -f1))" - name: Set up SSH env: VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }} VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }} run: | if [ -z "$VPS_HOST" ] || [ -z "$VPS_USER" ] || [ -z "$VPS_SSH_KEY" ]; then echo "::error::VPS_HOST, VPS_USER and VPS_SSH_KEY must be set as Actions secrets (see site/README.md)" exit 1 fi port=${VPS_PORT:-22} # ssh refuses a config that is group/world-writable or owned by # another user ("Bad owner or permissions on ~/.ssh/config"), which # a runner with a loose umask or a reused HOME produces: create every # file here private and fresh. umask 077 mkdir -p ~/.ssh && chmod 700 ~/.ssh rm -f ~/.ssh/config printf '%s\n' "$VPS_SSH_KEY" | tr -d '\r' > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key # Fail here, not at the server, when the secret is not a usable key, # and log its fingerprint (public) so it can be compared with # `ssh-keygen -lf ~deploy/.ssh/authorized_keys` on the server. if ! ssh-keygen -y -P "" -f ~/.ssh/deploy_key > ~/.ssh/deploy_key.pub 2>/dev/null; then echo "::error::VPS_SSH_KEY does not load as an unencrypted private key; paste the whole OpenSSH-format key including the BEGIN/END lines" exit 1 fi echo "key in VPS_SSH_KEY: $(ssh-keygen -lf ~/.ssh/deploy_key.pub)" if [ -n "$VPS_KNOWN_HOSTS" ]; then printf '%s\n' "$VPS_KNOWN_HOSTS" >> ~/.ssh/known_hosts else for i in 1 2 3; do ssh-keyscan -T 10 -p "$port" "$VPS_HOST" > ~/.ssh/keyscan.out && [ -s ~/.ssh/keyscan.out ] && break echo "ssh-keyscan attempt $i failed; retrying in 15 s" >&2 sleep 15 done if [ ! -s ~/.ssh/keyscan.out ]; then echo "::error::ssh-keyscan could not reach $VPS_HOST:$port (server down or runner IP blocked?)" exit 1 fi cat ~/.ssh/keyscan.out >> ~/.ssh/known_hosts fi { echo "Host webhost" echo " HostName $VPS_HOST" echo " User $VPS_USER" echo " Port $port" echo " IdentityFile ~/.ssh/deploy_key" echo " IdentitiesOnly yes" echo " BatchMode yes" } > ~/.ssh/config chmod 600 ~/.ssh/config ~/.ssh/known_hosts - name: Ensure rsync is available run: command -v rsync || (sudo apt-get update && sudo apt-get install -y rsync) || (apt-get update && apt-get install -y rsync) - name: Deploy (rsync to the idle copy, then rename swap) # Two persistent directories alternate as live and previous, as for # the blog: rsync only ever writes the one that is not live, and the # swap is three same-filesystem renames, so Caddy never serves a # half-written tree. The previous build stays on disk for rollback. run: | # SC2029: the paths are meant to expand on the runner side # shellcheck disable=SC2029 ssh webhost "mkdir -p '$SITE_DIR' '$SITE_DIR-prev' && rm -rf '$SITE_DIR-tmp'" rsync -az --delete site/dist/ "webhost:$SITE_DIR-prev/" # shellcheck disable=SC2029 ssh webhost "mv '$SITE_DIR' '$SITE_DIR-tmp' && mv '$SITE_DIR-prev' '$SITE_DIR' && mv '$SITE_DIR-tmp' '$SITE_DIR-prev'" # shellcheck disable=SC2029 live=$(ssh webhost "cat '$SITE_DIR/version.txt'") echo "live on server: $live" [ "$live" = "$(cat site/dist/version.txt)" ] || { echo "::error::server holds $live, expected $(cat site/dist/version.txt)"; exit 1; } - name: Check the public URL # Warning only: DNS or a basic_auth login can make this # fail while the files on the server are already correct. run: | expected=$(cat site/dist/version.txt) got=$(curl -sS --max-time 20 "$SITE_URL/version.txt?nocache=$expected" || true) if [ "$got" = "$expected" ]; then echo "$SITE_URL serves $expected" else echo "::warning::$SITE_URL/version.txt returned '${got:0:80}', expected $expected" fi