Колофон — это запись в конце книги о том, как её сделали. Релиз 3.6.2 с таким названием отвечает на тот же вопрос про архив: он теперь одинаков на любой машине, а CI пересобирает каждый выпуск, чтобы это подтвердить. Заодно исправлена ошибка, из-за которой настройки панели тихо терялись при обновлении. Основной разбор платформы — в статье MD Viewer: просмотрщик Markdown без браузерного движка; здесь про MD.Scribe 3.6.2, замеры архива и что проверить после обновления.

TL;DR: три вещи, которые надо знать
- Архив стал детерминированным. Две сборки одного коммита раньше отличались по платформе, теперь совпадают байт в байт.
- CI пересобирает релиз. Задача на событие публикации собирает архив дважды, сверяет между собой и с опубликованным digest.
- Настройки снова переживают деплой. Атомарная запись разыменовывает симлинк, а не заменяет его файлом.
- Манифест: 988 записей, версия 3.6.2, минимум PHP 8.5.
- Проверка. Распаковать, убедиться, что
.md.ini— ссылка, а настройки после перезапуска на месте.
Что нового
| Область | Изменение |
|---|---|
| Сборка | детерминированный zip вместо ZipArchive |
| CI | workflow release.yml: пересборка и сравнение digest |
| Деплой | запись настроек по симлинку в цель, а не поверх ссылки |
| Тесты | два новых теста записи настроек, один — на висячую ссылку |
| Манифест | 988 файлов, версия 3.6.2 |
| Название | «Colophon» |
flowchart TB SRC["коммит тега"] --> BUILD["сборка архива"] BUILD --> TIME["время из коммита"] BUILD --> MODE["режимы 0644 и порядок записей"] TIME --> ZIP["zip без лишних полей"] MODE --> ZIP ZIP --> DIG["контрольная сумма"] DIG --> CI["CI: пересборка и сравнение"]
Название релиза не случайно: колофон описывает производство, а здесь предмет разговора именно производство архива — из чего он собирается и почему результат reproducible.
Ключевые изменения
Детерминированный zip
Старый архив собирался ZipArchive, и тот сам решает, что писать в метаданные контейнера. Отсюда расхождение: две сборки одного коммита на Windows и Linux имели одинаковый размер и одинаковые 987 хешей в манифесте, но разные digest — 1259cc70… и ab6c1f2d….
Теперь архив пишется вручную, с зафиксированными решениями: Unix «version made by», режим 0644 на запись, время коммита в DOS-формате по UTC, без extra-полей, без комментария, отсортированные записи, уровень сжатия 9.
flowchart LR
REL["событие release: published"] --> B1["сборка 1"]
B1 --> B2["сборка 2"]
B2 --> EQ{"байт в байт?"}
EQ -- да --> API["сверка digest с ассетом"]
EQ -- нет --> FAIL["падение задачи"]
API --> VER["verify-release на пересборке"]Настройки и симлинк
Второе исправление касается того, что происходит при обновлении. Деплой указывает .md.ini на mdscribe-data/.md.ini — единственное состояние, которое обновление не заменяет. Но rename() заменяет ссылку, а не её цель: атомарная запись приложения разворачивала раскладку наизнанку. Замер в продакшене показал, что после одного запроса страницы файл .md.ini в каталоге релиза стал обычным файлом на 1 125 байт, а в каталоге данных остался затравочный на 18 байт.
Последствие серьёзное: каждый деплой засевает новый релиз из этой заготовки, значит все настройки панели — включая хеш пароля администратора — были бы молча потеряны при следующем обновлении.
Теперь запись разыменовывает симлинк один раз и пишет в цель, атомарно, оставляя ссылку на месте. Два теста покрывают это поведение, один из них — случай висячей ссылки.
Установка и обновление
unzip MD.Scribe-3.6.2.zip -d /srv/docs
chmod -R u+w /srv/docs/storage
# docroot: /srv/docs/public
| Шаг | Что сделать |
|---|---|
| 1 | сохранить каталог данных (в раскладке релизов — mdscribe-data/) |
| 2 | распаковать новый архив рядом со старым релизом |
| 3 | проверить, что .md.ini в новом каталоге — симлинк на данные |
| 4 | переключить симлинк docroot на новый релиз |
| 5 | сбросить opcache, если он включён |
| 6 | открыть страницу и убедиться, что настройки прежние |
Шестой шаг — главный. Именно он проверяет исправление: до 3.6.2 настройки после обновления оказывались заводскими.
Проверка после обновления
flowchart TD LINK[".md.ini — симлинк на данные"] --> WR["атомарная запись настроек"] WR --> OLD["rename заменяет ссылку"] OLD --> FIX["исправление: разыменовать путь"] FIX --> OK["запись в цель, ссылка цела"]
| Проверка | Ожидаемо |
|---|---|
.md.ini в каталоге релиза |
символическая ссылка, а не файл |
| Размер файла данных | прежний, не 18 байт |
| Вход в админку | прежний пароль работает |
| Страница документации | HTTP 200, содержимое на месте |
| Манифест | версия 3.6.2, php_min 8.5.0 |
| Повторное обновление | настройки снова сохраняются |
Замеры и цифры
Проверено 14.11.2026, двумя проверками.
| Что | Значение |
|---|---|
Архив MD.Scribe-3.6.2.zip |
4 267 955 байт, SHA-256 aa6ae601…3a9f5b |
| Записей в архиве | 1 022 |
| Отсортированы по имени | да |
| Режимы файлов | все 420 (0o644) |
| Комментарий архива | пустой |
| Записей с extra-полями | 0 |
| Даты внутри архива | все 2026-11-14 (одна дата) |
| Манифест | MD.Scribe 3.6.2, php_min 8.5.0, 988 файлов |
| Страница релиза | 200 |
Файл .github/workflows/release.yml |
200, 5 809 байт |
Эти числа — прямая проверка заявленного: если архив детерминирован, в нём не должно быть ничего, что зависит от машины сборки. Отсортированные записи, единый режим 0644, отсутствие extra-полей и комментария, одна дата на весь архив — ровно тот набор, который убирает источники расхождения. Дважды одну и ту же сумму мы не получали (пересборку делает CI), но структура архива проверяема и подтверждает подход.
Отдельно про workflow: он собирает архив дважды, сверяет байты, сравнивает digest с опубликованным через API и прогоняет verify-release на пересборке. И он намеренно ничего не загружает — задача, которая могла бы заменить опубликованный ассет, сделала бы сравнение бессмысленным.
Совместимость
| Параметр | 3.5.0 | 3.6.2 |
|---|---|---|
| Минимум PHP | 8.5.0 | 8.5.0 |
| Файлов в манифесте | 979 | 988 |
| Архив | 4 199 751 байт | 4 267 955 байт |
| Сборка архива | ZipArchive |
свой zip |
| CI-пересборка релиза | нет | есть |
Минимум PHP не менялся — 8.5. Это важно, если вы обновляетесь с 3.1.0 или 3.4.x: там минимум был ниже, и на 8.3–8.4 приложение просто не запустится.
| Кто обновляется | Что учесть |
|---|---|
| с 3.6.0 и 3.6.1 | проверьте .md.ini: до 3.6.2 настройки могли уже потеряться |
| с 3.5.0 | тот же минимум PHP, меняется сборка архива |
| с 3.1.0 | нужен PHP 8.5, шаг через версии не требуется |
Подводные камни
- Настройки могли потеряться раньше. Если обновлялись на 3.6.0 или 3.6.1, проверьте
.md.ini: возможно, там заготовка. - Симлинк — часть схемы. Если распаковать архив поверх каталога, ссылку легко превратить в файл.
rename()заменяет ссылку. Это общее свойство, а не особенность приложения — теперь оно обойдено.- PHP 8.5 обязателен. На 8.4 архив не запустится, проверяйте до переключения.
- opcache держит старый байткод. После переключения релиза сбрасывайте кэш, иначе увидите старую версию.
- CI не загружает ассет. Пересборка только сравнивает: руками digest не подменяется.
- Детерминизм требует одинакового времени. Время берётся из коммита, поэтому ручная сборка «сейчас» даст другой архив.
- Режим 0644 у всех записей. Исполняемых файлов в архиве нет — для PHP это нормально.
- Свой сборщик zip — свой код. Ошибка в нём видна только сравнением сборок, поэтому CI и существует.
- Манифест описывает не всё. Состав манифеста и состав архива — разные вещи; не путайте при проверках.
- Висячая ссылка — отдельный случай. Он покрыт тестом, но при ручном деплое остаётся вашей заботой.
- Старые релизы не пересобраны. Детерминизм действует с 3.6.2, у прежних архивов digest другой.
FAQ
Что значит «воспроизводимый архив»?
Один и тот же коммит, собранный на разных машинах, даёт одинаковые байты — и, значит, одинаковую контрольную сумму.
Как это проверяется?
CI на событие публикации собирает архив дважды, сравнивает их между собой и с digest опубликованного ассета, плюс прогоняет verify-release.
Что было с настройками?
Атомарная запись заменяла симлинк .md.ini обычным файлом, и при следующем деплое настройки брались из затравочного файла на 18 байт.
Как понять, что у меня настройки уже потерялись?
Посмотреть на .md.ini в каталоге релиза: если это обычный файл на тысячу с лишним байт, а в каталоге данных — 18 байт, дело было в этом.
Можно ли проверить подлинность архива?
Да: сверьте SHA-256 с тем, что указан в релизе; после 3.6.2 эту же сумму можно получить пересборкой тега.
Нужен ли PHP 8.5?
Да, php_min в манифесте — 8.5.0.
Итог
MD.Scribe 3.6.2 — редкий случай релиза, который целиком про производство: архив стал детерминированным, а CI теперь сам доказывает это при каждой публикации. Второе исправление практичнее: настройки панели снова переживают деплой, а не превращаются в заготовку на 18 байт. Замеры подтверждают структуру: 1 022 записи, все с режимом 0644, без extra-полей и комментария, одна дата, манифест на 988 файлов с минимумом PHP 8.5. Проверка после обновления проста: .md.ini должен остаться ссылкой, а пароль — прежним.
Цитата: Воспроизводимая сборка — это когда «собери у себя и сравни» перестаёт быть просьбой поверить.
Связанные материалы
- MD Viewer: просмотрщик Markdown без браузерного движка — основной разбор платформы.
- MD.Scribe 3.5.0: Mermaid 12 и починенный ремонт — предыдущий разбор релиза.
- MD.Scribe 3.1.0: переименование и починка обновлятора — как продукт менял имя и чинил самообновление.
- ctf-collect-to-file: весь код в один документ — про контроль целостности при сборке контекста.
- Релиз v3.6.2 — заметки и архив.
LLM Summary
- Тема: MD.Scribe 3.6.2 «Colophon» — воспроизводимая сборка архива и исправление потери настроек при деплое.
- Детерминированный zip: архив пишется вручную вместо
ZipArchive: Unix «version made by», режим 0644, время коммита в DOS-формате по UTC, без extra-полей и комментария, отсортированные записи, deflate 9. До этого сборки одного коммита на Windows и Linux давали разные digest:1259cc70…иab6c1f2d…при одинаковом размере. - CI:
.github/workflows/release.yml(5 809 байт) на событиеrelease: publishedсобирает архив дважды, сравнивает байты, сверяет digest с опубликованным ассетом через API и прогоняетverify-release; ничего не загружает. - Исправление настроек: атомарная запись заменяла симлинк
.md.iniфайлом; в продакшене это дало обычный файл на 1 125 байт в каталоге релиза при заготовке 18 байт в каталоге данных — при следующем деплое все настройки, включая хеш пароля администратора, потерялись бы. Теперь путь разыменовывается и запись идёт в цель. - Проверено 14.11.2026: архив — 4 267 955 байт, SHA-256
aa6ae601cd3dc219e3ba7a600966334dd7ed0f431067e7a94b953711ac3a9f5b; 1 022 записи, отсортированы, все режимы 420 (0o644), комментарий пуст, extra-полей 0, все даты 2026-11-14; манифест —MD.Scribe3.6.2,php_min8.5.0, 988 файлов. - Живая проверка: страница релиза — 200, файл workflow — 200.
- Совместимость: минимум PHP 8.5 не изменился; файлов в манифесте 988 против 979 в 3.5.0; архив вырос с 4 199 751 до 4 267 955 байт.
- Риски: распаковка поверх каталога ломает симлинк, opcache держит старый байткод, ручная сборка даёт другую сумму, старые релизы не пересобраны.
- Не проверялось: пересборка тега на другой машине и реальный прогон CI на публикации; потеря настроек на живом стенде 3.6.0/3.6.1.
- Ссылки: https://github.com/paulmann/MD.Scribe, https://sysadministrator.ru/md-viewer/
Промт для агента (установка и настройка)
Готовый промт: скопируйте блок целиком. Источник шагов — AGENTS.md и README.md репозитория (оба файла в архиве), сверено с проверкой архива 3.6.2.
Задача: обновить MD.Scribe до 3.6.2 и убедиться, что настройки панели пережили деплой, а архив соответствует релизу.
Материалы:
- репозиторий: https://github.com/paulmann/MD.Scribe
- AGENTS.md: https://github.com/paulmann/MD.Scribe/blob/main/AGENTS.md
- README: https://github.com/paulmann/MD.Scribe/blob/main/README.md
- релиз: https://github.com/paulmann/MD.Scribe/releases/tag/v3.6.2
Шаг 1. Сверить SHA-256 скачанного архива с указанным в релизе.
Шаг 2. Проверить PHP: 8.5 (php_min в манифесте) и расширения mbstring, dom, libxml, json, zip.
Шаг 3. Сохранить каталог данных, в котором лежит .md.ini и storage.
Шаг 4. Распаковать архив в новый каталог релиза, не поверх старого.
Шаг 5. Проверить, что .md.ini в новом каталоге — симлинк на файл в каталоге данных.
Шаг 6. Переключить симлинк docroot на новый релиз.
Шаг 7. Сбросить opcache.
Шаг 8. Открыть главную и админку: пароль должен быть прежним, настройки на месте.
Шаг 9. Проверить размер файла данных в каталоге данных: он не должен быть 18 байт.
Шаг 10. Проверить манифест: версия 3.6.2, php_min 8.5.0, 988 файлов.
Шаг 11. Сделать контрольное обновление на следующий релиз и убедиться, что настройки снова сохранились.
Запрещено:
- распаковывать архив поверх каталога с симлинком .md.ini;
- обновляться на PHP ниже 8.5;
- забывать сброс opcache после переключения релиза;
- собирать архив вручную и сверять его digest с опубликованным: время в архиве берётся из коммита;
- считать «одинаковый размер» доказательством идентичности;
- доверять CI-задаче, которая загружает ассеты: проверка должна быть только на чтение.
Что считать успехом: digest совпал с релизом, .md.ini остался симлинком, пароль и настройки прежние, манифест показывает 3.6.2, контрольное обновление не потеряло настройки.