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:
@@ -42,6 +42,49 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
|||||||
асинхронного исполнителя, в трёх местах принимал за доказательство отсутствие
|
асинхронного исполнителя, в трёх местах принимал за доказательство отсутствие
|
||||||
наблюдения.
|
наблюдения.
|
||||||
|
|
||||||
|
Восьмой проход — сами релизные гейты. Проверка, которая инвертирует свой
|
||||||
|
результат на достаточно большом входе, опаснее отсутствующей: отсутствующая
|
||||||
|
ничего не обещает.
|
||||||
|
|
||||||
|
### Исправлено — гейты сборки
|
||||||
|
|
||||||
|
- **Пайплайн в поиск с флагом `-q` под `pipefail` инвертирует смысл проверки.**
|
||||||
|
Такой поиск прекращает чтение на первом совпадении и закрывает свой конец
|
||||||
|
канала; продюсер, которому осталось что писать, получает `SIGPIPE` и
|
||||||
|
завершается кодом 141, а `pipefail` делает 141 статусом всей конструкции:
|
||||||
|
|
||||||
|
```text
|
||||||
|
совпадение НАЙДЕНО -> продюсер оборван -> статус 141 -> «не найдено»
|
||||||
|
```
|
||||||
|
|
||||||
|
Для утвердительных проверок это ложный FAIL. Для отрицательных — «такой
|
||||||
|
конструкции в коде нет» — **ложный PASS**: запрещённая конструкция найдена, а
|
||||||
|
гейт зелёный. Именно отрицательными проверками закреплена половина инвариантов
|
||||||
|
приёмки, включая запрет обхода тестов и запрет `pnpm audit --prod`.
|
||||||
|
|
||||||
|
Порог резкий и измеримый: пока вывод продюсера помещается в буфер канала
|
||||||
|
(64 KiB на Linux), он не блокируется и успевает завершиться раньше, чем
|
||||||
|
потребитель начнёт читать, — 0 отказов на 60 прогонах для любого размера до
|
||||||
|
60 KiB и 58/60 ровно на 64 KiB. То есть проверка выглядит исправной ровно до
|
||||||
|
первого источника крупнее буфера, а такие файлы в репозитории уже есть.
|
||||||
|
|
||||||
|
Все 56 мест переведены на here-string (`grep -q PATTERN <<<"$content"`);
|
||||||
|
продюсеры-команды (`ss | awk`, `dpkg-query`, `/proc/cpuinfo`,
|
||||||
|
`systemctl list-unit-files`) сначала читаются в переменную. Возврат пайплайна
|
||||||
|
запрещён отдельной приёмкой.
|
||||||
|
|
||||||
|
- **Корректность десяти отрицательных сканов держалась на `|| true`.** В
|
||||||
|
`code_without_comments` подавление статуса гасило и SIGPIPE-141, поэтому
|
||||||
|
сканы работали — но по причине, которую никто не заявлял и которую снял бы
|
||||||
|
первый же, кто убрал бы маскировку ошибок. Введён `code_has`, читающий код в
|
||||||
|
переменную отдельным оператором.
|
||||||
|
|
||||||
|
- **Несуществующий путь в скане означал успех.** `2>/dev/null || true` в
|
||||||
|
`code_without_comments` превращал опечатку в имени файла в пустой вывод, а
|
||||||
|
пустой вывод для проверки «запрещённой конструкции здесь нет» — это PASS.
|
||||||
|
Теперь отсутствующий файл останавливает сборку. Проверка явная, а не через
|
||||||
|
`set -e`: в контексте `! code_has …` bash отключает `errexit` на весь вызов.
|
||||||
|
|
||||||
### Исправлено — барьер покоя и контракт транзиентного таймера
|
### Исправлено — барьер покоя и контракт транзиентного таймера
|
||||||
|
|
||||||
- **Отказ запроса к systemd выдавался за отсутствие guard'а.** Листинг
|
- **Отказ запроса к systemd выдавался за отсутствие guard'а.** Листинг
|
||||||
|
|||||||
@@ -1447,3 +1447,47 @@ hy2xs-orchestrator doctor
|
|||||||
73. отработавший таймер не блокирует операцию навсегда: `RemainAfterElapse=no` выгружает его, а барьер дополнительно опознаёт `SubState=elapsed` у `*.timer` как покой
|
73. отработавший таймер не блокирует операцию навсегда: `RemainAfterElapse=no` выгружает его, а барьер дополнительно опознаёт `SubState=elapsed` у `*.timer` как покой
|
||||||
74. обещанное окно отката — контракт systemd, а не намерение: у транзиентного таймера явно задан `AccuracySec=1s`, иначе умолчание `AccuracySec=1min` превращало «45 секунд» в 45–105
|
74. обещанное окно отката — контракт systemd, а не намерение: у транзиентного таймера явно задан `AccuracySec=1s`, иначе умолчание `AccuracySec=1min` превращало «45 секунд» в 45–105
|
||||||
75. состояние guard читает один наблюдатель: `status` берёт его у того же кода, что и барьер, и сообщает `unknown` вместо тихого «guard'ов нет» при отказе systemd
|
75. состояние guard читает один наблюдатель: `status` берёт его у того же кода, что и барьер, и сообщает `unknown` вместо тихого «guard'ов нет» при отказе systemd
|
||||||
|
76. ни один релизный гейт не подаёт вывод в поиск с флагом `-q` через пайплайн: под `set -o pipefail` оборванный продюсер отдаёт 141, и «совпадение найдено» превращается в ненулевой код — для отрицательных проверок это ложный PASS. Сравнение идёт через here-string, и возврат пайплайна запрещён отдельной приёмкой
|
||||||
|
|
||||||
|
### Почему пайплайн в `grep -q` запрещён
|
||||||
|
|
||||||
|
Поиск с флагом `-q` прекращает чтение на **первом** совпадении и закрывает свой
|
||||||
|
конец канала. Продюсер, которому осталось что писать, получает `SIGPIPE` и
|
||||||
|
завершается кодом 141, а `set -o pipefail` делает 141 статусом всей
|
||||||
|
конструкции. Смысл инвертируется:
|
||||||
|
|
||||||
|
```text
|
||||||
|
совпадение НАЙДЕНО -> продюсер оборван -> статус 141 -> «не найдено»
|
||||||
|
```
|
||||||
|
|
||||||
|
Для утвердительной проверки это ложный FAIL — гейт отвергает корректный
|
||||||
|
артефакт. Для отрицательной («такой конструкции в коде нет») — **ложный PASS**:
|
||||||
|
запрещённая конструкция найдена, а гейт зелёный.
|
||||||
|
|
||||||
|
Порог измерим и резкий. Пока весь вывод продюсера помещается в буфер канала —
|
||||||
|
64 KiB на Linux, — он записывает всё, не блокируясь, и успевает завершиться
|
||||||
|
раньше, чем потребитель вообще начнёт читать. Замер на 60 прогонах каждого
|
||||||
|
размера:
|
||||||
|
|
||||||
|
```text
|
||||||
|
4 KiB … 60 KiB отказов 0
|
||||||
|
64 KiB отказов 58/60
|
||||||
|
96 KiB и больше отказов 60/60
|
||||||
|
```
|
||||||
|
|
||||||
|
Поэтому такая проверка годами выглядит исправной, а переворачивается на первом
|
||||||
|
источнике крупнее буфера. В этом репозитории файлы такого размера уже есть
|
||||||
|
(`tools/build/lib/acceptance.sh` — 123 KiB, `orchestrator/src/steps/firewall.ts`
|
||||||
|
— 67 KiB).
|
||||||
|
|
||||||
|
Правильная форма — here-string, у которого пайплайна нет вовсе:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -q 'PATTERN' <<<"$content" || fail "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Для содержимого файла есть `code_has FILE [флаги] -- PATTERN`: он читает код без
|
||||||
|
комментариев в переменную **отдельным оператором** и сравнивает через
|
||||||
|
here-string. Отдельный оператор важен: в контексте `! code_has …` bash отключает
|
||||||
|
`errexit` на весь вызов, поэтому неудачное чтение проверяется явно, а не
|
||||||
|
рассчитывает на `set -e`.
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh
|
|||||||
Проверить AVX2:
|
Проверить AVX2:
|
||||||
|
|
||||||
```bash
|
```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:
|
Проверить Bun:
|
||||||
|
|||||||
@@ -22,13 +22,45 @@ set -euo pipefail
|
|||||||
# Решётка при этом отбрасывается ТОЛЬКО в shell. В шаблоне Vue строка вполне
|
# Решётка при этом отбрасывается ТОЛЬКО в shell. В шаблоне Vue строка вполне
|
||||||
# может начинаться с `#default="scope"` — это сокращение для v-slot, а не
|
# может начинаться с `#default="scope"` — это сокращение для v-slot, а не
|
||||||
# комментарий, и общий фильтр молча выбрасывал бы её из скана.
|
# комментарий, и общий фильтр молча выбрасывал бы её из скана.
|
||||||
|
#
|
||||||
|
# Несуществующий путь — ОТКАЗ, а не пустой вывод. Прежнее `2>/dev/null || true`
|
||||||
|
# превращало опечатку в имени файла в пустую строку, а пустая строка для
|
||||||
|
# проверок вида «запрещённой конструкции здесь нет» означает успех. Скан,
|
||||||
|
# который молча проверяет пустоту, хуже отсутствующего: он отчитывается зелёным.
|
||||||
code_without_comments() {
|
code_without_comments() {
|
||||||
|
[ -f "$1" ] || fail "code_without_comments: файл не найден: $1"
|
||||||
case "$1" in
|
case "$1" in
|
||||||
*.sh) grep -vE '^[[:space:]]*#' "$1" 2>/dev/null || true ;;
|
# grep -v возвращает 1, когда КАЖДАЯ строка файла — комментарий. Это
|
||||||
*) grep -v '^[[:space:]]*//' "$1" 2>/dev/null || true ;;
|
# законный результат, а не отказ, поэтому статус здесь гасится.
|
||||||
|
*.sh) grep -vE '^[[:space:]]*#' "$1" || true ;;
|
||||||
|
*) grep -v '^[[:space:]]*//' "$1" || true ;;
|
||||||
esac
|
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 отдаёт список файлов, где подстрока встречается В КОДЕ.
|
||||||
code_mentions_in() {
|
code_mentions_in() {
|
||||||
local needle="$1"
|
local needle="$1"
|
||||||
@@ -37,7 +69,7 @@ code_mentions_in() {
|
|||||||
local file hits=""
|
local file hits=""
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
[ -n "$file" ] || continue
|
[ -n "$file" ] || continue
|
||||||
if code_without_comments "$file" | grep -qF -- "$needle"; then
|
if code_has "$file" -F -- "$needle"; then
|
||||||
hits="${hits}${hits:+ }${file}"
|
hits="${hits}${hits:+ }${file}"
|
||||||
fi
|
fi
|
||||||
done <<EOF
|
done <<EOF
|
||||||
@@ -437,7 +469,7 @@ run_clean_install_acceptance() {
|
|||||||
# с паролем, уже напечатанным на экран.
|
# с паролем, уже напечатанным на экран.
|
||||||
grep -q 'errors.Is(err, dao.ErrAdminUserNotFound)' apps/cmd/reset.go \
|
grep -q 'errors.Is(err, dao.ErrAdminUserNotFound)' apps/cmd/reset.go \
|
||||||
|| fail "acceptance: reset-admin обязан различать sentinel-ошибки слоя данных"
|
|| 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 уедет пустая строка"
|
|| fail "acceptance: ошибка хеширования пароля снова проглатывается, в password_hash уедет пустая строка"
|
||||||
"$BUN_BIN" -e '
|
"$BUN_BIN" -e '
|
||||||
const source = require("node:fs").readFileSync("apps/cmd/reset.go", "utf8");
|
const source = require("node:fs").readFileSync("apps/cmd/reset.go", "utf8");
|
||||||
@@ -587,7 +619,7 @@ run_scheduler_lifecycle_acceptance() {
|
|||||||
# Ключевая регрессия: смена настройки не имеет права ронять HTTP-сервер.
|
# Ключевая регрессия: смена настройки не имеет права ронять HTTP-сервер.
|
||||||
# Именно этот путь плодил планировщики — StopServer возвращал управление в
|
# Именно этот путь плодил планировщики — StopServer возвращал управление в
|
||||||
# `for { runServer() }`, а InitCron создавал новый cron.New() и терял ссылку.
|
# `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-сервер вместо перепланирования джобы"
|
|| fail "acceptance: смена настройки снова перезапускает HTTP-сервер вместо перепланирования джобы"
|
||||||
grep -q 'RescheduleResetTraffic' apps/controller/config.go \
|
grep -q 'RescheduleResetTraffic' apps/controller/config.go \
|
||||||
|| fail "acceptance: расписание должно переноситься на месте, через RescheduleResetTraffic"
|
|| fail "acceptance: расписание должно переноситься на месте, через RescheduleResetTraffic"
|
||||||
@@ -661,9 +693,9 @@ run_dead_config_acceptance() {
|
|||||||
|
|
||||||
# Серверный конфиг Hysteria читается только из файла: ветка «сначала SQLite»
|
# Серверный конфиг Hysteria читается только из файла: ветка «сначала SQLite»
|
||||||
# была вторым источником истины и читалась ПЕРВОЙ.
|
# была вторым источником истины и читалась ПЕРВОЙ.
|
||||||
! code_without_comments apps/service/config.go | grep -q 'Hysteria2Config)' \
|
! code_has apps/service/config.go -- 'Hysteria2Config)' \
|
||||||
|| fail "acceptance: чтение серверного конфига снова обращается к таблице config"
|
|| 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"
|
|| fail "acceptance: выгрузка серверного конфига снова обращается к таблице config"
|
||||||
|
|
||||||
log_step "Acceptance: share URI remark is derived, not stored as dead state"
|
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, а этот файл отдаётся кнопкой выгрузки.
|
# /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-пароль снова пишется в журнал"
|
|| fail "acceptance: bootstrap-пароль снова пишется в журнал"
|
||||||
grep -q 'HY2XS_ADMIN_INITIAL_PASSWORD не задан' apps/dao/sqlite.go \
|
grep -q 'HY2XS_ADMIN_INITIAL_PASSWORD не задан' apps/dao/sqlite.go \
|
||||||
|| fail "acceptance: отсутствие HY2XS_ADMIN_INITIAL_PASSWORD должно быть отказом старта"
|
|| 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"
|
log_step "Acceptance: the admin log is sanitized on export, like the Hysteria one"
|
||||||
grep -q 'func ExportAdminLog' apps/service/journal.go \
|
grep -q 'func ExportAdminLog' apps/service/journal.go \
|
||||||
|| fail "acceptance: журнал админки выгружается без санитайза"
|
|| fail "acceptance: журнал админки выгружается без санитайза"
|
||||||
! code_without_comments apps/controller/log.go | grep -q 'c.File(' \
|
! code_has apps/controller/log.go -- 'c.File(' \
|
||||||
|| fail "acceptance: журнал снова отдаётся сырым файлом мимо санитайза"
|
|| fail "acceptance: журнал снова отдаётся сырым файлом мимо санитайза"
|
||||||
|
|
||||||
log_step "Acceptance: JWT stack is the maintained major line, with a fixed algorithm"
|
log_step "Acceptance: JWT stack is the maintained major line, with a fixed algorithm"
|
||||||
@@ -704,7 +736,7 @@ run_dependency_hygiene_acceptance() {
|
|||||||
|| fail "acceptance: разбор токена должен ограничивать набор алгоритмов подписи"
|
|| fail "acceptance: разбор токена должен ограничивать набор алгоритмов подписи"
|
||||||
|
|
||||||
log_step "Acceptance: password storage has exactly one format"
|
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"
|
|| fail "acceptance: вернулась проверка пароля по несолёному SHA-224"
|
||||||
grep -q 'if !IsBcryptHash(storedHash)' apps/util/encrypt.go \
|
grep -q 'if !IsBcryptHash(storedHash)' apps/util/encrypt.go \
|
||||||
|| fail "acceptance: VerifyPassword обязан отклонять всё, кроме bcrypt"
|
|| fail "acceptance: VerifyPassword обязан отклонять всё, кроме bcrypt"
|
||||||
@@ -726,12 +758,37 @@ run_dependency_hygiene_acceptance() {
|
|||||||
# `--prod` не показывал уязвимости в vite/rollup, хотя они исполняются на
|
# `--prod` не показывал уязвимости в vite/rollup, хотя они исполняются на
|
||||||
# build-машине и порождают production-бандл. Ровно такой класс и был найден
|
# build-машине и порождают production-бандл. Ровно такой класс и был найден
|
||||||
# (DOM clobbering в Rollup затрагивал генерируемый бандл).
|
# (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"
|
|| fail "acceptance: гейт зависимостей снова проверяет только production-подграф npm"
|
||||||
code_without_comments tools/build/lib/security.sh \
|
code_has tools/build/lib/security.sh -- 'audit --audit-level "\$PNPM_AUDIT_LEVEL"' \
|
||||||
| grep -q 'audit --audit-level "\$PNPM_AUDIT_LEVEL"' \
|
|
||||||
|| fail "acceptance: гейт зависимостей обязан проверять весь lock-граф frontend"
|
|| 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"
|
log_step "Acceptance: the release is built with a mandatory test gate"
|
||||||
# SKIP_TESTS=true доходил до конца сборки и выдавал внешне неотличимый
|
# SKIP_TESTS=true доходил до конца сборки и выдавал внешне неотличимый
|
||||||
# production-артефакт: ни metadata, ни финальная приёмка не проверяли, что
|
# production-артефакт: ни metadata, ни финальная приёмка не проверяли, что
|
||||||
@@ -1229,19 +1286,19 @@ run_secret_channel_acceptance() {
|
|||||||
log_code="$(grep -v '^[[:space:]]*//' apps/middleware/log.go || true)"
|
log_code="$(grep -v '^[[:space:]]*//' apps/middleware/log.go || true)"
|
||||||
server_code="$(grep -v '^[[:space:]]*//' apps/cmd/server.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"
|
|| 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"
|
|| fail "acceptance: RequestURI carries the machine token in its query string"
|
||||||
! grep -rq 'ReqUri' apps/model/vo apps/frontend/src/api \
|
! grep -rq 'ReqUri' apps/model/vo apps/frontend/src/api \
|
||||||
|| fail "acceptance: the reqUri log field came back"
|
|| fail "acceptance: the reqUri log field came back"
|
||||||
|
|
||||||
log_step "Acceptance: the admin has exactly one HTTP log channel"
|
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"
|
|| 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()"
|
|| 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"
|
|| fail "acceptance: gin.New() must still install the recovery middleware"
|
||||||
|
|
||||||
log_step "Acceptance: exported logs are sanitized on both sides"
|
log_step "Acceptance: exported logs are sanitized on both sides"
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ apt_install_missing() {
|
|||||||
local missing=()
|
local missing=()
|
||||||
local pkg
|
local pkg
|
||||||
for pkg in "$@"; do
|
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")
|
missing+=("$pkg")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -135,7 +137,9 @@ ensure_go() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cpu_has_avx2() {
|
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() {
|
select_bun_artifact() {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ HYSTERIA_GATE_TIMEOUT_SECONDS="${HYSTERIA_GATE_TIMEOUT_SECONDS:-15}"
|
|||||||
|
|
||||||
validate_hysteria_version_format() {
|
validate_hysteria_version_format() {
|
||||||
local value="$1"
|
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)"
|
|| 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"
|
fail "HYSTERIA_ARTIFACT_SHA256 must be a real release sha256"
|
||||||
;;
|
;;
|
||||||
esac
|
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"
|
|| 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"
|
local proto_flag="$2"
|
||||||
|
|
||||||
if command -v ss >/dev/null 2>&1; then
|
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"
|
fail "compatibility gate port $port is already in use; override HYSTERIA_GATE_PORT/HYSTERIA_GATE_STATS_PORT"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -246,10 +251,13 @@ gate_run_single_profile() {
|
|||||||
if ! kill -0 "$gate_pid" 2>/dev/null; then
|
if ! kill -0 "$gate_pid" 2>/dev/null; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
if command -v ss >/dev/null 2>&1 \
|
if command -v ss >/dev/null 2>&1; then
|
||||||
&& ss -H -lun 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${HYSTERIA_GATE_PORT}$"; then
|
local udp_sockets
|
||||||
ready="true"
|
udp_sockets="$(ss -H -lun 2>/dev/null | awk '{print $4}')" || udp_sockets=""
|
||||||
break
|
if grep -Eq "[:.]${HYSTERIA_GATE_PORT}$" <<<"$udp_sockets"; then
|
||||||
|
ready="true"
|
||||||
|
break
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
waited=$((waited + 1))
|
waited=$((waited + 1))
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ run_frontend_vulnerability_gate() {
|
|||||||
|
|
||||||
# pnpm audit ходит в реестр npm. Недоступность реестра — это отказ проверки,
|
# 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 — проверка не выполнена:
|
fail "pnpm audit не смог обратиться к реестру npm — проверка не выполнена:
|
||||||
$report"
|
$report"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+41
-41
@@ -45,22 +45,22 @@ verify_archive() {
|
|||||||
local listing
|
local listing
|
||||||
listing="$(tar -tzf "$archive")"
|
listing="$(tar -tzf "$archive")"
|
||||||
|
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/install.sh$' || fail "archive missing install.sh"
|
grep -q '^hy2xs-install/install.sh$' <<<"$listing" || fail "archive missing install.sh"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/LICENSE$' || fail "archive missing LICENSE"
|
grep -q '^hy2xs-install/LICENSE$' <<<"$listing" || fail "archive missing LICENSE"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/orchestrator/hy2xs-orchestrator$' || fail "archive missing orchestrator artifact"
|
grep -q '^hy2xs-install/orchestrator/hy2xs-orchestrator$' <<<"$listing" || fail "archive missing orchestrator artifact"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/ui/hy2xs-admin/hy2xs-admin$' || fail "archive missing bundled UI binary"
|
grep -q '^hy2xs-install/ui/hy2xs-admin/hy2xs-admin$' <<<"$listing" || fail "archive missing bundled UI binary"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hysteria-server.service$' || fail "archive missing hysteria systemd unit"
|
grep -q '^hy2xs-install/systemd/hysteria-server.service$' <<<"$listing" || fail "archive missing hysteria systemd unit"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hy2xs-admin.service$' || fail "archive missing admin systemd unit"
|
grep -q '^hy2xs-install/systemd/hy2xs-admin.service$' <<<"$listing" || 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"
|
grep -q '^hy2xs-install/templates/hysteria/config.yaml.tpl$' <<<"$listing" || fail "archive missing Hysteria config template"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/config/hy2xs.env$' || fail "archive missing canonical runtime config"
|
grep -q '^hy2xs-install/config/hy2xs.env$' <<<"$listing" || 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"
|
grep -q '^hy2xs-install/templates/env/post-install.env.tpl$' <<<"$listing" || fail "archive missing post-install env template"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/checksums.txt$' || fail "archive missing checksums"
|
grep -q '^hy2xs-install/metadata/checksums.txt$' <<<"$listing" || fail "archive missing checksums"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.version$' || fail "archive missing pinned Hysteria version"
|
grep -q '^hy2xs-install/metadata/hysteria.version$' <<<"$listing" || fail "archive missing pinned Hysteria version"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.url$' || fail "archive missing pinned Hysteria artifact url"
|
grep -q '^hy2xs-install/metadata/hysteria.url$' <<<"$listing" || fail "archive missing pinned Hysteria artifact url"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.sha256$' || fail "archive missing pinned Hysteria sha256"
|
grep -q '^hy2xs-install/metadata/hysteria.sha256$' <<<"$listing" || fail "archive missing pinned Hysteria sha256"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.resolution$' || fail "archive missing Hysteria resolution marker"
|
grep -q '^hy2xs-install/metadata/hysteria.resolution$' <<<"$listing" || fail "archive missing Hysteria resolution marker"
|
||||||
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/package.release_line$' || fail "archive missing product release line"
|
grep -q '^hy2xs-install/metadata/package.release_line$' <<<"$listing" || 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/metadata/package.config_schema_version$' <<<"$listing" || fail "archive missing packaged config schema version"
|
||||||
|
|
||||||
# Контракт установки проверяется структурно: install.sh — read-only bootstrap
|
# Контракт установки проверяется структурно: install.sh — read-only bootstrap
|
||||||
# целиком, у PHASE 1 ровно один владелец — оркестратор.
|
# целиком, у PHASE 1 ровно один владелец — оркестратор.
|
||||||
@@ -73,9 +73,9 @@ verify_archive() {
|
|||||||
# Владение мутацией нельзя было отследить, потому что мутировали двое.
|
# Владение мутацией нельзя было отследить, потому что мутировали двое.
|
||||||
local packaged_installer mutation_hits
|
local packaged_installer mutation_hits
|
||||||
packaged_installer="$(tar -xOzf "$archive" hy2xs-install/install.sh)"
|
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"
|
|| 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"
|
|| 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:
|
|| fail "packaged install.sh must not mutate the host; PHASE 1 belongs to the orchestrator alone. Found:
|
||||||
$mutation_hits"
|
$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"
|
|| fail "packaged install.sh must hand the whole mutating phase to the orchestrator via exec"
|
||||||
|
|
||||||
local license_text
|
local license_text
|
||||||
license_text="$(tar -xOzf "$archive" hy2xs-install/LICENSE)"
|
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"
|
|| 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"
|
|| fail "packaged LICENSE is not AGPL version 3"
|
||||||
|
|
||||||
local env_content
|
local env_content
|
||||||
env_content="$(tar -xOzf "$archive" hy2xs-install/config/hy2xs.env)"
|
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"
|
grep -q 'replace-with-your-domain.example' <<<"$env_content" && 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-email@example.com' <<<"$env_content" && fail "packaged hy2xs.env contains placeholder email"
|
||||||
|
|
||||||
local hysteria_tpl
|
local hysteria_tpl
|
||||||
hysteria_tpl="$(tar -xOzf "$archive" hy2xs-install/templates/hysteria/config.yaml.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"
|
grep -q '/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}' <<<"$hysteria_tpl" || 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 '{{OBFS_BLOCK}}' <<<"$hysteria_tpl" || fail "hysteria template must render a typed obfs block"
|
||||||
|
|
||||||
# Замороженная версия обязана совпадать во всех местах пакета.
|
# Замороженная версия обязана совпадать во всех местах пакета.
|
||||||
local pinned_version pinned_sha pinned_url meta_env
|
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')"
|
pinned_url="$(tar -xOzf "$archive" hy2xs-install/metadata/hysteria.url | tr -d '\n')"
|
||||||
meta_env="$(tar -xOzf "$archive" hy2xs-install/metadata/package.env)"
|
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"
|
|| 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"
|
|| 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"
|
|| 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"
|
fail "packaged Hysteria artifact url must be immutable, not a moving latest: $pinned_url"
|
||||||
fi
|
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"
|
|| 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"
|
|| fail "metadata package.env disagrees with metadata/hysteria.sha256"
|
||||||
|
|
||||||
local post_install_tpl
|
local post_install_tpl
|
||||||
post_install_tpl="$(tar -xOzf "$archive" hy2xs-install/templates/env/post-install.env.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
|
local tmp
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
@@ -144,22 +144,22 @@ $mutation_hits"
|
|||||||
|
|
||||||
local meta
|
local meta
|
||||||
meta="$(cat "$tmp/hy2xs-install/metadata/package.env")"
|
meta="$(cat "$tmp/hy2xs-install/metadata/package.env")"
|
||||||
printf '%s\n' "$meta" | grep -q '^source_git_commit=' || fail "metadata missing source_git_commit"
|
grep -q '^source_git_commit=' <<<"$meta" || fail "metadata missing source_git_commit"
|
||||||
printf '%s\n' "$meta" | grep -q '^dirty_tree=' || fail "metadata missing dirty_tree"
|
grep -q '^dirty_tree=' <<<"$meta" || fail "metadata missing dirty_tree"
|
||||||
printf '%s\n' "$meta" | grep -q '^build_profile=production$' || fail "metadata missing build_profile=production"
|
grep -q '^build_profile=production$' <<<"$meta" || 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 '^license=AGPL-3.0-only$' <<<"$meta" || fail "metadata missing license=AGPL-3.0-only"
|
||||||
|
|
||||||
# Поколение продукта обязано доехать до пакета: install-state строится из
|
# Поколение продукта обязано доехать до пакета: install-state строится из
|
||||||
# него, а reconfigure/repair по нему отличают v1 от чужой установки.
|
# него, а 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}"
|
|| 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}"
|
|| 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"
|
|| 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"
|
|| 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"
|
|| fail "metadata target_arch disagrees with versions.env"
|
||||||
|
|
||||||
local packaged_schema
|
local packaged_schema
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ REQUIRED_CONTRACT_VARS=(
|
|||||||
validate_sha256_value() {
|
validate_sha256_value() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local value="$2"
|
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"
|
|| 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 BUN_LINUX_X64_BASELINE_SHA256 "$BUN_LINUX_X64_BASELINE_SHA256"
|
||||||
validate_sha256_value NODE_LINUX_X64_SHA256 "$NODE_LINUX_X64_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"
|
|| 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"
|
|| 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"
|
|| fail "versions.env: HY2XS_CONFIG_SCHEMA_VERSION must be an integer"
|
||||||
|
|
||||||
# HY2XS_TARGET_OS — это дистрибутив (debian), а GOOS/`target_os` в metadata
|
# HY2XS_TARGET_OS — это дистрибутив (debian), а GOOS/`target_os` в metadata
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ verify_clean_host() {
|
|||||||
|
|
||||||
local unit
|
local unit
|
||||||
for unit in "${UNITS[@]}"; do
|
for unit in "${UNITS[@]}"; do
|
||||||
if systemctl list-unit-files --no-legend "$unit" 2>/dev/null | grep -q .; then
|
local unit_files
|
||||||
|
unit_files="$(systemctl list-unit-files --no-legend "$unit" 2>/dev/null)" || unit_files=""
|
||||||
|
if [ -n "$unit_files" ]; then
|
||||||
leftovers+=("systemd unit: $unit")
|
leftovers+=("systemd unit: $unit")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -104,7 +104,12 @@ preflight() {
|
|||||||
local port
|
local port
|
||||||
for port in "$SERVER_PORT" "$STATS_PORT" "$AUTH_PORT" \
|
for port in "$SERVER_PORT" "$STATS_PORT" "$AUTH_PORT" \
|
||||||
"$ORIGIN_TCP_PORT" "$ORIGIN_UDP_PORT" "$FORWARD_TCP_PORT" "$FORWARD_UDP_PORT"; do
|
"$ORIGIN_TCP_PORT" "$ORIGIN_UDP_PORT" "$FORWARD_TCP_PORT" "$FORWARD_UDP_PORT"; do
|
||||||
if ss -H -lantu 2>/dev/null | awk '{print $5}' | grep -Eq "[:.]${port}\$"; then
|
# Вывод собирается ДО поиска: `grep -Eq` обрывает чтение на первом
|
||||||
|
# совпадении, и под `pipefail` SIGPIPE у продюсера превратил бы найденный
|
||||||
|
# занятый порт в ненулевой статус, то есть в вывод «порт свободен».
|
||||||
|
local sockets
|
||||||
|
sockets="$(ss -H -lantu 2>/dev/null | awk '{print $5}')" || sockets=""
|
||||||
|
if grep -Eq "[:.]${port}\$" <<<"$sockets"; then
|
||||||
die "port $port is already in use"
|
die "port $port is already in use"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -134,7 +139,9 @@ wait_for_port() {
|
|||||||
local port="$1" proto_flag="$2" label="$3" attempts="${4:-30}"
|
local port="$1" proto_flag="$2" label="$3" attempts="${4:-30}"
|
||||||
local i=0
|
local i=0
|
||||||
while [ "$i" -lt "$attempts" ]; do
|
while [ "$i" -lt "$attempts" ]; do
|
||||||
if ss -H "$proto_flag" 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}\$"; then
|
local sockets
|
||||||
|
sockets="$(ss -H "$proto_flag" 2>/dev/null | awk '{print $4}')" || sockets=""
|
||||||
|
if grep -Eq "[:.]${port}\$" <<<"$sockets"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
@@ -258,7 +265,9 @@ start_server() {
|
|||||||
cat "$WORK_DIR/server.log" >&2 || true
|
cat "$WORK_DIR/server.log" >&2 || true
|
||||||
die "server exited while starting"
|
die "server exited while starting"
|
||||||
fi
|
fi
|
||||||
if ss -H -lun 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${SERVER_PORT}\$"; then
|
local udp_sockets
|
||||||
|
udp_sockets="$(ss -H -lun 2>/dev/null | awk '{print $4}')" || udp_sockets=""
|
||||||
|
if grep -Eq "[:.]${SERVER_PORT}\$" <<<"$udp_sockets"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
@@ -311,11 +320,11 @@ assert_production_share_uri_shape() {
|
|||||||
*) die "production share URI has an unexpected scheme: $production_uri" ;;
|
*) die "production share URI has an unexpected scheme: $production_uri" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
printf '%s' "$production_uri" | grep -q "obfs=$obfs_type" \
|
grep -q "obfs=$obfs_type" <<<"$production_uri" \
|
||||||
|| die "production share URI does not carry obfs=$obfs_type"
|
|| die "production share URI does not carry obfs=$obfs_type"
|
||||||
printf '%s' "$production_uri" | grep -q 'insecure=0' \
|
grep -q 'insecure=0' <<<"$production_uri" \
|
||||||
|| die "production share URI must carry insecure=0"
|
|| die "production share URI must carry insecure=0"
|
||||||
printf '%s' "$production_uri" | grep -q "sni=$SNI_NAME" \
|
grep -q "sni=$SNI_NAME" <<<"$production_uri" \
|
||||||
|| die "production share URI does not carry the SNI"
|
|| die "production share URI does not carry the SNI"
|
||||||
|
|
||||||
pass "production share URI keeps certificate verification enabled"
|
pass "production share URI keeps certificate verification enabled"
|
||||||
@@ -350,7 +359,9 @@ start_client() {
|
|||||||
cat "$WORK_DIR/client.log" >&2 || true
|
cat "$WORK_DIR/client.log" >&2 || true
|
||||||
die "client exited while connecting via the generated share URI"
|
die "client exited while connecting via the generated share URI"
|
||||||
fi
|
fi
|
||||||
if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${FORWARD_TCP_PORT}\$"; then
|
local tcp_sockets
|
||||||
|
tcp_sockets="$(ss -H -ltn 2>/dev/null | awk '{print $4}')" || tcp_sockets=""
|
||||||
|
if grep -Eq "[:.]${FORWARD_TCP_PORT}\$" <<<"$tcp_sockets"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|||||||
Reference in New Issue
Block a user