NaN/NaN/NaN

リリースノート、テストレポート、請求書、ドキュメント——これらをCI/CDパイプラインで自動生成できれば、手作業のミスをなくし、レビューのたびに最新のPDFが手元に揃います。

この記事では、FUNBREW PDFのAPIをGitHub Actionsから呼び出して、PDF生成を完全に自動化する方法を解説します。単純なHTML→PDF変換から、スケジュール実行による定期レポート生成まで、実践的なワークフロー例を豊富なコードとともに紹介します。

API自体の基本的な使い方はクイックスタートガイドを参照してください。

CI/CDでPDFを自動生成するユースケース

GitHub Actionsでのデプロイフローにプルできるユースケースは多岐にわたります。

ユースケース トリガー 出力
リリースノート タグのプッシュ GitHub Release添付PDF
PRチェンジログ PR作成・更新 アーティファクトPDF
週次レポート cron(毎週月曜) Slackに送信
請求書生成 PR承認後 ストレージ保存
APIドキュメント mainへのマージ デプロイ成果物
テスト結果サマリ テスト完了後 ダウンロード可能PDF

CI/CDに限らず、PDF生成全般のユースケースはユースケース一覧も参考にしてください。

準備:APIキーのSecrets管理

GitHub SecretsへのAPIキー登録

FUNBREW PDFのAPIキーは、絶対にコードやワークフローファイルに直接書かないでください。GitHub Secretsに登録して、ワークフロー内で環境変数として参照します。

# GitHub CLIでシークレットを登録(ローカルから実行)
gh secret set FUNBREW_API_KEY --body "sk-your-api-key-here"

# または、リポジトリのSettings > Secrets and variables > Actions から手動で登録

ワークフローでのSecrets参照

jobs:
  generate-pdf:
    runs-on: ubuntu-latest
    steps:
      - name: Generate PDF
        env:
          FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
        run: |
          # $FUNBREW_API_KEY が安全に展開される
          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 }} はログに出力されることなく、ステップの環境変数として安全に渡されます。APIキーのセキュリティについてはセキュリティガイドで詳しく解説しています。

Organization Secretsと環境変数の使い分け

複数リポジトリで同じAPIキーを共有する場合、Organization Secretsが便利です。

# Organization Secretsを使う場合も、参照方法は同じ
env:
  FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}

環境(development / staging / production)ごとに異なるAPIキーを使いたい場合は、GitHub Environmentsを使います。

jobs:
  generate-pdf:
    runs-on: ubuntu-latest
    environment: production  # この環境のSecretsが参照される
    steps:
      - name: Generate PDF
        env:
          FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
        run: |
          # 本番環境用のAPIキーが使われる

HTML→PDFワークフロー実装例

基本:curlでHTML→PDF変換

最もシンプルな構成です。HTMLを直接ワークフロー内で組み立て、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: |
          # HTMLファイルをJSONにエスケープして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スクリプトで動的HTML生成

データを取得してHTMLを動的に組み立て、PDFに変換するパターンです。より複雑なテンプレートに向いています。

# .github/workflows/dynamic-pdf.yml
name: Dynamic PDF Generation

on:
  workflow_dispatch:
    inputs:
      report_type:
        description: 'レポートの種類'
        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');
  }

  // レポートデータを組み立て(実際はDBや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="ja">
    <head>
      <meta charset="UTF-8">
      <style>
        body {
          font-family: 'Noto Sans JP', 'Helvetica Neue', 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>生成日時: ${data.generatedAt}</p>
      <div class="metrics">
        <div class="metric-card">
          <div class="metric-value">${data.metrics.totalRequests.toLocaleString()}</div>
          <div class="metric-label">総リクエスト数</div>
        </div>
        <div class="metric-card">
          <div class="metric-value">${data.metrics.successRate}%</div>
          <div class="metric-label">成功率</div>
        </div>
        <div class="metric-card">
          <div class="metric-value">${data.metrics.avgResponseTime}s</div>
          <div class="metric-label">平均レスポンス時間</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);
});

HTMLテンプレートの設計についてはテンプレートエンジンガイドを参照してください。

Markdown→PDFワークフロー実装例

PRのチェンジログをPDFに変換

PRに含まれるコミットメッセージやCHANGELOGをMarkdown→PDFに変換してアーティファクトとして保存する例です。

# .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  # 全履歴を取得(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

          # コミット一覧を追記
          git log --oneline origin/${BASE}..HEAD --pretty=format:"- %h %s (%an)" >> changelog.md

          echo "" >> changelog.md
          echo "## Files Changed" >> changelog.md
          echo "" >> changelog.md

          # 変更ファイル一覧を追記
          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

ドキュメントをPDFに変換してリリースに添付

タグをプッシュしてリリースを作成するとき、MarkdownのドキュメントをPDFに変換してGitHub Releasesに自動添付します。

# .github/workflows/release-pdf.yml
name: Release with PDF Documentation

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write  # 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 }}"

          # 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)"

Markdown→PDF変換のオプション(テーマ、フォント、余白など)はMarkdown to PDF APIガイドを参照してください。

アーティファクトとしてのPDF保存

actions/upload-artifact の詳細設定

