42db78c6a0
Раз v1 принципиально не мигрирует состояние 0.x, политика должна быть операционно завершённой: у оператора обязан быть явный способ привести сервер в состояние, которое установщик примет. tools/legacy/purge-v0.sh делает это отдельной осознанной операцией: - по умолчанию печатает план и НЕ меняет ничего; - выполнение требует --apply вместе с --yes-i-know; - снимает таймеры отката firewall hy2xs-fw-rollback-*, которые переживают неудачную установку и иначе продолжили бы менять ruleset уже после очистки; - из /etc/nftables.conf убирает только include HY2XS: остальной ruleset принадлежит оператору; - в конце проверяет чистоту хоста по тому же контракту, что и установщик. Из install.sh он не вызывается никогда: встроенная очистка вернула бы destructive migration logic обратно в путь свежей установки - ровно то, от чего мы ушли. Acceptance-набор дополнен проверками, которые не дают инвариантам тихо развалиться: порядок фаз в install.sh, наличие read-only guard, preflight раньше первой записи install-state, ownership-aware rollback, отказ по отсутствующей схеме, проверка поколения в reconfigure/repair, структурная редакция, отсутствие удалённых маршрутов, e2e на production-генераторе, сверка с upstream hashes.txt, контрольные суммы в versions.env, версия админки из контракта.
243 lines
16 KiB
Bash
243 lines
16 KiB
Bash
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
run_fix20_acceptance_subset() {
|
||
local package_dir="$1"
|
||
[ -d "$package_dir" ] || fail "acceptance: package dir not found: $package_dir"
|
||
|
||
log_step "Acceptance: package layout sanity"
|
||
[ -x "$package_dir/install.sh" ] || fail "acceptance: install.sh is missing or not executable"
|
||
[ -x "$package_dir/orchestrator/hy2xs-orchestrator" ] || fail "acceptance: orchestrator artifact is missing"
|
||
|
||
log_step "Acceptance: project license is shipped with the package"
|
||
[ -f "$package_dir/LICENSE" ] || fail "acceptance: LICENSE is missing from the package"
|
||
grep -q 'GNU AFFERO GENERAL PUBLIC LICENSE' "$package_dir/LICENSE" \
|
||
|| fail "acceptance: packaged LICENSE must be AGPL-3.0-only"
|
||
grep -q '^license=AGPL-3.0-only$' "$package_dir/metadata/package.env" \
|
||
|| fail "acceptance: package metadata must declare license=AGPL-3.0-only"
|
||
|
||
log_step "Acceptance: orchestrator CLI help path"
|
||
"$package_dir/orchestrator/hy2xs-orchestrator" diagnostics collect --package-dir "$package_dir" >/dev/null 2>&1 || true
|
||
|
||
log_step "Acceptance: firewall mode defaults in config"
|
||
grep -q '^HY2XS_FIREWALL_MODE=' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FIREWALL_MODE missing in runtime config"
|
||
|
||
log_step "Acceptance: baseline domain/public host/ssh defaults"
|
||
grep -q '^HY2XS_DOMAIN=fi.api.withen.pro$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_DOMAIN must default to fi.api.withen.pro"
|
||
grep -q '^HY2XS_PUBLIC_HOST=fi.api.withen.pro$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_PUBLIC_HOST must default to fi.api.withen.pro"
|
||
grep -q '^HY2XS_SSH_PORT=2323$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_SSH_PORT must default to 2323"
|
||
|
||
log_step "Acceptance: force password change production default"
|
||
grep -q '^HY2XS_FORCE_PASSWORD_CHANGE=false$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FORCE_PASSWORD_CHANGE must default to false"
|
||
|
||
log_step "Acceptance: config schema version is declared"
|
||
grep -q '^HY2XS_CONFIG_SCHEMA_VERSION=2$' "$package_dir/config/hy2xs.env" \
|
||
|| fail "acceptance: HY2XS_CONFIG_SCHEMA_VERSION must be 2 in the packaged baseline"
|
||
|
||
log_step "Acceptance: fresh install defaults to Gecko obfuscation"
|
||
grep -q '^HY2XS_HYSTERIA_OBFS_TYPE=gecko$' "$package_dir/config/hy2xs.env" \
|
||
|| fail "acceptance: fresh installations must default to HY2XS_HYSTERIA_OBFS_TYPE=gecko"
|
||
|
||
log_step "Acceptance: obfs type is not hardcoded in the Hysteria template"
|
||
grep -q '{{OBFS_BLOCK}}' "$package_dir/templates/hysteria/config.yaml.tpl" \
|
||
|| fail "acceptance: hysteria template must render the obfs block from the orchestrator"
|
||
! grep -Eq '^\s*type:\s*(gecko|salamander)\s*$' "$package_dir/templates/hysteria/config.yaml.tpl" \
|
||
|| fail "acceptance: hysteria template must not hardcode an obfs type"
|
||
|
||
log_step "Acceptance: modern server baseline is present in the template"
|
||
grep -q 'disableLossCompensation: {{DISABLE_LOSS_COMPENSATION}}' "$package_dir/templates/hysteria/config.yaml.tpl" \
|
||
|| fail "acceptance: bandwidth.disableLossCompensation missing from hysteria template"
|
||
grep -q '{{CONGESTION_BLOCK}}' "$package_dir/templates/hysteria/config.yaml.tpl" \
|
||
|| fail "acceptance: congestion block missing from hysteria template"
|
||
grep -q '{{QUIC_BLOCK}}' "$package_dir/templates/hysteria/config.yaml.tpl" \
|
||
|| fail "acceptance: quic block missing from hysteria template"
|
||
|
||
log_step "Acceptance: post-install env derives obfs type from resolved context"
|
||
grep -q '^HY2_OBFS_TYPE={{OBFS_TYPE}}$' "$package_dir/templates/env/post-install.env.tpl" \
|
||
|| fail "acceptance: post-install env must render the resolved obfs type, not a second set of defaults"
|
||
! grep -Eq '^HY2_OBFS_TYPE=(gecko|salamander)$' "$package_dir/templates/env/post-install.env.tpl" \
|
||
|| fail "acceptance: post-install env must not hardcode an obfs type"
|
||
|
||
log_step "Acceptance: production defaults are declared in exactly one module"
|
||
grep -q 'DEFAULT_HYSTERIA_OBFS_TYPE' orchestrator/src/config/profile.ts \
|
||
|| fail "acceptance: the default obfs type must be declared in orchestrator/src/config/profile.ts"
|
||
local default_declarations
|
||
default_declarations="$(grep -rl 'DEFAULT_HYSTERIA_OBFS_TYPE\s*[:=]' orchestrator/src \
|
||
| grep -v 'orchestrator/src/config/profile.ts' || true)"
|
||
[ -z "$default_declarations" ] \
|
||
|| fail "acceptance: the default obfs type must not be re-declared in: $default_declarations"
|
||
|
||
log_step "Acceptance: runtime env is derived from config, not literals"
|
||
! grep -Eq 'HY2XS_HYSTERIA_OBFS_TYPE=(gecko|salamander)' orchestrator/src/config/env.ts \
|
||
|| fail "acceptance: renderRuntimeEnv must not print a hardcoded obfs type"
|
||
grep -q 'HY2XS_HYSTERIA_OBFS_TYPE=\${config.hysteriaObfsType}' orchestrator/src/config/env.ts \
|
||
|| fail "acceptance: renderRuntimeEnv must derive the obfs type from the parsed config"
|
||
! grep -Eq '\|\|\s*"(gecko|salamander)"' orchestrator/src/config/env.ts \
|
||
|| fail "acceptance: env.ts must not carry its own obfs fallback default"
|
||
|
||
log_step "Acceptance: package metadata records how the Hysteria version was chosen"
|
||
grep -q '^hysteria_resolution=' "$package_dir/metadata/package.env" \
|
||
|| fail "acceptance: metadata must record hysteria_resolution"
|
||
grep -q '^hysteria_resolved_at=' "$package_dir/metadata/package.env" \
|
||
|| fail "acceptance: metadata must record hysteria_resolved_at"
|
||
grep -q '^hysteria_compat_gate=true$' "$package_dir/metadata/package.env" \
|
||
|| fail "acceptance: release packages must be built with the Hysteria compatibility gate enabled"
|
||
grep -Eq '^hysteria_artifact_url=https://github\.com/HyNetworks/hysteria/' "$package_dir/metadata/package.env" \
|
||
|| fail "acceptance: Hysteria artifact must come from the canonical HyNetworks upstream"
|
||
|
||
log_step "Acceptance: install-time never resolves a moving latest"
|
||
! grep -rq 'api.github.com' orchestrator/src/commands orchestrator/src/steps \
|
||
|| fail "acceptance: install-time code must not query the upstream release API"
|
||
! grep -rq 'download.hysteria.network' orchestrator/src \
|
||
|| fail "acceptance: install-time code must not use the moving latest download URL"
|
||
|
||
log_step "Acceptance: smoke verifies config semantics, not substrings"
|
||
grep -q 'assertHysteriaConfigMatchesProfile' orchestrator/src/steps/smoke.ts \
|
||
|| fail "acceptance: smoke must verify the effective config semantically"
|
||
|
||
log_step "Acceptance: admin export preserves unknown upstream fields and strips secrets"
|
||
grep -q 'ExportHysteria2ConfigYaml' apps/controller/config.go \
|
||
|| fail "acceptance: hysteria config export must go through the sanitizing raw-YAML path"
|
||
grep -q 'GetRawHysteria2Config' apps/service/hysteria2_export.go \
|
||
|| fail "acceptance: export must read the raw YAML instead of the typed model"
|
||
|
||
log_step "Acceptance: frontend ACME registry matches current upstream"
|
||
# Ищем именно предлагаемое значение, а не упоминание в комментарии.
|
||
! grep -q '"namedotcom"' apps/frontend/src/views/hysteria/list/index.vue \
|
||
|| fail "acceptance: namedotcom was removed upstream in Hysteria 2.11.0 and must not be offered"
|
||
local provider
|
||
for provider in cloudflare duckdns gandi godaddy namecheap njalla porkbun vultr; do
|
||
grep -q "\"${provider}\"" apps/frontend/src/views/hysteria/list/index.vue \
|
||
|| fail "acceptance: ACME DNS provider ${provider} is missing from the UI registry"
|
||
done
|
||
|
||
log_step "Acceptance: systemd unit production env"
|
||
grep -q '^Environment=GIN_MODE=release$' "$package_dir/systemd/hy2xs-admin.service" || fail "acceptance: GIN_MODE=release missing"
|
||
|
||
log_step "Acceptance: docs matrix markers"
|
||
grep -q 'Fix20 production matrix' docs/11-testing-and-acceptance.md || fail "acceptance: fix20 matrix section missing"
|
||
|
||
log_step "Acceptance: machine auth URL in templates"
|
||
grep -q '/hui/hysteria2/auth?access_token={{HYSTERIA_API_SECRET}}' "$package_dir/templates/hysteria/config.yaml.tpl" || fail "acceptance: machine token missing in hysteria auth URL template"
|
||
grep -q '^HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/hui/hysteria2/auth?access_token={{HYSTERIA_API_SECRET}}$' "$package_dir/templates/env/post-install.env.tpl" || fail "acceptance: machine token missing in post-install HY2_AUTH_URL"
|
||
|
||
log_step "Acceptance: smoke auth checks are tokenized"
|
||
grep -q 'unexpected auth status without machine token' orchestrator/src/steps/smoke.ts || fail "acceptance: missing 403 negative smoke for auth without machine token"
|
||
grep -q 'hysteria2/auth?access_token=\${context.config.hysteriaTrafficStatsSecret}' orchestrator/src/steps/smoke.ts || fail "acceptance: smoke auth URL is not tokenized"
|
||
|
||
log_step "Acceptance: bootstrap peer can pass auth smoke"
|
||
grep -q 'quota := int64(-1)' apps/dao/sqlite.go || fail "acceptance: bootstrap peer quota must be unlimited (-1), otherwise install auth smoke fails"
|
||
|
||
log_step "Acceptance: frontend i18n does not touch Pinia at module import"
|
||
! grep -q 'useAppStore' apps/frontend/src/lang/index.ts || fail "acceptance: lang/index.ts must not import/use Pinia store"
|
||
|
||
log_step "Acceptance: env rendering maps machine token and fails on unresolved placeholders"
|
||
grep -q 'HYSTERIA_API_SECRET: context.config.hysteriaTrafficStatsSecret' orchestrator/src/steps/env.ts || fail "acceptance: writePostInstallEnv must pass HYSTERIA_API_SECRET"
|
||
grep -q 'template render failed: unresolved placeholders' orchestrator/src/lib/fs.ts || fail "acceptance: renderTemplate must fail on unresolved placeholders"
|
||
|
||
run_clean_install_acceptance "$package_dir"
|
||
}
|
||
|
||
# Приёмка политики clean-install-only и связанных с ней инвариантов.
|
||
run_clean_install_acceptance() {
|
||
local package_dir="$1"
|
||
|
||
log_step "Acceptance: installer runs a read-only preflight before touching the host"
|
||
grep -q 'preflight-install' "$package_dir/install.sh" \
|
||
|| fail "acceptance: install.sh must run the read-only preflight before mutating the host"
|
||
grep -q 'PHASE 0' "$package_dir/install.sh" \
|
||
|| fail "acceptance: install.sh must document the read-only phase boundary"
|
||
grep -q 'preflightInstall' orchestrator/src/cli.ts \
|
||
|| fail "acceptance: orchestrator must expose the preflight-install command"
|
||
|
||
log_step "Acceptance: the read-only phase is enforced by a guard, not by convention"
|
||
grep -q 'enableReadOnlyGuard' orchestrator/src/commands/preflight-install.ts \
|
||
|| fail "acceptance: preflight-install must enable the read-only guard"
|
||
grep -q 'assertMutationAllowed' orchestrator/src/lib/fs.ts \
|
||
|| fail "acceptance: fs writes must be guarded during the read-only phase"
|
||
grep -q 'assertMutationAllowed' orchestrator/src/lib/process.ts \
|
||
|| fail "acceptance: mutating runners must be guarded during the read-only phase"
|
||
|
||
log_step "Acceptance: preflight passes before the first install-state write"
|
||
"$BUN_BIN" -e '
|
||
const source = require("node:fs").readFileSync("orchestrator/src/commands/install.ts", "utf8");
|
||
const preflight = source.indexOf("await preflight(context");
|
||
const state = source.indexOf("await advanceInstallState(");
|
||
if (preflight < 0 || state < 0) {
|
||
throw new Error("could not locate preflight/advanceInstallState in install.ts");
|
||
}
|
||
if (preflight > state) {
|
||
throw new Error("install writes install-state before preflight");
|
||
}
|
||
' || fail "acceptance: install must not write install-state before a successful preflight"
|
||
|
||
log_step "Acceptance: rollback is ownership-aware, not message-driven"
|
||
grep -q 'classifyFailure(ownership' orchestrator/src/commands/install.ts \
|
||
|| fail "acceptance: failure classification must be driven by ownership, not by the error message"
|
||
grep -q 'systemd units were not deployed by this operation' orchestrator/src/commands/install.ts \
|
||
|| fail "acceptance: rollback must never stop services it did not deploy"
|
||
|
||
log_step "Acceptance: missing config schema marker is treated as legacy"
|
||
grep -q 'HY2XS_CONFIG_SCHEMA_VERSION отсутствует' orchestrator/src/config/env.ts \
|
||
|| fail "acceptance: a missing HY2XS_CONFIG_SCHEMA_VERSION must be rejected as legacy, not defaulted"
|
||
|
||
log_step "Acceptance: reconfigure/repair verify the installation generation"
|
||
grep -q 'assertCurrentGeneration' orchestrator/src/commands/reconfigure.ts \
|
||
|| fail "acceptance: reconfigure/repair must verify release line and config schema in install-state"
|
||
grep -q 'release_line' orchestrator/src/lib/installState.ts \
|
||
|| fail "acceptance: install-state must carry the product release line"
|
||
|
||
log_step "Acceptance: diagnostics redaction is structural"
|
||
grep -q 'Bun.YAML.parse' orchestrator/src/lib/redaction.ts \
|
||
|| fail "acceptance: YAML redaction must walk the document, not match lines"
|
||
# Регрессия: правило `.replace(/(auth:\s*).*/gi, ...)` подставляло маркер в
|
||
# заголовок mapping'а и оставляло вложенный auth.http.url с machine token.
|
||
! grep -qF 'replace(/(auth:' orchestrator/src/lib/redaction.ts \
|
||
|| fail "acceptance: the line-based auth redaction rule leaked nested auth.http.url"
|
||
|
||
log_step "Acceptance: dead updater/config-write routes stay removed"
|
||
# Ищется регистрация маршрута (имя в кавычках), а не любое упоминание:
|
||
# комментарий, объясняющий, почему маршрута нет, должен быть разрешён.
|
||
local dead_route
|
||
for dead_route in hysteria2ChangeVersion listRelease updateHysteria2Config importHysteria2Config restartServer uploadCertFile hysteria2AcmePath; do
|
||
! grep -rqF "${dead_route}\"" apps/router apps/controller \
|
||
|| fail "acceptance: removed route ${dead_route} came back"
|
||
! grep -rqF "${dead_route}\"" apps/frontend/src/api \
|
||
|| fail "acceptance: frontend still calls the removed route ${dead_route}"
|
||
done
|
||
|
||
log_step "Acceptance: e2e uses the production share URI generator"
|
||
grep -q 'tools/share-uri' tools/test/e2e-hysteria.sh \
|
||
|| fail "acceptance: e2e must build the share URI with production code"
|
||
! grep -q 'build_share_uri' tools/test/e2e-hysteria.sh \
|
||
|| fail "acceptance: the bash share-URI implementation must not come back"
|
||
|
||
log_step "Acceptance: upstream checksums are verified before the lock is written"
|
||
grep -q 'resolve_expected_sha_from_upstream' tools/build/lib/hysteria.sh \
|
||
|| fail "acceptance: the Hysteria artifact must be verified against upstream hashes.txt"
|
||
|
||
log_step "Acceptance: toolchain checksums live in versions.env"
|
||
grep -q '^BUN_LINUX_X64_SHA256=' versions.env \
|
||
|| fail "acceptance: versions.env must pin the Bun x64 artifact"
|
||
grep -q '^BUN_LINUX_X64_BASELINE_SHA256=' versions.env \
|
||
|| fail "acceptance: versions.env must pin the Bun baseline artifact"
|
||
grep -q '^GO_LINUX_AMD64_SHA256=' versions.env \
|
||
|| fail "acceptance: versions.env must pin the Go toolchain archive"
|
||
grep -q '^NODE_LINUX_X64_SHA256=' versions.env \
|
||
|| fail "acceptance: versions.env must pin the Node.js archive"
|
||
! grep -q '^HYSTERIA_VERSION=' versions.env \
|
||
|| fail "acceptance: versions.env declares the Hysteria policy, not a pinned version"
|
||
|
||
log_step "Acceptance: admin version comes from the build contract"
|
||
grep -q 'var Version = "dev"' apps/model/constant/system.go \
|
||
|| fail "acceptance: admin version must be injected via ldflags, not hardcoded"
|
||
grep -q 'constant.Version=v' tools/build/lib/package.sh \
|
||
|| fail "acceptance: the build must inject the admin version from versions.env"
|
||
|
||
log_step "Acceptance: legacy cleanup is a separate, explicit helper"
|
||
[ -f tools/legacy/purge-v0.sh ] || fail "acceptance: legacy cleanup helper is missing"
|
||
[ -f docs/14-legacy-cleanup.md ] || fail "acceptance: legacy cleanup runbook is missing"
|
||
! grep -q 'purge-v0' "$package_dir/install.sh" \
|
||
|| fail "acceptance: the installer must never run destructive cleanup on its own"
|
||
}
|