Write the handbook, and make it fail the build when it goes stale

README says what plMail is and gets you running; CONTRIBUTING says how to
change it. Nothing covered the part in between — using it, operating it,
and registering the Google and Microsoft applications it talks to. That
is docs/: 34 pages across features, installation, providers and
internals, mirrored to the GitHub wiki.

The wiki is a mirror and never a source. bin/mirror-wiki.php flattens the
tree into the wiki's flat namespace, rewrites every relative link,
generates the sidebar from the index so the two cannot disagree, and
stamps each page with a footer saying where it came from. A browser edit
survives until the next push to main. That cost is deliberate:
documentation not versioned with the code drifts from it, and the drift
is invisible until somebody follows an instruction that stopped being
true.

Which is the whole problem, so the checkable half is checked.
DocumentationCoverageTest fails the build when a command is missing from
the reference or documented and gone, when a variable in .env is absent
from the configuration page, when a relative link dangles, when a page is
unreachable from the index or promised and missing, or when a page has no
"Things that bite" section. It cannot check whether a paragraph is TRUE,
and says so — this is the floor, so review attention goes to the part a
machine cannot have an opinion about. CONTRIBUTING has claimed since it
was written that the command table "is part of the definition of done";
nothing enforced it until now, and it was wrong in three places.

Writing it found things. They are fixed here rather than documented
around, because a handbook describing a bug accurately is still a
handbook nobody can follow.

  compose.yaml            mounted no volume for var/attachments, var/raw
                          or var/uploads. Workers write those during sync
                          and the web container serves them back, so each
                          kept its own copy in its own layer — downloads
                          404, and everything died on a recreate. The
                          override and the TrueNAS file both mount them;
                          the file the README tells an operator to run did
                          not. The same omission cost real data once
                          before. See the CHANGELOG for moving what is
                          currently stranded.

  app:calendar:materialise  new, nightly. Occurrences are drawn when an
                          event is saved and nothing moved that window
                          again, so a weekly standup ran out of dates in
                          two years and its reminders stopped with them —
                          silently. The query it runs had existed since
                          the materialiser did, documented as exactly this
                          sweep, and had never been called.

  Microsoft wizard        asked for three of the nine permissions plMail
                          requests, and a token already issued is not
                          upgraded when the rest are added later.

  GmailWatchService       told you to grant Pub/Sub Publisher to
                          gmail-api@system.gserviceaccount.com. The
                          account is gmail-api-push@; a topic granted
                          exactly what the docblock asked for never
                          delivered a notification.

  IcsExporter             said it wrote no VALARM. It has written them
                          since alerts landed.

  CLIENT_DEVELOPMENT.md   said there was no JMAP calendar API and that
                          SearchSnippet/get was absent. Both shipped.

  CONTRIBUTING            said only app:calendar:push registers a push
                          channel; subscribing has done it since the
                          subscribe flow learned to.

  Three docblocks         named getters removed long ago, or described a
                          method two below the one they sat on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5 committed 2026-08-05 21:13:25 +02:00
1 parent b0a1d07977
commit 4d103cfbdf
53 files changed
+9258 -62

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
name: Wiki
# The wiki is a mirror of docs/, never a source. Authoring happens in the code
# repository so that a change and the paragraph describing it land in one commit
# and one review; this job exists only to make the published copy match.
#
# Consequently an edit made in the browser survives until the next push to main
# and no longer. Every generated page carries a footer saying so, because a
# contributor who does not know it will lose work.
on:
push:
branches: [main]
paths:
- "docs/**"
- "bin/mirror-wiki.php"
- ".github/workflows/wiki.yml"
# So the mirror can be re-run after the wiki has been initialised, without
# having to touch a page to trigger it.
workflow_dispatch:
# Only what pushing to the wiki repository needs. The wiki lives under the same
# contents scope as the code, which is why no personal access token is involved.
permissions:
contents: write
# A second push landing mid-run would race this one to the same repository, and
# the loser force-fails rather than merging — the mirror is a wholesale
# overwrite, so there is nothing to merge.
concurrency:
group: wiki
cancel-in-progress: false
jobs:
mirror:
name: Publish docs/ to the wiki
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
coverage: none
# Before touching the wiki at all: a dangling link is a bug in the
# documentation, and publishing it is worse than not publishing.
# DocumentationCoverageTest asserts the same thing in the main
# suite; this is the gate for the rendered output specifically,
# where relative paths have been rewritten and can break differently.
- name: Check the render
run: php bin/mirror-wiki.php --check
- name: Clone the wiki
id: clone
run: |
if git clone "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.wiki.git" wiki; then
echo "cloned=true" >> "$GITHUB_OUTPUT"
else
echo "cloned=false" >> "$GITHUB_OUTPUT"
echo "::warning::The wiki repository does not exist yet. Open the repository's Wiki tab and save any page once — GitHub creates the repository on first use and offers no API for it — then re-run this workflow."
fi
# Deleted first, so a page removed from docs/ is removed from the
# wiki too. Without this a renamed page leaves its old self behind
# for ever, still indexed and still wrong.
- name: Render
if: steps.clone.outputs.cloned == 'true'
run: |
find wiki -maxdepth 1 -name '*.md' -delete
php bin/mirror-wiki.php wiki
- name: Push
if: steps.clone.outputs.cloned == 'true'
working-directory: wiki
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
# No commit means docs/ changed in a way that renders
# identically — a code-block tweak inside a fenced example,
# say. Not a failure.
if git diff --cached --quiet; then
echo "The wiki already matches docs/."
exit 0
fi
git commit -m "Mirror docs/ from ${GITHUB_SHA::7}"
git push