Files
HY2XS_flamy/tools/build/lib/verify.sh
T
founder a1c74caa0c fix(build): исключить SIGPIPE из релизных гейтов под pipefail
Поиск с флагом -q прекращает чтение на первом совпадении и закрывает свой конец
канала. Продюсер, которому осталось что писать, получает SIGPIPE и завершается
кодом 141, а `set -o pipefail` делает 141 статусом всей конструкции:

    совпадение НАЙДЕНО -> продюсер оборван -> статус 141 -> «не найдено»

Для утвердительных проверок это ложный FAIL. Для отрицательных — «такой
конструкции в коде нет» — ложный PASS: запрещённая конструкция найдена, а гейт
зелёный. Отрицательными проверками закреплена половина инвариантов приёмки,
включая запрет обхода тестов и запрет `pnpm audit --prod`.

Порог резкий: пока вывод продюсера помещается в буфер канала (64 KiB на Linux),
он не блокируется и успевает завершиться раньше, чем потребитель начнёт читать.
Замер, 60 прогонов на размер: до 60 KiB — 0 отказов, ровно на 64 KiB — 58/60,
от 96 KiB — 60/60. То есть проверка выглядит исправной ровно до первого
источника крупнее буфера, а такие файлы в репозитории уже есть.

- 56 мест переведены на here-string: `grep -q PATTERN <<<"$content"`;
- продюсеры-команды (ss|awk, dpkg-query, /proc/cpuinfo, systemctl
  list-unit-files) сначала читаются в переменную;
- введён code_has: десять отрицательных сканов держались на `|| true` внутри
  code_without_comments, гасившем 141, — то есть на побочном эффекте
  подавления ошибок, а не на заявленном свойстве;
- несуществующий путь в скане больше не означает успех: `2>/dev/null || true`
  превращал опечатку в пустой вывод, а пустой вывод для проверки «этого в коде
  нет» — это PASS. Проверка явная, а не через set -e: в контексте `! code_has`
  bash отключает errexit на весь вызов;
- возврат пайплайна запрещён отдельной приёмкой.
2026-09-01 04:28:54 +05:00

