Guide
HTML to PDF in GitHub Actions
Generating PDFs in CI lets you produce report files on push, attach release notes to GitHub Releases, or build documentation artifacts as part of a publish pipeline. The traditional approach requires installing headless Chrome, wkhtmltopdf, or WeasyPrint in the runner image, which adds minutes to install time and megabytes of cached state. PDFPipe is a single HTTP call with no runner dependencies.
Why not just install Puppeteer in CI?
| Approach | Setup time | Cache size | Complexity |
|---|---|---|---|
| apt install chromium | ~45 s | No cache | Fails on arm runners |
| npx puppeteer + chrome | ~60 s | ~400 MB | Needs --no-sandbox |
| wkhtmltopdf | ~20 s | ~100 MB | Old engine, CSS gaps |
| PDFPipe API | 0 s | 0 bytes | Just curl + a secret |
Step 1: Add the API key as a secret
Get a free API key at pdfpipe.xyz/signup. Add it to your repository at Settings → Secrets and variables → Actions → New repository secret. Name it PDFPIPE_API_KEY.
Basic workflow: render and upload artifact
This workflow renders an HTML file and uploads the PDF as a build artifact, downloadable from the Actions run page.
.github/workflows/pdf.yml
name: Generate PDF
on:
push:
branches: [main]
workflow_dispatch:
jobs:
generate-pdf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate PDF from HTML file
run: |
curl -s -X POST https://api.pdfpipe.xyz/v1/pdf \
-H "Authorization: Bearer ${{ secrets.PDFPIPE_API_KEY }}" \
-H "Content-Type: application/json" \
-d "{\"html\": \"$(cat report.html | jq -Rs .)\"}" \
--output report.pdf
- name: Upload PDF artifact
uses: actions/upload-artifact@v4
with:
name: report-pdf
path: report.pdf
retention-days: 30Using a Node.js script
For complex templates or multi-page documents, a Node.js script gives you full control over the HTML before sending it to the API:
// scripts/generate-pdf.mjs
// Runs in a GitHub Actions step: node scripts/generate-pdf.mjs
import { readFileSync, writeFileSync } from "fs";
const html = readFileSync("report.html", "utf-8");
const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: {
format: "A4",
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
print_background: true,
},
}),
});
if (!resp.ok) {
const err = await resp.text();
console.error("PDF generation failed:", err);
process.exit(1);
}
const buffer = Buffer.from(await resp.arrayBuffer());
writeFileSync("report.pdf", buffer);
console.log(`Generated report.pdf (${buffer.length} bytes)`);In your workflow, call it with node scripts/generate-pdf.mjs and set PDFPIPE_API_KEY: ${{ secrets.PDFPIPE_API_KEY }} in the step's env.
Attach a PDF to a GitHub Release
Trigger on the release: published event to automatically attach a rendered document to every release:
.github/workflows/release-pdf.yml
name: Attach PDF to release
on:
release:
types: [published]
jobs:
generate-and-attach:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Build HTML (example: use Node to compile a template)
run: node scripts/build-release-notes.js > release.html
- name: Render to PDF
run: |
curl -s -X POST https://api.pdfpipe.xyz/v1/pdf \
-H "Authorization: Bearer ${{ secrets.PDFPIPE_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"html": "'$(cat release.html | base64 -w0)'", "options": {"format":"A4"}}' \
--output release-notes.pdf
- name: Attach PDF to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload ${{ github.event.release.tag_name }} release-notes.pdfStore the PDF and post a URL
Pass "store": true to get back a stable URL instead of raw bytes. The URL can be shared in a Slack message, email, or PR comment. Document retention depends on your plan.
# Store the PDF and post its URL to a Slack channel
- name: Render and store PDF
id: pdf
run: |
RESPONSE=$(curl -s -X POST https://api.pdfpipe.xyz/v1/pdf \
-H "Authorization: Bearer ${{ secrets.PDFPIPE_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"html":"...","store":true,"options":{"format":"A4"}}')
echo "url=$(echo $RESPONSE | jq -r '.document_url // empty')" >> $GITHUB_OUTPUT
# Note: "store":true returns JSON instead of raw bytes.
# The document URL is stable until the plan's retention window expires.
- name: Post to Slack
if: steps.pdf.outputs.url != ''
run: |
curl -s -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H "Content-Type: application/json" \
-d '{"text":"Report ready: ${{ steps.pdf.outputs.url }}"}'Common questions
Does this work on arm64 / M1 runners?
Yes. PDFPipe is a remote API call so it has no native binary dependencies. It runs identically on ubuntu, macos, windows, and arm GitHub-hosted runners.
How do I handle large HTML files?
The API accepts up to 5 MB of HTML per request. For larger documents, split them into multiple requests or use the "url" field to point the renderer at a served URL rather than inlining the HTML.
Can I render from a URL that requires authentication?
The renderer runs in an isolated environment without your session cookies. Use the "html" field instead: generate the HTML server-side in your script and send it inline. That way, authentication happens in your workflow, not the renderer.
Get started
500 renders a month free. Key issued instantly, no card needed.