Skip to content
Open source. Every PR matters.

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+
python3 --version

# poppler (pdftoppm) and qpdf
sudo apt install poppler-utils qpdf # Debian/Ubuntu
brew install poppler qpdf           # macOS

# GPG
sudo apt install gnupg # Debian/Ubuntu
brew install gnupg     # macOS

# Python dependencies
pip install "mkdocs-material[imaging]" pillow numpy
# 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λ

python scripts/build_guide_pdf.py --both

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:

python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
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:

mkdocs serve
# Opens at http://127.0.0.1:8000

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 main branch
  • 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 build locally if you changed docs/ 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.