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.

Open an Issue Release Pipeline

## Setup

Install these before anything else.

Linux / macOSWindows

    # 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.