mirror of
https://github.com/Anon-Planet/thgtoa.git
synced 2026-06-21 04:58:04 +02:00
Compare commits
20 Commits
v1.2.3
..
d1817e9049
| Author | SHA1 | Date | |
|---|---|---|---|
| d1817e9049 | |||
| ede2a53437 | |||
| 91a77ed552 | |||
| 1c3cf75cf0 | |||
| 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"
|
||||||
|
|
||||||
|
# enforce sign-off below as well
|
||||||
|
extra_arguments = ["-S", "--signoff"]
|
||||||
|
|
||||||
|
# harmless redundancy
|
||||||
|
[tool.commitizen.customize]
|
||||||
|
schema_pattern = '^(feat|add|fix|bugfix|revert|security|perf|refactor|change|chore|ci|docs|style|test|build)(\(.+\))?(!)?: .{1,72}(\n.*)*\nSigned-off-by: .+ <.+@.+>'
|
||||||
@@ -1,218 +1,25 @@
|
|||||||
|
name: 🗑️ DEPRECATED — Build & Sign & Release (combined)
|
||||||
|
|
||||||
# DEPRECATED — replaced by build.yml, sign.yml, and release.yml
|
# DEPRECATED — replaced by build.yml, sign.yml, and release.yml
|
||||||
# This file is kept temporarily so in-flight runs are not broken.
|
# This workflow is disabled. It is kept only as a reference until the
|
||||||
#
|
# split workflows have been confirmed stable in production.
|
||||||
# name: 📖 Build & Sign PDFs
|
# Do not trigger this workflow.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: # manual only — no automatic triggers (deprecated)
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
permissions:
|
_disabled:
|
||||||
contents: write
|
description: 'This workflow is deprecated. Use build.yml → sign.yml → release.yml instead.'
|
||||||
id-token: write
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-sign-release:
|
noop:
|
||||||
name: Build, Sign & Release PDFs
|
name: Deprecated — no-op
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 🛠️ Checkout
|
- name: ❌ Workflow is deprecated
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: 🐍 Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.13"
|
|
||||||
|
|
||||||
- name: 📦 Install Python dependencies
|
|
||||||
run: pip install mkdocs-material pillow numpy
|
|
||||||
|
|
||||||
- name: 🖼️ Install poppler (pdftoppm) and qpdf
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
echo "This workflow is deprecated."
|
||||||
sudo apt-get install -y poppler-utils qpdf
|
echo "Use build.yml → sign.yml → release.yml instead."
|
||||||
|
exit 1
|
||||||
- name: Setup Chrome
|
|
||||||
uses: browser-actions/setup-chrome@v2
|
|
||||||
with:
|
|
||||||
chrome-version: 120
|
|
||||||
install-dependencies: true
|
|
||||||
install-chromedriver: true
|
|
||||||
|
|
||||||
- name: 🔑 Install GPG tools
|
|
||||||
run: sudo apt-get install -y gnupg
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Build PDFs
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
- name: 🖨️ Build PDFs
|
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
run: python scripts/build_guide_pdf.py --${{ inputs.build_mode || 'both' }}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Hash (SHA-256 + BLAKE2b)
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
- name: #️⃣ Hash PDFs
|
|
||||||
id: hashes
|
|
||||||
run: |
|
|
||||||
mkdir -p export
|
|
||||||
|
|
||||||
sha256sum export/thgtoa.pdf | awk '{print $1}' > export/thgtoa.pdf.sha256
|
|
||||||
sha256sum export/thgtoa-dark.pdf | awk '{print $1}' > export/thgtoa-dark.pdf.sha256
|
|
||||||
b2sum export/thgtoa.pdf | awk '{print $1}' > export/thgtoa.pdf.b2
|
|
||||||
b2sum export/thgtoa-dark.pdf | awk '{print $1}' > export/thgtoa-dark.pdf.b2
|
|
||||||
|
|
||||||
# Also write combined human-readable files
|
|
||||||
sha256sum export/thgtoa.pdf export/thgtoa-dark.pdf > export/sha256sums.txt
|
|
||||||
b2sum export/thgtoa.pdf export/thgtoa-dark.pdf > export/b2sums.txt
|
|
||||||
|
|
||||||
# Expose hashes as step outputs for the release body
|
|
||||||
echo "light_sha256=$(cat export/thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
|
|
||||||
echo "dark_sha256=$(cat export/thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
|
|
||||||
echo "light_b2=$(cat export/thgtoa.pdf.b2)" >> $GITHUB_OUTPUT
|
|
||||||
echo "dark_b2=$(cat export/thgtoa-dark.pdf.b2)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# GPG sign (detached .sig for each PDF + each hash file)
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
- name: 🔏 Import GPG key
|
|
||||||
env:
|
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
||||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
|
||||||
run: |
|
|
||||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
|
||||||
# Pre-cache the passphrase so signing doesn't prompt
|
|
||||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
|
||||||
--pinentry-mode loopback --list-secret-keys
|
|
||||||
|
|
||||||
- name: 🔏 GPG sign PDFs and hash files
|
|
||||||
env:
|
|
||||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
|
||||||
run: |
|
|
||||||
sign() {
|
|
||||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
|
||||||
--pinentry-mode loopback \
|
|
||||||
--detach-sign --armor --output "${1}.sig" "$1"
|
|
||||||
}
|
|
||||||
sign export/thgtoa.pdf
|
|
||||||
sign export/thgtoa-dark.pdf
|
|
||||||
sign export/sha256sums.txt
|
|
||||||
sign export/b2sums.txt
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# VirusTotal
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
- name: 🦠 Upload PDFs to VirusTotal
|
|
||||||
id: vt
|
|
||||||
uses: crazy-max/ghaction-virustotal@v5
|
|
||||||
with:
|
|
||||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
|
||||||
files: |
|
|
||||||
export/thgtoa.pdf
|
|
||||||
export/thgtoa-dark.pdf
|
|
||||||
|
|
||||||
- name: 🔗 Build VT report URLs
|
|
||||||
id: vt_urls
|
|
||||||
run: |
|
|
||||||
light_hash=$(cat export/thgtoa.pdf.sha256)
|
|
||||||
dark_hash=$(cat export/thgtoa-dark.pdf.sha256)
|
|
||||||
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT
|
|
||||||
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Create GitHub Release
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
- name: 🏷️ Generate release tag
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
TAG="release-$(date -u +'%Y%m%d-%H%M%S')"
|
|
||||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
|
||||||
echo "name=Release $(date -u +'%Y-%m-%d %H:%M UTC')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: 🚀 Create GitHub Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.tag.outputs.tag }}
|
|
||||||
name: ${{ steps.tag.outputs.name }}
|
|
||||||
body: |
|
|
||||||
## 📖 The Hitchhiker's Guide to Online Anonymity
|
|
||||||
|
|
||||||
Built from commit ${{ github.sha }} on `${{ github.ref_name }}`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📄 Files
|
|
||||||
|
|
||||||
| File | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `thgtoa.pdf` | Light mode PDF |
|
|
||||||
| `thgtoa-dark.pdf` | Dark mode PDF (hacker theme) |
|
|
||||||
| `sha256sums.txt` | SHA-256 checksums |
|
|
||||||
| `b2sums.txt` | BLAKE2b checksums |
|
|
||||||
| `*.sig` | GPG detached signatures (ASCII armor) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### #️⃣ Hashes
|
|
||||||
|
|
||||||
#### thgtoa.pdf (Light)
|
|
||||||
```
|
|
||||||
SHA-256: ${{ steps.hashes.outputs.light_sha256 }}
|
|
||||||
BLAKE2b: ${{ steps.hashes.outputs.light_b2 }}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### thgtoa-dark.pdf (Dark)
|
|
||||||
```
|
|
||||||
SHA-256: ${{ steps.hashes.outputs.dark_sha256 }}
|
|
||||||
BLAKE2b: ${{ steps.hashes.outputs.dark_b2 }}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔏 GPG Signatures
|
|
||||||
|
|
||||||
Detached signatures (`.sig`) are included in the release assets.
|
|
||||||
Verify with:
|
|
||||||
```bash
|
|
||||||
gpg --verify thgtoa.pdf.sig thgtoa.pdf
|
|
||||||
gpg --verify thgtoa-dark.pdf.sig thgtoa-dark.pdf
|
|
||||||
```
|
|
||||||
The signing key is published at `pgp/anonymousplanet-release.asc`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🦠 VirusTotal Scans
|
|
||||||
|
|
||||||
| File | Report |
|
|
||||||
|------|--------|
|
|
||||||
| `thgtoa.pdf` | ${{ steps.vt_urls.outputs.light_vt }} |
|
|
||||||
| `thgtoa-dark.pdf` | ${{ steps.vt_urls.outputs.dark_vt }} |
|
|
||||||
|
|
||||||
files: |
|
|
||||||
export/thgtoa.pdf
|
|
||||||
export/thgtoa-dark.pdf
|
|
||||||
export/sha256sums.txt
|
|
||||||
export/b2sums.txt
|
|
||||||
export/thgtoa.pdf.sha256
|
|
||||||
export/thgtoa-dark.pdf.sha256
|
|
||||||
export/thgtoa.pdf.b2
|
|
||||||
export/thgtoa-dark.pdf.b2
|
|
||||||
export/thgtoa.pdf.sig
|
|
||||||
export/thgtoa-dark.pdf.sig
|
|
||||||
export/sha256sums.txt.sig
|
|
||||||
export/b2sums.txt.sig
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
fail_on_unmatched_files: true
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Upload everything as a workflow artifact (90-day archive)
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
- name: 📤 Upload export as workflow artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: pdf-release-${{ steps.tag.outputs.tag }}
|
|
||||||
path: export/*
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 90
|
|
||||||
compression-level: 0
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
# 1. Push to main → build.yml runs automatically → note the run ID
|
||||||
|
# 2. Manually trigger sign.yml with that build run ID → note the sign run ID
|
||||||
|
# 3. Manually trigger release.yml with: version=v1.2.5, sign_run_id=<id>
|
||||||
|
# 4. Manually trigger changelog.yml with: version=v1.2.5
|
||||||
|
|
||||||
name: 📖 Build PDFs
|
name: 📖 Build PDFs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
name: 📝 Update Changelog
|
name: 📝 Update Changelog
|
||||||
|
|
||||||
# Runs after build.yml completes on main — at that point we know what changed.
|
# Manual only — run after a release is published. Provide the exact version
|
||||||
# Can also be triggered manually to backfill a missing entry.
|
# string (e.g. v1.2.4) to prepend to the changelog. Version is required to
|
||||||
|
# prevent silent auto-increment drift from release tags.
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
|
||||||
workflows: ["📖 Build PDFs"]
|
|
||||||
types: [completed]
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Version string (e.g. v1.2.4) — leave blank to auto-increment'
|
description: 'Version string to record (e.g. v1.2.4) — required'
|
||||||
required: false
|
required: true
|
||||||
type: string
|
type: string
|
||||||
dry_run:
|
dry_run:
|
||||||
description: 'Dry run — print entry without committing'
|
description: 'Dry run — print entry without committing'
|
||||||
@@ -25,9 +22,6 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
changelog:
|
changelog:
|
||||||
name: Prepend changelog entry
|
name: Prepend changelog entry
|
||||||
if: >
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
github.event.workflow_run.conclusion == 'success'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -46,7 +40,7 @@ jobs:
|
|||||||
- name: 📝 Generate and prepend changelog entry
|
- name: 📝 Generate and prepend changelog entry
|
||||||
env:
|
env:
|
||||||
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||||
MANUAL_VERSION: ${{ inputs.version || '' }}
|
MANUAL_VERSION: ${{ inputs.version }}
|
||||||
GH_SHA: ${{ github.sha }}
|
GH_SHA: ${{ github.sha }}
|
||||||
GH_REF: ${{ github.ref_name }}
|
GH_REF: ${{ github.ref_name }}
|
||||||
TRIGGERING_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
TRIGGERING_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
name: 🚀 Release
|
name: 🚀 Release
|
||||||
|
|
||||||
# Can be triggered:
|
# Manual only — run this deliberately after build and sign are confirmed good.
|
||||||
# 1. Automatically after sign.yml completes on main
|
# Provide the sign.yml run ID to pull artifacts from. The release tag is
|
||||||
# 2. Manually, pointing at specific build/sign runs to pull artifacts from
|
# generated automatically as release-YYYYMMDD-<short-sha> — no version input
|
||||||
|
# needed, no semver drift possible.
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
|
||||||
workflows: ["🔏 Sign PDFs"]
|
|
||||||
types: [completed]
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
sign_run_id:
|
sign_run_id:
|
||||||
description: 'sign.yml run ID to pull signatures from'
|
description: 'sign.yml run ID to pull signatures and PDFs from'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
build_run_id:
|
|
||||||
description: 'build.yml run ID to pull PDFs from (leave blank to use pdfs-signed from sign run)'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
prerelease:
|
prerelease:
|
||||||
description: 'Mark as pre-release?'
|
description: 'Mark as pre-release?'
|
||||||
required: false
|
required: false
|
||||||
@@ -31,52 +24,32 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Publish GitHub Release
|
name: Publish GitHub Release
|
||||||
if: >
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
github.event.workflow_run.conclusion == 'success'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 🛠️ Checkout (for commit metadata only)
|
- name: 🛠️ Checkout (for pgp/)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
sparse-checkout: pgp
|
sparse-checkout: pgp
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Resolve which run IDs to pull artifacts from
|
# Download artifacts from the specified sign run
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
- name: 🔍 Resolve run IDs
|
|
||||||
id: runs
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
SIGN_RUN="${{ inputs.sign_run_id }}"
|
|
||||||
BUILD_RUN="${{ inputs.build_run_id }}"
|
|
||||||
else
|
|
||||||
SIGN_RUN="${{ github.event.workflow_run.id }}"
|
|
||||||
BUILD_RUN=""
|
|
||||||
fi
|
|
||||||
echo "sign_run=$SIGN_RUN" >> $GITHUB_OUTPUT
|
|
||||||
echo "build_run=$BUILD_RUN" >> $GITHUB_OUTPUT
|
|
||||||
echo "Sign run: $SIGN_RUN"
|
|
||||||
echo "Build run: ${BUILD_RUN:-'(using pdfs-signed from sign run)'}"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Download artifacts
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
- name: 📥 Download signatures artifact
|
- name: 📥 Download signatures artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signatures
|
name: signatures
|
||||||
path: release/
|
path: release/
|
||||||
run-id: ${{ steps.runs.outputs.sign_run }}
|
run-id: ${{ inputs.sign_run_id }}
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: 📥 Download PDFs (from sign run)
|
- name: 📥 Download signed PDFs artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pdfs-signed
|
name: pdfs-signed
|
||||||
path: release/
|
path: release/
|
||||||
run-id: ${{ steps.runs.outputs.sign_run }}
|
run-id: ${{ inputs.sign_run_id }}
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: 📋 List release assets
|
- name: 📋 List release assets
|
||||||
@@ -85,18 +58,17 @@ 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.b2sum)" >> $GITHUB_OUTPUT
|
||||||
echo "dark_b2=$(read_hash thgtoa-dark.pdf.b2)" >> $GITHUB_OUTPUT
|
echo "dark_b2=$(read_hash thgtoa-dark.pdf.b2sum)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# VirusTotal — upload whichever PDFs are present
|
# VirusTotal
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
- name: 🦠 Upload PDFs to VirusTotal
|
- name: 🦠 Upload PDFs to VirusTotal
|
||||||
id: vt
|
id: vt
|
||||||
@@ -112,23 +84,34 @@ 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
|
# Generate release tag — timestamp + short SHA, always unique
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
- 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)"
|
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||||
|
DATE=$(date -u +'%Y%m%d')
|
||||||
|
TAG="release-${DATE}-${SHORT_SHA}"
|
||||||
|
NAME="Release ${DATE} (${SHORT_SHA})"
|
||||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
echo "name=Release $(date -u +'%Y-%m-%d') (${TAG})" >> $GITHUB_OUTPUT
|
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||||
|
echo "Tag: $TAG"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Create GitHub Release
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
- name: 🚀 Create GitHub Release
|
- name: 🚀 Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
@@ -154,22 +137,22 @@ jobs:
|
|||||||
| `b2sums.txt` | BLAKE2b checksums (both files) |
|
| `b2sums.txt` | BLAKE2b checksums (both files) |
|
||||||
| `thgtoa.pdf.sha256` | SHA-256 — light PDF |
|
| `thgtoa.pdf.sha256` | SHA-256 — light PDF |
|
||||||
| `thgtoa-dark.pdf.sha256` | SHA-256 — dark PDF |
|
| `thgtoa-dark.pdf.sha256` | SHA-256 — dark PDF |
|
||||||
| `thgtoa.pdf.b2` | BLAKE2b — light PDF |
|
| `thgtoa.pdf.b2sum` | BLAKE2b — light PDF |
|
||||||
| `thgtoa-dark.pdf.b2` | BLAKE2b — dark PDF |
|
| `thgtoa-dark.pdf.b2sum` | BLAKE2b — dark PDF |
|
||||||
| `*.sig` | GPG detached signatures (ASCII armor) |
|
| `*.asc` | GPG detached signatures (ASCII armor) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### #️⃣ Hashes
|
### #️⃣ Hashes
|
||||||
|
|
||||||
**thgtoa.pdf** (light)
|
**thgtoa.pdf** (light)
|
||||||
```
|
```text
|
||||||
SHA-256 ${{ steps.hashes.outputs.light_sha256 }}
|
SHA-256 ${{ steps.hashes.outputs.light_sha256 }}
|
||||||
BLAKE2b ${{ steps.hashes.outputs.light_b2 }}
|
BLAKE2b ${{ steps.hashes.outputs.light_b2 }}
|
||||||
```
|
```
|
||||||
|
|
||||||
**thgtoa-dark.pdf** (dark)
|
**thgtoa-dark.pdf** (dark)
|
||||||
```
|
```text
|
||||||
SHA-256 ${{ steps.hashes.outputs.dark_sha256 }}
|
SHA-256 ${{ steps.hashes.outputs.dark_sha256 }}
|
||||||
BLAKE2b ${{ steps.hashes.outputs.dark_b2 }}
|
BLAKE2b ${{ steps.hashes.outputs.dark_b2 }}
|
||||||
```
|
```
|
||||||
@@ -183,12 +166,12 @@ jobs:
|
|||||||
gpg --import pgp/anonymousplanet-release.asc
|
gpg --import pgp/anonymousplanet-release.asc
|
||||||
|
|
||||||
# Verify PDFs
|
# Verify PDFs
|
||||||
gpg --verify thgtoa.pdf.sig thgtoa.pdf
|
gpg --verify thgtoa.pdf.asc thgtoa.pdf
|
||||||
gpg --verify thgtoa-dark.pdf.sig thgtoa-dark.pdf
|
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
|
||||||
|
|
||||||
# Verify hash files
|
# Verify hash files
|
||||||
gpg --verify sha256sums.txt.sig sha256sums.txt
|
gpg --verify sha256sums.txt.asc sha256sums.txt
|
||||||
gpg --verify b2sums.txt.sig b2sums.txt
|
gpg --verify b2sums.txt.asc b2sums.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -207,9 +190,9 @@ jobs:
|
|||||||
release/b2sums.txt
|
release/b2sums.txt
|
||||||
release/thgtoa.pdf.sha256
|
release/thgtoa.pdf.sha256
|
||||||
release/thgtoa-dark.pdf.sha256
|
release/thgtoa-dark.pdf.sha256
|
||||||
release/thgtoa.pdf.b2
|
release/thgtoa.pdf.b2sum
|
||||||
release/thgtoa-dark.pdf.b2
|
release/thgtoa-dark.pdf.b2sum
|
||||||
release/thgtoa.pdf.sig
|
release/thgtoa.pdf.asc
|
||||||
release/thgtoa-dark.pdf.sig
|
release/thgtoa-dark.pdf.asc
|
||||||
release/sha256sums.txt.sig
|
release/sha256sums.txt.asc
|
||||||
release/b2sums.txt.sig
|
release/b2sums.txt.asc
|
||||||
|
|||||||
+67
-44
@@ -4,28 +4,28 @@ name: 🔏 Sign PDFs
|
|||||||
# 1. Automatically after build.yml completes on main
|
# 1. Automatically after build.yml completes on main
|
||||||
# 2. Manually, pointing at a specific build run to pull PDFs from
|
# 2. Manually, pointing at a specific build run to pull PDFs from
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
# workflow_run:
|
||||||
workflows: ["📖 Build PDFs"]
|
# workflows: ["📖 Build PDFs"]
|
||||||
types: [completed]
|
# types: [completed]
|
||||||
branches: [main]
|
# branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
build_run_id:
|
build_run_id:
|
||||||
description: 'build.yml run ID to download PDFs from (leave blank for latest)'
|
description: 'build.yml run ID to download PDFs from'
|
||||||
required: false
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: read # download artifacts from other runs
|
actions: read # download artifacts from other runs
|
||||||
contents: read
|
contents: write # needed to commit export/ files back to the repo
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sign:
|
sign:
|
||||||
name: Hash & Sign PDFs
|
name: Hash & Sign PDFs
|
||||||
# On workflow_run, only proceed if the build actually succeeded
|
# On workflow_run, only proceed if the build actually succeeded
|
||||||
if: >
|
# if: >
|
||||||
github.event_name == 'workflow_dispatch' ||
|
# github.event_name == 'workflow_dispatch' ||
|
||||||
github.event.workflow_run.conclusion == 'success'
|
# github.event.workflow_run.conclusion == 'success'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
light_sha256: ${{ steps.hashes.outputs.light_sha256 }}
|
light_sha256: ${{ steps.hashes.outputs.light_sha256 }}
|
||||||
@@ -39,36 +39,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
sparse-checkout: pgp
|
sparse-checkout: pgp
|
||||||
|
|
||||||
- name: 🔑 Install GPG
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y gnupg
|
|
||||||
|
|
||||||
# Download PDFs from the triggering build run, or a manually specified one
|
|
||||||
- name: 📥 Resolve source run ID
|
|
||||||
id: src
|
|
||||||
run: |
|
|
||||||
if [ -n "${{ inputs.build_run_id }}" ]; then
|
|
||||||
echo "run_id=${{ inputs.build_run_id }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "run_id=${{ github.event.workflow_run.id }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 📥 Download PDF artifacts
|
- name: 📥 Download PDF artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pdfs
|
name: pdfs
|
||||||
path: export/
|
path: export/
|
||||||
run-id: ${{ steps.src.outputs.run_id }}
|
run-id: ${{ inputs.build_run_id }}
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: 📋 List downloaded files
|
- name: 📋 List downloaded files
|
||||||
run: ls -lh export/
|
run: ls -lh export/
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Hash
|
# Hash — extensions match export/ conventions: .sha256, .b2sum
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
- name: #️⃣ Hash PDFs
|
- name: "#️⃣ Hash PDFs"
|
||||||
id: hashes
|
id: hashes
|
||||||
run: |
|
run: |
|
||||||
cd export
|
cd export
|
||||||
@@ -76,20 +61,19 @@ jobs:
|
|||||||
for f in thgtoa.pdf thgtoa-dark.pdf; do
|
for f in thgtoa.pdf thgtoa-dark.pdf; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||||
b2sum "$f" | awk '{print $1}' > "${f}.b2"
|
b2sum "$f" | awk '{print $1}' > "${f}.b2sum"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Combined files (only include files that exist)
|
# Combined summary files
|
||||||
sha256sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > sha256sums.txt || \
|
sha256sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > sha256sums.txt || \
|
||||||
sha256sum thgtoa.pdf 2>/dev/null > sha256sums.txt
|
sha256sum thgtoa.pdf 2>/dev/null > sha256sums.txt
|
||||||
b2sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > b2sums.txt || \
|
b2sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > b2sums.txt || \
|
||||||
b2sum thgtoa.pdf 2>/dev/null > b2sums.txt
|
b2sum thgtoa.pdf 2>/dev/null > b2sums.txt
|
||||||
|
|
||||||
# Expose individual hashes as outputs (empty string if file absent)
|
|
||||||
light_sha256=$(cat thgtoa.pdf.sha256 2>/dev/null || echo "")
|
light_sha256=$(cat thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||||
dark_sha256=$(cat thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
dark_sha256=$(cat thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||||
light_b2=$(cat thgtoa.pdf.b2 2>/dev/null || echo "")
|
light_b2=$(cat thgtoa.pdf.b2sum 2>/dev/null || echo "")
|
||||||
dark_b2=$(cat thgtoa-dark.pdf.b2 2>/dev/null || echo "")
|
dark_b2=$(cat thgtoa-dark.pdf.b2sum 2>/dev/null || echo "")
|
||||||
|
|
||||||
echo "light_sha256=$light_sha256" >> $GITHUB_OUTPUT
|
echo "light_sha256=$light_sha256" >> $GITHUB_OUTPUT
|
||||||
echo "dark_sha256=$dark_sha256" >> $GITHUB_OUTPUT
|
echo "dark_sha256=$dark_sha256" >> $GITHUB_OUTPUT
|
||||||
@@ -102,15 +86,19 @@ jobs:
|
|||||||
cat b2sums.txt
|
cat b2sums.txt
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# GPG sign
|
# GPG sign — detached ASCII-armor signatures use .asc extension
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
- name: 🔑 Install GPG
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y gnupg
|
||||||
|
|
||||||
- name: 🔏 Import GPG signing key
|
- name: 🔏 Import GPG signing key
|
||||||
env:
|
env:
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
run: |
|
run: |
|
||||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
# Pre-cache passphrase to prevent interactive prompt
|
|
||||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||||
--pinentry-mode loopback --list-secret-keys
|
--pinentry-mode loopback --list-secret-keys
|
||||||
|
|
||||||
@@ -123,8 +111,8 @@ jobs:
|
|||||||
[ -f "$file" ] || return 0
|
[ -f "$file" ] || return 0
|
||||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||||
--pinentry-mode loopback \
|
--pinentry-mode loopback \
|
||||||
--detach-sign --armor --output "${file}.sig" "$file"
|
--detach-sign --armor --output "${file}.asc" "$file"
|
||||||
echo "Signed: $file"
|
echo "Signed: $file → ${file}.asc"
|
||||||
}
|
}
|
||||||
sign export/thgtoa.pdf
|
sign export/thgtoa.pdf
|
||||||
sign export/thgtoa-dark.pdf
|
sign export/thgtoa-dark.pdf
|
||||||
@@ -132,7 +120,42 @@ jobs:
|
|||||||
sign export/b2sums.txt
|
sign export/b2sums.txt
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Upload — PDFs + all signatures and hashes together
|
# Commit export/ back to main
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
- name: 📦 Checkout full repo for commit
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
path: repo
|
||||||
|
|
||||||
|
- name: 📂 Copy export files into repo
|
||||||
|
run: cp -v export/* repo/export/
|
||||||
|
|
||||||
|
- name: 🔏 Configure SSH commit signing
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.ACTIONS_SSH_SIGNING_KEY }}" > ~/.ssh/signing_key
|
||||||
|
chmod 600 ~/.ssh/signing_key
|
||||||
|
git config --global gpg.format ssh
|
||||||
|
git config --global user.signingKey ~/.ssh/signing_key
|
||||||
|
git config --global commit.gpgSign true
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: 📤 Commit and push export/ to main
|
||||||
|
working-directory: repo
|
||||||
|
run: |
|
||||||
|
git add export/
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "Nothing to commit — export/ is already up to date."
|
||||||
|
else
|
||||||
|
git commit -S -m "chore(export): update PDFs, hashes and signatures [skip ci]"
|
||||||
|
git push origin main
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Upload artifacts for release.yml to consume
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
- name: 📤 Upload signatures artifact
|
- name: 📤 Upload signatures artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -143,12 +166,12 @@ jobs:
|
|||||||
export/b2sums.txt
|
export/b2sums.txt
|
||||||
export/thgtoa.pdf.sha256
|
export/thgtoa.pdf.sha256
|
||||||
export/thgtoa-dark.pdf.sha256
|
export/thgtoa-dark.pdf.sha256
|
||||||
export/thgtoa.pdf.b2
|
export/thgtoa.pdf.b2sum
|
||||||
export/thgtoa-dark.pdf.b2
|
export/thgtoa-dark.pdf.b2sum
|
||||||
export/thgtoa.pdf.sig
|
export/thgtoa.pdf.asc
|
||||||
export/thgtoa-dark.pdf.sig
|
export/thgtoa-dark.pdf.asc
|
||||||
export/sha256sums.txt.sig
|
export/sha256sums.txt.asc
|
||||||
export/b2sums.txt.sig
|
export/b2sums.txt.asc
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 90
|
retention-days: 90
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|||||||
+4
-4
@@ -21,9 +21,9 @@ _site/
|
|||||||
_site_test/
|
_site_test/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
# Export directory - but track hash files and signatures
|
# Export directory — track only hashes and signatures, not the PDFs themselves
|
||||||
export/thgtoa.pdf.sha256
|
export/thgtoa.pdf.sha256
|
||||||
export/thgtoa-dark.pdf.sha256
|
export/thgtoa-dark.pdf.sha256
|
||||||
export/thgtoa.pdf.b2
|
export/thgtoa.pdf.b2sum
|
||||||
export/thgtoa-dark.pdf.b2
|
export/thgtoa-dark.pdf.b2sum
|
||||||
*.sig
|
export/*.asc
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+12
-5
@@ -10,14 +10,21 @@ 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]
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: require-commit-body
|
||||||
|
name: Avoid bad commits
|
||||||
|
language: script
|
||||||
|
entry: scripts/hooks/require-commit-body.sh
|
||||||
|
stages: [commit-msg]
|
||||||
|
|||||||
+20
-12
@@ -20,35 +20,43 @@ Notable changes to the guide and its tooling. Follows [Keep a Changelog](https:/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [v1.2.3] — 2026-05-22
|
## [v1.2.3]
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
+369
-32
@@ -1,50 +1,387 @@
|
|||||||
# 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 publish a 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
|
```bash
|
||||||
- Set up Python and MkDocs Material
|
# Python 3.11+
|
||||||
- Install Chromium browser
|
python3 --version
|
||||||
- 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
|
# poppler (pdftoppm) and qpdf
|
||||||
|
sudo apt install poppler-utils qpdf # Debian / Ubuntu
|
||||||
|
brew install poppler qpdf # macOS
|
||||||
|
|
||||||
!!! Note "**How it works**"
|
# GPG
|
||||||
|
sudo apt install gnupg # Debian / Ubuntu
|
||||||
|
brew install gnupg # macOS
|
||||||
|
|
||||||
- Each PDF gets a unique SHA256 hash calculated at build time
|
# Python dependencies
|
||||||
- Hash stored in `.sha256` files alongside the PDFs
|
pip install "mkdocs-material[imaging]" pillow numpy
|
||||||
- Combined `sha256sum.txt` for batch verification
|
```
|
||||||
|
|
||||||
### GPG Signature Verification
|
=== "Windows"
|
||||||
|
|
||||||
**Purpose:** Verify authenticity and prevent tampering
|
```powershell
|
||||||
|
# Python 3.11+ from https://python.org
|
||||||
|
|
||||||
!!! Note "How it works"
|
# poppler — download from https://github.com/oschwartz10612/poppler-windows/releases
|
||||||
|
# Extract and add the bin\ folder to PATH
|
||||||
|
|
||||||
- Detached signatures created for each PDF and hash file
|
# qpdf — download from https://github.com/qpdf/qpdf/releases
|
||||||
- Public keys available in `/pgp/` directory
|
# Extract and add the bin\ folder to PATH
|
||||||
|
|
||||||
**Verification command:**
|
# GPG — download Gpg4win from https://gpg4win.org
|
||||||
```bash
|
|
||||||
gpg --import pgp/anonymousplanet-master.asc
|
# Python dependencies
|
||||||
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
|
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/
|
||||||
|
build.yml ← builds PDFs, uploads artifact
|
||||||
|
sign.yml ← hashes + GPG signs, uploads signatures artifact
|
||||||
|
release.yml ← publishes GitHub Release with all assets
|
||||||
|
changelog.yml ← prepends a new entry to docs/changelog/index.md
|
||||||
|
publish.yml ← deploys MkDocs site to GitHub Pages
|
||||||
|
build-sign-release.yml← DEPRECATED — fails on trigger, kept for reference
|
||||||
|
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
|
||||||
|
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD pipeline overview
|
||||||
|
|
||||||
|
The pipeline is fully manual after the initial build — no step automatically triggers the next. This prevents version mismatches between what was built, what was signed, and what gets released.
|
||||||
|
|
||||||
|
```
|
||||||
|
push to main (or manual trigger)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
build.yml
|
||||||
|
Builds thgtoa.pdf + thgtoa-dark.pdf.
|
||||||
|
Uploads artifact: pdfs
|
||||||
|
Note the run ID.
|
||||||
|
│
|
||||||
|
│ ← manually trigger sign.yml with the build run ID
|
||||||
|
▼
|
||||||
|
sign.yml
|
||||||
|
Downloads pdfs artifact. Hashes (SHA-256 + BLAKE2b) and GPG-signs
|
||||||
|
all files. Commits export/ back to main. Uploads artifacts:
|
||||||
|
signatures, pdfs-signed
|
||||||
|
Note the run ID.
|
||||||
|
│
|
||||||
|
│ ← manually trigger release.yml with the sign run ID
|
||||||
|
▼
|
||||||
|
release.yml
|
||||||
|
Downloads signatures + pdfs-signed artifacts. Runs VirusTotal.
|
||||||
|
Creates GitHub Release tagged release-YYYYMMDD-<short-sha>.
|
||||||
|
│
|
||||||
|
│ ← manually trigger changelog.yml with the version string
|
||||||
|
▼
|
||||||
|
changelog.yml
|
||||||
|
Runs update_changelog.py, prepends a new ## [vX.Y.Z] entry,
|
||||||
|
commits back to main.
|
||||||
|
```
|
||||||
|
|
||||||
|
Each stage is independent. If signing fails (e.g. an expired key), re-run only `sign.yml` pointing at the existing build artifact — no need to rebuild 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]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release process (step by step)
|
||||||
|
|
||||||
|
### 1. Trigger a build
|
||||||
|
|
||||||
|
Push to `main` — `build.yml` runs automatically when `docs/`, `mkdocs.yml`, or `scripts/` change. You can also trigger it manually from **Actions → Build PDFs → Run workflow**.
|
||||||
|
|
||||||
|
Once it completes successfully, **note the run ID** from the URL or the Actions list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Sign the PDFs
|
||||||
|
|
||||||
|
Go to **Actions → Sign PDFs → Run workflow**.
|
||||||
|
|
||||||
|
| Input | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| `build_run_id` | The run ID from step 1 |
|
||||||
|
|
||||||
|
`sign.yml` will:
|
||||||
|
|
||||||
|
- Download the PDFs artifact from the build run
|
||||||
|
- Compute SHA-256 and BLAKE2b hashes, writing `thgtoa.pdf.sha256`, `thgtoa.pdf.b2sum`, `sha256sums.txt`, `b2sums.txt`, and the dark equivalents
|
||||||
|
- GPG-sign all PDFs and hash files, writing `.asc` detached signature files
|
||||||
|
- Commit the updated `export/` directory back to `main`
|
||||||
|
- Upload two artifacts: `signatures` and `pdfs-signed`
|
||||||
|
|
||||||
|
Once it completes successfully, **note the run ID**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Publish the release
|
||||||
|
|
||||||
|
Go to **Actions → Release → Run workflow**.
|
||||||
|
|
||||||
|
| Input | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| `sign_run_id` | The run ID from step 2 |
|
||||||
|
| `prerelease` | `false` for a normal release |
|
||||||
|
|
||||||
|
`release.yml` will:
|
||||||
|
|
||||||
|
- Download `signatures` and `pdfs-signed` artifacts from the sign run
|
||||||
|
- Upload both PDFs to VirusTotal
|
||||||
|
- Auto-generate a release tag in the format `release-YYYYMMDD-<short-sha>` (e.g. `release-20260527-abc1234`)
|
||||||
|
- Create a GitHub Release with all PDFs, hash files, and signatures attached, and the VirusTotal report URLs in the body
|
||||||
|
|
||||||
|
No version number needs to be chosen at this step — the tag is derived from the date and commit SHA, so it is always unique and always traceable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Update the changelog
|
||||||
|
|
||||||
|
Go to **Actions → Update Changelog → Run workflow**.
|
||||||
|
|
||||||
|
| Input | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| `version` | The human-readable version string, e.g. `v1.2.4` |
|
||||||
|
| `dry_run` | `true` to preview without committing |
|
||||||
|
|
||||||
|
`changelog.yml` runs `scripts/update_changelog.py`, which:
|
||||||
|
|
||||||
|
- Reads git log since the last `## [vX.Y.Z]` heading in the changelog
|
||||||
|
- Categorises commits into Added / Changed / Fixed using conventional-commit prefixes
|
||||||
|
- Prepends a new `## [version]` admonition block to `docs/changelog/index.md`
|
||||||
|
- Commits the result back to `main`
|
||||||
|
|
||||||
|
The version string is the only human decision in the release process. It goes into the changelog only — it does not affect the release tag.
|
||||||
|
|
||||||
|
!!! tip "Previewing the changelog entry"
|
||||||
|
Run with `dry_run: true` first to review the generated entry before it is committed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release tag format
|
||||||
|
|
||||||
|
Release tags use the format `release-YYYYMMDD-<short-sha>`, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
release-20260527-abc1234
|
||||||
|
```
|
||||||
|
|
||||||
|
This format is always unique, requires no version decision at release time, and is directly traceable to the commit that was built. The version string (e.g. `v1.2.4`) is a separate, human-assigned label that lives only in the changelog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit message format
|
||||||
|
|
||||||
|
All commits must follow the [Conventional Commits](https://www.conventionalcommits.org) format. This is enforced by the `commitizen` pre-commit hook.
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <description>
|
||||||
|
```
|
||||||
|
|
||||||
|
Accepted types and their changelog bucket:
|
||||||
|
|
||||||
|
| Type | Bucket |
|
||||||
|
|------|--------|
|
||||||
|
| `feat`, `feature`, `add` | Added |
|
||||||
|
| `fix`, `bugfix`, `revert`, `security` | Fixed |
|
||||||
|
| `perf`, `refactor`, `change`, `chore`, `ci`, `docs`, `style`, `test`, `build` | Changed |
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GitHub Secrets
|
||||||
|
|
||||||
|
Configure these 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
|
||||||
|
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.
|
||||||
|
|
||||||
|
### `ACTIONS_SSH_SIGNING_KEY`
|
||||||
|
|
||||||
|
An SSH private key used by `sign.yml` to sign the commit that pushes `export/` back to `main`. Generate a dedicated key for this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-keygen -t ed25519 -C "github-actions signing key" -f actions_signing_key
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the **private key** as the `ACTIONS_SSH_SIGNING_KEY` secret, and the **public key** to the repository's Deploy Keys (Settings → Deploy Keys) with write access.
|
||||||
|
|
||||||
|
### `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. Get one by creating a free account at `virustotal.com` → API key under your profile. The free tier (4 lookups/minute, 500/day) is sufficient.
|
||||||
|
|
||||||
|
### `CHANGELOG_PAT`
|
||||||
|
|
||||||
|
A GitHub Personal Access Token with `contents: write` scope on this repository. Needed because `changelog.yml` commits back to `main` — commits made with the default `GITHUB_TOKEN` do not trigger further workflow runs (GitHub loop-prevention). A PAT bypasses this. If absent, falls back to `GITHUB_TOKEN` — the commit still happens, it just won't trigger downstream workflows.
|
||||||
|
|
||||||
|
**Creating one:** GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → set Contents to Read and write for this repo only.
|
||||||
|
|
||||||
|
### Secrets summary
|
||||||
|
|
||||||
|
| Secret | Required by | What happens if missing |
|
||||||
|
|--------|------------|------------------------|
|
||||||
|
| `GPG_PRIVATE_KEY` | `sign.yml` | Signing step fails — no `.asc` files produced |
|
||||||
|
| `GPG_PASSPHRASE` | `sign.yml` | GPG import succeeds but signing fails |
|
||||||
|
| `ACTIONS_SSH_SIGNING_KEY` | `sign.yml` | Export commit is unsigned (may fail if branch protection requires signed commits) |
|
||||||
|
| `VT_API_KEY` | `release.yml` | VirusTotal step fails — release is not published |
|
||||||
|
| `CHANGELOG_PAT` | `changelog.yml` | Falls back to `GITHUB_TOKEN` — changelog updates but commit won't trigger downstream workflows |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.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
|
||||||
|
```
|
||||||
|
|
||||||
|
A successful verify looks like:
|
||||||
|
|
||||||
|
```
|
||||||
|
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]"`. Required by the `social` plugin.
|
||||||
|
|
||||||
|
**`KeyError: 'JPEG'` in convert.py**
|
||||||
|
Pillow needs libjpeg. Reinstall after installing the system lib: `sudo apt install libjpeg-dev && pip install --force-reinstall pillow`.
|
||||||
|
|
||||||
|
**`qpdf: can't find PDF header`**
|
||||||
|
Ensure you are on the current version of `convert.py` — qpdf only accepts PDF inputs, not PNG.
|
||||||
|
|
||||||
|
**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 header and footer lines.
|
||||||
|
|
||||||
|
**GPG signing fails with `Bad passphrase`**
|
||||||
|
The `GPG_PASSPHRASE` secret has a trailing space or newline. Paste it again 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 after a few minutes.
|
||||||
|
|
||||||
|
**`sign.yml` fails downloading PDF artifact**
|
||||||
|
The `build_run_id` is wrong, or the artifact has expired (90-day retention). Trigger a new build and use the fresh run ID.
|
||||||
|
|
||||||
|
**Changelog already contains version X**
|
||||||
|
`update_changelog.py` will error if `MANUAL_VERSION` is set to a version already in the changelog. Choose the next version string.
|
||||||
|
|
||||||
|
**Footnote warnings from MkDocs (`link '#fnref:N' has no anchor`)**
|
||||||
|
A footnote definition `[^N]:` exists without a matching inline citation. Add the citation 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._
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
7ed217a5e21353a79e0cea393f20d1c3d12549a9a3ed8b968cbbb7ca487f0db5d381af06fdbf09bed8f15066074a841fd79106bdb810e447cd123016e129da76 thgtoa.pdf
|
||||||
|
c84086635084be469074df60b5083e4e72cbabaa2687ef55d5bdd9876efc202a56b70d07e8ac177a6456cdb8bb248cd52550f9f32ba0a263be4aefdca841df95 thgtoa-dark.pdf
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
f1fdb5ae16f197747f648ecf539906361eb7110d118cb9dae69b3ca0c1927313 thgtoa.pdf
|
||||||
|
17e06922819a8805e42ad0a1cdec9e15ab7fcac0ee49aac6ce77aa2ca5102f59 thgtoa-dark.pdf
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
# scripts/archived
|
||||||
|
|
||||||
|
Scripts kept for reference but no longer part of the active pipeline.
|
||||||
|
|
||||||
|
| Script | Why archived |
|
||||||
|
|--------|-------------|
|
||||||
|
| `tag_release.py` | Created GPG-signed `vX.Y.Z` annotated tags. Superseded by the `release-YYYYMMDD-<sha>` timestamp tagging built into `release.yml`. Re-enable if semver release tagging is reintroduced. |
|
||||||
@@ -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"
|
||||||
|
|
||||||
+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')
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
COMMIT_MSG_FILE="$1"
|
||||||
|
|
||||||
|
if [ -z "$COMMIT_MSG_FILE" ]; then
|
||||||
|
echo "require-commit-body: no commit message file supplied" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stripped=$(grep -v '^\s*#' "$COMMIT_MSG_FILE" | sed 's/[[:space:]]*$//')
|
||||||
|
|
||||||
|
subject=$(echo "$stripped" | sed -n '1p')
|
||||||
|
separator=$(echo "$stripped" | sed -n '2p')
|
||||||
|
body=$(echo "$stripped" | tail -n +3 | grep -v '^\s*$')
|
||||||
|
|
||||||
|
if [ -z "$subject" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " COMMIT REJECTED: subject line is empty." >&2
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$separator" ] && [ -z "$body" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " COMMIT REJECTED: commit has no body." >&2
|
||||||
|
echo ""
|
||||||
|
echo " Every commit must explain *why*, not just *what*." >&2
|
||||||
|
echo " Format:" >&2
|
||||||
|
echo ""
|
||||||
|
echo " type(scope): short subject" >&2
|
||||||
|
echo ""
|
||||||
|
echo " Explain the motivation for this change. What problem does" >&2
|
||||||
|
echo " it solve? Why is this the right approach? Reference any" >&2
|
||||||
|
echo " relevant issues, prior art, or context." >&2
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$separator" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " COMMIT REJECTED: no blank line between subject and body." >&2
|
||||||
|
echo ""
|
||||||
|
echo " The second line of a commit message must be blank." >&2
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$body" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " COMMIT REJECTED: commit body is empty." >&2
|
||||||
|
echo ""
|
||||||
|
echo " Add at least one line after the blank separator explaining" >&2
|
||||||
|
echo " why this change was made." >&2
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
+55
-31
@@ -2,14 +2,18 @@
|
|||||||
"""Auto-generate and prepend a changelog entry to docs/changelog/index.md.
|
"""Auto-generate and prepend a changelog entry to docs/changelog/index.md.
|
||||||
|
|
||||||
Called by .github/workflows/changelog.yml. Reads git log since the last
|
Called by .github/workflows/changelog.yml. Reads git log since the last
|
||||||
changelog version tag, categorises commits by conventional-commit prefix,
|
changelog version, categorises commits by conventional-commit prefix,
|
||||||
and prepends a new ## [vX.Y.Z] section in the MkDocs admonition format used
|
and prepends a new ## [vX.Y.Z] section in the MkDocs admonition format used
|
||||||
by the rest of the file.
|
by the rest of the file.
|
||||||
|
|
||||||
Environment variables (all optional — reasonable defaults apply):
|
Environment variables:
|
||||||
MANUAL_VERSION Override the auto-incremented version string.
|
MANUAL_VERSION Version string to record (required when run from CI).
|
||||||
|
Falls back to auto-increment from the changelog for local runs.
|
||||||
TRIGGERING_SHA The commit SHA that triggered this run (used as range end).
|
TRIGGERING_SHA The commit SHA that triggered this run (used as range end).
|
||||||
DRY_RUN If "true", print the entry and exit without writing.
|
DRY_RUN If "true", print the entry and exit without writing.
|
||||||
|
|
||||||
|
Note: version is sourced from the changelog file, not from git tags. Git tags
|
||||||
|
are no longer used as the version authority. The changelog is the source of truth.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -17,7 +21,6 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -52,15 +55,6 @@ def run(cmd: list[str]) -> str:
|
|||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
def latest_version_tag() -> str | None:
|
|
||||||
"""Return the most recent vX.Y.Z tag reachable from HEAD, or None."""
|
|
||||||
out = run(["git", "tag", "--sort=-version:refname", "--list", "v*"])
|
|
||||||
for line in out.splitlines():
|
|
||||||
if re.match(r"^v\d+\.\d+\.\d+", line):
|
|
||||||
return line
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def auto_increment_version(current: str | None) -> str:
|
def auto_increment_version(current: str | None) -> str:
|
||||||
"""Bump the patch number of the current version, or default to v1.0.0."""
|
"""Bump the patch number of the current version, or default to v1.0.0."""
|
||||||
if not current:
|
if not current:
|
||||||
@@ -73,7 +67,11 @@ def auto_increment_version(current: str | None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def version_from_changelog() -> str | None:
|
def version_from_changelog() -> str | None:
|
||||||
"""Parse the most recent ## [vX.Y.Z] heading from the changelog file."""
|
"""Parse the most recent ## [vX.Y.Z] heading from the changelog file.
|
||||||
|
|
||||||
|
This is the primary version source — the changelog is the authority,
|
||||||
|
not git tags.
|
||||||
|
"""
|
||||||
if not CHANGELOG.exists():
|
if not CHANGELOG.exists():
|
||||||
return None
|
return None
|
||||||
for line in CHANGELOG.read_text(encoding="utf-8").splitlines():
|
for line in CHANGELOG.read_text(encoding="utf-8").splitlines():
|
||||||
@@ -84,11 +82,20 @@ 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 we fall back to the merge-base between HEAD and
|
||||||
|
origin/main to avoid dumping the entire history into the changelog.
|
||||||
|
"""
|
||||||
if ref:
|
if ref:
|
||||||
log_range = f"{ref}..{until}"
|
log_range = f"{ref}..{until}"
|
||||||
else:
|
else:
|
||||||
log_range = until
|
merge_base = run(["git", "merge-base", "HEAD", "origin/main"])
|
||||||
|
if merge_base:
|
||||||
|
log_range = f"{merge_base}..{until}"
|
||||||
|
else:
|
||||||
|
# Truly brand new repo with no remote — limit to last 50 commits
|
||||||
|
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,12 +104,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}
|
||||||
|
|
||||||
|
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
|
if NOISE.search(msg):
|
||||||
if re.search(r"\[skip ci\]|^Merge |^chore: bump|update changelog", msg, re.I):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Strip conventional-commit prefix to get the plain description
|
|
||||||
m = re.match(r"^(\w+)(?:\([^)]+\))?!?:\s*(.+)$", msg)
|
m = re.match(r"^(\w+)(?:\([^)]+\))?!?:\s*(.+)$", msg)
|
||||||
if m:
|
if m:
|
||||||
prefix = m.group(1).lower()
|
prefix = m.group(1).lower()
|
||||||
@@ -112,7 +137,6 @@ def categorise(messages: list[str]) -> dict[str, list[str]]:
|
|||||||
description = msg
|
description = msg
|
||||||
bucket = "Changed"
|
bucket = "Changed"
|
||||||
|
|
||||||
# Capitalise first letter
|
|
||||||
description = description[0].upper() + description[1:] if description else description
|
description = description[0].upper() + description[1:] if description else description
|
||||||
buckets[bucket].append(description)
|
buckets[bucket].append(description)
|
||||||
|
|
||||||
@@ -130,7 +154,7 @@ def format_admonition(bucket: str, items: list[str]) -> str:
|
|||||||
def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
|
def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
|
||||||
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||||
lines = [f"## [{version}]", ""]
|
lines = [f"## [{version}]", ""]
|
||||||
lines.append(f'!!! Note "Meta"')
|
lines.append('!!! Note "Meta"')
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f" - Released {date} from [`{sha[:7]}`](https://github.com/Anon-Planet/thgtoa/commit/{sha})")
|
lines.append(f" - Released {date} from [`{sha[:7]}`](https://github.com/Anon-Planet/thgtoa/commit/{sha})")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -139,7 +163,6 @@ def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
|
|||||||
if buckets.get(bucket):
|
if buckets.get(bucket):
|
||||||
lines.append(format_admonition(bucket, buckets[bucket]))
|
lines.append(format_admonition(bucket, buckets[bucket]))
|
||||||
|
|
||||||
# If no commits were categorised, add a placeholder
|
|
||||||
if not any(buckets[b] for b in BUCKET_ORDER):
|
if not any(buckets[b] for b in BUCKET_ORDER):
|
||||||
lines.append('!!! Note "Changed"')
|
lines.append('!!! Note "Changed"')
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -153,10 +176,8 @@ def prepend_entry(entry: str) -> None:
|
|||||||
"""Insert the new entry after the # Release Notes heading."""
|
"""Insert the new entry after the # Release Notes heading."""
|
||||||
content = CHANGELOG.read_text(encoding="utf-8")
|
content = CHANGELOG.read_text(encoding="utf-8")
|
||||||
|
|
||||||
# Find the first ## heading and insert before it
|
|
||||||
insert_at = content.find("\n## ")
|
insert_at = content.find("\n## ")
|
||||||
if insert_at == -1:
|
if insert_at == -1:
|
||||||
# No existing version section — append after the header block
|
|
||||||
content = content.rstrip() + "\n\n" + entry + "\n"
|
content = content.rstrip() + "\n\n" + entry + "\n"
|
||||||
else:
|
else:
|
||||||
content = content[: insert_at + 1] + entry + "\n" + content[insert_at + 1 :]
|
content = content[: insert_at + 1] + entry + "\n" + content[insert_at + 1 :]
|
||||||
@@ -175,13 +196,11 @@ def main() -> int:
|
|||||||
manual_version = os.environ.get("MANUAL_VERSION", "").strip()
|
manual_version = os.environ.get("MANUAL_VERSION", "").strip()
|
||||||
triggering_sha = os.environ.get("TRIGGERING_SHA", "HEAD").strip() or "HEAD"
|
triggering_sha = os.environ.get("TRIGGERING_SHA", "HEAD").strip() or "HEAD"
|
||||||
|
|
||||||
# Determine version
|
# Version authority: MANUAL_VERSION (required in CI) → changelog → auto-increment.
|
||||||
last_tag = latest_version_tag()
|
# Git tags are intentionally not consulted.
|
||||||
last_cl_ver = version_from_changelog()
|
last_cl_ver = version_from_changelog()
|
||||||
base_version = last_tag or last_cl_ver
|
new_version = manual_version or auto_increment_version(last_cl_ver)
|
||||||
new_version = manual_version or auto_increment_version(base_version)
|
|
||||||
|
|
||||||
print(f"Last tag: {last_tag or '(none)'}")
|
|
||||||
print(f"Last CL version: {last_cl_ver or '(none)'}")
|
print(f"Last CL version: {last_cl_ver or '(none)'}")
|
||||||
print(f"New version: {new_version}")
|
print(f"New version: {new_version}")
|
||||||
print(f"Triggering SHA: {triggering_sha}")
|
print(f"Triggering SHA: {triggering_sha}")
|
||||||
@@ -190,8 +209,13 @@ def main() -> int:
|
|||||||
print(f"Changelog already contains {new_version} — nothing to do.")
|
print(f"Changelog already contains {new_version} — nothing to do.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Collect commits since the last tag (or all commits if no tag)
|
if already_has_version(new_version) and manual_version:
|
||||||
messages = commits_since(last_tag, triggering_sha)
|
print(f"::error::Changelog already contains {new_version}. Choose a different version.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Collect commits since the last changelog version (using it as a git ref
|
||||||
|
# is a best-effort — if the tag doesn't exist, commits_since handles it gracefully).
|
||||||
|
messages = commits_since(last_cl_ver, triggering_sha)
|
||||||
print(f"Commits found: {len(messages)}")
|
print(f"Commits found: {len(messages)}")
|
||||||
for m in messages:
|
for m in messages:
|
||||||
print(f" {m}")
|
print(f" {m}")
|
||||||
|
|||||||
Reference in New Issue
Block a user