fuzzyworld

tower / commit

Merge branch 'main' into forest

eebd8dbcdf4bcd93a42b11cd0e6a968c8a544a20 · Pavel Safronov · 2026-09-27 17:04:48 -0700 · parents f905889c3b20 342ab851a436

Merge branch 'main' into forest

Brings in the terrain worker fix (3413c44), without which a built site hangs
on "Reading the plans", and the Pages build of every branch (342ab85).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 .gitlab-ci.yml         | 21 ++++++++++++--
 src/terrain/service.ts | 11 +++++++-
 tools/build-pages.sh   | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 104 insertions(+), 4 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cd728c4..011dab1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,14 +11,29 @@ test:
 
 pages:
   stage: deploy
+  variables:
+    GIT_DEPTH: "0"
   script:
-    - npm ci
-    - npm run build
-    - mv dist public
+    # The default branch at the root, every other branch at /<branch>/ (build-pages.sh).
+    - sh tools/build-pages.sh public
     # Fail loudly rather than publishing a 404. `mv` silently nests when the
     # destination exists, which is exactly how this broke the first time.
     - test -f public/index.html || (echo "no index.html at artifact root" && exit 1)
+    # The terrain worker must be bundled as its own chunk. When it was not, the build
+    # passed and the site hung forever on "Reading the plans" (service.ts).
+    - ls public/assets/worker-*.js || (echo "terrain worker was not bundled" && exit 1)
   artifacts:
     paths: [public]
   rules:
     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+
+# A push to any other branch rebuilds the site from the default branch, which is what
+# builds that branch's preview at /<branch>/. Parallel deployments would do this
+# natively, but they are Premium-only and this project is on the Free plan.
+preview:
+  stage: deploy
+  trigger:
+    project: PavelSafronov/tower
+    branch: main
+  rules:
+    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
diff --git a/src/terrain/service.ts b/src/terrain/service.ts
index d1c66a6..01c7b47 100644
--- a/src/terrain/service.ts
+++ b/src/terrain/service.ts
@@ -18,6 +18,15 @@ import type { CompiledWorld } from './compile.js'
 import { type Runtime, prepare } from './field.js'
 import type { TileData, TileGrid, TileKey } from './tile.js'
 import type { WorkerApi } from './worker.js'
+/**
+ * The worker, bundled as a worker, and its URL. This must go through Vite's `?worker`
+ * import: a bare `new URL('./worker.ts', import.meta.url)` held in a variable is not
+ * recognised as a worker, so the build copied worker.ts raw as an asset — inlined as a
+ * `data:video/mp2t` URL, since `.ts` is also the MPEG transport stream extension. The
+ * worker failed to start, nothing listened for that, and the first compile call waited
+ * forever on "Reading the plans". Dev served the file transformed, so only builds broke.
+ */
+import workerUrl from './worker.ts?worker&url'
 
 /** The map image, matching v1's raster so the map reads at the same resolution. */
 export const MAP_WIDTH = 2048
@@ -63,7 +72,7 @@ export class TerrainService {
 
   /** The compiled world was read from the cache last time rather than compiled. */
   lastFromCache = false
-  private readonly workerUrl = new URL('./worker.ts', import.meta.url)
+  private readonly workerUrl = new URL(workerUrl, import.meta.url)
   /**
    * Whether the compiled world may be cached. In a build the worker's URL carries a hash
    * of its code, so a new build can never read an old build's world. In dev the URL never
diff --git a/tools/build-pages.sh b/tools/build-pages.sh
new file mode 100644
index 0000000..475d717
--- /dev/null
+++ b/tools/build-pages.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+# Build the GitLab Pages site: the default branch at the root, and every other branch
+# at /<branch>/, so any branch can be looked at in a browser without a dev server.
+#
+# GitLab's own way to do this (parallel deployments) is Premium-only, and this project
+# is on the Free plan. So the default branch's pipeline builds every branch into one
+# site, and a push to any other branch triggers a default-branch pipeline to rebuild it
+# (.gitlab-ci.yml). Assets are relative (vite.config.ts), so a build works in a folder.
+#
+# Every folder also gets a branches.json listing what was built, with links relative to
+# that folder, which is what the app's branch picker reads (src/ui/app.ts).
+#
+#   sh tools/build-pages.sh [out]    run from the repository root; out defaults to public
+set -eu
+
+DEFAULT="${CI_DEFAULT_BRANCH:-main}"
+OUT="${1:-public}"
+WORK="$(mktemp -d)"
+trap 'rm -rf "$WORK"' EXIT
+
+# The default branch, as checked out.
+npm ci
+npm run build
+rm -rf "$OUT"
+mv dist "$OUT"
+# What was built, as branch:folder pairs; the default branch's folder is the root.
+# Neither a git branch name nor a folder name can hold a colon or a space.
+built="$DEFAULT:"
+
+# Every other branch GitLab has, each into its own folder. CI clones shallowly and
+# fetches only the branch it is building, so fetch the rest first.
+git fetch --quiet --prune origin '+refs/heads/*:refs/remotes/origin/*'
+for branch in $(git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin); do
+  case "$branch" in "$DEFAULT"|HEAD) continue ;; esac
+  # A folder name a URL can carry: fix/worker-loading becomes fix-worker-loading.
+  name="$(printf '%s' "$branch" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9.\n-' '-')"
+  if [ -e "$OUT/$name" ]; then
+    echo "skipping $branch: /$name/ is already part of the site"
+    continue
+  fi
+  echo "building $branch into /$name/"
+  git worktree add --quiet --detach "$WORK/$name" "origin/$branch"
+  # A branch that does not build is left out rather than failing the whole site.
+  if (cd "$WORK/$name" && npm ci && npm run build); then
+    mv "$WORK/$name/dist" "$OUT/$name"
+    built="$built $branch:$name"
+  else
+    echo "WARNING: $branch did not build; it has no preview this time"
+  fi
+  git worktree remove --force "$WORK/$name"
+done
+
+# branches.json in every folder: each branch, a link to it from here, and which is this.
+write_branches() {
+  here="$1"
+  up=""
+  [ -n "$here" ] && up="../"
+  sep=""
+  {
+    printf '['
+    for pair in $built; do
+      b="${pair%%:*}"
+      f="${pair#*:}"
+      href="$up${f:+$f/}"
+      [ -n "$href" ] || href="./"
+      cur=false
+      [ "$f" = "$here" ] && cur=true
+      printf '%s\n  {"branch": "%s", "href": "%s", "current": %s}' "$sep" "$b" "$href" "$cur"
+      sep=","
+    done
+    printf '\n]\n'
+  } > "$OUT/${here:+$here/}branches.json"
+}
+for pair in $built; do
+  write_branches "${pair#*:}"
+done