A patch release. The pagination-cap fixes, and the documentation that missed the 0.11.0 installer by a few minutes. docs/UPGRADE.md is one of the documents the Windows installer puts on the server, and its buildings-and-levels section was committed after 0.11.0 was tagged. So 0.11.0 shipped the page an operator reads WHILE upgrading without the guidance about that upgrade - which matters most on an air-gapped box, where there is no other way to reach it. The installer now also ships FLOOR-MAP.md for the same reason the Windows runbooks ship. No application code changed on the server side and the plugin contract stays at 0.20.0. The version and the changelog are the release; the detail is in the entry.
125 lines
6.3 KiB
Bash
Executable File
125 lines
6.3 KiB
Bash
Executable File
#!/bin/bash
|
|
# Build a LEAN per-site artifact from a site profile (ADR-013 Phase 5).
|
|
#
|
|
# A site declares its plugins in a profile (deploy/site-profile.example.json).
|
|
# This resolves the hard-dependency closure, builds the frontend carrying only
|
|
# those plugins (via SITE_PLUGINS -> scripts/stage-frontend.mjs), and stages a
|
|
# backend tree containing core + only the chosen plugin dirs. A plugin a site
|
|
# did not choose ends up in neither the bundle nor the image.
|
|
#
|
|
# Usage: scripts/build-site.sh <site-profile.json> [output-dir]
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
PROFILE="${1:?usage: build-site.sh <site-profile.json> [output-dir]}"
|
|
OUT="${2:-$REPO/build/site}"
|
|
|
|
[ -f "$PROFILE" ] || { echo "profile not found: $PROFILE"; exit 1; }
|
|
|
|
# Resolve the chosen plugins + their hard-dependency closure from the manifests.
|
|
# Shared with the Windows builder (deploy/windows/installer/build-installer.ps1)
|
|
# so both stage the same set from the same profile.
|
|
CLOSURE=$(python3 "$REPO/scripts/resolve_plugin_closure.py" "$PROFILE" "$REPO")
|
|
|
|
echo "Site profile: $PROFILE"
|
|
echo "Plugin closure: $CLOSURE"
|
|
|
|
# Frontend: TWO builds, because Vite compiles the mount path into the bundle and
|
|
# it therefore cannot be chosen at install time from a single build - the page
|
|
# would load and then request its assets from the wrong path.
|
|
# dist -> own IIS site (method A, the default)
|
|
# dist-subpath -> IIS Application under an existing site (method B)
|
|
# SUBPATH_ALIAS is fixed per bundle so the three places that must agree - the IIS
|
|
# application alias, MOUNT_PATH and this base - cannot drift apart.
|
|
SUBPATH_ALIAS="${SUBPATH_ALIAS:-shopdb}"
|
|
SUBPATH_TMP="$(mktemp -d)"
|
|
trap 'rm -rf "$SUBPATH_TMP"' EXIT
|
|
|
|
echo "==> Building frontend for /$SUBPATH_ALIAS/ (SITE_PLUGINS=$CLOSURE) ..."
|
|
( cd "$REPO/frontend" && SITE_PLUGINS="$CLOSURE" VITE_BASE_PATH="/$SUBPATH_ALIAS/" npm run build --silent )
|
|
cp -r "$REPO/frontend/dist" "$SUBPATH_TMP/dist-subpath"
|
|
echo "$SUBPATH_ALIAS" > "$SUBPATH_TMP/dist-subpath/.alias"
|
|
|
|
# Root build LAST, so frontend/dist is left in the state a developer expects.
|
|
echo "==> Building frontend for / (SITE_PLUGINS=$CLOSURE) ..."
|
|
( cd "$REPO/frontend" && SITE_PLUGINS="$CLOSURE" npm run build --silent )
|
|
|
|
# Backend: stage core + only the chosen plugin dirs.
|
|
echo "==> Staging backend into $OUT ..."
|
|
rm -rf "$OUT"
|
|
mkdir -p "$OUT/plugins"
|
|
# cp (not rsync) so a minimal runner/deploy box without rsync can stage;
|
|
# bytecode is pruned afterwards to match the old --exclude filters.
|
|
cp -a "$REPO/shopdb" "$OUT/"
|
|
for name in ${CLOSURE//,/ }; do
|
|
cp -a "$REPO/plugins/$name" "$OUT/plugins/"
|
|
done
|
|
cp -r "$REPO/frontend/dist" "$OUT/frontend-dist"
|
|
# Staged after the rm -rf above, or it would be deleted with everything else.
|
|
cp -r "$SUBPATH_TMP/dist-subpath" "$OUT/frontend-dist-subpath"
|
|
|
|
# Runtime files a deployable tree needs beyond the Python packages. Without these
|
|
# the staged tree can be imported but not actually run or migrated, so the
|
|
# Windows installer (which consumes this output as its app\ payload) had to
|
|
# assemble them separately - and could assemble a tree whose plugin set did not
|
|
# match the profile it was built from.
|
|
cp "$REPO/wsgi.py" "$REPO/requirements.txt" "$OUT/"
|
|
cp -a "$REPO/migrations" "$OUT/"
|
|
# ONLY web.config, not all of deploy/. Two reasons, and the first is fatal:
|
|
# the Windows builder stages its output at deploy/windows/installer/bundle, so
|
|
# copying deploy/ wholesale recursed into the destination and cp aborted with
|
|
# "cannot copy a directory into itself" - the documented build could not finish.
|
|
# Second, the rest of deploy/ is installer SOURCE (scripts, artwork, MSIs); none
|
|
# of it belongs in an application tree that gets copied onto a server.
|
|
# shopdb-install.ps1 reads it from exactly this path.
|
|
if [ -f "$REPO/deploy/windows/web.config" ]; then
|
|
mkdir -p "$OUT/deploy/windows"
|
|
cp -a "$REPO/deploy/windows/web.config" "$OUT/deploy/windows/"
|
|
fi
|
|
|
|
# A CycloneDX SBOM of everything this tree depends on, Python and npm together.
|
|
# Staged INTO the tree so it installs onto the server with the application: an
|
|
# air-gapped site cannot be scanned remotely, so the only way to answer "are we
|
|
# exposed to this CVE, and where" is for the answer to be sitting on the box.
|
|
# Generated from requirements.txt and package-lock.json, both already pinned and
|
|
# committed, so it is a translation rather than a scan - and deterministic.
|
|
echo "==> Generating SBOM ..."
|
|
python3 "$REPO/scripts/generate_sbom.py" "$REPO" -o "$OUT/sbom.cdx.json"
|
|
|
|
# Docs the RUNNING SITE serves or the operator needs on the box. Without
|
|
# openapi.json and llms.txt the self-hosted /api/docs page is broken on every
|
|
# installed server - which matters most for the sites least able to debug it, and
|
|
# for the operators who will point an assistant at their own instance. The
|
|
# Windows runbooks ship because an air-gapped server has no other way to reach
|
|
# them; docs/ is not otherwise staged, so shipping the whole tree would be noise.
|
|
echo "==> Staging docs ..."
|
|
mkdir -p "$OUT/docs"
|
|
for doc in openapi.json llms.txt api-inventory.json \
|
|
INSTALL-WINDOWS.md OPERATE-WINDOWS.md BACKUP-RESTORE.md UPGRADE.md \
|
|
FLOOR-MAP.md; do
|
|
[ -f "$REPO/docs/$doc" ] && cp -a "$REPO/docs/$doc" "$OUT/docs/"
|
|
done
|
|
# Never let a missing optional doc fail the build - the loop's last test governs
|
|
# the exit status under `set -e`.
|
|
true
|
|
|
|
# Stage the profile INTO the tree. This is what makes the set self-describing:
|
|
# `flask plugin apply-profile` at provisioning time reads the same profile the
|
|
# tree was staged from, so the installed plugin set and the shipped plugin code
|
|
# cannot drift. It is also what `flask plugin prune-schema` effectively keys off
|
|
# (via what ends up installed), so a mismatch here would drop the wrong tables.
|
|
cp "$PROFILE" "$OUT/site-profile.json"
|
|
|
|
find "$OUT" -type d -name '__pycache__' -prune -exec rm -rf {} +
|
|
find "$OUT" -type f -name '*.pyc' -delete
|
|
|
|
echo ""
|
|
echo "Lean site staged at: $OUT"
|
|
echo " backend plugins: $(ls "$OUT/plugins" | tr '\n' ' ')"
|
|
echo " (a plugin not listed is absent from both the backend tree and the bundle)"
|
|
echo " profile staged as: $OUT/site-profile.json"
|
|
echo ""
|
|
echo "At provisioning, after 'flask db upgrade' and 'flask plugin upgrade-all':"
|
|
echo " flask plugin apply-profile site-profile.json"
|
|
echo " flask plugin prune-schema --yes --force # ADR-014; BOTH flags required"
|