Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .github/scripts/diff-book-pdf.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/bin/bash
#
# Generate a PDF diff comment and per-page montage assets for the book PR.
#
# Adapted from stamped-principles/stamped-paper's gh-pages-diff-pdf.sh; the
# main difference is that the base PDF is a local file (built in the same
# CI job) rather than fetched from a deployed URL.
#
# Usage: diff-book-pdf.sh <pr-pdf> <base-pdf> <preview-branch> \
# <repo> <server> <head-sha> <base-ref>
#
# Inputs:
# pr-pdf path to the PR-built book PDF
# base-pdf path to the base-branch-built book PDF (may be missing)
# preview-branch name of the per-PR preview branch (e.g. gh-pages-pr-42)
# repo GitHub repository (owner/name)
# server GitHub server URL (https://github.com)
# head-sha full SHA of the PR head commit
# base-ref name of the base branch (e.g. main) — used in comment text
#
# Outputs in the working directory:
# pr-comment.md markdown body for the PR comment
# diff.pdf visual diff PDF (only when differences exist)
# diff-page-NN.png per-page 3-panel montages for changed pages

set -euo pipefail

pr_pdf="$1"
base_pdf="$2"
preview_branch="$3"
repo="$4"
server="$5"
head_sha="$6"
base_ref="${7:-main}"

raw_base="https://github.com/ghraw/${repo}/${preview_branch}"
view_url="${server}/${repo}/blob/${preview_branch}/book.pdf"
diff_view_url="${server}/${repo}/blob/${preview_branch}/diff.pdf"

{
echo "<!-- pdf-preview -->"
echo "### Book PDF"
echo "Built from ${head_sha}."
echo "**[View PDF](${view_url})** | **[Download](${raw_base}/book.pdf)**"
} > pr-comment.md

if [ ! -f "$base_pdf" ]; then
echo "" >> pr-comment.md
echo "*Base PDF (\`${base_ref}\`) was not available — diff skipped.*" >> pr-comment.md
exit 0
fi

if diff-pdf --output-diff=diff.pdf --dpi=300 --channel-tolerance=0 -g \
"$base_pdf" "$pr_pdf"; then
echo "" >> pr-comment.md
echo "No visual differences from \`${base_ref}\`." >> pr-comment.md
exit 0
fi

# Differences exist — build per-page montages.
mkdir -p diff-pages
pdftoppm -png -r 150 diff.pdf diff-pages/page
pdftoppm -png -r 150 "$base_pdf" diff-pages/base
pdftoppm -png -r 150 "$pr_pdf" diff-pages/pr

mapfile -t diff_pngs < <(find diff-pages -name 'page-*.png' | sort -V)
mapfile -t base_pngs < <(find diff-pages -name 'base-*.png' | sort -V)
mapfile -t pr_pngs < <(find diff-pages -name 'pr-*.png' | sort -V)

