SkillPilot Deployment Process
This document describes the current automated deployment workflow for Linux
servers. Operators use the stable repository-root entrypoint
./deploy_skillpilot.sh; it delegates to the generic deployment engine
scripts/deploy.sh.
Stable production entrypoint
The normal production command is:
./deploy_skillpilot.sh
The root entrypoint pins VITE_SKILLPILOT_COACH_VARIANT=openai-mcp, which is
the current production architecture for the German coach, and then executes
scripts/deploy.sh. The engine continues to validate the variant before the
build and again in the local and publicly served frontend artifacts. This keeps
one operational command without removing the deployment guardrail.
An intentional rollback uses the same entrypoint with an explicit command-line option:
./deploy_skillpilot.sh --coach-variant visible-session
On the production server, the historical launcher in the operator home directory can remain the everyday entrypoint as a symlink to the versioned script:
ln -s /home/enpasos/skillpilot/deploy_skillpilot.sh \
/home/enpasos/deploy_skillpilot.sh
The versioned entrypoint resolves symlinks before locating the repository, so
running /home/enpasos/deploy_skillpilot.sh still executes the checked-in
deployment engine from /home/enpasos/skillpilot. There is no second copy of
the deployment logic to maintain.
Overview
The deployment process currently does all of the following:
1. Require an explicit, valid frontend coach variant for this artifact.
2. Check that the target systemd service is reachable and that the exact
restart command has a passwordless sudo grant.
3. Stash local working-tree changes.
4. Pull the latest code from Git and, if HEAD changed, restart the freshly
checked-out deployment engine.
5. Validate the OpenAI V1 release contract and any explicit public-URL
overrides before copying assets, building, or restarting the service.
6. Deploy curriculum decks from curricula/.../json/ into both frontend and backend static data folders.
7. Deploy whitepaper assets into app/public/whitepaper and the comic folders.
8. Deploy quickstart/story assets into app/public/.
9. Install frontend dependencies and verify the committed AI-transparency inventory against the exact assets to be deployed.
10. Rebuild the React app.
11. Verify the requested coach variant and the referenced CSS/JavaScript shell assets in the generated backend static artifact.
12. Build the backend jar with the exact deployed Git commit embedded as the
OpenAI server build and MCP server version.
13. Verify that the processed backend resources contain that commit.
14. For the openai-mcp variant, run the focused backend security and contract
tests.
15. Restart the skillpilot system service.
16. Wait until the public readiness endpoint returns HTTP 200.
17. Verify the deployed CSS/JavaScript shell assets, coach variant, and AI-transparency copy against the public host.
18. For the openai-mcp variant, require the public path-based OpenAI V1 smoke;
then run the source-rationale deployment smoke against the public host.
The Deployment Engine (scripts/deploy.sh)
This is the current automation flow:
#!/bin/bash
set -e
# Deploy from the repository that contains this script.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_ROOT}"
SERVICE_NAME="${SKILLPILOT_SERVICE_NAME:-skillpilot}"
echo "Pruefe explizite Coach-Variante..."
# The script accepts only visible-session, openai-mcp, or legacy and aborts
# before Git/build/restart when VITE_SKILLPILOT_COACH_VARIANT is absent.
echo "Pruefe Restart-Voraussetzungen..."
# The script validates systemctl access and the command-specific NOPASSWD grant
# before doing expensive build work.
if [ "${SKILLPILOT_SKIP_GIT_UPDATE:-0}" = "1" ]; then
echo "Ueberspringe Git-Update (SKILLPILOT_SKIP_GIT_UPDATE=1)."
else
echo "Stash local changes..."
git stash
echo "Hole Updates..."
git pull
fi
echo "Pruefe konsistente OpenAI-Plugin-V1-Versionierung..."
node scripts/check_openai_plugin_versioning.mjs
if [ "${VITE_SKILLPILOT_COACH_VARIANT}" = "openai-mcp" ]; then
echo "Pruefe exakte OpenAI-Plugin-V1-Runtime-Konfiguration..."
node scripts/validate_openai_v1_runtime_config.mjs
fi
echo "Pruefe unveraenderten OpenAI-Plugin-V1-Snapshot..."
node scripts/openai_plugin_release.mjs verify
echo "Deploying Vocabulary Decks..."
python3 scripts/deploy_decks.py
echo "Deploying Whitepaper assets..."
python3 scripts/deploy_whitepaper.py
echo "Deploying Story assets..."
python3 scripts/deploy_story.py
cd app
echo "Installiere Abhaengigkeiten..."
npm install
echo "Baue Anwendung..."
npm run build
echo "Pruefe KI-Transparenznachweis..."
npm run check:ai-transparency-inventory
echo "Pruefe Coach-Variante im Frontend-Artefakt..."
node ../scripts/verify_frontend_coach_variant.mjs \
../backend/src/main/resources/static \
"${VITE_SKILLPILOT_COACH_VARIANT}"
echo "Pruefe KI-Transparenz im Frontend-Artefakt..."
node ../scripts/verify_ai_transparency_artifact.mjs \
../backend/src/main/resources/static
echo "Pruefe Frontend-Shell-Assets im Build-Artefakt..."
node ../scripts/verify_frontend_shell_assets.mjs \
../backend/src/main/resources/static
cd ../backend
chmod +x gradlew
./gradlew clean build -x test
if [ "${VITE_SKILLPILOT_COACH_VARIANT}" = "openai-mcp" ]; then
echo "Pruefe eingebettete Backend-Buildkennung..."
node ../scripts/validate_openai_v1_runtime_config.mjs \
--built-application build/resources/main/application.yml
fi
cd ..
echo "Starte Service neu..."
sudo -n -- "$(command -v systemctl)" restart "${SERVICE_NAME}"
SMOKE_BASE_URL="${SKILLPILOT_BASE_URL:-https://skillpilot.com}"
echo "Warte auf oeffentliche Readiness..."
# Polls /actuator/health/readiness until HTTP 200 or the configured timeout.
echo "Pruefe ausgelieferte Frontend-Shell-Assets..."
node scripts/verify_frontend_shell_assets.mjs \
"${SMOKE_BASE_URL}"
echo "Pruefe ausgelieferte Coach-Variante..."
node scripts/verify_frontend_coach_variant.mjs \
"${SMOKE_BASE_URL}" \
"${VITE_SKILLPILOT_COACH_VARIANT}"
echo "Pruefe ausgelieferte KI-Transparenz..."
node scripts/verify_ai_transparency_artifact.mjs \
"${SMOKE_BASE_URL}"
echo "Pruefe Quellenbegruendungs-Smoke-Test..."
cd app
npm run smoke:goal-source-rationales:deployment -- --base-url="${SMOKE_BASE_URL}"
Why this order?
- Coach-variant preflight first: every deploy must state the intended frontend contract; there is no production default that could silently choose Visible Session or MCP.
- Restart preflight: the script fails before stashing, copying assets, or
building if the current environment cannot reach the
systemdservice or lacks a command-specific, passwordless restart grant. It never opens a generalsudopassword prompt. git stash+git pull: the current script assumes deployment happens from a possibly dirty working tree and protects the pull by stashing first. If the pull changesHEAD, it restarts the newly checked-out deployment engine before continuing.- OpenAI V1 preflight: the checked-in release contract is validated before asset copying, build and restart. The three canonical V1 public URL variables may be absent, in which case the versioned application defaults are used. An explicitly supplied value, including an empty value, must equal the canonical V1 value exactly; a stale alias, typo, whitespace or other origin fails closed.
- Deck/story/whitepaper deployment must happen before the frontend build so it receives the exact public asset set.
- Frontend build before inventory verification synchronizes the generated runtime assets into both frontend and the non-versioned backend build tree. This prevents the inventory check from comparing a current frontend asset with a stale backend copy.
- AI-transparency inventory check binds current visualization providers and C2PA container markers, illustration collections, canonical goal/card counts, and podcast hashes to the reviewed inventory under
docs/legal/. Asset drift therefore stops deployment before backend build or restart. - Frontend artifact verification must finish before backend build
or restart. The shell verifier reads
index.html, rejects cross-origin stylesheet/module references, and checks that every referenced local file is present and nonempty. - Backend build and build-identity verification produce the updated
server artifact. Gradle embeds the full lowercase
HEADcommit into bothskillpilot.openai.coach.v1.server-buildand the MCPserver-version; the deployment engine verifies the processed resource before restart.SKILLPILOT_SERVER_BUILDis not a runtime setting and cannot replace this artifact identity. - Focused OpenAI security and contract tests run before restart for the
openai-mcpartifact. systemctl restartactivates the freshly built frontend/backend bundle.- Public readiness wait absorbs the normal Spring Boot and reverse-proxy
startup window after
systemctl restart. A temporary502therefore does not produce a false failed deployment. - Public shell verification after readiness fetches
index.htmland the exact referenced CSS/module assets with cache bypass headers. It requires successful, nonempty same-origin responses with the expected content types, so missing hashed assets or an HTML error page served as CSS stop deployment. - Mandatory OpenAI V1 public-contract smoke runs after readiness for
every
openai-mcpdeployment. It verifies the dedicatedmcp-coach-v1.skillpilot.comTLS certificate, direct responses without redirects, HTTP200plus the exact resource in path-specific protected-resource metadata, and HTTP401plus the exactWWW-Authenticatemetadata reference athttps://mcp-coach-v1.skillpilot.com/mcp. The discarded main-origin routes and the internal transport route must return HTTP404; all five reserved sibling hosts must remain fail-closed with HTTP404. - Further deployment smoke tests check that the public host serves the intended coach variant in both version metadata and HTML and contains the reviewed DE/EN audio, coach, and legal transparency copy. The source-rationale smoke then detects the active curriculum mode: repository deployments must serve the two exact compatibility indexes, while package deployments must expose Catalog API 1.2 and working generation-bound source-evidence routes.
Asset deployment details
scripts/deploy_decks.py- scans
curricula/**/json/for files matching_deck*.json - copies them to:
app/public/data/backend/src/main/resources/static/data/
scripts/deploy_whitepaper.py- copies
docs/whitepaper/intoapp/public/whitepaper/ - copies comic assets for
comic1,comic2, andcomic3 scripts/deploy_story.py- copies
docs/quickstart/*intoapp/public/
Operational notes
./deploy_skillpilot.shis the normal production entrypoint and always pinsopenai-mcp. Stale ambientVITE_SKILLPILOT_COACH_VARIANTvalues are intentionally ignored.VITE_SKILLPILOT_COACH_VARIANTremains mandatory for a directscripts/deploy.shengine call and must be exactlyvisible-session,openai-mcp, orlegacy.- Multilingual MCP deployment:
./deploy_skillpilot.sh - Visible Session rollback:
./deploy_skillpilot.sh --coach-variant visible-session - The
openai-mcpbuild uses the same V1 App for every backend-supported communication locale. - The canonical OpenAI V1 public values are safe, versioned application defaults:
- MCP endpoint:
https://mcp-coach-v1.skillpilot.com/mcp - OAuth resource:
https://mcp-coach-v1.skillpilot.com/mcp - protected-resource metadata:
https://mcp-coach-v1.skillpilot.com/.well-known/oauth-protected-resource/mcpV1 binds exactly two active content-addressed MCP Apps UI resources on the fixed widget domainhttps://mcp-coach-v1.skillpilot.com: the image-only goal renderer and interactive card learning. Previously advertised image hash URIs remain passive and byte-identically readable. The card-review tool is app-only and unbound; ordinary tools remain UI-free. Permanent-ID and Level-2 setup remain exclusively in the SkillPilot WebGUI. Bare MCPImageContentis not the visibility contract, and the runtime applies noopenai/userAgentor client-surface gate. These URLs are immutable contract values rather than environment settings. ObsoleteSKILLPILOT_OPENAI_DE_*URL names and newly inventedSKILLPILOT_OPENAI_COACH_V1_*URL overrides fail closed. Remove staleSKILLPILOT_OPENAI_DE_UI_ORIGIN, obsolete V1-origin and old locale-bound mTLS/smoke variables before the first subdomain deployment. The current neutral edge uses onlySKILLPILOT_OPENAI_COACH_V1_MTLS_EDGE_MODE=disabled|observe|enforceand a root-owned Nginx mode file; oldSKILLPILOT_OPENAI_DE_*names stay forbidden. - Treat MCP tool descriptions, input/output schemas, annotations, server instructions, and skills as versioned model-facing metadata. A server deploy updates compatible live result behavior, but it does not rewrite an existing ChatGPT conversation or its earlier tool results. For a developer-mode connection, select Refresh on the connection after deployment, verify the discovered metadata, and test in a new conversation. For a published plugin, scan, review, and publish a new metadata snapshot. A browser-page reload is not a substitute. See OpenAI's metadata refresh procedure and published MCP metadata versioning.
- One
skillpilot-serverSpring Boot artifact hosts every coach line. Values belonging to V1 useSKILLPILOT_OPENAI_COACH_V1_*; genuine shared process policies useSKILLPILOT_OPENAI_*without locale/version segments. - The additive Nginx templates are
deploy/nginx/skillpilot-mcp-coaches.conffor inclusion insidehttp {}anddeploy/nginx/skillpilot-main-vhost-openai-deny-locations.conffor inclusion only inside the existingskillpilot.comHTTPSserver {}block before its generallocation /. The first file activates only neutral V1 and keeps neutral V2 through V9 at404; the second prevents a main-origin or internal-path alias. The dedicated coaches template also applies the OpenAI client-mTLS boundary only to V1/mcp; it does not replace existing vHosts or protect OAuth/discovery paths with a client certificate. - Production uses exactly one systemd
EnvironmentFile, normally/etc/skillpilot/skillpilot.env. Before copying assets or building,./deploy_skillpilot.shverifies that this is the file configured for the service and rejects removed OpenAI names as well as attempted public-URL overrides. It inspects variable names only; OAuth, database, and other secret values are not logged or printed. The same forbidden names must not be supplied by unit-levelEnvironment=orPassEnvironment=settings. A stale global systemd manager environment is rejected as well. A nonstandard file path must be selected explicitly withSKILLPILOT_SERVICE_ENV_FILE. - If systemd marks that one file as optional (
ignore_errors=yes) and it is absent, the preflight accepts the canonical application defaults after the other environment channels have been checked. A missing required file (ignore_errors=no) remains a deployment error. - Keep an environment file containing OAuth or database secrets root-owned and
mode
0600. Do not weaken it to make the deployment preflight read it. When the deploy user cannot traverse or read the root-protected file, the allowlisted content check is reported asSKIP; Spring's exact V1 startup validation remains the final fail-closed boundary. Unit-level and global systemd sources are still checked before the build. - Do not maintain
SKILLPILOT_SERVER_BUILDin/etc/skillpilot/skillpilot.env. The backend build embeds the full Git commit into the jar and the deploy verifies it before restart. Rebuilding a commit therefore carries the correct build identity without an operator editing the service environment. git stashis part of the current script behavior.- Operators should be aware that locally modified files will be stashed, not merged or deployed.
- The initial backend build runs with
-x test; anopenai-mcpdeployment then runs the focused OpenAI security, OAuth, contract and end-to-end tests before restart. - CI remains responsible for the complete backend regression suite.
- The service name defaults to
skillpilot. - Override with
SKILLPILOT_SERVICE_NAME=<service-name>when deploying an environment with a different unit name. - The script normally stashes local changes and pulls from Git before building.
- Set
SKILLPILOT_SKIP_GIT_UPDATE=1only when the exact desired tree is already present on the server, for example after applying a patch manually in an SSH recovery deployment. - The post-restart smoke test defaults to
https://skillpilot.com. - Override with
SKILLPILOT_BASE_URL=https://staging.example.orgfor another host. - The public readiness wait defaults to 180 seconds with a 5-second interval.
- Override with
SKILLPILOT_DEPLOY_READINESS_TIMEOUT_SECONDS=<seconds>andSKILLPILOT_DEPLOY_READINESS_INTERVAL_SECONDS=<seconds>when a target environment has a different startup profile. - Deployments as
enpasosneed a command-specific, passwordlesssudogrant for the restart. The script deliberately usessudo -nand never asks for a Linux password:
enpasos ALL=(root) NOPASSWD: /usr/bin/systemctl restart skillpilot
Confirm the actual binary path on the server with command -v systemctl
before creating the rule. Do not grant unrestricted passwordless sudo.
Prerequisites on Server
- Python 3: Required to run
scripts/deploy_decks.py,scripts/deploy_whitepaper.py, andscripts/deploy_story.py. - Node.js & npm: Required for installing dependencies and building the frontend.
- Java: Required for the backend Gradle build.
- Git: Required for pulling updates.
- Sudo Access: A command-specific
NOPASSWDgrant is required for restarting theskillpilotsystem service when deploying as a non-root user.