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 на весь вызов;
- возврат пайплайна запрещён отдельной приёмкой.
This commit is contained in:
@@ -22,13 +22,45 @@ set -euo pipefail
|
||||
# Решётка при этом отбрасывается ТОЛЬКО в shell. В шаблоне Vue строка вполне
|
||||
# может начинаться с `#default="scope"` — это сокращение для v-slot, а не
|
||||
# комментарий, и общий фильтр молча выбрасывал бы её из скана.
|
||||
#
|
||||
# Несуществующий путь — ОТКАЗ, а не пустой вывод. Прежнее `2>/dev/null || true`
|
||||
# превращало опечатку в имени файла в пустую строку, а пустая строка для
|
||||
# проверок вида «запрещённой конструкции здесь нет» означает успех. Скан,
|
||||
# который молча проверяет пустоту, хуже отсутствующего: он отчитывается зелёным.
|
||||
code_without_comments() {
|
||||
[ -f "$1" ] || fail "code_without_comments: файл не найден: $1"
|
||||
case "$1" in
|
||||
*.sh) grep -vE '^[[:space:]]*#' "$1" 2>/dev/null || true ;;
|
||||
*) grep -v '^[[:space:]]*//' "$1" 2>/dev/null || true ;;
|
||||
# grep -v возвращает 1, когда КАЖДАЯ строка файла — комментарий. Это
|
||||
# законный результат, а не отказ, поэтому статус здесь гасится.
|
||||
*.sh) grep -vE '^[[:space:]]*#' "$1" || true ;;
|
||||
*) grep -v '^[[:space:]]*//' "$1" || true ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# code_has / code_lacks — поиск по КОДУ файла без пайплайна.
|
||||
#
|
||||
# Пайплайн здесь был ловушкой. Поиск с флагом -q прекращает чтение на первом
|
||||
# совпадении, продюсер получает SIGPIPE, и под `pipefail` статус всей
|
||||
# конструкции становится 141 — то есть НЕНУЛЕВЫМ именно тогда, когда совпадение
|
||||
# НАЙДЕНО. Для отрицательных проверок («такой конструкции в коде нет») это
|
||||
# давало бы ложный PASS: найденная запрещённая конструкция читалась бы как её
|
||||
# отсутствие. Здесь беду удерживало `|| true` внутри помощника, гасившее 141, —
|
||||
# то есть корректность гейта держалась на побочном эффекте подавления ошибок,
|
||||
# а не на чём-то заявленном.
|
||||
#
|
||||
# Отказ чтения проверяется ЯВНО, а не через errexit. В контексте `! code_has …`
|
||||
# bash отключает errexit на весь вызов, поэтому неудачное чтение прошло бы
|
||||
# незамеченным и снова дало бы ложный PASS. `fail` выполняется в самой функции,
|
||||
# а не в подоболочке подстановки, поэтому его `exit` останавливает сборку.
|
||||
code_has() {
|
||||
local file="$1"
|
||||
shift
|
||||
local code
|
||||
code="$(code_without_comments "$file")" \
|
||||
|| fail "acceptance: не удалось прочитать код файла $file"
|
||||
grep -q "$@" <<<"$code"
|
||||
}
|
||||
|
||||
# code_mentions_in отдаёт список файлов, где подстрока встречается В КОДЕ.
|
||||
code_mentions_in() {
|
||||
local needle="$1"
|
||||
@@ -37,7 +69,7 @@ code_mentions_in() {
|
||||
local file hits=""
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] || continue
|
||||
if code_without_comments "$file" | grep -qF -- "$needle"; then
|
||||
if code_has "$file" -F -- "$needle"; then
|
||||
hits="${hits}${hits:+ }${file}"
|
||||
fi
|
||||
done <<EOF
|
||||
@@ -437,7 +469,7 @@ run_clean_install_acceptance() {
|
||||
# с паролем, уже напечатанным на экран.
|
||||
grep -q 'errors.Is(err, dao.ErrAdminUserNotFound)' apps/cmd/reset.go \
|
||||
|| fail "acceptance: reset-admin обязан различать sentinel-ошибки слоя данных"
|
||||
! code_without_comments apps/cmd/reset.go | grep -qF 'hash, _ :=' \
|
||||
! code_has apps/cmd/reset.go -F -- 'hash, _ :=' \
|
||||
|| fail "acceptance: ошибка хеширования пароля снова проглатывается, в password_hash уедет пустая строка"
|
||||
"$BUN_BIN" -e '
|
||||
const source = require("node:fs").readFileSync("apps/cmd/reset.go", "utf8");
|
||||
@@ -587,7 +619,7 @@ run_scheduler_lifecycle_acceptance() {
|
||||
# Ключевая регрессия: смена настройки не имеет права ронять HTTP-сервер.
|
||||
# Именно этот путь плодил планировщики — StopServer возвращал управление в
|
||||
# `for { runServer() }`, а InitCron создавал новый cron.New() и терял ссылку.
|
||||
! code_without_comments apps/controller/config.go | grep -q 'StopServer' \
|
||||
! code_has apps/controller/config.go -- 'StopServer' \
|
||||
|| fail "acceptance: смена настройки снова перезапускает HTTP-сервер вместо перепланирования джобы"
|
||||
grep -q 'RescheduleResetTraffic' apps/controller/config.go \
|
||||
|| fail "acceptance: расписание должно переноситься на месте, через RescheduleResetTraffic"
|
||||
@@ -661,9 +693,9 @@ run_dead_config_acceptance() {
|
||||
|
||||
# Серверный конфиг Hysteria читается только из файла: ветка «сначала SQLite»
|
||||
# была вторым источником истины и читалась ПЕРВОЙ.
|
||||
! code_without_comments apps/service/config.go | grep -q 'Hysteria2Config)' \
|
||||
! code_has apps/service/config.go -- 'Hysteria2Config)' \
|
||||
|| fail "acceptance: чтение серверного конфига снова обращается к таблице config"
|
||||
! code_without_comments apps/service/hysteria2_export.go | grep -q 'Hysteria2Config)' \
|
||||
! code_has apps/service/hysteria2_export.go -- 'Hysteria2Config)' \
|
||||
|| fail "acceptance: выгрузка серверного конфига снова обращается к таблице config"
|
||||
|
||||
log_step "Acceptance: share URI remark is derived, not stored as dead state"
|
||||
@@ -679,7 +711,7 @@ run_dependency_hygiene_acceptance() {
|
||||
|
||||
# Ключевой канал утечки: сгенерированный пароль печатался в
|
||||
# /var/log/hy2xs/hy2xs-admin.log, а этот файл отдаётся кнопкой выгрузки.
|
||||
! code_without_comments apps/dao/sqlite.go | grep -q 'Initial admin password' \
|
||||
! code_has apps/dao/sqlite.go -- 'Initial admin password' \
|
||||
|| fail "acceptance: bootstrap-пароль снова пишется в журнал"
|
||||
grep -q 'HY2XS_ADMIN_INITIAL_PASSWORD не задан' apps/dao/sqlite.go \
|
||||
|| fail "acceptance: отсутствие HY2XS_ADMIN_INITIAL_PASSWORD должно быть отказом старта"
|
||||
@@ -689,7 +721,7 @@ run_dependency_hygiene_acceptance() {
|
||||
log_step "Acceptance: the admin log is sanitized on export, like the Hysteria one"
|
||||
grep -q 'func ExportAdminLog' apps/service/journal.go \
|
||||
|| fail "acceptance: журнал админки выгружается без санитайза"
|
||||
! code_without_comments apps/controller/log.go | grep -q 'c.File(' \
|
||||
! code_has apps/controller/log.go -- 'c.File(' \
|
||||
|| fail "acceptance: журнал снова отдаётся сырым файлом мимо санитайза"
|
||||
|
||||
log_step "Acceptance: JWT stack is the maintained major line, with a fixed algorithm"
|
||||
@@ -704,7 +736,7 @@ run_dependency_hygiene_acceptance() {
|
||||
|| fail "acceptance: разбор токена должен ограничивать набор алгоритмов подписи"
|
||||
|
||||
log_step "Acceptance: password storage has exactly one format"
|
||||
! code_without_comments apps/util/encrypt.go | grep -q 'SHA224String' \
|
||||
! code_has apps/util/encrypt.go -- 'SHA224String' \
|
||||
|| fail "acceptance: вернулась проверка пароля по несолёному SHA-224"
|
||||
grep -q 'if !IsBcryptHash(storedHash)' apps/util/encrypt.go \
|
||||
|| fail "acceptance: VerifyPassword обязан отклонять всё, кроме bcrypt"
|
||||
@@ -726,12 +758,37 @@ run_dependency_hygiene_acceptance() {
|
||||
# `--prod` не показывал уязвимости в vite/rollup, хотя они исполняются на
|
||||
# build-машине и порождают production-бандл. Ровно такой класс и был найден
|
||||
# (DOM clobbering в Rollup затрагивал генерируемый бандл).
|
||||
! code_without_comments tools/build/lib/security.sh | grep -q 'audit --prod' \
|
||||
! code_has tools/build/lib/security.sh -- 'audit --prod' \
|
||||
|| fail "acceptance: гейт зависимостей снова проверяет только production-подграф npm"
|
||||
code_without_comments tools/build/lib/security.sh \
|
||||
| grep -q 'audit --audit-level "\$PNPM_AUDIT_LEVEL"' \
|
||||
code_has tools/build/lib/security.sh -- 'audit --audit-level "\$PNPM_AUDIT_LEVEL"' \
|
||||
|| fail "acceptance: гейт зависимостей обязан проверять весь lock-граф frontend"
|
||||
|
||||
log_step "Acceptance: release gates never pipe into an early-exiting matcher"
|
||||
# Поиск с флагом -q прекращает чтение на ПЕРВОМ совпадении и закрывает свой
|
||||
# конец канала. Продюсер, у которого осталось что писать, получает SIGPIPE и
|
||||
# завершается кодом 141, а `set -o pipefail` делает 141 статусом всей
|
||||
# конструкции. Получается инверсия смысла: «совпадение найдено» становится
|
||||
# ненулевым кодом.
|
||||
#
|
||||
# Для утвердительных проверок это ложный FAIL — гейт отвергает корректный
|
||||
# артефакт. Для отрицательных («такой конструкции в коде нет») — ЛОЖНЫЙ PASS:
|
||||
# найденная запрещённая конструкция читается как её отсутствие, и гейт
|
||||
# отчитывается зелёным ровно тогда, когда обязан был остановить сборку.
|
||||
#
|
||||
# Порог не гипотетический: пока весь вывод продюсера помещается в буфер канала
|
||||
# (64 KiB на Linux), он успевает записать всё и не блокируется, поэтому такая
|
||||
# проверка годами выглядит исправной. Первый же источник крупнее буфера
|
||||
# переворачивает её результат — а в этом репозитории такие файлы уже есть.
|
||||
#
|
||||
# Замена — here-string: `matcher PATTERN <<<"$content"`. Пайплайна нет, статус
|
||||
# у конструкции ровно один, и pipefail к нему не применяется.
|
||||
local piped_matcher
|
||||
piped_matcher="$(grep -rn -E '\|[[:space:]]*grep[[:space:]]+-[A-Za-z]*q' \
|
||||
tools/build/lib tools/build/build.sh tools/test tools/legacy package || true)"
|
||||
[ -z "$piped_matcher" ] \
|
||||
|| fail "acceptance: пайплайн в grep -q под pipefail вернулся; используйте here-string:
|
||||
$piped_matcher"
|
||||
|
||||
log_step "Acceptance: the release is built with a mandatory test gate"
|
||||
# SKIP_TESTS=true доходил до конца сборки и выдавал внешне неотличимый
|
||||
# production-артефакт: ни metadata, ни финальная приёмка не проверяли, что
|
||||
@@ -1229,19 +1286,19 @@ run_secret_channel_acceptance() {
|
||||
log_code="$(grep -v '^[[:space:]]*//' apps/middleware/log.go || true)"
|
||||
server_code="$(grep -v '^[[:space:]]*//' apps/cmd/server.go || true)"
|
||||
|
||||
printf '%s\n' "$log_code" | grep -q 'c.Request.URL.Path' \
|
||||
grep -q 'c.Request.URL.Path' <<<"$log_code" \
|
||||
|| fail "acceptance: the request logger must log the path, not RequestURI"
|
||||
! printf '%s\n' "$log_code" | grep -q 'RequestURI' \
|
||||
! grep -q 'RequestURI' <<<"$log_code" \
|
||||
|| fail "acceptance: RequestURI carries the machine token in its query string"
|
||||
! grep -rq 'ReqUri' apps/model/vo apps/frontend/src/api \
|
||||
|| fail "acceptance: the reqUri log field came back"
|
||||
|
||||
log_step "Acceptance: the admin has exactly one HTTP log channel"
|
||||
! printf '%s\n' "$server_code" | grep -q 'gin.Default()' \
|
||||
! grep -q 'gin.Default()' <<<"$server_code" \
|
||||
|| fail "acceptance: gin.Default() logs the query string to stdout and then to journald"
|
||||
printf '%s\n' "$server_code" | grep -q 'gin.New()' \
|
||||
grep -q 'gin.New()' <<<"$server_code" \
|
||||
|| fail "acceptance: the admin engine must be built with gin.New()"
|
||||
printf '%s\n' "$server_code" | grep -q 'gin.Recovery()' \
|
||||
grep -q 'gin.Recovery()' <<<"$server_code" \
|
||||
|| fail "acceptance: gin.New() must still install the recovery middleware"
|
||||
|
||||
log_step "Acceptance: exported logs are sanitized on both sides"
|
||||
|
||||
@@ -59,7 +59,9 @@ apt_install_missing() {
|
||||
local missing=()
|
||||
local pkg
|
||||
for pkg in "$@"; do
|
||||
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q 'install ok installed'; then
|
||||
local pkg_status
|
||||
pkg_status="$(dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null)" || pkg_status=""
|
||||
if ! grep -q 'install ok installed' <<<"$pkg_status"; then
|
||||
missing+=("$pkg")
|
||||
fi
|
||||
done
|
||||
@@ -135,7 +137,9 @@ ensure_go() {
|
||||
}
|
||||
|
||||
cpu_has_avx2() {
|
||||
grep -m1 '^flags' /proc/cpuinfo 2>/dev/null | grep -qw avx2
|
||||
local flags
|
||||
flags="$(grep -m1 '^flags' /proc/cpuinfo 2>/dev/null)" || flags=""
|
||||
grep -qw avx2 <<<"$flags"
|
||||
}
|
||||
|
||||
select_bun_artifact() {
|
||||
|
||||
@@ -30,7 +30,7 @@ HYSTERIA_GATE_TIMEOUT_SECONDS="${HYSTERIA_GATE_TIMEOUT_SECONDS:-15}"
|
||||
|
||||
validate_hysteria_version_format() {
|
||||
local value="$1"
|
||||
printf '%s' "$value" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' <<<"$value" \
|
||||
|| fail "invalid Hysteria version format: $value (expected vX.Y.Z)"
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ validate_hysteria_sha256_format() {
|
||||
fail "HYSTERIA_ARTIFACT_SHA256 must be a real release sha256"
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$value" | grep -Eq '^[a-fA-F0-9]{64}$' \
|
||||
grep -Eq '^[a-fA-F0-9]{64}$' <<<"$value" \
|
||||
|| fail "HYSTERIA_ARTIFACT_SHA256 must be a 64-char hex SHA256, got: $value"
|
||||
}
|
||||
|
||||
@@ -198,7 +198,12 @@ assert_gate_port_is_free() {
|
||||
local proto_flag="$2"
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
if ss -H "$proto_flag" 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"; then
|
||||
# Вывод собирается ДО поиска: `grep -q` обрывает чтение на первом совпадении,
|
||||
# и под `pipefail` SIGPIPE у продюсера превратил бы найденный занятый порт в
|
||||
# ненулевой статус, то есть в вывод «порт свободен».
|
||||
local sockets
|
||||
sockets="$(ss -H "$proto_flag" 2>/dev/null | awk '{print $4}')" || sockets=""
|
||||
if grep -Eq "[:.]${port}$" <<<"$sockets"; then
|
||||
fail "compatibility gate port $port is already in use; override HYSTERIA_GATE_PORT/HYSTERIA_GATE_STATS_PORT"
|
||||
fi
|
||||
fi
|
||||
@@ -246,10 +251,13 @@ gate_run_single_profile() {
|
||||
if ! kill -0 "$gate_pid" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
if command -v ss >/dev/null 2>&1 \
|
||||
&& ss -H -lun 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${HYSTERIA_GATE_PORT}$"; then
|
||||
ready="true"
|
||||
break
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
local udp_sockets
|
||||
udp_sockets="$(ss -H -lun 2>/dev/null | awk '{print $4}')" || udp_sockets=""
|
||||
if grep -Eq "[:.]${HYSTERIA_GATE_PORT}$" <<<"$udp_sockets"; then
|
||||
ready="true"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
|
||||
@@ -129,7 +129,7 @@ run_frontend_vulnerability_gate() {
|
||||
|
||||
# pnpm audit ходит в реестр npm. Недоступность реестра — это отказ проверки,
|
||||
# а не её отрицательный результат, и молча пропускать его нельзя.
|
||||
if printf '%s' "$report" | grep -qiE 'ERR_PNPM_AUDIT_ENDPOINT|ENOTFOUND|ECONNREFUSED|network|getaddrinfo'; then
|
||||
if grep -qiE 'ERR_PNPM_AUDIT_ENDPOINT|ENOTFOUND|ECONNREFUSED|network|getaddrinfo' <<<"$report"; then
|
||||
fail "pnpm audit не смог обратиться к реестру npm — проверка не выполнена:
|
||||
$report"
|
||||
fi
|
||||
|
||||
+41
-41
@@ -45,22 +45,22 @@ verify_archive() {
|
||||
local listing
|
||||
listing="$(tar -tzf "$archive")"
|
||||
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/install.sh$' || fail "archive missing install.sh"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/LICENSE$' || fail "archive missing LICENSE"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/orchestrator/hy2xs-orchestrator$' || fail "archive missing orchestrator artifact"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/ui/hy2xs-admin/hy2xs-admin$' || fail "archive missing bundled UI binary"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hysteria-server.service$' || fail "archive missing hysteria systemd unit"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hy2xs-admin.service$' || fail "archive missing admin systemd unit"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/templates/hysteria/config.yaml.tpl$' || fail "archive missing Hysteria config template"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/config/hy2xs.env$' || fail "archive missing canonical runtime config"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/templates/env/post-install.env.tpl$' || fail "archive missing post-install env template"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/checksums.txt$' || fail "archive missing checksums"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.version$' || fail "archive missing pinned Hysteria version"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.url$' || fail "archive missing pinned Hysteria artifact url"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.sha256$' || fail "archive missing pinned Hysteria sha256"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.resolution$' || fail "archive missing Hysteria resolution marker"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/package.release_line$' || fail "archive missing product release line"
|
||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/package.config_schema_version$' || fail "archive missing packaged config schema version"
|
||||
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 ровно один владелец — оркестратор.
|
||||
@@ -73,9 +73,9 @@ verify_archive() {
|
||||
# Владение мутацией нельзя было отследить, потому что мутировали двое.
|
||||
local packaged_installer mutation_hits
|
||||
packaged_installer="$(tar -xOzf "$archive" hy2xs-install/install.sh)"
|
||||
printf '%s\n' "$packaged_installer" | grep -q 'preflight-install' \
|
||||
grep -q 'preflight-install' <<<"$packaged_installer" \
|
||||
|| fail "packaged install.sh does not run the read-only preflight"
|
||||
printf '%s\n' "$packaged_installer" | grep -q 'preflight-install --package-dir' \
|
||||
grep -q 'preflight-install --package-dir' <<<"$packaged_installer" \
|
||||
|| fail "packaged install.sh: could not locate the preflight invocation"
|
||||
|
||||
# Комментарии отбрасываются: строка, ОБЪЯСНЯЮЩАЯ, почему установщик больше
|
||||
@@ -87,25 +87,25 @@ verify_archive() {
|
||||
|| fail "packaged install.sh must not mutate the host; PHASE 1 belongs to the orchestrator alone. Found:
|
||||
$mutation_hits"
|
||||
|
||||
printf '%s\n' "$packaged_installer" | grep -q 'exec .*install --package-dir' \
|
||||
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)"
|
||||
printf '%s\n' "$license_text" | grep -q 'GNU AFFERO GENERAL PUBLIC LICENSE' \
|
||||
grep -q 'GNU AFFERO GENERAL PUBLIC LICENSE' <<<"$license_text" \
|
||||
|| fail "packaged LICENSE is not the GNU Affero General Public License"
|
||||
printf '%s\n' "$license_text" | grep -q 'Version 3, 19 November 2007' \
|
||||
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)"
|
||||
printf '%s\n' "$env_content" | grep -q 'replace-with-your-domain.example' && fail "packaged hy2xs.env contains placeholder domain"
|
||||
printf '%s\n' "$env_content" | grep -q 'replace-with-your-email@example.com' && fail "packaged hy2xs.env contains placeholder email"
|
||||
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)"
|
||||
printf '%s\n' "$hysteria_tpl" | grep -q '/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}' || fail "hysteria auth template must include machine access_token"
|
||||
printf '%s\n' "$hysteria_tpl" | grep -q '{{OBFS_BLOCK}}' || fail "hysteria template must render a typed obfs block"
|
||||
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
|
||||
@@ -114,24 +114,24 @@ $mutation_hits"
|
||||
pinned_url="$(tar -xOzf "$archive" hy2xs-install/metadata/hysteria.url | tr -d '\n')"
|
||||
meta_env="$(tar -xOzf "$archive" hy2xs-install/metadata/package.env)"
|
||||
|
||||
printf '%s' "$pinned_version" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' <<<"$pinned_version" \
|
||||
|| fail "packaged Hysteria version is not a concrete vX.Y.Z: $pinned_version"
|
||||
printf '%s' "$pinned_sha" | grep -Eq '^[a-f0-9]{64}$' \
|
||||
grep -Eq '^[a-f0-9]{64}$' <<<"$pinned_sha" \
|
||||
|| fail "packaged Hysteria sha256 is not a 64-char hex digest"
|
||||
printf '%s' "$pinned_url" | grep -Fq "$pinned_version" \
|
||||
grep -Fq "$pinned_version" <<<"$pinned_url" \
|
||||
|| fail "packaged Hysteria artifact url does not point at the pinned version: $pinned_url"
|
||||
if printf '%s' "$pinned_url" | grep -q 'latest'; then
|
||||
if grep -q 'latest' <<<"$pinned_url"; then
|
||||
fail "packaged Hysteria artifact url must be immutable, not a moving latest: $pinned_url"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$meta_env" | grep -q "^hysteria_version=${pinned_version}$" \
|
||||
grep -q "^hysteria_version=${pinned_version}$" <<<"$meta_env" \
|
||||
|| fail "metadata package.env disagrees with metadata/hysteria.version"
|
||||
printf '%s\n' "$meta_env" | grep -q "^hysteria_artifact_sha256=${pinned_sha}$" \
|
||||
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)"
|
||||
printf '%s\n' "$post_install_tpl" | grep -q '^HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}$' || fail "post-install env template must include machine access_token in HY2_AUTH_URL"
|
||||
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)"
|
||||
@@ -144,22 +144,22 @@ $mutation_hits"
|
||||
|
||||
local meta
|
||||
meta="$(cat "$tmp/hy2xs-install/metadata/package.env")"
|
||||
printf '%s\n' "$meta" | grep -q '^source_git_commit=' || fail "metadata missing source_git_commit"
|
||||
printf '%s\n' "$meta" | grep -q '^dirty_tree=' || fail "metadata missing dirty_tree"
|
||||
printf '%s\n' "$meta" | grep -q '^build_profile=production$' || fail "metadata missing build_profile=production"
|
||||
printf '%s\n' "$meta" | grep -q '^license=AGPL-3.0-only$' || fail "metadata missing license=AGPL-3.0-only"
|
||||
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 от чужой установки.
|
||||
printf '%s\n' "$meta" | grep -q "^version=${HY2XS_VERSION}$" \
|
||||
grep -q "^version=${HY2XS_VERSION}$" <<<"$meta" \
|
||||
|| fail "metadata version disagrees with versions.env HY2XS_VERSION=${HY2XS_VERSION}"
|
||||
printf '%s\n' "$meta" | grep -q "^release_line=${HY2XS_RELEASE_LINE}$" \
|
||||
grep -q "^release_line=${HY2XS_RELEASE_LINE}$" <<<"$meta" \
|
||||
|| fail "metadata release_line disagrees with versions.env HY2XS_RELEASE_LINE=${HY2XS_RELEASE_LINE}"
|
||||
printf '%s\n' "$meta" | grep -q "^config_schema_version=${HY2XS_CONFIG_SCHEMA_VERSION}$" \
|
||||
grep -q "^config_schema_version=${HY2XS_CONFIG_SCHEMA_VERSION}$" <<<"$meta" \
|
||||
|| fail "metadata config_schema_version disagrees with versions.env"
|
||||
printf '%s\n' "$meta" | grep -q "^target_version=${HY2XS_TARGET_OS_VERSION}$" \
|
||||
grep -q "^target_version=${HY2XS_TARGET_OS_VERSION}$" <<<"$meta" \
|
||||
|| fail "metadata target_version disagrees with versions.env"
|
||||
printf '%s\n' "$meta" | grep -q "^target_arch=${HY2XS_TARGET_ARCH}$" \
|
||||
grep -q "^target_arch=${HY2XS_TARGET_ARCH}$" <<<"$meta" \
|
||||
|| fail "metadata target_arch disagrees with versions.env"
|
||||
|
||||
local packaged_schema
|
||||
|
||||
@@ -41,7 +41,7 @@ REQUIRED_CONTRACT_VARS=(
|
||||
validate_sha256_value() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
printf '%s' "$value" | grep -Eq '^[a-f0-9]{64}$' \
|
||||
grep -Eq '^[a-f0-9]{64}$' <<<"$value" \
|
||||
|| fail "versions.env: $name must be a 64-char lowercase hex SHA256, got: $value"
|
||||
}
|
||||
|
||||
@@ -70,11 +70,11 @@ load_versions_contract() {
|
||||
validate_sha256_value BUN_LINUX_X64_BASELINE_SHA256 "$BUN_LINUX_X64_BASELINE_SHA256"
|
||||
validate_sha256_value NODE_LINUX_X64_SHA256 "$NODE_LINUX_X64_SHA256"
|
||||
|
||||
printf '%s' "$HY2XS_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"$HY2XS_VERSION" \
|
||||
|| fail "versions.env: HY2XS_VERSION must be X.Y.Z, got: $HY2XS_VERSION"
|
||||
printf '%s' "$HY2XS_RELEASE_LINE" | grep -Eq '^[0-9]+$' \
|
||||
grep -Eq '^[0-9]+$' <<<"$HY2XS_RELEASE_LINE" \
|
||||
|| fail "versions.env: HY2XS_RELEASE_LINE must be an integer, got: $HY2XS_RELEASE_LINE"
|
||||
printf '%s' "$HY2XS_CONFIG_SCHEMA_VERSION" | grep -Eq '^[0-9]+$' \
|
||||
grep -Eq '^[0-9]+$' <<<"$HY2XS_CONFIG_SCHEMA_VERSION" \
|
||||
|| fail "versions.env: HY2XS_CONFIG_SCHEMA_VERSION must be an integer"
|
||||
|
||||
# HY2XS_TARGET_OS — это дистрибутив (debian), а GOOS/`target_os` в metadata
|
||||
|
||||
Reference in New Issue
Block a user