178 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
require_repo_layout() {
[ -f "LICENSE" ] || fail "missing LICENSE"
[ -f "versions.env" ] || fail "missing versions.env (product/platform/toolchain contract)"
[ -d "orchestrator/src" ] || fail "missing orchestrator/src"
[ -f "orchestrator/package.json" ] || fail "missing orchestrator/package.json"
[ -f "orchestrator/bun.lock" ] || fail "missing orchestrator/bun.lock"
[ -f "package/install.sh" ] || fail "missing package/install.sh"
[ -d "package/templates" ] || fail "missing package/templates"
[ -f "package/config/hy2xs.env" ] || fail "missing package/config/hy2xs.env"
[ -f "package/templates/env/post-install.env.tpl" ] || fail "missing package/templates/env/post-install.env.tpl"
[ -d "package/systemd" ] || fail "missing package/systemd"
[ -d "apps" ] || fail "missing apps HY2XS admin source"
[ -f "apps/go.mod" ] || fail "missing apps/go.mod"
[ -f "apps/go.sum" ] || fail "missing apps/go.sum"
[ -f "apps/frontend/package.json" ] || fail "missing apps/frontend/package.json"
[ -f "apps/frontend/pnpm-lock.yaml" ] || fail "missing apps/frontend/pnpm-lock.yaml"
}
verify_source_tree_policy() {
local allow_dirty="${ALLOW_DIRTY_BUILD:-false}"
local dirty="false"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
fail "build must run inside git work tree"
fi
if [ -n "$(git status --porcelain 2>/dev/null || true)" ]; then
dirty="true"
fi
if [ "$dirty" = "true" ] && [ "$allow_dirty" != "true" ]; then
fail "dirty git tree is not allowed for production build; set ALLOW_DIRTY_BUILD=true to override"
fi
}
verify_archive() {
local version="$1"
local archive="dist/hy2xs-install-${version}.tar.gz"
[ -f "$archive" ] || fail "archive was not created: $archive"
local listing
listing="$(tar -tzf "$archive")"
grep -q '^hy2xs-install/install.sh$' <<<"$listing" || fail "archive missing install.sh"
grep -q '^hy2xs-install/LICENSE$' <<<"$listing" || fail "archive missing LICENSE"
grep -q '^hy2xs-install/orchestrator/hy2xs-orchestrator$' <<<"$listing" || fail "archive missing orchestrator artifact"
grep -q '^hy2xs-install/ui/hy2xs-admin/hy2xs-admin$' <<<"$listing" || fail "archive missing bundled UI binary"
grep -q '^hy2xs-install/systemd/hysteria-server.service$' <<<"$listing" || fail "archive missing hysteria systemd unit"
grep -q '^hy2xs-install/systemd/hy2xs-admin.service$' <<<"$listing" || fail "archive missing admin systemd unit"
grep -q '^hy2xs-install/templates/hysteria/config.yaml.tpl$' <<<"$listing" || fail "archive missing Hysteria config template"
grep -q '^hy2xs-install/config/hy2xs.env$' <<<"$listing" || fail "archive missing canonical runtime config"
grep -q '^hy2xs-install/templates/env/post-install.env.tpl$' <<<"$listing" || fail "archive missing post-install env template"
grep -q '^hy2xs-install/metadata/checksums.txt$' <<<"$listing" || fail "archive missing checksums"
grep -q '^hy2xs-install/metadata/hysteria.version$' <<<"$listing" || fail "archive missing pinned Hysteria version"
grep -q '^hy2xs-install/metadata/hysteria.url$' <<<"$listing" || fail "archive missing pinned Hysteria artifact url"
grep -q '^hy2xs-install/metadata/hysteria.sha256$' <<<"$listing" || fail "archive missing pinned Hysteria sha256"
grep -q '^hy2xs-install/metadata/hysteria.resolution$' <<<"$listing" || fail "archive missing Hysteria resolution marker"
grep -q '^hy2xs-install/metadata/package.release_line$' <<<"$listing" || fail "archive missing product release line"
grep -q '^hy2xs-install/metadata/package.config_schema_version$' <<<"$listing" || fail "archive missing packaged config schema version"
# Контракт установки проверяется структурно: install.sh — read-only bootstrap
# целиком, у PHASE 1 ровно один владелец — оркестратор.
#
# Раньше здесь проверялся порядок «preflight раньше первой мутации», и сама
# проверка ТРЕБОВАЛА наличия мутирующей строки в install.sh. Пока shell сам
# раскладывал оркестратор и runtime-пакет, между двумя фазами существовало
# окно: PHASE 0 проходила, install.sh изменял хост, а отказ следующего
# preflight внутри оркестратора объявлялся «на сервере ничего не изменено».
# Владение мутацией нельзя было отследить, потому что мутировали двое.
local packaged_installer mutation_hits
packaged_installer="$(tar -xOzf "$archive" hy2xs-install/install.sh)"
grep -q 'preflight-install' <<<"$packaged_installer" \
|| fail "packaged install.sh does not run the read-only preflight"
grep -q 'preflight-install --package-dir' <<<"$packaged_installer" \
|| fail "packaged install.sh: could not locate the preflight invocation"
# Комментарии отбрасываются: строка, ОБЪЯСНЯЮЩАЯ, почему установщик больше
# не выполняет `install -d`, не является выполнением `install -d`.
mutation_hits="$(printf '%s\n' "$packaged_installer" \
| grep -nE '^[[:space:]]*(install|ln|cp|mv|rm|mkdir|chown|chmod|systemctl|apt-get|useradd|groupadd|nft|tee)[[:space:]]' \
|| true)"
[ -z "$mutation_hits" ] \
|| fail "packaged install.sh must not mutate the host; PHASE 1 belongs to the orchestrator alone. Found:
$mutation_hits"
grep -q 'exec .*install --package-dir' <<<"$packaged_installer" \
|| fail "packaged install.sh must hand the whole mutating phase to the orchestrator via exec"
local license_text
license_text="$(tar -xOzf "$archive" hy2xs-install/LICENSE)"
grep -q 'GNU AFFERO GENERAL PUBLIC LICENSE' <<<"$license_text" \
|| fail "packaged LICENSE is not the GNU Affero General Public License"
grep -q 'Version 3, 19 November 2007' <<<"$license_text" \
|| fail "packaged LICENSE is not AGPL version 3"
local env_content
env_content="$(tar -xOzf "$archive" hy2xs-install/config/hy2xs.env)"
grep -q 'replace-with-your-domain.example' <<<"$env_content" && fail "packaged hy2xs.env contains placeholder domain"
grep -q 'replace-with-your-email@example.com' <<<"$env_content" && fail "packaged hy2xs.env contains placeholder email"
local hysteria_tpl
hysteria_tpl="$(tar -xOzf "$archive" hy2xs-install/templates/hysteria/config.yaml.tpl)"
grep -q '/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}' <<<"$hysteria_tpl" || fail "hysteria auth template must include machine access_token"
grep -q '{{OBFS_BLOCK}}' <<<"$hysteria_tpl" || fail "hysteria template must render a typed obfs block"
# Замороженная версия обязана совпадать во всех местах пакета.
local pinned_version pinned_sha pinned_url meta_env
pinned_version="$(tar -xOzf "$archive" hy2xs-install/metadata/hysteria.version | tr -d '\n')"
pinned_sha="$(tar -xOzf "$archive" hy2xs-install/metadata/hysteria.sha256 | tr -d '\n')"
pinned_url="$(tar -xOzf "$archive" hy2xs-install/metadata/hysteria.url | tr -d '\n')"
meta_env="$(tar -xOzf "$archive" hy2xs-install/metadata/package.env)"
grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' <<<"$pinned_version" \
|| fail "packaged Hysteria version is not a concrete vX.Y.Z: $pinned_version"
grep -Eq '^[a-f0-9]{64}$' <<<"$pinned_sha" \
|| fail "packaged Hysteria sha256 is not a 64-char hex digest"
grep -Fq "$pinned_version" <<<"$pinned_url" \
|| fail "packaged Hysteria artifact url does not point at the pinned version: $pinned_url"
if grep -q 'latest' <<<"$pinned_url"; then
fail "packaged Hysteria artifact url must be immutable, not a moving latest: $pinned_url"
fi
grep -q "^hysteria_version=${pinned_version}$" <<<"$meta_env" \
|| fail "metadata package.env disagrees with metadata/hysteria.version"
grep -q "^hysteria_artifact_sha256=${pinned_sha}$" <<<"$meta_env" \
|| fail "metadata package.env disagrees with metadata/hysteria.sha256"
local post_install_tpl
post_install_tpl="$(tar -xOzf "$archive" hy2xs-install/templates/env/post-install.env.tpl)"
grep -q '^HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}$' <<<"$post_install_tpl" || fail "post-install env template must include machine access_token in HY2_AUTH_URL"
local tmp
tmp="$(mktemp -d)"
tar -xzf "$archive" -C "$tmp"
[ -x "$tmp/hy2xs-install/install.sh" ] || fail "install.sh is not executable"
[ -x "$tmp/hy2xs-install/orchestrator/hy2xs-orchestrator" ] || fail "orchestrator is not executable"
[ -x "$tmp/hy2xs-install/ui/hy2xs-admin/hy2xs-admin" ] || fail "hy2xs-admin is not executable"
"$tmp/hy2xs-install/orchestrator/hy2xs-orchestrator" status --package-dir "$tmp/hy2xs-install" >/dev/null 2>&1 || fail "orchestrator status sanity check failed"
local meta
meta="$(cat "$tmp/hy2xs-install/metadata/package.env")"
grep -q '^source_git_commit=' <<<"$meta" || fail "metadata missing source_git_commit"
grep -q '^dirty_tree=' <<<"$meta" || fail "metadata missing dirty_tree"
grep -q '^build_profile=production$' <<<"$meta" || fail "metadata missing build_profile=production"
grep -q '^license=AGPL-3.0-only$' <<<"$meta" || fail "metadata missing license=AGPL-3.0-only"
# Поколение продукта обязано доехать до пакета: install-state строится из
# него, а reconfigure/repair по нему отличают v1 от чужой установки.
grep -q "^version=${HY2XS_VERSION}$" <<<"$meta" \
|| fail "metadata version disagrees with versions.env HY2XS_VERSION=${HY2XS_VERSION}"
grep -q "^release_line=${HY2XS_RELEASE_LINE}$" <<<"$meta" \
|| fail "metadata release_line disagrees with versions.env HY2XS_RELEASE_LINE=${HY2XS_RELEASE_LINE}"
grep -q "^config_schema_version=${HY2XS_CONFIG_SCHEMA_VERSION}$" <<<"$meta" \
|| fail "metadata config_schema_version disagrees with versions.env"
grep -q "^target_version=${HY2XS_TARGET_OS_VERSION}$" <<<"$meta" \
|| fail "metadata target_version disagrees with versions.env"
grep -q "^target_arch=${HY2XS_TARGET_ARCH}$" <<<"$meta" \
|| fail "metadata target_arch disagrees with versions.env"
local packaged_schema
packaged_schema="$(grep -E '^HY2XS_CONFIG_SCHEMA_VERSION=' "$tmp/hy2xs-install/config/hy2xs.env" | head -n1 | cut -d= -f2-)"
[ "$packaged_schema" = "$HY2XS_CONFIG_SCHEMA_VERSION" ] \
|| fail "packaged hy2xs.env declares schema $packaged_schema, versions.env declares $HY2XS_CONFIG_SCHEMA_VERSION"
verify_admin_version_contract "$tmp/hy2xs-install/ui/hy2xs-admin/hy2xs-admin"
if declare -F run_fix20_acceptance_subset >/dev/null 2>&1; then
run_fix20_acceptance_subset "$tmp/hy2xs-install"
fi
rm -rf "$tmp"
}