Отладка¶
Реальные ошибки, возникшие при выполнении работы, в порядке появления. Для каждой: текст ошибки, гипотеза, способ проверки, решение.
1. Плагин privacy не может скачать бейдж статуса¶
FileNotFoundError: [Errno 2] No such file or directory: '.cache/plugin/privacy/assets/external/
github.com/bailaa/pyengin-ssg/actions/workflows/pages.yml/badge.svg'
Гипотеза. Плагин privacy пытается скачать все внешние картинки, включая бейдж статуса workflow
на главной. Репозиторий ещё не создан, GitHub отвечает 404, файл в кэш не попадает, а при копировании MkDocs падает
с исключением вместо понятного предупреждения.
Проверка. curl -I https://github.com/bailaa/pyengin-ssg/actions/workflows/pages.yml/badge.svg → 404. После удаления
бейджа из index.md сборка проходит.
Решение. Бейдж должен оставаться «живым», а локальная копия показывала бы статус на момент сборки. Поэтому
внешние ресурсы GitHub исключены из локализации: privacy: assets_exclude: [github.com/*].
2. Шрифты MathJax отдают 404 при включённом privacy¶
404 assets/external/cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff
404 .../MathJax_Math-Italic.woff
404 .../MathJax_Zero.woff
Гипотеза. privacy видит только ресурсы, которые упомянуты в HTML/CSS на этапе сборки. tex-chtml.js он скачал,
а шрифты MathJax запрашивает уже в браузере, вычисляя URL относительно собственного расположения. Плагин об этих
файлах не знает.
Проверка. В Playwright записаны все ответы ≥ 400 (scripts/t4_measure.py): у варианта privacy 8 ошибок, из них
6 — шрифты MathJax, причём и при открытой сети. document.fonts показывает MJXTEX: error, формула набрана
системным шрифтом (скриншот в T4).
Решение. MathJax размещён в репозитории целиком, вместе с каталогом шрифтов (scripts/vendor_mathjax.py берёт
тарбол из npm и проверяет sha512), а в extra_javascript указан относительный путь assets/mathjax/tex-chtml.js.
3. Формулы пустые во всех вариантах сборки¶
Симптом: на месте формулы пустая строка нужной высоты. В консоли ошибок нет, mjx-container в DOM есть.
Гипотеза 1 (неверная). Шрифты не загружаются. Проверка: в варианте local ошибок загрузки нет, а формула
всё равно пустая. В варианте cdn с открытой сетью — тоже.
Гипотеза 2. Глифы есть в DOM, но у них нет содержимого. Проверка:
getComputedStyle(document.querySelector('mjx-c'), '::before').content → "none". MathJax CHTML выводит символы
через CSS-правила mjx-c.mjx-c1D45D::before { content: "\1D45D" }, и этих правил нет.
Причина. В docs/javascripts/mathjax.js был взят фрагмент из документации Material, который на каждое событие
document$ вызывает MathJax.startup.output.clearCache() и typesetPromise(). Первое событие document$
приходит одновременно с первичной отрисовкой MathJax. clearCache() сбрасывает таблицу CSS-правил
глифов, уже выведенных в DOM, и повторно их никто не добавляет. Проверка: без подписки на document$ у глифа
content: "p" и формула видна.
Решение. Первое событие пропускается, а последующие (мгновенная навигация) ставятся в очередь после
MathJax.startup.promise. Отдельно: \eqref{} вне математического режима не обрабатывается, потому что
arithmatex передаёт MathJax только свои элементы. Ссылка записана как \(\eqref{eq:pok}\), а формула —
в окружении equation, чтобы получить номер (1).
4. Отсутствующий сниппет обрушивает сборку¶
TimeoutError: Locator.scroll_into_view_if_needed: Timeout 30000ms exceeded. waiting for locator(".arithmatex")
Гипотеза. Замер T4 не нашёл формулу, потому что страница не собралась: t4.md подключает
snippets/t4_table.md, который создаёт сам скрипт замера. При check_paths: true отсутствующий сниппет — ошибка
сборки, а вывод сборки в скрипте подавлялся grep.
Проверка. mkdocs build без фильтра → Snippet at path 'snippets/t4_table.md' could not be found.
Решение. Сниппеты с таблицами замеров хранятся в репозитории (snippets/), а не генерируются перед сборкой.
check_paths: true оставлен: это именно то поведение, которое нужно в --strict.
5. Helios не отвечает: «SSH-баннер не пришёл»¶
$ ssh-keyscan -p 2222 helios.cs.ifmo.ru
Connection timed out during banner exchange
$ curl https://se.ifmo.ru/~t261087/
curl: (28) Connection timed out after 15005 milliseconds
Гипотеза. TCP-порт открыт (nc -z успешен), но SSH-баннер не приходит, а HTTPS виснет. Значит, фильтрация идёт
не по порту, а по адресу клиента. Локальная машина выходила в сеть через VPN с адресом в Нидерландах
(ifconfig.co/country). Предположение: Helios не обслуживает зарубежные адреса, и тогда раннеры GitHub (США/ЕС)
тоже не смогут выкладывать сайт.
Проверка. Отдельный workflow helios-probe.yml
с раннера GitHub: TCP 2222 открыт, баннер SSH-2.0-OpenSSH_10.0 FreeBSD-20250801 получен, se.ifmo.ru → 200 за 1,5 с.
Гипотеза опровергнута для раннеров: блокируется конкретная VPN-сеть, а не все зарубежные адреса.
Решение. Деплой с раннеров GitHub возможен. На случай изменения фильтрации healthcheck сделан в два шага: сначала
запрос с раннера, при сетевой недоступности (код 000) — тот же запрос с самого Helios (ssh … ssg check).
Однократная ручная настройка сервера (scripts/helios-bootstrap.sh) выполняется без VPN.
6. rsync --link-dest выкладывает старую версию файла¶
Найдено на локальном прогоне полного цикла деплоя (шлюз + rsync в поддельный $HOME):
активен релиз R2 → …/public_html
public_html: <meta name="build-commit" content="v1"> ← ожидалось v2
du -sh releases/main/R2 → 0B
Гипотеза. rsync по умолчанию считает файлы одинаковыми, если совпадают размер и время модификации (quick check).
Тестовые index.html релизов R1 и R2 одинаковой длины (v1/v2) и созданы в одну секунду. Поэтому
--link-dest=../R1 сделал файл R2 жёсткой ссылкой на файл R1, то есть на старое содержимое.
Проверка. du релиза R2 = 0 байт, то есть все файлы — жёсткие ссылки; stat -f %l показывает 2 ссылки
на inode.
Решение. Загрузка и активация выполняются с --checksum (сравнение по содержимому). После исправления R2
весит 4 КБ (изменился только index.html, остальное — жёсткие ссылки), откат и повторный откат работают.
На CI ошибка проявилась бы иначе: у свежесобранных файлов всегда новое время модификации, поэтому без
--checksum --link-dest просто ничего бы не связывал, и каждый релиз занимал бы полный объём.
7. Шлюз отклоняет rsync --stats¶
ssg-gate: запрещено: опция --stats
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(232) [sender=3.2.7]
Гипотеза. Ошибка протокола rsync — не сетевая: соединение открылось, ssg current через тот же ключ
прошёл. Вероятно, команда на сервере была отвергнута шлюзом, а rsync сообщил лишь, что поток закрылся.
Проверка. Сообщение шлюза идёт в stderr и видно в логе шага над строкой rsync: опция --stats клиент
передаёт серверу в аргументах rsync --server, а белый список шлюза её не содержит.
Решение. --stats убран из workflow. Шлюз на сервере ключом изменить нельзя, и это сознательно: иначе
украденный ключ мог бы ослабить собственные ограничения. Время загрузки берётся из длительности шага.
8. Секрет создан пустым¶
env:
SSHPASS: ← без значения (должно быть ***)
##[error]Process completed with exit code 5. (sshpass: неверный пароль)
Гипотеза. Команда gh secret set NAME в окружении без терминала не получила значение и создала секрет
пустым, при этом без единой ошибки. Подозрение на сам пароль или на ключ хоста отпало: SSH-соединение дошло
до запроса пароля, а шаг проверки ключа прошёл.
Проверка. В логе GitHub значение непустого секрета выводится как ***, пустого — как пустая строка. Первые два
запуска показывали пустую строку, после pbpaste | gh secret set HELIOS_PASSWORD — *** и успешную настройку.
Решение. Значение секрета подаётся через stdin (pbpaste | gh secret set …), а не вводится интерактивно.
Временный секрет и одноразовый workflow удалены сразу после настройки.