- name: Upload PDF artifacts
  uses: actions/upload-artifact@v4
  with:
    name: generated-pdfs-${{ github.run_number }}
    path: |
      *.pdf
      reports/*.pdf
    retention-days: 90      # 保存期間(デフォルト90日、最大400日)
    compression-level: 0    # PDFは既に圧縮済みのため0が効率的
    if-no-files-found: error  # PDFが生成されなかった場合はエラー

複数のPDFをまとめてアーカイブ

# .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

  # すべてのPDFをひとつのアーティファクトに統合
  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/

バッチ処理でのPDF大量生成についてはバッチ処理ガイドも参考にしてください。

スケジュール実行(cron)の設定

定期レポートをSlackに送信

毎週月曜の朝、週次レポートを自動生成してSlackに送信する例です。Slackとの連携をさらに深めたい場合は、スラッシュコマンドでPDFをオンデマンド生成できるSlack Bot実装ガイドも参照してください。

# .github/workflows/weekly-report.yml
name: Weekly PDF Report

on:
  schedule:
    # 毎週月曜 09:00 JST(= 00:00 UTC)
    - cron: '0 0 * * MON'
  workflow_dispatch:  # 手動実行も可能

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: |
          # 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\": \"週次レポートが生成されました($(date +%Y-%m-%d))\"
            }"

月次請求書の自動生成

月末に請求書を自動生成し、ストレージに保存する例です。

# .github/workflows/monthly-invoices.yml
name: Monthly Invoice Generation

on:
  schedule:
    # 毎月末日 18:00 JST(= 09:00 UTC)
    - 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 2>/dev/null || \
                    date -v+1m -v1d -v-1d +%d)  # macOS互換
          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="ja">
    <head>
      <meta charset="UTF-8">
      <style>
        body { font-family: 'Noto Sans JP', 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>〒000-0000 東京都...<br>Tel: 03-XXXX-XXXX</p>
        </div>
        <div>
          <h1>請求書</h1>
          <p>No. INV-${customer.id}-${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}</p>
        </div>
      </div>

      <p><strong>請求先:</strong> ${customer.name} 御中</p>
      <p><strong>請求日:</strong> ${new Date().toLocaleDateString('ja-JP')}</p>
      <p><strong>お支払い期限:</strong> ${new Date(Date.now() + 30 * 86400000).toLocaleDateString('ja-JP')}</p>

      <table>
        <thead>
          <tr><th>品目</th><th>数量</th><th>単価</th><th>金額</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">合計(税込)</td>
            <td>¥${customer.total.toLocaleString()}</td>
          </tr>
        </tfoot>
      </table>

      <div class="footer">
        <p>お振込先: ○○銀行 △△支店 普通 XXXXXXX</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() {
  // 実際はDBから取得
  const customers = [
    {
      id: 'C001',
      name: '株式会社サンプル',
      total: 110000,
      items: [
        { name: 'FUNBREW PDF Pro プラン', quantity: 1, unitPrice: 100000 },
      ],
    },
  ];

  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);
});

請求書PDFの自動化については請求書PDF自動化ガイドでさらに詳しく解説しています。

実践的なTips

Tips 1: PDFの生成成功を検証する

- name: Validate PDF output
  run: |
    # PDFのサイズが最低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

    # PDFのマジックバイトを確認(%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."

Tips 2: 並列生成でワークフローを高速化

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  # 同時に最大3つ並列実行

    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

Tips 3: エラー時にSlack通知を送る

- 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生成ワークフローが失敗しました\",
        \"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 }}>\"
            }
          }
        ]
      }"

Tips 4: ワークフローの再試行ロジック

API呼び出しが一時的なネットワークエラーで失敗した場合に自動リトライする例です。

# 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))  # 指数バックオフ
    fi

    attempt=$((attempt + 1))
  done

  echo "All ${MAX_RETRIES} attempts failed."
  return 1
}

generate_pdf "<h1>Hello World</h1>"

エラーハンドリングのパターンについてはエラーハンドリングガイドを参照してください。

Tips 5: キャッシュを活用してNode.js依存を高速化

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # package-lock.json のハッシュでキャッシュ

- run: npm ci   # npm install より再現性が高い

Tips 6: ジョブのタイムアウトを設定する

PDF生成が無限に待機しないよう、ジョブとステップの両方にタイムアウトを設定します。

jobs:
  generate:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # ジョブ全体のタイムアウト

    steps:
      - name: Generate large PDF
        timeout-minutes: 5   # このステップのタイムアウト
        env:
          FUNBREW_API_KEY: ${{ secrets.FUNBREW_API_KEY }}
        run: |
          curl --max-time 120 \  # curlのタイムアウト(秒)
            -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
            ...

本番ワークフローのチェックリスト

ワークフローを本番環境に適用する前に確認しましょう。

  • APIキーを FUNBREW_API_KEY シークレットに登録している
  • コードやワークフローファイルにAPIキーをハードコードしていない
  • permissions を最小限に絞っている(contents: write が本当に必要かどうか)
  • ジョブとステップ両方にタイムアウトを設定している
  • PDF生成後にファイルサイズの検証を行っている
  • 失敗時に適切なエラーメッセージとSlack通知を設定している
  • retention-days を用途に応じて設定している(テスト用は短く、重要な成果物は長く)
  • cronトリガーを使う場合、UTCとJSTの時差を考慮している

本番環境での運用全般は本番環境ガイドを参照してください。

まとめ

GitHub ActionsとFUNBREW PDF APIの組み合わせで、PDF生成を完全にCI/CDパイプラインへ組み込めます。

  • Secrets管理: APIキーはGitHub Secretsに登録し、${{ secrets.FUNBREW_API_KEY }} で参照
  • HTML→PDF: curlかNode.jsスクリプトで動的にHTMLを生成してAPI呼び出し
  • Markdown→PDF: git logやCHANGELOG.mdをPDFに変換してリリースに添付
  • アーティファクト: actions/upload-artifact@v4 で保存期間と圧縮を制御
  • cron: UTC基準でスケジュールを設定し、週次・月次レポートを自動生成
  • 並列実行: matrixとmax-parallelで複数PDFを同時生成

まずは無料アカウントを作成して、PlaygroundでAPIの動作を確認してみてください。

関連リンク

Powered by FUNBREW PDF