Skip to content

MD.Scribe 3.6.2 — Colophon

Choose a tag to compare

@paulmann paulmann released this 14 Nov 14:39
· 7 commits to main since this release

MD.Scribe 3.6.2 — Colophon

A colophon is the note at the end of a book that says how it was made. This release answers the same question for the archive: it is now byte-identical wherever it is built, and a CI job rebuilds every published release to say so.

Fixed

  • Settings survive a deploy again. In the release-directory layout the deploy points .md.ini at mdscribe-data/.md.ini, the only runtime state an upgrade does not replace. rename() replaces the link, not its target, so the app's atomic write turned the layout inside out. Measured in production on 2026-11-14: after one page request mdscribe.releases/3.6.1/.md.ini was a 1 125-byte regular file while the data directory kept an 18-byte seed — and every deploy seeds the new release from that seed, so each panel setting (the admin password hash included) would have been discarded silently on the next upgrade. Writes now resolve a symlinked path once and land in the target, atomically, leaving the link in place. Two tests, one of them for a dangling link.

Added

  • .github/workflows/release.yml — the published archive is rebuilt and compared. On every release: published (and on demand) it builds the tag twice, asserts both archives are byte-identical, compares the digest with the published asset through the API, and runs verify-release on the rebuilt copy. It never uploads: a job that could replace the published asset would make the comparison pointless.

Changed

  • The archive is a deterministic zip. Two builds of one commit used to differ by platform — same size, same 987 manifest hashes, digests 1259cc70… on Windows and ab6c1f2d… on Linux — because ZipArchive decides the container's metadata itself. The archive is now written by hand with every choice fixed: Unix "version made by", mode 0644 per entry, the commit time in DOS format computed in UTC, no extra fields, no comment, sorted entries, deflate level 9. Measured after the change: the same commit built on Windows and on Debian (different PHP patch level, different Composer build, different timezone) gives 6fe11549… on both; unzip -t and python's zipfile.testzip() accept it, and verify-release passes 17/17 on it.
  • COMPOSER_ROOT_VERSION is pinned to the version being built. Composer records the root package version in vendor/composer/installed.php, resolved from git — dev-main on a branch or an untagged commit, v3.6.1 at the tag. That single file moved the digest by its whole value between a build made before tagging and a rebuild made after.
  • The cost of a shorter opcache window is measured. One page request compiles 41 PHP files; a stat() of such a file costs 4.2 µs on this filesystem (2.2 µs when PHP's stat cache answers). opcache.revalidate_freq=0 would therefore add about 0.17 ms per request against a measured mean of 39 ms (n=30, p95 46 ms). The window can be shortened without touching response time. The direct A/B was not run — it needs a second pool on a host that serves 25 sites — and is recorded as not measured.
  • The production config layout was reconciled: mdscribe-data/.md.ini is the runtime state again at 1 125 bytes, mode 0640, and all five release directories point at it through their own symlinks. Backups of both files are kept on the host.

Verification

  • PHPUnit OK, but some tests were skipped! Tests: 236, Assertions: 846, Skipped: 1 — the skip is the dangling-symlink case, which Windows cannot create; CI covers it on Linux.
  • PHPStan level 6 clean; doctrine checker 12 checks, 0 errors, 0 warnings.
  • Archive MD.Scribe-3.6.2.zip, 4 267 955 bytes, sha256 aa6ae601…, 988 manifest entries, 1022 files, flat layout; verify-release on the built archive: 17 checks, 0 failed.
  • Machine API on the live 3.6.1 before this release: status reported 987 files all present, install&dry_run=1 verified the real release and would replace 0 files, and install without a dry run was refused because ALLOW_UPDATE is false.

Upgrade: replace the application tree with the archive, keep content/, storage/ and .md.ini, reset opcache. Every file is verified against the manifest embedded in the archive, and the first mismatch refuses the whole update.