485 lines
18 KiB
Bash
Executable File
485 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Verify canonical local/server installation manuals and their base+override Compose paths.
|
|
set -euo pipefail
|
|
|
|
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
mode="${1:-}"
|
|
|
|
trim() {
|
|
local value="$1"
|
|
value="${value#"${value%%[![:space:]]*}"}"
|
|
value="${value%"${value##*[![:space:]]}"}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
is_safe_absolute_path() {
|
|
local value="$1" segment
|
|
local -a segments
|
|
[[ "$value" == /* && "$value" != *//* ]] || return 1
|
|
IFS=/ read -r -a segments <<<"$value"
|
|
for segment in "${segments[@]}"; do
|
|
[[ "$segment" != . && "$segment" != .. ]] || return 1
|
|
done
|
|
}
|
|
|
|
verify_path_variable_values() {
|
|
local source="$1" line trimmed name value
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
trimmed="$(trim "$line")"
|
|
if [[ "$trimmed" == *=* || "$trimmed" == *:* ]]; then
|
|
name="$(trim "${trimmed%%[=:]*}")"
|
|
value="$(trim "${trimmed#"$name"}")"
|
|
value="$(trim "${value#[:=]}")"
|
|
if [[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*_(FILE|SOURCE)$ ]]; then
|
|
value="$(trim "${value%%#*}")"
|
|
value="${value#\"}"; value="${value%\"}"
|
|
value="${value#\'}"; value="${value%\'}"
|
|
if [[ -n "$value" ]] && ! is_safe_absolute_path "$value"; then
|
|
echo "unsafe path value for $name in $source" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
fi
|
|
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"
|
|
local -a headings
|
|
if [[ "$profile" == local ]]; then
|
|
headings=(
|
|
"Prerequisites"
|
|
"Git remote: SSH and HTTPS"
|
|
"Shared Git values, local bindings, and secret files"
|
|
"Direct PostgreSQL, REST, and SSH tunnel bindings"
|
|
"Bootstrap, first pull, and diagnostics"
|
|
"Publish, update, backup, outage recovery, and rollback"
|
|
"Troubleshooting"
|
|
)
|
|
else
|
|
headings=(
|
|
"Service account, storage, and firewall"
|
|
"Gitea and remote Git setup"
|
|
"Git credentials, CA, SSH key, and known-hosts mounts"
|
|
"Shared Git values, local bindings, and secret files"
|
|
"Direct PostgreSQL, REST, and SSH tunnel bindings"
|
|
"Same-origin reverse proxy, bootstrap, and health"
|
|
"Pull, publish, upgrade, backup, and recovery"
|
|
"Troubleshooting and snapshot rollback"
|
|
)
|
|
fi
|
|
for heading in "${headings[@]}"; do
|
|
grep -Fqx "## $heading" "$manual" || {
|
|
echo "missing required heading in $profile manual: $heading" >&2
|
|
return 1
|
|
}
|
|
done
|
|
for expected in \
|
|
'export THT_SOURCE_ROOT=/absolute/path/to/ThothII' \
|
|
'--env-file "$THT_OPERATOR_ENV"' \
|
|
"-f \"\$THT_SOURCE_ROOT/compose.yaml\" -f \"\$THT_SOURCE_ROOT/deploy/compose.$profile.yaml\"" \
|
|
'"$THT_SOURCE_ROOT/scripts/generate-connector-secrets-override.sh"'; do
|
|
grep -Fq -- "$expected" "$manual" || {
|
|
echo "$profile manual lacks canonical operator step: $expected" >&2
|
|
return 1
|
|
}
|
|
done
|
|
if rg -n 'local-compose\.workspace-registry|server-compose\.workspace-registry|connector-secrets\.workspace-registry|docker compose' "$manual"; then
|
|
echo "$profile manual documents a superseded or bypassed Compose path" >&2
|
|
return 1
|
|
fi
|
|
verify_path_variable_values "$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"
|
|
chmod 0600 "$path"
|
|
}
|
|
|
|
verify_compose_fixtures() {
|
|
local fixture connector_override profile rendered
|
|
fixture="$(mktemp -d "${TMPDIR%/}/thoth-install-fixtures.XXXXXX")"
|
|
trap 'rm -rf "$fixture"' RETURN
|
|
mkdir -p "$fixture/data" "$fixture/pi-state" "$fixture/workspace-registry"
|
|
|
|
write_private "$fixture/pi-auth.json" '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}'
|
|
write_private "$fixture/thothii.secrets" 'THT_MODEL_API_KEY=fixture-model-api-key'
|
|
write_private "$fixture/git-ssh-key" 'fixture-git-ssh-key'
|
|
write_private "$fixture/git-known-hosts" 'fixture-git-known-hosts'
|
|
write_private "$fixture/dwh-password" 'fixture-dwh-password'
|
|
write_private "$fixture/vector-api-key" 'fixture-vector-api-key'
|
|
write_private "$fixture/session-runtime-password" 'fixture-session-runtime-password'
|
|
write_private "$fixture/session-migrator-password" 'fixture-session-migrator-password'
|
|
write_private "$fixture/session-ca.pem" 'fixture-session-ca'
|
|
cp "$root/deploy/workspaces/server-sessions.yaml.example" "$fixture/server-sessions.yaml"
|
|
|
|
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' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_TRANSPORT=rest_api' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE=/run/secrets/north-star-research-vector-api-key' \
|
|
>"$fixture/workspace-bindings.env"
|
|
|
|
printf '%s\n' \
|
|
'THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git' \
|
|
"PI_AUTH_FILE=$fixture/pi-auth.json" \
|
|
"THT_SECRETS_FILE=$fixture/thothii.secrets" \
|
|
"THT_WORKSPACE_BINDINGS_ENV_FILE=$fixture/workspace-bindings.env" \
|
|
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$fixture/git-ssh-key" \
|
|
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture/git-known-hosts" \
|
|
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$fixture/dwh-password" \
|
|
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$fixture/vector-api-key" \
|
|
"THT_DATA_ROOT=$fixture/data" \
|
|
"THT_PI_STATE_ROOT=$fixture/pi-state" \
|
|
"THT_WORKSPACE_REGISTRY_ROOT=$fixture/workspace-registry" \
|
|
"THT_SERVER_WORKSPACE_CONFIG=$fixture/server-sessions.yaml" \
|
|
'THT_SESSION_DB_HOST=sessions.example.invalid' \
|
|
'THT_SESSION_DB_NAME=thoth_sessions' \
|
|
'THT_SESSION_RUNTIME_USER=thoth_sessions_app' \
|
|
'THT_SESSION_MIGRATOR_USER=thoth_sessions_migrate' \
|
|
"THT_SESSION_RUNTIME_PASSWORD_SOURCE=$fixture/session-runtime-password" \
|
|
"THT_SESSION_MIGRATOR_PASSWORD_SOURCE=$fixture/session-migrator-password" \
|
|
"THT_SESSION_CA_SOURCE=$fixture/session-ca.pem" \
|
|
>"$fixture/operator.env"
|
|
|
|
connector_override="$fixture/connector-secrets.local.yaml"
|
|
"$root/scripts/generate-connector-secrets-override.sh" \
|
|
--bindings-env "$fixture/workspace-bindings.env" \
|
|
--operator-env "$fixture/operator.env" \
|
|
--output "$connector_override" >/dev/null
|
|
|
|
for profile in local server; do
|
|
rendered="$fixture/$profile.json"
|
|
files=(
|
|
-f "$root/compose.yaml"
|
|
-f "$root/deploy/compose.$profile.yaml"
|
|
)
|
|
if [[ "$profile" == server ]]; then
|
|
files+=(-f "$root/deploy/compose.session-server.yaml.example")
|
|
fi
|
|
files+=(
|
|
-f "$root/deploy/compose.git-ssh.yaml"
|
|
-f "$connector_override"
|
|
)
|
|
"$root/scripts/compose-with-preflight.sh" --env-file "$fixture/operator.env" \
|
|
"${files[@]}" config --format json >"$rendered"
|
|
|
|
node - "$rendered" "$profile" <<'NODE'
|
|
const fs = require("fs");
|
|
const [path, profile] = process.argv.slice(2);
|
|
const config = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
if (Object.keys(config.services).sort().join(",") !== "core,frontend") {
|
|
throw new Error(profile + ": mandatory stack must be exactly core,frontend");
|
|
}
|
|
const core = config.services.core;
|
|
for (const target of [
|
|
"/home/thoth/.pi/agent/auth.json",
|
|
"/home/thoth/.pi/agent/models.json",
|
|
"/home/thoth/.pi/agent/settings.json",
|
|
]) {
|
|
if (!(core.volumes || []).some((mount) => mount.target === target && mount.read_only)) {
|
|
throw new Error(profile + ": missing read-only Pi mount " + target);
|
|
}
|
|
}
|
|
for (const [name, value] of Object.entries({
|
|
THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE: "/run/secrets/north-star-research-dwh-password",
|
|
THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE: "/run/secrets/north-star-research-vector-api-key",
|
|
})) {
|
|
if (core.environment?.[name] !== value) throw new Error(profile + ": missing binding " + name);
|
|
}
|
|
const secretTargets = new Set((core.secrets || []).map((secret) => secret.target));
|
|
for (const target of [
|
|
"thothii.secrets",
|
|
"north-star-research-dwh-password",
|
|
"north-star-research-vector-api-key",
|
|
]) {
|
|
if (!secretTargets.has(target)) throw new Error(profile + ": missing secret target " + target);
|
|
}
|
|
if (profile === "server") {
|
|
for (const target of ["session_runtime_password", "session_ca.pem"]) {
|
|
if (!secretTargets.has(target)) throw new Error("server: missing session secret " + target);
|
|
}
|
|
}
|
|
if ((config.services.frontend.secrets || []).length !== 0) {
|
|
throw new Error(profile + ": frontend received a runtime secret");
|
|
}
|
|
const rendered = JSON.stringify(config);
|
|
for (const value of [
|
|
"fixture-native-auth-key", "fixture-model-api-key", "fixture-git-ssh-key",
|
|
"fixture-git-known-hosts", "fixture-dwh-password", "fixture-vector-api-key",
|
|
"fixture-session-runtime-password", "fixture-session-migrator-password", "fixture-session-ca",
|
|
]) {
|
|
if (rendered.includes(value)) throw new Error(profile + ": rendered Compose leaked " + value);
|
|
}
|
|
NODE
|
|
echo "canonical $profile base+override fixture passed"
|
|
done
|
|
|
|
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=relative/secret\n' >"$fixture/unsafe.env"
|
|
if verify_path_variable_values "$fixture/unsafe.env" >/dev/null 2>&1; then
|
|
echo "relative secret-source fixture was accepted" >&2
|
|
return 1
|
|
fi
|
|
echo "relative secret-source fixture rejected passed"
|
|
}
|
|
|
|
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
|
|
;;
|
|
--profile)
|
|
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 =="
|
|
(
|
|
cd "$root"
|
|
env -u WORKSPACE_GIT_REMOTE ./scripts/workspace-registry-smoke.sh
|
|
)
|
|
echo "$profile installation documentation verification passed"
|
|
;;
|
|
*)
|
|
echo "usage: $0 --fixtures-only | --profile {local|server}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|