mirror of
https://github.com/Anon-Planet/thgtoa.git
synced 2026-06-21 04:58:04 +02:00
Compare commits
16 Commits
v1.2.3
...
121be79cd8
| Author | SHA1 | Date | |
|---|---|---|---|
| 121be79cd8 | |||
| 3b550119a8 | |||
| c19389ce49 | |||
| aabcbac3d9 | |||
| e11a1eb1ce | |||
| df6cfbc94b | |||
| 095bb0d8be | |||
| 8b81081089 | |||
| ccc97461c9 | |||
| 8d74635d49 | |||
| f71e5e2a28 | |||
| 3e28ec19ad | |||
| 192da89138 | |||
| c658c354ee | |||
| 343ad7f037 | |||
| 85ea1fee66 |
@@ -0,0 +1,9 @@
|
|||||||
|
[tool.commitizen]
|
||||||
|
name = "cz_conventional_commits"
|
||||||
|
version_scheme = "semver"
|
||||||
|
tag_format = "v$version"
|
||||||
|
update_changelog_on_bump = false
|
||||||
|
major_version_zero = false
|
||||||
|
|
||||||
|
[tool.commitizen.customize]
|
||||||
|
schema_pattern = '^(feat|feature|add|fix|bugfix|revert|security|perf|refactor|change|chore|ci|docs|style|test|build)(\(.+\))?(!)?: .{1,72}(\n.*)*$'
|
||||||
@@ -37,9 +37,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 🛠️ Checkout (for commit metadata only)
|
- name: 🛠️ Checkout (for tags and pgp/)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
sparse-checkout: pgp
|
sparse-checkout: pgp
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -85,11 +86,10 @@ jobs:
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Read hashes for the release body
|
# Read hashes for the release body
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
- name: #️⃣ Read hashes
|
- name: "#️⃣ Read hashes"
|
||||||
id: hashes
|
id: hashes
|
||||||
run: |
|
run: |
|
||||||
read_hash() { cat "release/$1" 2>/dev/null || echo "(not built)"; }
|
read_hash() { cat "release/$1" 2>/dev/null || echo "(not built)"; }
|
||||||
|
|
||||||
echo "light_sha256=$(read_hash thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
|
echo "light_sha256=$(read_hash thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||||
echo "dark_sha256=$(read_hash thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
|
echo "dark_sha256=$(read_hash thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||||
echo "light_b2=$(read_hash thgtoa.pdf.b2)" >> $GITHUB_OUTPUT
|
echo "light_b2=$(read_hash thgtoa.pdf.b2)" >> $GITHUB_OUTPUT
|
||||||
@@ -112,22 +112,39 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
light_hash=$(cat release/thgtoa.pdf.sha256 2>/dev/null || echo "")
|
light_hash=$(cat release/thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||||
dark_hash=$(cat release/thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
dark_hash=$(cat release/thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||||
[ -n "$light_hash" ] && \
|
if [ -n "$light_hash" ]; then
|
||||||
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT || \
|
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
echo "light_vt=(not built)" >> $GITHUB_OUTPUT
|
echo "light_vt=(not built)" >> $GITHUB_OUTPUT
|
||||||
[ -n "$dark_hash" ] && \
|
fi
|
||||||
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT || \
|
if [ -n "$dark_hash" ]; then
|
||||||
|
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
echo "dark_vt=(not built)" >> $GITHUB_OUTPUT
|
echo "dark_vt=(not built)" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Tag + Release
|
# Tag + Release — auto-increment vX.Y.Z from latest semver tag
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
- name: 🏷️ Generate release tag
|
- name: 🏷️ Generate release tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
TAG="v$(date -u +'%Y.%m.%d')-$(echo ${{ github.sha }} | cut -c1-7)"
|
git fetch --tags --quiet
|
||||||
|
|
||||||
|
LATEST=$(git tag --list 'v*' --sort=-version:refname \
|
||||||
|
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||||
|
| head -1)
|
||||||
|
LATEST=${LATEST:-v0.0.0}
|
||||||
|
|
||||||
|
MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v')
|
||||||
|
MINOR=$(echo "$LATEST" | cut -d. -f2)
|
||||||
|
PATCH=$(echo "$LATEST" | cut -d. -f3)
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
|
||||||
|
TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
echo "Previous tag: $LATEST → New tag: $TAG"
|
||||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
echo "name=Release $(date -u +'%Y-%m-%d') (${TAG})" >> $GITHUB_OUTPUT
|
echo "name=$TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 🚀 Create GitHub Release
|
- name: 🚀 Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
|||||||
+1
-18
@@ -62,24 +62,7 @@ MD012:
|
|||||||
# Consecutive blank lines
|
# Consecutive blank lines
|
||||||
maximum: 1
|
maximum: 1
|
||||||
# MD013/line-length - Line length
|
# MD013/line-length - Line length
|
||||||
#
|
MD013: false
|
||||||
MD013:
|
|
||||||
# Number of characters
|
|
||||||
line_length: 80
|
|
||||||
# Number of characters for headings
|
|
||||||
heading_line_length: 80
|
|
||||||
# Number of characters for code blocks
|
|
||||||
code_block_line_length: 160
|
|
||||||
# Include code blocks
|
|
||||||
code_blocks: false
|
|
||||||
# Include tables
|
|
||||||
tables: false
|
|
||||||
# Include headings
|
|
||||||
headings: true
|
|
||||||
# Strict length checking (e.g. allow for longer URLs)
|
|
||||||
strict: false
|
|
||||||
# Stern length checking
|
|
||||||
stern: false
|
|
||||||
|
|
||||||
# MD014/commands-show-output - Dollar signs used before commands without showing output
|
# MD014/commands-show-output - Dollar signs used before commands without showing output
|
||||||
# TODO: set false for now but we should consider enabling it
|
# TODO: set false for now but we should consider enabling it
|
||||||
|
|||||||
@@ -10,14 +10,13 @@ repos:
|
|||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-symlinks
|
- id: check-symlinks
|
||||||
- id: detect-private-key
|
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
args: [--fix=lf]
|
args: [--fix=lf]
|
||||||
|
|
||||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
- repo: https://github.com/commitizen-tools/commitizen
|
||||||
rev: v0.41.0
|
rev: v4.8.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: markdownlint
|
- id: commitizen
|
||||||
- id: markdownlint-fix
|
stages: [commit-msg]
|
||||||
|
|||||||
+44
-12
@@ -20,35 +20,67 @@ Notable changes to the guide and its tooling. Follows [Keep a Changelog](https:/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [v1.2.3] — 2026-05-22
|
## [v2026.5.25]
|
||||||
|
|
||||||
CI/CD pipeline split into independent stages, dark PDF quality improved, and the changelog is now updated automatically on every release. v1.2.2 was just a placeholder, this is a minor but CI breaking change.
|
!!! Note "Meta"
|
||||||
|
|
||||||
|
- Released 2026-05-24 from [`3b55011`](https://github.com/Anon-Planet/thgtoa/commit/3b550119a8f70129096774b3303278b50fed1529)
|
||||||
|
|
||||||
|
!!! Note "Added"
|
||||||
|
|
||||||
|
- Explain missing v1.2.2 tag
|
||||||
|
|
||||||
|
!!! Note "Changed"
|
||||||
|
|
||||||
|
- Commitizen passes
|
||||||
|
- V1.2.3
|
||||||
|
- Auto-increment using [vX.X.X]
|
||||||
|
- Only use "vX.X.X" in version tags
|
||||||
|
|
||||||
|
!!! Note "Fixed"
|
||||||
|
|
||||||
|
- We use the Anonymous Planet RSK for releases
|
||||||
|
- Sign using RSK instead
|
||||||
|
- Prevent history dump and filter noise commits
|
||||||
|
- Actually save per-page PDFs for qpdf, not PNGs
|
||||||
|
|
||||||
|
## [v1.2.3]
|
||||||
|
|
||||||
|
CI/CD pipeline split into independent stages, dark PDF quality improved, release signing automated, and the changelog now updates itself on every build. Skipping v1.2.2 which was a placeholder and contained broken Python unsuitable for a tag/release.
|
||||||
|
|
||||||
!!! success "Added"
|
!!! success "Added"
|
||||||
|
|
||||||
- **Dark mode PDF** (`scripts/convert.py`): pixel-level converter replaces the broken `--prefers-color-scheme=dark` Chromium flag. Produces a 200 DPI hacker-themed PDF (`#1f1f31` background, `#e0e0e0` text, `#5e8bde` links) with batched page processing to avoid OOM.
|
- **Dark mode PDF** (`scripts/convert.py`): pixel-level converter replaces the broken `--prefers-color-scheme=dark` Chromium flag. Produces a 200 DPI hacker-themed PDF (`#1f1f31` background, `#e0e0e0` text, `#5e8bde` links) with batched page processing to avoid OOM on large documents.
|
||||||
- **Three independent CI workflows** replacing the old monolithic `build-sign-release.yml`:
|
- **Three independent CI workflows** replacing the old monolithic `build-sign-release.yml`:
|
||||||
- `build.yml` — builds PDFs and uploads them as an artifact; no secrets required.
|
- `build.yml` — builds PDFs and uploads them as an artifact; no secrets required, can be re-run freely.
|
||||||
- `sign.yml` — downloads the PDF artifact, computes SHA-256 and BLAKE2b hashes, GPG-signs all outputs, and uploads a `signatures` artifact. Can be re-run against any historical build.
|
- `sign.yml` — downloads the PDF artifact, computes SHA-256 and BLAKE2b hashes, GPG-signs all outputs, and uploads a `signatures` artifact. Can be re-run against any historical build.
|
||||||
- `release.yml` — downloads both artifacts, uploads to VirusTotal, and publishes a tagged GitHub Release with all 12 assets attached. **Can be triggered manually against any previous sign run**.
|
- `release.yml` — downloads both artifacts, uploads to VirusTotal, and publishes a tagged GitHub Release with all 12 assets attached. Can be triggered manually against any previous sign run.
|
||||||
- **`scripts/update_changelog.py`**: reads `git log` since the last version tag, categorises commits by conventional-commit prefix, and prepends a new entry here automatically after each successful build.
|
- **`scripts/update_changelog.py`**: reads `git log` since the last version tag, categorises commits by conventional-commit prefix, and prepends a new entry to this file automatically after each successful build.
|
||||||
- **`changelog.yml`** workflow: commits the auto-generated changelog entry back to `main` after every build, with `dry_run` and `manual_version` dispatch inputs for testing.
|
- **`changelog.yml`** workflow: commits the auto-generated changelog entry back to `main` after every build, with `dry_run` and `manual_version` dispatch inputs for safe local testing.
|
||||||
|
- **`scripts/tag_release.py`**: interactive guided helper for maintainers to create GPG-signed annotated tags. Checks clean tree and branch, auto-increments the version, pulls the message from the changelog, resolves the release signing key, creates and verifies the tag, then prints the push command.
|
||||||
|
- **`docs/code/develop.md`**: full developer reference covering prerequisites, local build instructions, the pipeline flow, all required GitHub Secrets, the release process, verification steps, and a troubleshooting section for every known CI failure mode.
|
||||||
|
|
||||||
!!! warning "Changed"
|
!!! warning "Changed"
|
||||||
|
|
||||||
- `build-sign-release.yml` is now deprecated — push triggers removed, manual dispatch only. Will be deleted once in-flight runs complete.
|
- `build-sign-release.yml` deprecated — push triggers removed, manual dispatch only. Will be deleted once in-flight runs complete.
|
||||||
- The full pipeline (build → sign → release) now chains automatically via `workflow_run` on every push to `main`.
|
- The full pipeline (build → sign → release → changelog) now chains automatically via `workflow_run` on every push to `main`.
|
||||||
- GPG signing uses `--pinentry-mode loopback` and `--passphrase-fd 0` to avoid interactive prompts on headless runners.
|
- GPG signing uses `--pinentry-mode loopback` and `--passphrase-fd 0` to avoid interactive prompts on headless runners.
|
||||||
- VirusTotal scans moved to the release stage so they run once per release, not once per build.
|
- VirusTotal scans moved to the release stage so they run once per release, not once per build.
|
||||||
|
- `.gitignore` updated to track `.b2` per-file hash files alongside existing `.sha256` and `.sig` entries.
|
||||||
|
- Stale information removed from the guide; deprecated ODT section in Appendix A6 commented out.
|
||||||
|
- Footer copyright information corrected.
|
||||||
|
|
||||||
!!! bug "Fixed"
|
!!! bug "Fixed"
|
||||||
|
|
||||||
- Broken internal links and a mismatched cross-reference in `docs/about/index.md`.
|
- `_save_images_as_pdf` in `convert.py` was passing raw PNG files to `qpdf --pages`, which only accepts PDF inputs. Fixed by quantizing each page to palette mode (256 colours, FASTOCTREE) and saving as a single-page PDF before merging.
|
||||||
- Deprecated ODT section commented out in Appendix A6 of the guide.
|
- `convert.py` now fails immediately with install instructions if `pdftoppm` or `qpdf` are missing, instead of crashing with an unhelpful `FileNotFoundError`.
|
||||||
|
- Pillow `KeyError: 'JPEG'` on CI resolved by installing `mkdocs-material[imaging]` and using palette-mode PDF encoding instead of RGB+JPEG.
|
||||||
|
- Orphaned footnote citations `[^536]` and `[^537]` (Australian privacy law and the Identify and Disrupt Act) restored at the key disclosure law paragraph in the guide.
|
||||||
|
- Broken internal links and mismatched cross-references throughout the guide corrected.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [v1.2.1] — 2025
|
## [v1.2.1]
|
||||||
|
|
||||||
First automated PDF build and the start of the CI pipeline.
|
First automated PDF build and the start of the CI pipeline.
|
||||||
|
|
||||||
|
|||||||
+314
-39
@@ -1,50 +1,325 @@
|
|||||||
# Development
|
# Developer Guide
|
||||||
|
|
||||||
??? Note "How the pipeline works"
|
This page covers everything you need to contribute to the project, run the build pipeline locally, configure GitHub secrets, and cut a signed release.
|
||||||
|
|
||||||
**Automatic PDF Generation:** - Builds both light and dark mode PDFs from MkDocs source
|
---
|
||||||
**SHA256 Hash Generation:** - Creates hash files for integrity verification
|
|
||||||
**GPG Signature Signing:** - Signs all PDFs and hash files with repository GPG key
|
|
||||||
**VirusTotal Scanning:** - Automatically scans PDFs and updates release notes
|
|
||||||
**Release Automation:** - Packages everything into GitHub releases
|
|
||||||
|
|
||||||
## Architecture
|
## Prerequisites
|
||||||
|
|
||||||
### Build PDF Workflow (`build-sign-release.yml`)
|
Install these before anything else.
|
||||||
|
|
||||||
!!! Note "Steps"
|
=== "Linux / macOS"
|
||||||
|
|
||||||
- Checkout repository
|
|
||||||
- Set up Python and MkDocs Material
|
|
||||||
- Install Chromium browser
|
|
||||||
- Generate both light and dark mode PDFs with `scripts\build_guide_pdf.py`
|
|
||||||
- Create SHA256 and blake2 hash files in `export/`
|
|
||||||
- Sign all files with GPG in `export/`
|
|
||||||
- Upload artifacts to GitHub Actions **manually**
|
|
||||||
|
|
||||||
### SHA256 Hash Verification
|
|
||||||
|
|
||||||
!!! Note "**How it works**"
|
|
||||||
|
|
||||||
- Each PDF gets a unique SHA256 hash calculated at build time
|
|
||||||
- Hash stored in `.sha256` files alongside the PDFs
|
|
||||||
- Combined `sha256sum.txt` for batch verification
|
|
||||||
|
|
||||||
### GPG Signature Verification
|
|
||||||
|
|
||||||
**Purpose:** Verify authenticity and prevent tampering
|
|
||||||
|
|
||||||
!!! Note "How it works"
|
|
||||||
|
|
||||||
- Detached signatures created for each PDF and hash file
|
|
||||||
- Public keys available in `/pgp/` directory
|
|
||||||
|
|
||||||
**Verification command:**
|
|
||||||
```bash
|
```bash
|
||||||
gpg --import pgp/anonymousplanet-master.asc
|
# Python 3.11+
|
||||||
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Windows"
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Python 3.11+ from https://python.org
|
||||||
|
|
||||||
|
# poppler — download from https://github.com/oschwartz10612/poppler-windows/releases
|
||||||
|
# Extract and add the bin\ folder to PATH
|
||||||
|
|
||||||
|
# qpdf — download from https://github.com/qpdf/qpdf/releases
|
||||||
|
# Extract and add the bin\ folder to PATH
|
||||||
|
|
||||||
|
# GPG — download Gpg4win from 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.github/
|
||||||
|
workflows/
|
||||||
|
build.yml ← builds PDFs, uploads artifact
|
||||||
|
sign.yml ← hashes + GPG signs, uploads signatures artifact
|
||||||
|
release.yml ← publishes GitHub Release with all assets
|
||||||
|
changelog.yml ← auto-updates 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 (gitignored except .sha256, .b2, .sig)
|
||||||
|
pgp/ ← public signing keys
|
||||||
|
scripts/
|
||||||
|
build_guide_pdf.py ← MkDocs + Chromium PDF builder
|
||||||
|
convert.py ← pixel-based dark mode PDF converter
|
||||||
|
tag_release.py ← interactive signed-tag helper for maintainers
|
||||||
|
update_changelog.py← auto-generates changelog entries from git log
|
||||||
|
setup_workflow.py ← GitHub Secrets setup assistant
|
||||||
|
verify_pdf.py ← signature verification helper
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*This workflow is designed for security-conscious users who need to verify the authenticity and integrity of downloaded documents.*
|
## Building locally
|
||||||
|
|
||||||
|
### Build both PDFs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/build_guide_pdf.py --both
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds the MkDocs site, renders it to `export/thgtoa.pdf` via headless Chromium, then calls `scripts/convert.py` to produce `export/thgtoa-dark.pdf`.
|
||||||
|
|
||||||
|
| Flag | Effect |
|
||||||
|
|------|--------|
|
||||||
|
| `--both` | Light PDF then dark PDF |
|
||||||
|
| (no flag) | Light PDF only |
|
||||||
|
| `--dark` | Dark PDF only (light PDF must already exist) |
|
||||||
|
|
||||||
|
### Build only the dark PDF from an existing light PDF
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `--dpi` | `200` | Rasterization DPI. 150 = smaller file, 300 = sharper but slow |
|
||||||
|
| `--batch-size` | `50` | Pages per batch. Reduce if you hit OOM |
|
||||||
|
| `--bg` | `1f1f31` | Background colour (hex) |
|
||||||
|
| `--text` | `e0e0e0` | Body text colour (hex) |
|
||||||
|
| `--link` | `5e8bde` | Link / blue element colour (hex) |
|
||||||
|
|
||||||
|
### Preview the MkDocs site
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdocs serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Opens at `http://127.0.0.1:8000`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pushing changes
|
||||||
|
|
||||||
|
The pipeline triggers automatically when you push to `main` — no manual steps are needed for normal contributions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
push to main
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
build.yml builds thgtoa.pdf + thgtoa-dark.pdf
|
||||||
|
│ (workflow_run on success)
|
||||||
|
▼
|
||||||
|
sign.yml SHA-256 + BLAKE2b hashes, GPG detached signatures
|
||||||
|
│ (workflow_run on success)
|
||||||
|
▼
|
||||||
|
release.yml VirusTotal scan → tagged GitHub Release
|
||||||
|
│
|
||||||
|
changelog.yml prepends new ## [vX.Y.Z] entry → commits back to main
|
||||||
|
```
|
||||||
|
|
||||||
|
Each stage runs independently and can be re-triggered manually from the Actions tab. If the build succeeds but signing fails (e.g. an expired key), you can re-run only `sign.yml` pointing at the existing build artifact without rebuilding the PDFs.
|
||||||
|
|
||||||
|
!!! warning "Before you push"
|
||||||
|
|
||||||
|
- Make sure the working tree is clean (`git status`)
|
||||||
|
- Run `mkdocs build` locally if you changed `docs/` to catch broken links before CI does
|
||||||
|
- If you added new footnotes, verify they have both a definition `[^N]:` and at least one inline citation `[^N]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GitHub Secrets
|
||||||
|
|
||||||
|
These must be configured in **Settings → Secrets and variables → Actions** before the pipeline will fully work. The build step requires no secrets; signing and releasing require all of them.
|
||||||
|
|
||||||
|
### `GPG_PRIVATE_KEY`
|
||||||
|
|
||||||
|
The ASCII-armored private key used to sign PDFs and hash files.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export the release signing key
|
||||||
|
gpg --armor --export-secret-keys C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the entire output (including `-----BEGIN PGP PRIVATE KEY BLOCK-----` and the closing line) and paste it as the secret value.
|
||||||
|
|
||||||
|
!!! danger "Key security"
|
||||||
|
This is the release signing key. Only repository admins should have access to it. Never commit it to the repository or share it outside of GitHub Secrets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GPG_PASSPHRASE`
|
||||||
|
|
||||||
|
The passphrase protecting the private key above. Must match exactly — no trailing newline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `VT_API_KEY`
|
||||||
|
|
||||||
|
A [VirusTotal](https://www.virustotal.com) API key with file upload permissions. Used by `release.yml` to scan both PDFs before publishing the release.
|
||||||
|
|
||||||
|
Get one by creating a free account at `virustotal.com` → API key under your profile. The free tier allows 4 lookups/minute and 500/day, which is sufficient for the two PDFs per release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `CHANGELOG_PAT`
|
||||||
|
|
||||||
|
A GitHub **Personal Access Token** with `contents: write` scope on this repository.
|
||||||
|
|
||||||
|
**Why it's needed:** `changelog.yml` commits back to `main` after each build. Commits made with the default `GITHUB_TOKEN` do not trigger further workflow runs (GitHub's loop-prevention policy). A PAT bypasses this so the changelog commit itself can be picked up by downstream workflows if needed.
|
||||||
|
|
||||||
|
**Creating one:**
|
||||||
|
|
||||||
|
1. Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
|
||||||
|
2. Set repository access to **Only select repositories** → this repo
|
||||||
|
3. Under Permissions → Repository permissions, set **Contents** to **Read and write**
|
||||||
|
4. Set an expiration and add it as the `CHANGELOG_PAT` secret
|
||||||
|
|
||||||
|
If this secret is absent, `changelog.yml` falls back to `GITHUB_TOKEN` — the commit still happens, it just won't trigger further workflows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Secrets summary
|
||||||
|
|
||||||
|
| Secret | Required by | What happens if missing |
|
||||||
|
|--------|------------|------------------------|
|
||||||
|
| `GPG_PRIVATE_KEY` | `sign.yml` | Signing step fails — no `.sig` files produced |
|
||||||
|
| `GPG_PASSPHRASE` | `sign.yml` | GPG import succeeds but signing fails |
|
||||||
|
| `VT_API_KEY` | `release.yml` | VirusTotal step fails — release is not published |
|
||||||
|
| `CHANGELOG_PAT` | `changelog.yml` | Falls back to `GITHUB_TOKEN` — changelog still updates, but commit won't trigger downstream workflows |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cutting a release
|
||||||
|
|
||||||
|
Releases are tagged manually by maintainers. The `tag_release.py` script handles everything interactively.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Your GPG keyring must contain the release signing key (`C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2`)
|
||||||
|
- The working tree must be clean
|
||||||
|
- You must be on the `main` branch
|
||||||
|
- A `## [vX.Y.Z]` entry must exist in `docs/changelog/index.md` for the version you are tagging
|
||||||
|
|
||||||
|
### Import the release key (first time only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --import pgp/anonymousplanet-release.asc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the release tagger
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/tag_release.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
|
||||||
|
1. Check the working tree is clean and you are on `main`
|
||||||
|
2. Detect the latest tag and propose the next patch version
|
||||||
|
3. Pull the matching changelog entry and format it as the tag message
|
||||||
|
4. Show you the full tag message for review
|
||||||
|
5. Ask for confirmation before creating anything
|
||||||
|
6. Create a GPG-signed annotated tag with `git tag -s`
|
||||||
|
7. Verify the signature
|
||||||
|
8. Print the push command
|
||||||
|
|
||||||
|
To specify a version explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/tag_release.py --version v1.2.4
|
||||||
|
```
|
||||||
|
|
||||||
|
To preview without creating the tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/tag_release.py --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a different signing key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/tag_release.py --key <fingerprint>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push the tag
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin v1.2.4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trigger the release workflow
|
||||||
|
|
||||||
|
Pushing a tag does **not** automatically trigger `release.yml` (it listens to `workflow_run` from `sign.yml`, not tag pushes). After pushing the tag, go to **Actions → Release → Run workflow** and paste the most recent `sign.yml` run ID to publish the GitHub Release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verifying a release
|
||||||
|
|
||||||
|
Anyone can verify the authenticity of a release download.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import the release signing key
|
||||||
|
gpg --import pgp/anonymousplanet-release.asc
|
||||||
|
|
||||||
|
# Verify the PDFs
|
||||||
|
gpg --verify thgtoa.pdf.sig thgtoa.pdf
|
||||||
|
gpg --verify thgtoa-dark.pdf.sig thgtoa-dark.pdf
|
||||||
|
|
||||||
|
# Verify the hash files themselves
|
||||||
|
gpg --verify sha256sums.txt.sig sha256sums.txt
|
||||||
|
gpg --verify b2sums.txt.sig b2sums.txt
|
||||||
|
|
||||||
|
# Check the PDF hashes match
|
||||||
|
sha256sum -c sha256sums.txt
|
||||||
|
b2sum -c b2sums.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
A successful verify looks like:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg: Signature made ...
|
||||||
|
gpg: Good signature from "Anonymous Planet (Release) ..."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**`cairosvg` missing during MkDocs build**
|
||||||
|
Install the imaging extras: `pip install "mkdocs-material[imaging]"`. This is required by the `social` plugin.
|
||||||
|
|
||||||
|
**`KeyError: 'JPEG'` in convert.py**
|
||||||
|
Pillow needs libjpeg for RGB→PDF encoding. The script works around this by quantizing to palette mode before saving, so this error should not appear with the current code. If it does, reinstall Pillow after installing libjpeg: `sudo apt install libjpeg-dev && pip install --force-reinstall pillow`.
|
||||||
|
|
||||||
|
**`qpdf: can't find PDF header`**
|
||||||
|
An older version of `convert.py` tried to pass PNG files to qpdf. Make sure you are running the current version — qpdf only accepts PDF inputs to `--pages`.
|
||||||
|
|
||||||
|
**GPG signing fails on CI with `No secret key`**
|
||||||
|
The `GPG_PRIVATE_KEY` secret is missing or malformed. Re-export with `gpg --armor --export-secret-keys <fingerprint>` and paste the full block including the header and footer lines.
|
||||||
|
|
||||||
|
**GPG signing fails with `Bad passphrase`**
|
||||||
|
The `GPG_PASSPHRASE` secret has a trailing space or newline. Paste it again carefully with no surrounding whitespace.
|
||||||
|
|
||||||
|
**`release.yml` fails on VirusTotal**
|
||||||
|
The `VT_API_KEY` is missing, invalid, or over the rate limit (500 requests/day on the free tier). Check the secret and re-run the workflow after a few minutes.
|
||||||
|
|
||||||
|
**Footnote warnings from MkDocs (`link '#fnref:N' has no anchor`)**
|
||||||
|
A footnote definition `[^N]:` exists without a matching inline citation `[^N]` in the body text. Add the citation where it belongs in the guide, or remove the orphaned definition.
|
||||||
|
|||||||
+2
-13
@@ -8583,7 +8583,7 @@ It is recommended that you learn about the common ways people mess up OPSEC <htt
|
|||||||
|
|
||||||
- Contact a lawyer if possible and hope for the best and if you cannot contact one (yet), **try to remain silent (if your country allows it) until you have a lawyer to help you and if your law allows you to remain silent.**
|
- Contact a lawyer if possible and hope for the best and if you cannot contact one (yet), **try to remain silent (if your country allows it) until you have a lawyer to help you and if your law allows you to remain silent.**
|
||||||
|
|
||||||
Keep in mind that many countries have specific laws to compel you to reveal your passwords that could override your "right to remain silent". See this Wikipedia article: <https://en.wikipedia.org/wiki/Key_disclosure_law> <sup>[[Wikiless]](https://wikiless.com/wiki/Key_disclosure_law)</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Key_disclosure_law)</sup> and this other visual resource with law references <https://www.gp-digital.org/world-map-of-encryption/> <sup>[[Archive.org]](https://web.archive.org/web/https://www.gp-digital.org/world-map-of-encryption/)</sup>.
|
Keep in mind that many countries have specific laws to compel you to reveal your passwords that could override your "right to remain silent". See this Wikipedia article: <https://en.wikipedia.org/wiki/Key_disclosure_law> <sup>[[Wikiless]](https://wikiless.com/wiki/Key_disclosure_law)</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Key_disclosure_law)</sup> and this other visual resource with law references <https://www.gp-digital.org/world-map-of-encryption/> <sup>[[Archive.org]](https://web.archive.org/web/https://www.gp-digital.org/world-map-of-encryption/)</sup>. Australia in particular has broad privacy laws[^536] and passed the Surveillance Legislation Amendment (Identify and Disrupt) Act 2021[^537], which grants authorities powers to modify, add, copy, and delete data on a suspect's devices and accounts.
|
||||||
|
|
||||||
## A small final editorial note
|
## A small final editorial note
|
||||||
|
|
||||||
@@ -10043,28 +10043,17 @@ Again, regarding the PDFs of this guide and as explained in the README of my rep
|
|||||||
|
|
||||||
- Run "python pdfid.py file-to-check.pdf" and you should see these at 0 in the case of the PDF files in this repository:
|
- Run "python pdfid.py file-to-check.pdf" and you should see these at 0 in the case of the PDF files in this repository:
|
||||||
|
|
||||||
```
|
```text
|
||||||
|
|
||||||
/JS 0 #This indicates the presence of Javascript
|
/JS 0 #This indicates the presence of Javascript
|
||||||
|
|
||||||
/JavaScript 0 #This indicates the presence of Javascript
|
/JavaScript 0 #This indicates the presence of Javascript
|
||||||
|
|
||||||
/AA 0 #This indicates the presence of automatic action on opening
|
/AA 0 #This indicates the presence of automatic action on opening
|
||||||
|
|
||||||
/OpenAction 0 #This indicates the presence of automatic action on opening
|
/OpenAction 0 #This indicates the presence of automatic action on opening
|
||||||
|
|
||||||
/AcroForm 0 #This indicates the presence of AcroForm which could contain JavaScript
|
/AcroForm 0 #This indicates the presence of AcroForm which could contain JavaScript
|
||||||
|
|
||||||
/JBIG2Decode 0 #This indicates the use of JBIG2 compression which could be used for obfuscating content
|
/JBIG2Decode 0 #This indicates the use of JBIG2 compression which could be used for obfuscating content
|
||||||
|
|
||||||
/RichMedia 0 #This indicates the presence of rich media within the PDF such as Flash
|
/RichMedia 0 #This indicates the presence of rich media within the PDF such as Flash
|
||||||
|
|
||||||
/Launch 0 #This counts the launch actions
|
/Launch 0 #This counts the launch actions
|
||||||
|
|
||||||
/EmbeddedFile 0 #This indicates there are embedded files within the PDF
|
/EmbeddedFile 0 #This indicates there are embedded files within the PDF
|
||||||
|
|
||||||
/XFA 0 #This indicates the presence of XML Forms within the PDF
|
/XFA 0 #This indicates the presence of XML Forms within the PDF
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, what if you think the PDF is still suspicious? Fear not ... there are more things you can do to ensure it is not malicious:
|
Now, what if you think the PDF is still suspicious? Fear not ... there are more things you can do to ensure it is not malicious:
|
||||||
|
|||||||
+11
-2
@@ -36,12 +36,14 @@ python scripts/verify_pdf.py --vt
|
|||||||
#### 1. Verify SHA256 Hash
|
#### 1. Verify SHA256 Hash
|
||||||
|
|
||||||
**Linux/macOS:**
|
**Linux/macOS:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/repo
|
cd /path/to/repo
|
||||||
sha256sum -c sha256sum-light.txt
|
sha256sum -c sha256sum-light.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows (PowerShell):**
|
**Windows (PowerShell):**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-FileHash -Algorithm SHA256 export\thgtoa.pdf | Select-Object Hash
|
Get-FileHash -Algorithm SHA256 export\thgtoa.pdf | Select-Object Hash
|
||||||
# Compare with the hash in thgtoa.pdf.sha256
|
# Compare with the hash in thgtoa.pdf.sha256
|
||||||
@@ -50,18 +52,21 @@ Get-FileHash -Algorithm SHA256 export\thgtoa.pdf | Select-Object Hash
|
|||||||
#### 2. Verify GPG Signature
|
#### 2. Verify GPG Signature
|
||||||
|
|
||||||
First, import the public key:
|
First, import the public key:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gpg --import pgp/anonymousplanet-master.asc
|
gpg --import pgp/anonymousplanet-master.asc
|
||||||
```
|
```
|
||||||
|
|
||||||
Then verify the signature:
|
Then verify the signature:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
|
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
|
||||||
gpg --verify export/thgtoa-dark.pdf.sig export/thgtoa-dark.pdf
|
gpg --verify export/thgtoa-dark.pdf.sig export/thgtoa-dark.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected output for successful verification:
|
Expected output for successful verification:
|
||||||
```
|
|
||||||
|
```text
|
||||||
gpg: Signature made Mon 20 Apr 2026 01:46:40 AM EDT
|
gpg: Signature made Mon 20 Apr 2026 01:46:40 AM EDT
|
||||||
gpg: using EDDSA key 9FA5436D0EE360985157382517ECA05F768DEDF6
|
gpg: using EDDSA key 9FA5436D0EE360985157382517ECA05F768DEDF6
|
||||||
gpg: Good signature from "Anonymous Planet Master Signing Key" [unknown]
|
gpg: Good signature from "Anonymous Planet Master Signing Key" [unknown]
|
||||||
@@ -77,6 +82,7 @@ Visit the VirusTotal report links (automatically generated in release notes):
|
|||||||
- Dark mode: `https://www.virustotal.com/gui/file/[hash]`
|
- Dark mode: `https://www.virustotal.com/gui/file/[hash]`
|
||||||
|
|
||||||
Or use the Python script with API key:
|
Or use the Python script with API key:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export VT_API_KEY=your_vt_api_key
|
export VT_API_KEY=your_vt_api_key
|
||||||
python scripts/verify_pdf.py --vt
|
python scripts/verify_pdf.py --vt
|
||||||
@@ -103,15 +109,18 @@ The GitHub Actions workflows automatically:
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### "Good signature" but wrong owner?
|
### "Good signature" but wrong owner?
|
||||||
|
|
||||||
- Ensure you imported the correct public key
|
- Ensure you imported the correct public key
|
||||||
- Check the key fingerprint matches the official one from the repository
|
- Check the key fingerprint matches the official one from the repository
|
||||||
|
|
||||||
### Hash mismatch?
|
### Hash mismatch?
|
||||||
|
|
||||||
- Re-download the file (corruption during transfer)
|
- Re-download the file (corruption during transfer)
|
||||||
- Verify you're checking against the correct hash file
|
- Verify you're checking against the correct hash file
|
||||||
- Check for disk errors on your system
|
- Check for disk errors on your system
|
||||||
|
|
||||||
### GPG not found?
|
### GPG not found?
|
||||||
|
|
||||||
- Install GPG: `sudo apt install gnupg` (Debian/Ubuntu) or `brew install gnupg` (macOS)
|
- Install GPG: `sudo apt install gnupg` (Debian/Ubuntu) or `brew install gnupg` (macOS)
|
||||||
- On Windows, use [Gpg4win](https://www.gpg4win.org/)
|
- On Windows, use [Gpg4win](https://www.gpg4win.org/)
|
||||||
|
|
||||||
@@ -123,4 +132,4 @@ The GitHub Actions workflows automatically:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*For questions or issues with verification, please open an issue on GitHub.*
|
_For questions or issues with verification, please open an issue on GitHub._
|
||||||
|
|||||||
+37
-10
@@ -93,22 +93,28 @@ def apply_dark_theme(
|
|||||||
|
|
||||||
|
|
||||||
def _save_images_as_pdf(images: list, output_path: str) -> None:
|
def _save_images_as_pdf(images: list, output_path: str) -> None:
|
||||||
"""Save a list of RGB PIL images as a PDF using PNG compression via qpdf.
|
"""Save a list of RGB PIL images as a PDF without requiring libjpeg.
|
||||||
|
|
||||||
Pillow's built-in PDF writer defaults to JPEG encoding for RGB images,
|
Pillow's PDF writer defaults to JPEG encoding for RGB images, which
|
||||||
which fails when libjpeg is not available in the environment. Instead we
|
fails when libjpeg is absent in the environment. Fix: quantize each
|
||||||
write each page as a lossless PNG to a temp directory and assemble them
|
image to palette mode (256 colours, FASTOCTREE) so Pillow uses
|
||||||
with qpdf, which embeds the PNGs directly without re-encoding.
|
zlib/deflate instead of JPEG, save each as an individual single-page
|
||||||
|
PDF, then merge the page PDFs with qpdf.
|
||||||
|
|
||||||
|
Colour fidelity is preserved — the hacker theme uses only a handful
|
||||||
|
of distinct colours so 256-colour quantization is visually lossless.
|
||||||
"""
|
"""
|
||||||
import tempfile as _tempfile
|
import tempfile as _tempfile
|
||||||
with _tempfile.TemporaryDirectory() as staging:
|
with _tempfile.TemporaryDirectory() as staging:
|
||||||
png_paths = []
|
page_pdfs = []
|
||||||
for i, img in enumerate(images):
|
for i, img in enumerate(images):
|
||||||
p = os.path.join(staging, f'p{i:05d}.png')
|
page_path = os.path.join(staging, f'p{i:05d}.pdf')
|
||||||
img.save(p, format='PNG')
|
img.quantize(colors=256, method=Image.Quantize.FASTOCTREE).save(
|
||||||
png_paths.append(p)
|
page_path, format='PDF'
|
||||||
|
)
|
||||||
|
page_pdfs.append(page_path)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
['qpdf', '--empty', '--pages'] + png_paths + ['--', output_path],
|
['qpdf', '--empty', '--pages'] + page_pdfs + ['--', output_path],
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,6 +125,25 @@ def _check_qpdf() -> bool:
|
|||||||
).returncode == 0
|
).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _check_dependencies() -> None:
|
||||||
|
"""Verify required system tools are available before doing any work."""
|
||||||
|
missing = []
|
||||||
|
for tool in ('pdftoppm', 'qpdf'):
|
||||||
|
if subprocess.run(['which', tool], capture_output=True).returncode != 0:
|
||||||
|
missing.append(tool)
|
||||||
|
if missing:
|
||||||
|
tools = ', '.join(missing)
|
||||||
|
instructions = (
|
||||||
|
f"Install with:\n"
|
||||||
|
f" Linux/WSL: sudo apt install poppler-utils qpdf\n"
|
||||||
|
f" macOS: brew install poppler qpdf\n"
|
||||||
|
f" Windows: see docs/code/develop.md"
|
||||||
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Missing required system tool(s): {tools}\n{instructions}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def convert_pdf_to_dark(
|
def convert_pdf_to_dark(
|
||||||
input_path: str | Path,
|
input_path: str | Path,
|
||||||
output_path: str | Path,
|
output_path: str | Path,
|
||||||
@@ -138,6 +163,8 @@ def convert_pdf_to_dark(
|
|||||||
input_path = str(input_path)
|
input_path = str(input_path)
|
||||||
output_path = str(output_path)
|
output_path = str(output_path)
|
||||||
|
|
||||||
|
_check_dependencies()
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
# 1. Rasterize all pages
|
# 1. Rasterize all pages
|
||||||
prefix = os.path.join(tmp, 'page')
|
prefix = os.path.join(tmp, 'page')
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# Default release signing key fingerprint.
|
# Default release signing key fingerprint.
|
||||||
# Maintainers with a different key can pass --key on the CLI.
|
# Maintainers with a different key can pass --key on the CLI.
|
||||||
DEFAULT_SIGNING_KEY = "9FA5436D0EE360985157382517ECA05F768DEDF6"
|
DEFAULT_SIGNING_KEY = "C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2"
|
||||||
|
|
||||||
CHANGELOG = Path(__file__).resolve().parent.parent / "docs" / "changelog" / "index.md"
|
CHANGELOG = Path(__file__).resolve().parent.parent / "docs" / "changelog" / "index.md"
|
||||||
|
|
||||||
|
|||||||
@@ -84,11 +84,25 @@ def version_from_changelog() -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def commits_since(ref: str | None, until: str) -> list[str]:
|
def commits_since(ref: str | None, until: str) -> list[str]:
|
||||||
"""Return one-line commit messages between ref and until (exclusive/inclusive)."""
|
"""Return one-line commit messages between ref and until (exclusive/inclusive).
|
||||||
|
|
||||||
|
When no ref is given (no prior tag exists) we fall back to the merge-base
|
||||||
|
between HEAD and origin/main rather than walking the entire history, which
|
||||||
|
would otherwise dump every commit ever made into the changelog.
|
||||||
|
"""
|
||||||
if ref:
|
if ref:
|
||||||
log_range = f"{ref}..{until}"
|
log_range = f"{ref}..{until}"
|
||||||
else:
|
else:
|
||||||
log_range = until
|
# No previous tag — scope to commits not yet on origin/main
|
||||||
|
merge_base = run(
|
||||||
|
["git", "merge-base", "HEAD", "origin/main"], check=False
|
||||||
|
).stdout.strip()
|
||||||
|
if merge_base:
|
||||||
|
log_range = f"{merge_base}..{until}"
|
||||||
|
else:
|
||||||
|
# Truly brand new repo with no remote — limit to last 50 commits
|
||||||
|
# to avoid dumping the whole history
|
||||||
|
log_range = f"-50 {until}"
|
||||||
out = run(["git", "log", "--pretty=format:%s", log_range])
|
out = run(["git", "log", "--pretty=format:%s", log_range])
|
||||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||||
|
|
||||||
@@ -97,9 +111,30 @@ def categorise(messages: list[str]) -> dict[str, list[str]]:
|
|||||||
"""Sort commit messages into Added / Changed / Fixed buckets."""
|
"""Sort commit messages into Added / Changed / Fixed buckets."""
|
||||||
buckets: dict[str, list[str]] = {b: [] for b in BUCKET_ORDER}
|
buckets: dict[str, list[str]] = {b: [] for b in BUCKET_ORDER}
|
||||||
|
|
||||||
|
# Patterns that are never useful in a human-readable changelog
|
||||||
|
NOISE = re.compile(
|
||||||
|
r"""
|
||||||
|
\[skip\ ci\] # CI skip marker
|
||||||
|
| ^Merge\ (pull\ request|branch) # merge commits
|
||||||
|
| ^chore:\ bump # version bump chores
|
||||||
|
| update\ changelog # self-referential
|
||||||
|
| ^\d+/\d+ # numbered commit series (e.g. 3/8)
|
||||||
|
| ^Tweaking # vague WIP messages
|
||||||
|
| ^Moving\ some # vague WIP messages
|
||||||
|
| \ pt\d+$ # "...pt2", "...pt3" suffixes
|
||||||
|
| ^Fix\ (workflow|path|README)$ # one-word infrastructure fixes
|
||||||
|
| ^Still\ broken # embarrassing mid-fix notes
|
||||||
|
| ^WIP\b # work in progress
|
||||||
|
| ^Forgot\ to # oops commits
|
||||||
|
| ^Revert\ " # reverts (surface the original instead)
|
||||||
|
| ^One\ job\ to\ rule # joke commit messages
|
||||||
|
""",
|
||||||
|
re.VERBOSE | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
# Skip automated / noise commits
|
# Skip noise
|
||||||
if re.search(r"\[skip ci\]|^Merge |^chore: bump|update changelog", msg, re.I):
|
if NOISE.search(msg):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Strip conventional-commit prefix to get the plain description
|
# Strip conventional-commit prefix to get the plain description
|
||||||
|
|||||||
Reference in New Issue
Block a user