tower / commit
ci(pages): put every branch on Pages, with a list for a branch picker
.gitlab-ci.yml | 18 ++++++++++--- tools/build-pages.sh | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 529dfa6..011dab1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,10 +11,11 @@ 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) @@ -25,3 +26,14 @@ pages: 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/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