From c6fd2891e02b15b7a66173b5ab073329ab004d2b Mon Sep 17 00:00:00 2001 From: nopeitsnothing Date: Fri, 22 May 2026 16:26:00 -0400 Subject: [PATCH] 3/8 ci: split monolithic workflow into build, sign, release stages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.yml — builds PDFs, uploads artifact, no secrets required sign.yml — hashes (SHA-256 + BLAKE2b) and GPG-signs, triggered via workflow_run after build or manually with a build_run_id release.yml — downloads artifacts, uploads to VirusTotal, publishes tagged GitHub Release with all 12 assets attached All three chain automatically on push to main. Each can be re-run or triggered independently against any historical run. Signed-off-by: nopeitsnothing --- .github/workflows/build.yml | 80 +++++++++++++ .github/workflows/release.yml | 215 ++++++++++++++++++++++++++++++++++ .github/workflows/sign.yml | 165 ++++++++++++++++++++++++++ 3 files changed, 460 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/sign.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c43de02 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,80 @@ +name: 📖 Build PDFs + +on: + workflow_dispatch: + inputs: + build_mode: + description: 'PDF build mode' + required: true + default: 'both' + type: choice + options: + - light + - dark + - both + push: + branches: + - main + paths: + - "docs/**" + - "mkdocs.yml" + - "scripts/**" + - ".github/workflows/build.yml" + +permissions: + contents: read + +jobs: + build: + name: Build PDFs + runs-on: ubuntu-latest + outputs: + build_mode: ${{ steps.mode.outputs.build_mode }} + run_id: ${{ github.run_id }} + + steps: + - name: 🛠️ Checkout + 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 and qpdf + run: | + sudo apt-get update -qq + sudo apt-get install -y poppler-utils qpdf + + - name: Setup Chrome + uses: browser-actions/setup-chrome@v2 + with: + chrome-version: 120 + install-dependencies: true + install-chromedriver: true + + - name: 🖨️ Resolve build mode + id: mode + run: | + MODE="${{ inputs.build_mode || 'both' }}" + echo "build_mode=$MODE" >> $GITHUB_OUTPUT + echo "Building in mode: $MODE" + + - name: 🖨️ Build PDFs + env: + CI: true + run: python scripts/build_guide_pdf.py --${{ steps.mode.outputs.build_mode }} + + - name: 📤 Upload PDF artifacts + uses: actions/upload-artifact@v4 + with: + name: pdfs + path: | + export/thgtoa.pdf + export/thgtoa-dark.pdf + if-no-files-found: warn + retention-days: 90 + compression-level: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..960cb93 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,215 @@ +name: 🚀 Release + +# Can be triggered: +# 1. Automatically after sign.yml completes on main +# 2. Manually, pointing at specific build/sign runs to pull artifacts from +on: + workflow_run: + workflows: ["🔏 Sign PDFs"] + types: [completed] + branches: [main] + workflow_dispatch: + inputs: + sign_run_id: + description: 'sign.yml run ID to pull signatures from' + required: true + 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: + description: 'Mark as pre-release?' + required: false + default: false + type: boolean + +permissions: + contents: write # create releases and tags + actions: read # download artifacts from other runs + +jobs: + release: + name: Publish GitHub Release + if: > + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + + steps: + - name: 🛠️ Checkout (for commit metadata only) + uses: actions/checkout@v4 + with: + sparse-checkout: pgp + + # ------------------------------------------------------------------ # + # Resolve which run IDs to pull artifacts from + # ------------------------------------------------------------------ # + - 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 + uses: actions/download-artifact@v4 + with: + name: signatures + path: release/ + run-id: ${{ steps.runs.outputs.sign_run }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: 📥 Download PDFs (from sign run) + uses: actions/download-artifact@v4 + with: + name: pdfs-signed + path: release/ + run-id: ${{ steps.runs.outputs.sign_run }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: 📋 List release assets + run: ls -lh release/ + + # ------------------------------------------------------------------ # + # Read hashes for the release body + # ------------------------------------------------------------------ # + - name: #️⃣ Read hashes + id: hashes + run: | + read_hash() { cat "release/$1" 2>/dev/null || echo "(not built)"; } + + echo "light_sha256=$(read_hash thgtoa.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 "dark_b2=$(read_hash thgtoa-dark.pdf.b2)" >> $GITHUB_OUTPUT + + # ------------------------------------------------------------------ # + # VirusTotal — upload whichever PDFs are present + # ------------------------------------------------------------------ # + - name: 🦠 Upload PDFs to VirusTotal + id: vt + uses: crazy-max/ghaction-virustotal@v5 + with: + vt_api_key: ${{ secrets.VT_API_KEY }} + files: | + release/thgtoa.pdf + release/thgtoa-dark.pdf + + - name: 🔗 Build VT report URLs + id: vt_urls + run: | + light_hash=$(cat release/thgtoa.pdf.sha256 2>/dev/null || echo "") + dark_hash=$(cat release/thgtoa-dark.pdf.sha256 2>/dev/null || echo "") + [ -n "$light_hash" ] && \ + echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT || \ + echo "light_vt=(not built)" >> $GITHUB_OUTPUT + [ -n "$dark_hash" ] && \ + echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT || \ + echo "dark_vt=(not built)" >> $GITHUB_OUTPUT + + # ------------------------------------------------------------------ # + # Tag + Release + # ------------------------------------------------------------------ # + - name: 🏷️ Generate release tag + id: tag + run: | + TAG="v$(date -u +'%Y.%m.%d')-$(echo ${{ github.sha }} | cut -c1-7)" + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "name=Release $(date -u +'%Y-%m-%d') (${TAG})" >> $GITHUB_OUTPUT + + - name: 🚀 Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: ${{ steps.tag.outputs.name }} + prerelease: ${{ inputs.prerelease || false }} + draft: false + fail_on_unmatched_files: false + body: | + ## 📖 The Hitchhiker's Guide to Online Anonymity + + Built from [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) on `${{ github.ref_name }}`. + + --- + + ### 📄 Release assets + + | File | Description | + |------|-------------| + | `thgtoa.pdf` | Light mode PDF | + | `thgtoa-dark.pdf` | Dark mode PDF (hacker theme) | + | `sha256sums.txt` | SHA-256 checksums (both files) | + | `b2sums.txt` | BLAKE2b checksums (both files) | + | `thgtoa.pdf.sha256` | SHA-256 — light PDF | + | `thgtoa-dark.pdf.sha256` | SHA-256 — dark PDF | + | `thgtoa.pdf.b2` | BLAKE2b — light PDF | + | `thgtoa-dark.pdf.b2` | BLAKE2b — dark PDF | + | `*.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 }} + ``` + + --- + + ### 🔏 Verifying GPG signatures + + ```bash + # Import the release signing key + gpg --import pgp/anonymousplanet-release.asc + + # Verify PDFs + gpg --verify thgtoa.pdf.sig thgtoa.pdf + gpg --verify thgtoa-dark.pdf.sig thgtoa-dark.pdf + + # Verify hash files + gpg --verify sha256sums.txt.sig sha256sums.txt + gpg --verify b2sums.txt.sig b2sums.txt + ``` + + --- + + ### 🦠 VirusTotal scans + + | File | Report | + |------|--------| + | `thgtoa.pdf` | ${{ steps.vt_urls.outputs.light_vt }} | + | `thgtoa-dark.pdf` | ${{ steps.vt_urls.outputs.dark_vt }} | + + files: | + release/thgtoa.pdf + release/thgtoa-dark.pdf + release/sha256sums.txt + release/b2sums.txt + release/thgtoa.pdf.sha256 + release/thgtoa-dark.pdf.sha256 + release/thgtoa.pdf.b2 + release/thgtoa-dark.pdf.b2 + release/thgtoa.pdf.sig + release/thgtoa-dark.pdf.sig + release/sha256sums.txt.sig + release/b2sums.txt.sig diff --git a/.github/workflows/sign.yml b/.github/workflows/sign.yml new file mode 100644 index 0000000..c41ec2d --- /dev/null +++ b/.github/workflows/sign.yml @@ -0,0 +1,165 @@ +name: 🔏 Sign PDFs + +# Can be triggered: +# 1. Automatically after build.yml completes on main +# 2. Manually, pointing at a specific build run to pull PDFs from +on: + workflow_run: + workflows: ["📖 Build PDFs"] + types: [completed] + branches: [main] + workflow_dispatch: + inputs: + build_run_id: + description: 'build.yml run ID to download PDFs from (leave blank for latest)' + required: false + type: string + +permissions: + actions: read # download artifacts from other runs + contents: read + +jobs: + sign: + name: Hash & Sign PDFs + # On workflow_run, only proceed if the build actually succeeded + if: > + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + outputs: + light_sha256: ${{ steps.hashes.outputs.light_sha256 }} + dark_sha256: ${{ steps.hashes.outputs.dark_sha256 }} + light_b2: ${{ steps.hashes.outputs.light_b2 }} + dark_b2: ${{ steps.hashes.outputs.dark_b2 }} + + steps: + - name: 🛠️ Checkout (for pgp/ key reference only) + uses: actions/checkout@v4 + with: + 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 + uses: actions/download-artifact@v4 + with: + name: pdfs + path: export/ + run-id: ${{ steps.src.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: 📋 List downloaded files + run: ls -lh export/ + + # ------------------------------------------------------------------ # + # Hash + # ------------------------------------------------------------------ # + - name: #️⃣ Hash PDFs + id: hashes + run: | + cd export + + for f in thgtoa.pdf thgtoa-dark.pdf; do + [ -f "$f" ] || continue + sha256sum "$f" | awk '{print $1}' > "${f}.sha256" + b2sum "$f" | awk '{print $1}' > "${f}.b2" + done + + # Combined files (only include files that exist) + sha256sum thgtoa.pdf thgtoa-dark.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 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 "") + dark_sha256=$(cat thgtoa-dark.pdf.sha256 2>/dev/null || echo "") + light_b2=$(cat thgtoa.pdf.b2 2>/dev/null || echo "") + dark_b2=$(cat thgtoa-dark.pdf.b2 2>/dev/null || echo "") + + echo "light_sha256=$light_sha256" >> $GITHUB_OUTPUT + echo "dark_sha256=$dark_sha256" >> $GITHUB_OUTPUT + echo "light_b2=$light_b2" >> $GITHUB_OUTPUT + echo "dark_b2=$dark_b2" >> $GITHUB_OUTPUT + + echo "--- SHA-256 ---" + cat sha256sums.txt + echo "--- BLAKE2b ---" + cat b2sums.txt + + # ------------------------------------------------------------------ # + # GPG sign + # ------------------------------------------------------------------ # + - name: 🔏 Import GPG signing key + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + echo "$GPG_PRIVATE_KEY" | gpg --batch --import + # Pre-cache passphrase to prevent interactive prompt + echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \ + --pinentry-mode loopback --list-secret-keys + + - name: 🔏 Sign PDFs and hash files + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + sign() { + local file="$1" + [ -f "$file" ] || return 0 + echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \ + --pinentry-mode loopback \ + --detach-sign --armor --output "${file}.sig" "$file" + echo "Signed: $file" + } + sign export/thgtoa.pdf + sign export/thgtoa-dark.pdf + sign export/sha256sums.txt + sign export/b2sums.txt + + # ------------------------------------------------------------------ # + # Upload — PDFs + all signatures and hashes together + # ------------------------------------------------------------------ # + - name: 📤 Upload signatures artifact + uses: actions/upload-artifact@v4 + with: + name: signatures + path: | + 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 + if-no-files-found: error + retention-days: 90 + compression-level: 0 + + - name: 📤 Upload signed PDFs artifact + uses: actions/upload-artifact@v4 + with: + name: pdfs-signed + path: | + export/thgtoa.pdf + export/thgtoa-dark.pdf + if-no-files-found: warn + retention-days: 90 + compression-level: 0