deploy: web Studio on isokit.buildfor.life (same pipeline as cnxvinyl.lol)
Deploy web Studio / deploy (push) Successful in 3m14s

Gitea Actions workflow builds dist/web (rustup + wasm32 target, prebuilt
wasm-bindgen CLI at the Cargo.lock version, tools/build_web.py) and
publishes it to /var/www/isokit.buildfor.life on s1 with the blog's
two-directory rename swap and the same VPS_* secrets as cnx_vinyl.
deploy/Caddyfile mirrors the vhost applied on s1 (no-cache so the
unversioned wasm glue never goes stale at Cloudflare).
This commit is contained in:
grabowski committed 2026-10-23 16:44:18 +07:00
1 parent 34a77892c9
commit c1d01331ac
3 files changed
+184

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
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: |
python3 tools/build_web.py
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
+4
View File
@@ -59,6 +59,9 @@ python tools/build_web.py # -> dist/web/ (static files,
``` ```
`dist/web/` runs from any static web server (http or https, not `file://`). `dist/web/` runs from any static web server (http or https, not `file://`).
A live build is at https://isokit.buildfor.life/: every push to `master` that
touches the engine or the front end rebuilds and deploys it
(`.gitea/workflows/deploy-web.yml`; server setup in `deploy/Caddyfile`).
The first visit opens a copy of the demo. Everything the desktop Studio does The first visit opens a copy of the demo. Everything the desktop Studio does
works the same, because the page runs the same engine and answers the same works the same, because the page runs the same engine and answers the same
requests. In addition: requests. In addition:
@@ -144,6 +147,7 @@ viewer/ isokit-view.js/.css: pan, zoom, levels, rotation, tooltip
studio/ the Studio GUI (served by `isokit studio`, and by the web Studio) studio/ the Studio GUI (served by `isokit studio`, and by the web Studio)
web/ web Studio shell: WASM backend for studio.js, projects, folders, zip, code editor web/ web Studio shell: WASM backend for studio.js, projects, folders, zip, code editor
examples/demo demo project examples/demo demo project
deploy/Caddyfile the web Studio's vhost on the buildfor.life web server (s1)
``` ```
The layers are independent. A Rust program can draw with `isokit-core` alone. The layers are independent. A Rust program can draw with `isokit-core` alone.
+25
View File
@@ -0,0 +1,25 @@
# isokit.buildfor.life: the web Studio on the buildfor.life web server (s1).
# Static web root /var/www/isokit.buildfor.life, filled by
# .gitea/workflows/deploy-web.yml (tools/build_web.py -> dist/web).
#
# Append this block to /etc/caddy/Caddyfile on s1, then
# runuser -u caddy -- caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
# systemctl reload caddy
# This file mirrors what s1 runs; keep the two in step.
#
# DNS: *.buildfor.life is a proxied wildcard at Cloudflare that reaches s1,
# so the name needs no record of its own. Caddy gets its certificate itself.
isokit.buildfor.life {
header X-Frame-Options "SAMEORIGIN"
# Testing deployment: keep it out of search engines.
header X-Robots-Tag "noindex, nofollow"
# Revalidate everything: isokit_wasm.js and its .wasm load without a
# version query, and a stale copy at Cloudflare would not match the
# new glue code after a deploy. Unchanged files still come back as 304.
header Cache-Control "no-cache"
root * /var/www/isokit.buildfor.life
encode zstd gzip
file_server
}