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:
2026-09-01 04:28:54 +05:00
parent 2259f7c847
commit a1c74caa0c
11 changed files with 251 additions and 82 deletions
+1 -1
View File
@@ -343,7 +343,7 @@ NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh
Проверить AVX2:
```bash
grep -m1 '^flags' /proc/cpuinfo | grep -qw avx2 && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'
grep -qw avx2 <<<"$(grep -m1 '^flags' /proc/cpuinfo)" && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'
```
Проверить Bun:
+75 -18
View File
@@ -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"
+6 -2
View File
@@ -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() {
+15 -7
View File
@@ -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))
+1 -1
View File
@@ -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
View File
@@ -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
+4 -4
View File
@@ -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