docs: add autonomous local installation guide

This commit is contained in:
2026-08-05 08:35:13 +02:00
parent c01202f06c
commit 53d257fb76
7 changed files with 779 additions and 0 deletions
@@ -9,6 +9,10 @@ trap 'rm -f "$output"' EXIT HUP INT TERM
"$root/scripts/verify-workspace-install-docs.sh" --fixtures-only >"$output"
for fixture in \
"local installation guide contract" \
"Windows line-ending recovery guide contract" \
"Pi management guide contract" \
"local installation example rendered from path with spaces" \
"local manual canonical base+override references" \
"server manual canonical base+override references" \
"canonical local base+override fixture" \
+231
View File
@@ -43,6 +43,132 @@ verify_path_variable_values() {
done <"$source"
}
require_headings() {
local source="$1" label="$2"
shift 2
local heading
for heading in "$@"; do
grep -Fqx "## $heading" "$source" || {
echo "missing required heading in $label: $heading" >&2
return 1
}
done
}
require_text() {
local source="$1" label="$2"
shift 2
local expected
for expected in "$@"; do
grep -Fq -- "$expected" "$source" || {
echo "$label lacks required instruction: $expected" >&2
return 1
}
done
}
verify_local_guide() {
local guide="$root/docs/install/local.md"
[[ -f "$guide" ]] || {
echo "missing local installation guide: docs/install/local.md" >&2
return 1
}
require_headings "$guide" "local installation guide" \
"Choose your platform" \
"Prerequisites" \
"Clone and verify LF" \
"Create the local operator files" \
"Address external services" \
"Build ThothII and thothctl" \
"Start and verify" \
"Update an installation" \
"Back up and restore" \
"Data-preserving uninstall" \
"Next: workspaces and Pi"
require_text "$guide" "local installation guide" \
"git clone" \
"bash scripts/verify-line-endings.sh" \
"deploy/env/local.env" \
"host.docker.internal" \
"host-gateway" \
"container 127.0.0.1" \
"bash scripts/build-local.sh" \
"scripts/build-local.ps1" \
"bash scripts/build-thothctl.sh" \
"thothctl --installation" \
"curl --fail http://127.0.0.1:8080/health" \
"http://127.0.0.1:8080" \
"git pull --ff-only" \
"docker compose down --volumes"
echo "local installation guide contract passed"
}
verify_windows_line_endings_guide() {
local guide="$root/docs/install/windows-line-endings.md"
[[ -f "$guide" ]] || {
echo "missing Windows line-ending guide: docs/install/windows-line-endings.md" >&2
return 1
}
require_headings "$guide" "Windows line-ending guide" \
"Recommended WSL2 clone" \
"Repository-local LF policy" \
"Verify after clone or pull" \
"Recover an existing CRLF clone"
require_text "$guide" "Windows line-ending guide" \
"git config --local core.autocrlf false" \
"bash scripts/verify-line-endings.sh" \
"git add --renormalize ." \
"git diff --cached --check" \
"reclone"
if grep -Fq 'git reset --hard' "$guide"; then
node - "$guide" <<'NODE'
const fs = require("fs");
const lines = fs.readFileSync(process.argv[2], "utf8").split(/\n/);
for (let index = 0; index < lines.length; index += 1) {
if (!lines[index].includes("git reset --hard")) continue;
const warning = lines.slice(Math.max(0, index - 4), index).join(" ").toLowerCase();
if (!warning.includes("warning") || !warning.includes("destructive") ||
!warning.includes("backup") || !warning.includes("commit")) {
throw new Error("git reset --hard lacks an immediate destructive warning requiring backup/commit");
}
}
NODE
fi
echo "Windows line-ending recovery guide contract passed"
}
verify_pi_management_guide() {
local guide="$root/docs/install/pi-management.md"
[[ -f "$guide" ]] || {
echo "missing Pi management guide: docs/install/pi-management.md" >&2
return 1
}
require_headings "$guide" "Pi management guide" \
"Who can use Pi Management" \
"Use the Pi Management page" \
"Use thothctl" \
"Handle credentials and secrets" \
"Update and roll back Pi" \
"Recover a failed update" \
"Direct support access"
require_text "$guide" "Pi management guide" \
"pi status" \
"pi doctor" \
"pi test" \
"pi check" \
"pi configure" \
"pi update" \
"pi rollback --yes" \
"pi maintenance status" \
"pi maintenance recover --yes" \
"pi logs" \
"/run/secrets" \
"docker compose exec core pi" \
"no browser shell" \
"does not mount the Docker socket"
echo "Pi management guide contract passed"
}
verify_manual() {
local profile="$1" manual
manual="$root/docs/install/$profile-workspace-registry.md"
@@ -93,6 +219,101 @@ verify_manual() {
echo "$profile manual canonical base+override references passed"
}
verify_local_installation_example() {
local example="$root/docs/install/examples/thothii-installation.local.yaml"
[[ -f "$example" ]] || {
echo "missing local installation example: docs/install/examples/thothii-installation.local.yaml" >&2
return 1
}
local fixture source_copy operator_dir copied_example connector_override env_file
fixture="$(mktemp -d "${TMPDIR%/}/thoth local install.XXXXXX")"
trap 'rm -rf "$fixture"' RETURN
[[ "$fixture" == *" "* ]] || {
echo "local installation fixture path does not contain spaces" >&2
return 1
}
source_copy="$fixture/ThothII source"
operator_dir="$fixture/operator files"
mkdir -p "$source_copy/deploy/pi" "$operator_dir"
cp "$root/compose.yaml" "$source_copy/compose.yaml"
cp "$root/deploy/compose.local.yaml" "$source_copy/deploy/compose.local.yaml"
cp "$root/deploy/compose.git-ssh.yaml" "$source_copy/deploy/compose.git-ssh.yaml"
cp "$root/deploy/pi/models.json" "$source_copy/deploy/pi/models.json"
cp "$root/deploy/pi/settings.json" "$source_copy/deploy/pi/settings.json"
write_private "$operator_dir/pi-auth.json" '{"zai":{"type":"api_key","key":"fixture-local-pi-key"}}'
write_private "$operator_dir/thothii.secrets" 'THT_MODEL_API_KEY=fixture-local-model-key'
write_private "$operator_dir/git-ssh-key" 'fixture-local-ssh-key'
write_private "$operator_dir/git-known-hosts" 'fixture-local-known-hosts'
write_private "$operator_dir/dwh-password" 'fixture-local-dwh-password'
printf '%s\n' \
'THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT=postgres_direct' \
'THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE=/run/secrets/north-star-research-dwh-password' \
>"$operator_dir/workspace-bindings.env"
env_file="$source_copy/deploy/env/local.env"
mkdir -p "$source_copy/deploy/env"
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$operator_dir/pi-auth.json" \
"THT_SECRETS_FILE=$operator_dir/thothii.secrets" \
"THT_WORKSPACE_BINDINGS_ENV_FILE=$operator_dir/workspace-bindings.env" \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$operator_dir/git-ssh-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$operator_dir/git-known-hosts" \
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$operator_dir/dwh-password" \
>"$env_file"
connector_override="$operator_dir/connector-secrets.local.yaml"
"$root/scripts/generate-connector-secrets-override.sh" \
--bindings-env "$operator_dir/workspace-bindings.env" \
--operator-env "$env_file" \
--output "$connector_override" >/dev/null
copied_example="$fixture/thothii-installation.yaml"
local contents
contents="$(<"$example")"
contents="${contents//\/absolute\/path\/to\/ThothII/$source_copy}"
contents="${contents//\/absolute\/path\/to\/thothii-operator/$operator_dir}"
printf '%s\n' "$contents" >"$copied_example"
local profile project_directory descriptor_env value
local -a overrides files
profile="$(sed -n 's/^profile: \([^[:space:]]*\)$/\1/p' "$copied_example")"
project_directory="$(sed -n 's/^projectDirectory: "\(.*\)"$/\1/p' "$copied_example")"
descriptor_env="$(sed -n 's/^envFile: "\(.*\)"$/\1/p' "$copied_example")"
while IFS= read -r value; do overrides+=("$value"); done < <(sed -n 's/^ - "\(.*\)"$/\1/p' "$copied_example")
[[ "$profile" == local && "$project_directory" == "$source_copy" && "$descriptor_env" == "$env_file" ]] || {
echo "local installation example does not resolve its required fields" >&2
return 1
}
[[ "${#overrides[@]}" -eq 2 && "${overrides[1]}" == "$connector_override" ]] || {
echo "local installation example does not select the expected optional overrides" >&2
return 1
}
files=(-f "$project_directory/compose.yaml" -f "$project_directory/deploy/compose.$profile.yaml")
for value in "${overrides[@]}"; do files+=(-f "$value"); done
local rendered="$fixture/local-installation.json"
"$root/scripts/compose-with-preflight.sh" --env-file "$descriptor_env" \
"${files[@]}" config --format json >"$rendered"
node - "$rendered" <<'NODE'
const fs = require("fs");
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (Object.keys(config.services).sort().join(",") !== "core,frontend") {
throw new Error("local installation example must render exactly core,frontend");
}
const output = JSON.stringify(config);
for (const secret of [
"fixture-local-pi-key",
"fixture-local-model-key",
"fixture-local-ssh-key",
"fixture-local-known-hosts",
"fixture-local-dwh-password",
]) {
if (output.includes(secret)) throw new Error("local installation rendering exposed a fixture secret");
}
NODE
echo "local installation example rendered from path with spaces passed"
}
write_private() {
local path="$1" value="$2"
printf '%s\n' "$value" >"$path"
@@ -229,6 +450,10 @@ NODE
case "$mode" in
--fixtures-only)
[[ $# -eq 1 ]] || { echo "usage: $0 --fixtures-only" >&2; exit 2; }
verify_local_guide
verify_windows_line_endings_guide
verify_pi_management_guide
verify_local_installation_example
verify_manual local
verify_manual server
verify_compose_fixtures
@@ -237,6 +462,12 @@ case "$mode" in
profile="${2:-}"
[[ $# -eq 2 && "$profile" =~ ^(local|server)$ ]] \
|| { echo "usage: $0 --profile {local|server}" >&2; exit 2; }
if [[ "$profile" == local ]]; then
verify_local_guide
verify_windows_line_endings_guide
verify_pi_management_guide
verify_local_installation_example
fi
verify_manual "$profile"
verify_compose_fixtures
echo "== Run isolated workspace-registry bootstrap and recovery smoke =="