ContributeHelp us improve the Hitchhiker's Guide..
Contributions range from fixing a typo to writing entire new sections.
Setupλ
Install these before anything else.
# Python 3.11+ from https://python.org
# poppler: https://github.com/oschwartz10612/poppler-windows/releases
# Extract and add the bin\ folder to PATH
# qpdf: https://github.com/qpdf/qpdf/releases
# Extract and add the bin\ folder to PATH
# GPG: https://gpg4win.org
# Python dependencies
pip install "mkdocs-material[imaging]" pillow numpy
You also need Google Chrome or Microsoft Edge installed for the light-mode PDF build (headless Chromium).
Repository Layoutλ
.github/
workflows/
01-build.yml # builds PDFs, uploads artifact
02-sign.yml # hashes + GPG signs, uploads signatures artifact
03-release.yml # publishes GitHub Release with all assets
04-changelog.yml # prepends a new entry to docs/changelog/index.md
publish.yml # deploys MkDocs site to GitHub Pages
docs/
guide/index.md # the guide (single Markdown file)
changelog/ # release notes
code/ # this page
export/ # PDF output (PDFs gitignored; .sha256, .b2sum, .asc tracked)
pgp/ # public signing keys
scripts/
build_guide_pdf.py # MkDocs + Chromium PDF builder
convert.py # pixel-based dark mode PDF converter
install_fonts.py # install fonts locally
update_changelog.py # auto-generates changelog entries from git log
setup_workflow.py # GitHub Secrets setup assistant
verify_pdf.py # signature verification helper
archived/
tag_release.py # ARCHIVED — GPG tag helper (not used in current flow)
Building Locallyλ
Builds the MkDocs site, renders it to export/thgtoa.pdf via headless Chromium, then produces export/thgtoa-dark.pdf.
| Flag | Effect |
|---|---|
--both |
Light PDF then dark PDF |
| (none) | Light PDF only |
--dark |
Dark PDF only (light PDF must already exist) |
Build only the dark PDF from an existing light:
| Flag | Default | Description |
|---|---|---|
--dpi |
200 |
Rasterization DPI |
--batch-size |
50 |
Pages per batch — reduce if OOM |
--bg |
1f1f31 |
Background colour (hex) |
--text |
e0e0e0 |
Body text colour (hex) |
--link |
5e8bde |
Link colour (hex) |
Preview the site:
Contributing Contentλ
Use a Topic Branch
Never commit directly to main. Use a topic branch per change so PRs stay reviewable and independent.
Small PRs
Split large changes into multiple PRs — one for new content, one for fixes, one for style. Big PRs block merges and create review debt.
Conventional Commits
All commits must follow <type>(<scope>): <description> format. Enforced by the commitizen pre-commit hook.
Describe Your Changes
Never leave a PR description blank. Include what changed, why, and any context a reviewer needs. Link related issues.
Commit Typesλ
| Type | Changelog bucket |
|---|---|
feat, feature, add |
Added |
fix, bugfix, revert, security |
Fixed |
perf, refactor, change, chore, ci, docs, style, test, build |
Changed |
Examples:
feat: add dark-mode PDF export
fix(scripts): handle locked PDF on Windows
docs: update developer workflow guide
chore(ci): pin Chrome version to 120
Rulesλ
- Do target PRs at the
mainbranch - Do write "WIP" or open a draft PR for unfinished work
- Do follow the 50/72 rule for commit messages
- Don't make mass search/replace changes without context-checking every instance
- Don't abandon a PR mid-review — stay responsive
- Don't modify a PR directly while it's under active review — submit changes to the review branch instead
For an example of what not to do, see PR #51.
Before you push
- Make sure the working tree is clean (
git status) - Run
mkdocs buildlocally if you changeddocs/to catch broken links - If you added footnotes, verify each has both a definition
[^N]:and at least one inline citation[^N]
Release Pipelineλ
The pipeline is fully manual after the initial build — no step triggers the next automatically. This prevents version mismatches between what was built, what was signed, and what gets released.
push to main (or manual trigger)
│
▼
01-build.yml
Builds thgtoa.pdf + thgtoa-dark.pdf.
Uploads artifact: pdfs — note the run ID.
│
│ manually trigger 02-sign.yml with the build run ID
▼
02-sign.yml
Hashes (SHA-256 + BLAKE2b) and GPG-signs all files.
Commits export/ back to main.
Uploads: signatures, pdfs-signed — note the run ID.
│
│ manually trigger 03-release.yml with the sign run ID
▼
03-release.yml
Runs VirusTotal. Creates GitHub Release tagged release-YYYYMMDD-<short-sha>.
│
│ manually trigger 04-changelog.yml with the version string
▼
04-changelog.yml
Prepends a new ## [vX.Y.Z] entry to docs/changelog/index.md and commits.
Release Tagsλ
Tags use the format release-YYYYMMDD-<short-sha>, e.g. release-20260527-abc1234. No version decision is needed at release time — the tag is always unique and traceable to the exact commit.
The version string (e.g. v1.2.4) is a separate, human-assigned label that lives only in the changelog.
Triggering Each Stepλ
Build: Push to main or go to Actions → Build PDFs → Run workflow. Note the run ID.
Sign: Actions → Sign PDFs → Run workflow, enter the build run ID. Note the run ID.
Release: Actions → Release → Run workflow, enter the sign run ID.
Changelog: Actions → Update Changelog → Run workflow, enter the version string. Use dry_run: true to preview.
Verifying a Releaseλ
# Import the release signing key
gpg --import pgp/anonymousplanet.asc
# Verify the PDFs
gpg --verify thgtoa.pdf.asc thgtoa.pdf
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
# Verify the hash files
gpg --verify sha256sums.txt.asc sha256sums.txt
gpg --verify b2sums.txt.asc b2sums.txt
# Check the PDF hashes match
sha256sum -c sha256sums.txt
b2sum -c b2sums.txt
Expected output:
gpg: Signature made Sun 31 May 2026 03:23:26 AM EDT
gpg: using EDDSA key C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
gpg: Good signature from "Anonymous Planet Release Signing Key" [ultimate]
Primary key fingerprint: C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2
You can safely ignore GitHub/Codeberg warnings like "The email in this signature doesn't match the committer email."
Troubleshootingλ
cairosvg missing during MkDocs build
pip install "mkdocs-material[imaging]" — required by the social plugin.
KeyError: 'JPEG' in convert.py
sudo apt install libjpeg-dev && pip install --force-reinstall pillow
qpdf: can't find PDF header
qpdf only accepts PDF inputs — ensure you are on the current version of convert.py.
GPG signing fails — No secret key
Re-export: gpg --armor --export-secret-keys <fingerprint> and re-paste the full block including headers into the GPG_PRIVATE_KEY secret.
GPG signing fails — Bad passphrase
The GPG_PASSPHRASE secret has a trailing space or newline. Re-paste without surrounding whitespace.
03-release.yml fails on VirusTotal
VT_API_KEY is missing, invalid, or over the 500 req/day free-tier limit. Re-run after a few minutes.
02-sign.yml fails downloading PDF artifact
Wrong build_run_id, or the artifact expired (90-day retention). Trigger a new build.
Changelog already contains version X
update_changelog.py errors if the version is already present. Choose the next version string.
Footnote warnings — link '#fnref:N' has no anchor
A definition [^N]: exists without a matching inline citation. Add the citation or remove the orphaned definition.