Two common CI pipeline needs: (1) convert Markdown/DOCX documentation to PDF and commit it alongside the source, (2) convert raw assets (HEIC photos, WAV recordings, PNG screenshots) into web-ready formats as part of a build step. Both patterns use the same ChangeThisFile API call — the difference is just what triggers the workflow and where the output files go.

TL;DR

  • Add secret: GitHub repo → Settings → Secrets and variables → Actions → CTF_API_KEY
  • API call: curl POST with -F file=@input.docx -F target=pdf
  • No system deps: API handles FFmpeg, LibreOffice, etc. server-side
  • Commit output: use git add + git commit in the workflow to save converted files to the repo

Pattern 1: Convert documentation to PDF on every commit

Useful for repos that maintain PDF exports of their docs alongside Markdown source.

# .github/workflows/docs-to-pdf.yml
name: Convert docs to PDF

on:
  push:
    branches: [main]
    paths:
      - 'docs/**/*.md'
      - 'docs/**/*.docx'

jobs:
  convert:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Convert all docs to PDF
        env:
          CTF_API_KEY: ${{ secrets.CTF_API_KEY }}
        run: |
          mkdir -p docs/pdf
          for doc in docs/*.md docs/*.docx; do
            [ -f "$doc" ] || continue
            base=$(basename "${doc%.*}")
            echo "Converting: $doc"
            curl -s -X POST https://changethisfile.com/v1/convert \
              -H "Authorization: Bearer $CTF_API_KEY" \
              -F "file=@$doc" \
              -F "target=pdf" \
              --output "docs/pdf/${base}.pdf"
            echo "Done: docs/pdf/${base}.pdf ($(du -h docs/pdf/${base}.pdf | cut -f1))"
          done

      - name: Commit converted PDFs
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add docs/pdf/
          git diff --staged --quiet || git commit -m "chore: update PDF exports [skip ci]"
          git push

Pattern 2: Convert raw assets to web formats

Useful for repos where designers commit HEIC/PNG source assets and the CI produces web-ready WebP/JPG.

# .github/workflows/optimize-assets.yml
name: Optimize assets

on:
  push:
    branches: [main]
    paths:
      - 'assets/raw/**'

jobs:
  optimize:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Convert images to WebP
        env:
          CTF_API_KEY: ${{ secrets.CTF_API_KEY }}
        run: |
          mkdir -p assets/web
          for img in assets/raw/*.heic assets/raw/*.png assets/raw/*.jpg; do
            [ -f "$img" ] || continue
            base=$(basename "${img%.*}")
            curl -s -X POST https://changethisfile.com/v1/convert \
              -H "Authorization: Bearer $CTF_API_KEY" \
              -F "file=@$img" \
              -F "target=webp" \
              --output "assets/web/${base}.webp"
          done

      - name: Commit web assets
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add assets/web/
          git diff --staged --quiet || git commit -m "chore: update web assets [skip ci]"
          git push

Pattern 3: Python script for complex workflows

When you need error handling, retry logic, or conditional conversions:

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install requests

      - name: Convert files
        env:
          CTF_API_KEY: ${{ secrets.CTF_API_KEY }}
        run: python3 scripts/convert_assets.py
# scripts/convert_assets.py
import requests
import os
import sys
from pathlib import Path

API_KEY = os.environ["CTF_API_KEY"]

def convert(src: Path, target: str, out: Path, retries: int = 3) -> bool:
    for attempt in range(retries):
        try:
            with open(src, "rb") as f:
                resp = requests.post(
                    "https://changethisfile.com/v1/convert",
                    headers={"Authorization": f"Bearer {API_KEY}"},
                    files={"file": f},
                    data={"target": target},
                    timeout=120,
                )
            resp.raise_for_status()
            out.write_bytes(resp.content)
            print(f"OK  {src.name} → {out.name} ({out.stat().st_size//1024}KB)")
            return True
        except Exception as e:
            print(f"ERR attempt {attempt+1}/{retries}: {src.name}: {e}")
    return False

# Convert all HEIC in assets/raw to WebP in assets/web
errors = 0
out_dir = Path("assets/web")
out_dir.mkdir(exist_ok=True)

for heic in sorted(Path("assets/raw").glob("*.heic")):
    out = out_dir / (heic.stem + ".webp")
    if not convert(heic, "webp", out):
        errors += 1

if errors:
    print(f"\n{errors} conversion(s) failed")
    sys.exit(1)

Caching: skip already-converted files

For large asset repos, skip files whose outputs already exist and are up-to-date:

# Only convert if source is newer than existing output
for img in assets/raw/*.png; do
  [ -f "$img" ] || continue
  base=$(basename "${img%.*}")
  out="assets/web/${base}.webp"
  # Skip if output exists and is newer than source
  if [ -f "$out" ] && [ "$out" -nt "$img" ]; then
    echo "Skip: $base.webp is up-to-date"
    continue
  fi
  echo "Converting: $img"
  curl -s -X POST https://changethisfile.com/v1/convert \
    -H "Authorization: Bearer $CTF_API_KEY" \
    -F "file=@$img" -F "target=webp" --output "$out"
done

Edge cases and gotchas

  • [skip ci] in commit message. Always add [skip ci] to automated commits. Otherwise, the CI commit triggers another run which triggers another commit — infinite loop.
  • Permissions for pushing from Actions. The default GITHUB_TOKEN needs write permission. In repo Settings → Actions → General, set Workflow permissions to "Read and write permissions". Or add permissions: contents: write to the job.
  • Large file counts and API rate limits. Free tier is 1,000 conversions/month. A repo with 500 images converted on every push will hit the limit quickly. Use the cache check pattern to only convert changed files.
  • Binary files in git. Committing large binary files (PDFs, WebP images) to the repo inflates clone size over time. Consider using Git LFS for converted assets or storing them in a separate artifact storage.
  • curl exit code. curl returns 0 even on HTTP 4xx errors by default. Add --fail-with-body (curl 7.76+) or check the response status explicitly to fail the workflow on API errors.

Complete production-ready workflow

# .github/workflows/convert-assets.yml
name: Convert assets

on:
  push:
    branches: [main]
    paths: ['assets/raw/**']
  workflow_dispatch:  # allow manual trigger

permissions:
  contents: write

jobs:
  convert:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Convert changed files only
        env:
          CTF_API_KEY: ${{ secrets.CTF_API_KEY }}
        run: |
          set -e
          mkdir -p assets/web
          converted=0
          failed=0

          for img in assets/raw/*.{png,jpg,jpeg,heic,HEIC}; do
            [ -f "$img" ] || continue
            base=$(basename "${img%.*}")
            out="assets/web/${base}.webp"
            [ -f "$out" ] && [ "$out" -nt "$img" ] && { echo "Skip: $base"; continue; }

            if curl -s --fail-with-body \
              -X POST https://changethisfile.com/v1/convert \
              -H "Authorization: Bearer $CTF_API_KEY" \
              -F "file=@$img" -F "target=webp" \
              --output "$out"; then
              echo "OK: $base.webp ($(du -h $out | cut -f1))"
              converted=$((converted+1))
            else
              echo "FAIL: $img"
              failed=$((failed+1))
            fi
          done

          echo "Converted: $converted, Failed: $failed"
          [ $failed -eq 0 ] || exit 1

      - name: Commit outputs
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add assets/web/
          git diff --staged --quiet || git commit -m "chore: update web assets [skip ci]" && git push

GitHub Actions + the ChangeThisFile API is a clean combination — no system lib installs, works on any ubuntu-latest runner, and your API key stays in secrets. The production workflow above handles incremental conversion, error detection, and commit-back in one step. Get a free API key to start.