base_count=${#base_pngs[@]}
pr_count=${#pr_pngs[@]}
diff_count=${#diff_pngs[@]}
max_count=$((base_count > pr_count ? base_count : pr_count))

changed=0
unchanged=0
added=0
removed=0
page_details=""

for ((i = 0; i < max_count; i++)); do
page_num=$((i + 1))
padded=$(printf '%02d' "$page_num")

if [ "$i" -lt "$base_count" ] && [ "$i" -lt "$pr_count" ]; then
if [ "$i" -lt "$diff_count" ]; then
pixels=$(compare -metric AE -fuzz 2% \
"${base_pngs[$i]}" "${pr_pngs[$i]}" /dev/null 2>&1 || true)

if [ "$pixels" = "0" ] || ! [[ "$pixels" =~ ^[0-9]+$ ]]; then
unchanged=$((unchanged + 1))
page_details+="- Page ${page_num} — unchanged\n"
else
changed=$((changed + 1))
compare -fuzz 2% -highlight-color '#FF000060' \
"${base_pngs[$i]}" "${pr_pngs[$i]}" \
-compose src "diff-pages/highlighted-${padded}.png" 2>/dev/null || true
montage_args=(-tile 3x1 -geometry '+4+4')
if ! montage \
-font DejaVu-Sans \
-label "${base_ref}" "${base_pngs[$i]}" \
-label "PR" "${pr_pngs[$i]}" \
-label "changes" "diff-pages/highlighted-${padded}.png" \
"${montage_args[@]}" "diff-page-${padded}.png" 2>/dev/null; then
montage +label \
"${base_pngs[$i]}" "${pr_pngs[$i]}" \
"diff-pages/highlighted-${padded}.png" \
"${montage_args[@]}" "diff-page-${padded}.png"
fi
diff_img_url="${raw_base}/diff-page-${padded}.png"
page_details+="<details><summary>Page ${page_num} — changed</summary>\n\n"
page_details+="![page ${page_num} diff](${diff_img_url})\n\n"
page_details+="</details>\n"
fi
fi
elif [ "$i" -lt "$pr_count" ]; then
added=$((added + 1))
page_details+="- Page ${page_num} — **new**\n"
else
removed=$((removed + 1))
page_details+="- Page ${page_num} — **removed**\n"
fi
done

parts=()
[ "$changed" -gt 0 ] && parts+=("${changed} changed")
[ "$unchanged" -gt 0 ] && parts+=("${unchanged} unchanged")
[ "$added" -gt 0 ] && parts+=("${added} added")
[ "$removed" -gt 0 ] && parts+=("${removed} removed")
summary="${pr_count} pages total: $(IFS=', '; echo "${parts[*]}")"
[ "$base_count" -ne "$pr_count" ] && summary+=" (was ${base_count})"

{
echo ""
echo "#### Diff vs \`${base_ref}\`"
echo "${summary}"
echo "**[View full diff PDF](${diff_view_url})**"
echo ""
printf '%b' "$page_details"
} >> pr-comment.md

rm -rf diff-pages
155 changes: 155 additions & 0 deletions .github/workflows/build-pdf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: Build PDF and preview on PRs

# Builds the book PDF on every pull request, builds the base-branch PDF
# for comparison, and pushes both plus a page-by-page visual diff to a
# per-PR preview branch so a reviewer can browse the rendered output
# from the PR conversation.
#
# Modeled on the stamped-principles/stamped-paper build-pdf.yml pattern,
# with two adaptations for this repo:
# 1. Base PDF is built locally (checked-out `main`), rather than
# fetched from a Pages URL — this repo does not publish the PDF.
# 2. mystmd's PDF export is pinned to plain_latex_book so we skip
# the current Typst export path (which trips on `**X**yz` bold
# markdown patterns in the sources).

on:
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: build-pdf-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write
pull-requests: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout PR head
uses: actions/checkout@v4
with:
path: pr

- name: Checkout base branch
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
path: base

- name: Install LaTeX
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
texlive-latex-base \
texlive-latex-recommended \
texlive-latex-extra \
texlive-fonts-recommended \
texlive-bibtex-extra \
latexmk

- uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install mystmd
run: npm install -g mystmd

- uses: astral-sh/setup-uv@v4

- name: Build PR PDF
working-directory: pr
run: uv run myst build --pdf --template plain_latex_book

- name: Build base PDF
if: github.event_name == 'pull_request'
working-directory: base
run: uv run myst build --pdf --template plain_latex_book

- name: Stage PDFs
run: |
cp pr/exports/book.pdf pr-book.pdf
if [ -f base/exports/book.pdf ]; then
cp base/exports/book.pdf base-book.pdf
fi

- name: Generate PDF diff
if: github.event_name == 'pull_request'
id: diff
run: |
docker run --rm -v "$PWD:/work" -w /work \
ghcr.io/stamped-principles/diff-pdf:latest \
bash pr/.github/scripts/diff-book-pdf.sh \
pr-book.pdf \
base-book.pdf \
"gh-pages-pr-${{ github.event.pull_request.number }}" \
"${{ github.repository }}" \
"${{ github.server_url }}" \
"${{ github.event.pull_request.head.sha }}" \
"${{ github.base_ref }}"
{
echo 'body<<ENDOFCOMMENT'
cat pr-comment.md
echo 'ENDOFCOMMENT'
} >> "$GITHUB_OUTPUT"

- name: Push PDFs and diffs to preview branch
if: github.event_name == 'pull_request'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
working-directory: pr
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
BRANCH="gh-pages-pr-${PR_NUMBER}"
git checkout --orphan "$BRANCH"
git rm -rf --cached .
cp ../pr-book.pdf book.pdf
git add -f book.pdf
[ -f ../diff.pdf ] && cp ../diff.pdf diff.pdf && git add -f diff.pdf
for f in ../diff-page-*.png; do
[ -f "$f" ] && cp "$f" . && git add -f "$(basename "$f")"
done
git commit -m "PDF preview for PR #${PR_NUMBER} (${HEAD_SHA:0:7})"
git push -f origin "$BRANCH"

- name: Upload PR PDF artifact
uses: actions/upload-artifact@v4
with:
name: book-pdf
path: pr-book.pdf
retention-days: 14

- name: Upload diff artifacts
if: github.event_name == 'pull_request' && hashFiles('diff.pdf') != ''
uses: actions/upload-artifact@v4
with:
name: book-pdf-diff
path: |
diff.pdf
diff-page-*.png
retention-days: 14

- name: Find existing PR comment
if: github.event_name == 'pull_request'
uses: peter-evans/find-comment@v3
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: '<!-- pdf-preview -->'

- name: Post or update PR comment
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: ${{ steps.diff.outputs.body }}
edit-mode: replace
19 changes: 19 additions & 0 deletions .github/workflows/cleanup-pr-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Clean up PR preview branch

on:
pull_request:
types: [closed]

permissions:
contents: write

jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Delete PR preview branch
env:
GH_TOKEN: ${{ github.token }}
run: |
BRANCH="gh-pages-pr-${{ github.event.pull_request.number }}"
gh api -X DELETE "repos/${{ github.repository }}/git/refs/heads/${BRANCH}" 2>/dev/null || true
Loading