Invalid Date

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_KEY secret
  • No API keys are hardcoded in code or workflow files
  • permissions are scoped to the minimum needed (contents: write only 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-days is 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

Powered by FUNBREW PDF