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
+44
View File
@@ -1447,3 +1447,47 @@ hy2xs-orchestrator doctor
73. отработавший таймер не блокирует операцию навсегда: `RemainAfterElapse=no` выгружает его, а барьер дополнительно опознаёт `SubState=elapsed` у `*.timer` как покой
74. обещанное окно отката — контракт systemd, а не намерение: у транзиентного таймера явно задан `AccuracySec=1s`, иначе умолчание `AccuracySec=1min` превращало «45 секунд» в 45–105
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`.