Release notes, test reports, invoices, documentation — if you can generate these automatically in your CI/CD pipeline, you eliminate manual errors and always have the latest PDF ready at review time.
This guide shows you how to call the FUNBREW PDF API from GitHub Actions to fully automate PDF generation. From simple HTML-to-PDF conversion to scheduled recurring reports, you'll find practical workflow examples with complete code.
For the basics of the API itself, see the quickstart guide.
Use Cases for Automated PDF Generation in CI/CD
There are many use cases that fit naturally into a GitHub Actions deployment flow.
| Use Case | Trigger | Output |
|---|---|---|
| Release notes | Tag push | Attached to GitHub Release |
| PR changelog | PR opened/updated | Artifact PDF |
| Weekly report | cron (every Monday) | Sent to Slack |
| Invoice generation | After PR approval | Saved to storage |
| API documentation | Merge to main | Deployment artifact |
| Test result summary | After test run | Downloadable PDF |
For PDF generation use cases beyond CI/CD, see the use cases page.
Setup: Managing API Keys with Secrets
Registering Your API Key in GitHub Secrets
Never write the FUNBREW PDF API key directly in your code or workflow files. Register it as a GitHub Secret and reference it as an environment variable in your workflow.
# Register the secret using GitHub CLI (run locally)
gh secret set FUNBREW_API_KEY --body "sk-your-api-key-here"
# Alternatively, go to Settings > Secrets and variables > Actions in your repository
Referencing Secrets in Workflows
jobs:
generate-pdf:
runs-on: ubuntu-latest
steps:
- name: Generate PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
# $FUNBREW_API_KEY is expanded safely
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<h1>Hello</h1>", "engine": "quality", "format": "A4"}' \
--output output.pdf
${{ secrets.FUNBREW_API_KEY }} is never printed in logs and is safely passed as a step environment variable. For more on API key security, see the security guide.
Organization Secrets and Environment Variables
If you share the same API key across multiple repositories, Organization Secrets are convenient. The reference syntax is identical.
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
When you need different API keys per environment (development / staging / production), use GitHub Environments.
jobs:
generate-pdf:
runs-on: ubuntu-latest
environment: production # Uses Secrets from this environment
steps:
- name: Generate PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
# Uses the production environment API key
HTML-to-PDF Workflow Examples
Basic: HTML-to-PDF Conversion with curl
The simplest setup. Build the HTML directly in the workflow and send it to the API.
# .github/workflows/generate-html-pdf.yml
name: HTML to PDF
on:
push:
branches: [main]
paths:
- 'templates/**'
- 'reports/**'
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate PDF from HTML file
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
# Escape HTML file as JSON string and send to API
HTML_CONTENT=$(cat templates/report.html | jq -Rs .)
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"html\": $HTML_CONTENT, \"engine\": \"quality\", \"format\": \"A4\"}" \
--output report.pdf
echo "PDF size: $(wc -c < report.pdf) bytes"
- name: Upload PDF artifact
uses: actions/upload-artifact@v4
with:
name: report-pdf
path: report.pdf
retention-days: 30
Node.js Script for Dynamic HTML Generation
A pattern for fetching data, dynamically building HTML, and converting it to PDF. Better suited for complex templates.
# .github/workflows/dynamic-pdf.yml
name: Dynamic PDF Generation
on:
workflow_dispatch:
inputs:
report_type:
description: 'Report type'
required: true
default: 'monthly'
type: choice
options:
- monthly
- quarterly
- annual
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Generate dynamic PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
REPORT_TYPE: ${{ inputs.report_type }}
run: node scripts/generate-report.js
// scripts/generate-report.js
const fs = require('fs');
async function generateReport() {
const reportType = process.env.REPORT_TYPE || 'monthly';
const apiKey = process.env.FUNBREW_API_KEY;
if (!apiKey) {
throw new Error('FUNBREW_API_KEY is not set');
}
// Build report data (in practice, fetch from DB or API)
const data = {
title: `${reportType.charAt(0).toUpperCase() + reportType.slice(1)} Report`,
generatedAt: new Date().toISOString(),
metrics: {
totalRequests: 12500,
successRate: 99.7,
avgResponseTime: 1.2,
},
};
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
padding: 40px;
color: #1a1a1a;
}
h1 { color: #2563eb; border-bottom: 2px solid #e5e7eb; padding-bottom: 12px; }
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 32px; }
.metric-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.metric-value { font-size: 2rem; font-weight: bold; color: #2563eb; }
.metric-label { font-size: 0.875rem; color: #64748b; margin-top: 4px; }
.footer { margin-top: 48px; font-size: 0.75rem; color: #94a3b8; }
</style>
</head>
<body>
<h1>${data.title}</h1>
<p>Generated: ${data.generatedAt}</p>
<div class="metrics">
<div class="metric-card">
<div class="metric-value">${data.metrics.totalRequests.toLocaleString()}</div>
<div class="metric-label">Total Requests</div>
</div>
<div class="metric-card">
<div class="metric-value">${data.metrics.successRate}%</div>
<div class="metric-label">Success Rate</div>
</div>
<div class="metric-card">
<div class="metric-value">${data.metrics.avgResponseTime}s</div>
<div class="metric-label">Avg Response Time</div>
</div>
</div>
<div class="footer">Generated by GitHub Actions + FUNBREW PDF API</div>
</body>
</html>
`;
console.log('Calling FUNBREW PDF API...');
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
engine: 'quality',
format: 'A4',
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API error ${response.status}: ${errorText}`);
}
const pdfBuffer = Buffer.from(await response.arrayBuffer());
const outputPath = `report-${reportType}-${Date.now()}.pdf`;
fs.writeFileSync(outputPath, pdfBuffer);
console.log(`PDF generated: ${outputPath} (${pdfBuffer.length} bytes)`);
}
generateReport().catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
For HTML template design patterns, see the template engine guide.
Markdown-to-PDF Workflow Examples
PR Changelog to PDF
Convert commit messages and changelogs from a PR to PDF using Markdown-to-PDF conversion and save as an artifact.
# .github/workflows/pr-changelog-pdf.yml
name: PR Changelog PDF
on:
pull_request:
types: [opened, synchronize]
jobs:
generate-changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history required for git log
- name: Build changelog markdown
run: |
BRANCH="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "# PR Changelog" > changelog.md
echo "" >> changelog.md
echo "**Branch:** \`${BRANCH}\` → \`${BASE}\`" >> changelog.md
echo "**PR:** #${{ github.event.pull_request.number }}" >> changelog.md
echo "**Author:** ${{ github.event.pull_request.user.login }}" >> changelog.md
echo "**Date:** $(date -u '+%Y-%m-%d %H:%M UTC')" >> changelog.md
echo "" >> changelog.md
echo "## Commits" >> changelog.md
echo "" >> changelog.md
# Append commit list
git log --oneline origin/${BASE}..HEAD --pretty=format:"- %h %s (%an)" >> changelog.md
echo "" >> changelog.md
echo "## Files Changed" >> changelog.md
echo "" >> changelog.md
# Append changed files
git diff --name-only origin/${BASE}..HEAD | while read file; do
echo "- \`$file\`" >> changelog.md
done
echo "Generated changelog.md:"
cat changelog.md
- name: Convert Markdown to PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
MARKDOWN_CONTENT=$(cat changelog.md | jq -Rs .)
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-markdown \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"markdown\": $MARKDOWN_CONTENT, \"theme\": \"github\", \"format\": \"A4\"}" \
--output changelog.pdf
echo "PDF generated: $(wc -c < changelog.pdf) bytes"
- name: Upload changelog PDF
uses: actions/upload-artifact@v4
with:
name: pr-changelog-pdf
path: changelog.pdf
retention-days: 7
Convert Documentation to PDF and Attach to Release
When pushing a tag to create a release, convert Markdown documentation to PDF and automatically attach it to GitHub Releases.
# .github/workflows/release-pdf.yml
name: Release with PDF Documentation
on:
push:
tags:
- 'v*'
permissions:
contents: write # Required for writing to GitHub Releases
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate release notes PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
TAG="${{ github.ref_name }}"
# Extract the relevant version section from CHANGELOG.md
awk "/^## \[${TAG}\]/,/^## \[/" CHANGELOG.md | head -n -1 > release-notes.md
if [ ! -s release-notes.md ]; then
echo "# Release ${TAG}" > release-notes.md
echo "" >> release-notes.md
echo "Released: $(date -u '+%Y-%m-%d')" >> release-notes.md
fi
MARKDOWN_CONTENT=$(cat release-notes.md | jq -Rs .)
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-markdown \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"markdown\": $MARKDOWN_CONTENT, \"theme\": \"github\", \"format\": \"A4\"}" \
--output "release-notes-${TAG}.pdf"
- name: Generate API reference PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
TAG="${{ github.ref_name }}"
MARKDOWN_CONTENT=$(cat docs/api-reference.md | jq -Rs .)
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-markdown \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"markdown\": $MARKDOWN_CONTENT, \"theme\": \"github\", \"format\": \"A4\"}" \
--output "api-reference-${TAG}.pdf"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ github.ref_name }}"
gh release create "${TAG}" \
--title "Release ${TAG}" \
--notes-file release-notes.md \
"release-notes-${TAG}.pdf#Release Notes (PDF)" \
"api-reference-${TAG}.pdf#API Reference (PDF)"
For Markdown-to-PDF options (themes, fonts, margins, etc.), see the Markdown to PDF API guide.
Saving PDFs as Artifacts
Advanced actions/upload-artifact Configuration
- name: Upload PDF artifacts
uses: actions/upload-artifact@v4
with:
name: generated-pdfs-${{ github.run_number }}
path: |
*.pdf
reports/*.pdf
retention-days: 90 # Retention period (default 90, max 400 days)
compression-level: 0 # PDFs are already compressed — level 0 is most efficient
if-no-files-found: error # Fail if no PDFs were generated
Merging Multiple PDFs into a Single Artifact
# .github/workflows/batch-pdf.yml
name: Batch PDF Generation
on:
workflow_dispatch:
jobs:
generate:
runs-on: ubuntu-latest
strategy:
matrix:
template: [invoice, report, certificate]
max-parallel: 3
steps:
- uses: actions/checkout@v4
- name: Generate ${{ matrix.template }} PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
TEMPLATE="${{ matrix.template }}"
HTML_CONTENT=$(cat "templates/${TEMPLATE}.html" | jq -Rs .)
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"html\": $HTML_CONTENT, \"engine\": \"quality\", \"format\": \"A4\"}" \
--output "${TEMPLATE}.pdf"
- name: Upload ${{ matrix.template }} PDF
uses: actions/upload-artifact@v4
with:
name: pdf-${{ matrix.template }}
path: ${{ matrix.template }}.pdf
retention-days: 30
# Merge all PDFs into a single artifact
merge-artifacts:
needs: generate
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: pdf-*
merge-multiple: true
path: all-pdfs/
- uses: actions/upload-artifact@v4
with:
name: all-pdfs
path: all-pdfs/
For high-volume PDF generation patterns, see the batch processing guide.
Scheduled Execution with cron
Send a Weekly Report to Slack
An example that automatically generates a weekly report every Monday morning and sends it to Slack.
# .github/workflows/weekly-report.yml
name: Weekly PDF Report
on:
schedule:
# Every Monday at 09:00 JST (= 00:00 UTC)
- cron: '0 0 * * MON'
workflow_dispatch: # Also allow manual runs
jobs:
weekly-report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Generate weekly report
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
DATA_API_KEY: ${{ secrets.DATA_API_KEY }}
run: |
node scripts/weekly-report.js
- name: Send PDF to Slack
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
run: |
# Upload file to Slack
curl -X POST https://slack.com/api/files.getUploadURLExternal \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"filename\": \"weekly-report-$(date +%Y-%m-%d).pdf\",
\"length\": $(wc -c < weekly-report.pdf)
}" | tee upload-url.json
UPLOAD_URL=$(cat upload-url.json | jq -r '.upload_url')
FILE_ID=$(cat upload-url.json | jq -r '.file_id')
curl -X POST "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
--data-binary @weekly-report.pdf
curl -X POST https://slack.com/api/files.completeUploadExternal \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"files\": [{\"id\": \"$FILE_ID\"}],
\"channel_id\": \"C0123456789\",
\"initial_comment\": \"Weekly report generated ($(date +%Y-%m-%d))\"
}"
Automated Monthly Invoice Generation
An example that automatically generates invoices at the end of each month and saves them to storage.
# .github/workflows/monthly-invoices.yml
name: Monthly Invoice Generation
on:
schedule:
# Run on days 28–31 at 09:00 UTC (covers end of every month)
- cron: '0 9 28-31 * *'
workflow_dispatch:
jobs:
check-last-day:
runs-on: ubuntu-latest
outputs:
is_last_day: ${{ steps.check.outputs.is_last_day }}
steps:
- id: check
run: |
TODAY=$(date +%d)
LAST_DAY=$(date -d "$(date +%Y-%m-01) +1 month -1 day" +%d)
if [ "$TODAY" = "$LAST_DAY" ]; then
echo "is_last_day=true" >> $GITHUB_OUTPUT
else
echo "is_last_day=false" >> $GITHUB_OUTPUT
fi
generate-invoices:
needs: check-last-day
if: needs.check-last-day.outputs.is_last_day == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Generate monthly invoices
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
DB_CONNECTION_STRING: ${{ secrets.DB_CONNECTION_STRING }}
run: node scripts/generate-monthly-invoices.js
- name: Upload to storage
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ./invoices/ s3://your-bucket/invoices/$(date +%Y/%m)/ \
--content-type application/pdf
// scripts/generate-monthly-invoices.js
const fs = require('fs');
const path = require('path');
async function generateInvoice(customer) {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-family: 'Helvetica Neue', Arial, sans-serif; padding: 40px; }
.header { display: flex; justify-content: space-between; align-items: start; }
.company-name { font-size: 1.5rem; font-weight: bold; color: #2563eb; }
h1 { font-size: 2rem; color: #1a1a1a; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th { background: #f1f5f9; text-align: left; padding: 10px 12px; }
td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; }
.total { font-weight: bold; font-size: 1.1rem; }
.footer { margin-top: 48px; font-size: 0.875rem; color: #64748b; }
</style>
</head>
<body>
<div class="header">
<div>
<div class="company-name">Your Company</div>
<p>123 Main Street, San Francisco, CA 94105<br>Tel: +1-555-0123</p>
</div>
<div>
<h1>Invoice</h1>
<p>No. INV-${customer.id}-${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}</p>
</div>
</div>
<p><strong>Bill to:</strong> ${customer.name}</p>
<p><strong>Invoice date:</strong> ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
<p><strong>Due date:</strong> ${new Date(Date.now() + 30 * 86400000).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Amount</th></tr>
</thead>
<tbody>
${customer.items.map(item => `
<tr>
<td>${item.name}</td>
<td>${item.quantity}</td>
<td>$${item.unitPrice.toLocaleString()}</td>
<td>$${(item.quantity * item.unitPrice).toLocaleString()}</td>
</tr>
`).join('')}
</tbody>
<tfoot>
<tr class="total">
<td colspan="3">Total</td>
<td>$${customer.total.toLocaleString()}</td>
</tr>
</tfoot>
</table>
<div class="footer">
<p>Payment: ACH / Wire transfer. Bank details provided separately.</p>
</div>
</body>
</html>
`;
const response = await fetch('https://api.pdf.funbrew.cloud/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, engine: 'quality', format: 'A4' }),
});
if (!response.ok) {
throw new Error(`API error ${response.status} for customer ${customer.id}`);
}
return Buffer.from(await response.arrayBuffer());
}
async function main() {
// In practice, fetch from your database
const customers = [
{
id: 'C001',
name: 'Acme Corporation',
total: 1000,
items: [
{ name: 'FUNBREW PDF Pro Plan', quantity: 1, unitPrice: 1000 },
],
},
];
fs.mkdirSync('./invoices', { recursive: true });
for (const customer of customers) {
console.log(`Generating invoice for ${customer.name}...`);
const pdf = await generateInvoice(customer);
const filename = `invoice-${customer.id}-${Date.now()}.pdf`;
fs.writeFileSync(path.join('./invoices', filename), pdf);
console.log(` Saved: ${filename} (${pdf.length} bytes)`);
}
console.log('All invoices generated.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
For a deep dive into invoice PDF automation, see the invoice PDF automation guide.
Practical Tips
Tip 1: Validate PDF Output
- name: Validate PDF output
run: |
# Verify the PDF is at least 1KB
PDF_SIZE=$(wc -c < output.pdf)
echo "PDF size: ${PDF_SIZE} bytes"
if [ "$PDF_SIZE" -lt 1024 ]; then
echo "ERROR: PDF is too small. Generation may have failed."
exit 1
fi
# Check the PDF magic bytes (must start with %PDF)
if ! xxd output.pdf | head -1 | grep -q "2550 4446"; then
echo "ERROR: Output is not a valid PDF file."
exit 1
fi
echo "PDF validation passed."
Tip 2: Parallelize Generation to Speed Up Workflows
jobs:
generate-pdfs:
runs-on: ubuntu-latest
strategy:
matrix:
document:
- { name: "report", template: "monthly-report", engine: "quality" }
- { name: "invoice", template: "standard-invoice", engine: "quality" }
- { name: "summary", template: "exec-summary", engine: "fast" }
max-parallel: 3 # Run up to 3 jobs simultaneously
steps:
- uses: actions/checkout@v4
- name: Generate ${{ matrix.document.name }} PDF
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
HTML=$(cat "templates/${{ matrix.document.template }}.html" | jq -Rs .)
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"html\": $HTML, \"engine\": \"${{ matrix.document.engine }}\", \"format\": \"A4\"}" \
--output "${{ matrix.document.name }}.pdf"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.document.name }}-pdf
path: ${{ matrix.document.name }}.pdf
Tip 3: Notify Slack on Failure
- name: Notify Slack on failure
if: failure()
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
run: |
curl -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"PDF generation workflow failed\",
\"blocks\": [
{
\"type\": \"section\",
\"text\": {
\"type\": \"mrkdwn\",
\"text\": \"*PDF Generation Failed*\n*Repository:* ${{ github.repository }}\n*Workflow:* ${{ github.workflow }}\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|#${{ github.run_number }}>\"
}
}
]
}"
Tip 4: Retry Logic for Transient API Errors
# retry.sh
#!/bin/bash
set -euo pipefail
MAX_RETRIES=3
RETRY_DELAY=5
generate_pdf() {
local attempt=1
while [ $attempt -le $MAX_RETRIES ]; do
echo "Attempt ${attempt}/${MAX_RETRIES}..."
if curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"html\": \"$1\", \"engine\": \"quality\", \"format\": \"A4\"}" \
--output output.pdf \
--fail \
--silent \
--show-error; then
echo "PDF generated successfully on attempt ${attempt}."
return 0
fi
echo "Attempt ${attempt} failed."
if [ $attempt -lt $MAX_RETRIES ]; then
echo "Retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
RETRY_DELAY=$((RETRY_DELAY * 2)) # Exponential backoff
fi
attempt=$((attempt + 1))
done
echo "All ${MAX_RETRIES} attempts failed."
return 1
}
generate_pdf "<h1>Hello World</h1>"
For error handling patterns, see the error handling guide.
Tip 5: Use Caching to Speed Up Node.js Dependencies
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Caches based on package-lock.json hash
- run: npm ci # More reproducible than npm install
Tip 6: Set Job and Step Timeouts
Set timeouts on both the job and individual steps so PDF generation never hangs indefinitely.
jobs:
generate:
runs-on: ubuntu-latest
timeout-minutes: 15 # Entire job timeout
steps:
- name: Generate large PDF
timeout-minutes: 5 # This step's timeout
env:
FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
run: |
curl --max-time 120 \ # curl timeout in seconds
-X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
...
Production Workflow Checklist
Review this before taking your workflow to production.
- API key is registered as the
FUNBREW_API_KEYsecret - No API keys are hardcoded in code or workflow files
-
permissionsare scoped to the minimum needed (contents: writeonly where required) - Timeouts are set on both jobs and individual steps
- PDF file size is validated after generation
- Failure triggers a Slack notification with a link to the run
-
retention-daysis set appropriately (short for test artifacts, longer for important documents) - cron schedules account for the UTC offset when targeting a local time zone
For general production operation guidance, see the production guide.
Summary
With GitHub Actions and FUNBREW PDF API, you can fully integrate PDF generation into your CI/CD pipeline.
- Secrets management: Register the API key in GitHub Secrets and reference it with
${{ secrets.FUNBREW_API_KEY }} - HTML to PDF: Use curl or a Node.js script to dynamically build HTML and call the API
- Markdown to PDF: Convert git logs and CHANGELOG.md to PDF and attach them to releases
- Artifacts: Control retention and compression with
actions/upload-artifact@v4 - cron: Set schedules in UTC and automate weekly and monthly report generation
- Parallel execution: Use matrix and max-parallel to generate multiple PDFs simultaneously
Get started by creating a free account and testing the API in the Playground.
Related
- FUNBREW PDF API Docs — Endpoint reference and all available options
- Quickstart by Language — Node.js, Python, PHP code examples
- Markdown to PDF API Guide — Convert Markdown to HTML and generate PDFs
- PDF API Error Handling — Retry strategies and failure handling in CI/CD
- Webhook Integration Guide — Push notifications on PDF job completion
- PDF API Production Guide — Security and monitoring for production
- PDF API Security Guide — API key management and access control best practices
- Batch PDF Generation — Parallel generation of multiple PDFs
- AWS Lambda Serverless Guide — Combining Lambda with GitHub Actions
- PDF Report Generation Guide — Automating periodic reports
- Use Cases — Common business scenarios for CI/CD-based PDF generation