tower / commit
Merge branch 'main' into forest
.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