Перейти к содержанию

Отладка

Реальные ошибки, возникшие при выполнении работы, в порядке появления. Для каждой: текст ошибки, гипотеза, способ проверки, решение.

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.

Найдено на локальном прогоне полного цикла деплоя (шлюз + 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 удалены сразу после настройки.

сборка 964f3e7 · 2026-10-12 15:01 UTC · данные 749742082a6a · github-pages