name: Deploy web Studio # Builds the web Studio (tools/build_web.py -> dist/web) and publishes it to # https://isokit.buildfor.life/ on the buildfor.life web server (s1): the # same Caddy, deploy user and two-directory swap as the blog and cnxvinyl.lol. # One-time server setup (Caddy vhost, web root): deploy/Caddyfile. # # Secrets (repo or org level), the same ones the blog and cnx_vinyl use: # VPS_HOST, VPS_USER, VPS_SSH_KEY required # 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: - "crates/**" - "studio/**" - "viewer/**" - "web/**" - "examples/demo/**" - "tools/build_web.py" - "Cargo.toml" - "Cargo.lock" - ".gitea/workflows/deploy-web.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-isokit-web 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/isokit.buildfor.life SITE_URL: https://isokit.buildfor.life steps: - name: Checkout uses: actions/checkout@v4 - name: Install curl, python3, rsync if missing run: | missing="" for c in curl python3 rsync; do command -v "$c" > /dev/null || missing="$missing $c" done if [ -n "$missing" ]; then sudo="" [ "$(id -u)" = 0 ] || sudo=sudo # shellcheck disable=SC2086 $sudo apt-get update && $sudo apt-get install -y $missing fi - name: Install Rust (rustup, wasm32 target) and the wasm-bindgen CLI # build_web.py wants rustup's cargo and the wasm-bindgen CLI at the # version in Cargo.lock; the CLI comes prebuilt from its GitHub release. run: | cargo_home=${CARGO_HOME:-$HOME/.cargo} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable --target wasm32-unknown-unknown --no-modify-path "$cargo_home/bin/rustup" target add wasm32-unknown-unknown echo "$cargo_home/bin" >> "$GITHUB_PATH" ver=$(grep -A1 '^name = "wasm-bindgen"$' Cargo.lock | sed -n 's/^version = "\(.*\)"$/\1/p') [ -n "$ver" ] || { echo "::error::no wasm-bindgen version in Cargo.lock"; exit 1; } tmp=$(mktemp -d) curl -sSfL "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/$ver/wasm-bindgen-$ver-x86_64-unknown-linux-musl.tar.gz" | tar xz -C "$tmp" install -m 755 "$tmp/wasm-bindgen-$ver-x86_64-unknown-linux-musl/wasm-bindgen" "$cargo_home/bin/" "$cargo_home/bin/rustc" --version "$cargo_home/bin/wasm-bindgen" --version - name: Build dist/web run: | # the agent window's default endpoint: the site's relay to B4L's # inference server (deploy/Caddyfile, handle_path /llm/*) python3 tools/build_web.py --agent-url /llm/v1 echo "$GITHUB_SHA" > dist/web/version.txt - 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 (the blog's / cnx_vinyl's values)" exit 1 fi port=${VPS_PORT:-22} mkdir -p ~/.ssh && chmod 700 ~/.ssh 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 (public) fingerprint for comparison 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 - name: Deploy (rsync to the idle copy, then rename swap) # Two persistent directories alternate as live and previous: 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 dist/web/ "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" = "$GITHUB_SHA" ] || { echo "::error::server holds $live, expected $GITHUB_SHA"; exit 1; } - name: Check the public URL # Warning only: DNS, Cloudflare or a login can make this fail while # the files on the server are already correct. run: | got=$(curl -sS --max-time 20 "$SITE_URL/version.txt?nocache=$GITHUB_SHA" || true) if [ "$got" = "$GITHUB_SHA" ]; then echo "$SITE_URL serves $GITHUB_SHA" else echo "::warning::$SITE_URL/version.txt returned '${got:0:80}', expected $GITHUB_SHA" fi