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 commitin 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: writeto 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.