Nine fixes from a review of the installer against its actual audience: DT leads at sister sites who are not Windows, IIS or Python specialists and who will lean on an AI assistant to get through it. TRUTHFULNESS. The preflight was advisory - an operator read 'IIS is not installed', pressed Next, answered five more pages and the install died partway through with Python already on the box. The results page now blocks while anything is failing, repaints on every run instead of latching after the first, and offers 'Check again' so a fixed problem does not mean starting over. On failure the wizard said 'Nothing was left running', which is false in every path because the stages run with -OnFailure never: it now says the server is part-configured, that re-running is safe, and how to remove it. The final page no longer reads 'ShopDB-Flask is ready' after a failed install. SECRETS. The generated MySQL root password went to Write-Host in a process the wizard runs hidden - so nobody saw it - and stdout is forwarded into the setup log operators are told to send to support, so it was permanently recorded for everyone who did not need it. It now goes to an ACL'd file. Database dumps, which contain every user password hash, landed in a ProgramData directory readable by every user on the box; the directory is now locked at creation. UPGRADES ON REMOTE-DATABASE SITES. mysqldump was looked for only under local MySQL install paths, so a site whose database is on another host silently skipped every pre-upgrade backup - after stage 2 had already stopped the pool and replaced the tree. Find-MysqlTool now prefers a client shipped in the bundle, stage 2 stages it onto the server, preflight reports when it is missing, and mysqlclient\ is an optional locked payload. UNINSTALL. A subpath install is an IIS Application, not a site; removing only the site left the application pointing at a deleted directory, so the parent site - at West Jefferson, the live classic ASP - served 503 on that path forever while Add/Remove Programs reported success. Uninstall now reads MOUNT_PATH and removes the application. The firewall rule was created as "$SiteName $SitePort" and removed as the literal 'ShopDB-Flask 8090', which matches nothing. DAY-2 TOOLING. Every shortcut now passes -AppRoot and -SitePort, and the console forwards them through its own elevation and 32-bit relaunches instead of discarding them - a non-default directory or port made it report a healthy site as broken, from a shortcut the installer wrote. 'Open ShopDB-Flask' resolved to a hardcoded localhost:8090 that was wrong for every subpath install; it now asks the console, which reads the address the installer recorded, and no longer demands administrator to open a browser. SMOKE TEST. The parent-site port lookup filtered for an http binding and defaulted to 80, so an https-only parent site failed a working install with a red dialog. DOCS AND /api/docs. The installer was invisible: nothing in docs/, README.md or CLAUDE.md mentioned it, so a DT lead or their assistant landed on the manual IIS runbook and hand-built the very server the installer then refuses to upgrade. docs/INSTALL-WINDOWS.md and docs/OPERATE-WINDOWS.md are now the canonical route, the two manual runbooks are bannered as reference-only, README and CLAUDE.md route by target, and llms.txt tells an assistant which document to follow and to ask for 'check -Json' before diagnosing. Both ship on the server, along with openapi.json and llms.txt - without those the self-hosted /api/docs was broken on every installed box, which matters most to the sites least able to debug it. Stage 5 now checks it actually serves. shopdb-admin.ps1 gains 'check -Json': one structured, secret-free block covering version, publishing method, IIS state, HTTP reachability, database, Python version, plugins and errors. That is the cheapest useful answer to 'the operator will ask an LLM' - it works with no infrastructure, which a install-time MCP server could not.
124 lines
6.2 KiB
Bash
Executable File
124 lines
6.2 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; 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"
|