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

md scribe 3 6 2 cover

TL;DR: три вещи, которые надо знать

  1. Архив стал детерминированным. Две сборки одного коммита раньше отличались по платформе, теперь совпадают байт в байт.
  2. CI пересобирает релиз. Задача на событие публикации собирает архив дважды, сверяет между собой и с опубликованным digest.
  3. Настройки снова переживают деплой. Атомарная запись разыменовывает симлинк, а не заменяет его файлом.
  4. Манифест: 988 записей, версия 3.6.2, минимум PHP 8.5.
  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 байт
MD.Scribe 3.6.2: детерминированный архив и его свойства

Эти числа — прямая проверка заявленного: если архив детерминирован, в нём не должно быть ничего, что зависит от машины сборки. Отсортированные записи, единый режим 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 должен остаться ссылкой, а пароль — прежним.

Цитата: Воспроизводимая сборка — это когда «собери у себя и сравни» перестаёт быть просьбой поверить.

Связанные материалы

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.Scribe 3.6.2, php_min 8.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, контрольное обновление не потеряло настройки.