CI automation guide
Compress images in CI: a GitHub Action that only commits real wins
Set up tinify-dev/compress-action to report or commit image savings in GitHub Actions, plus the raw API route for any other CI system.
Uncompressed images do not arrive in a repository through carelessness so much as through repetition: a screenshot pasted into the docs, a hero image swapped by a designer, a marketing PNG committed at 4 MB because the deadline was that afternoon. Manual compression discipline decays. A CI check does not.
This guide sets up automatic image compression in GitHub Actions using tinify-dev/compress-action, then covers the raw API route for every other CI system.
What the action does - and refuses to do
The action finds images matching a glob, sends them to the Tinify.dev API, and either reports or applies the savings. The design constraints matter more than the feature list:
- Files are only rewritten when they actually get smaller, and by at
least
min_savings_bytes(default 128). A byte-identical "optimized" copy is never written; when the API cannot shrink a file it says so viaoptimized: false. - Compression never grows a file. The API returns the original bytes rather than a larger result. (This guarantee is specific to compression - resizing and format conversion can legitimately grow files.)
- No hidden git pushes. The action edits files in the workspace only. Committing is your explicit, visible step.
- Files over the API's 40 MB limit are skipped with a warning, not failed.
Supported formats: PNG, JPEG, WebP, and AVIF.
Step 1: get an API key
Create a key at tinify.dev/developers - the free tier is
500 operations per month with no card - and store it as a repository
secret named TINIFY_API_KEY. Each processed file costs one operation,
so a repo where a handful of images change per week fits comfortably in
the free tier.
Step 2: report savings on every pull request
Start in check mode, which changes nothing and posts a summary:
name: Image check
on: pull_request
permissions:
contents: read
pull-requests: write # only needed for comment: true
jobs:
tinify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: tinify-dev/compress-action@v1
with:
api_key: ${{ secrets.TINIFY_API_KEY }}
mode: check
comment: true
comment: true maintains one sticky PR comment with a per-file results
table, so reviewers see exactly which images could shrink and by how
much before anything is merged.
Step 3: compress and commit automatically
When you trust the results, switch to write mode and pair it with a
commit action:
name: Compress images
on:
push:
branches: [main]
paths: ["**/*.png", "**/*.jpg", "**/*.jpeg", "**/*.webp", "**/*.avif"]
permissions:
contents: write
jobs:
tinify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: tinify-dev/compress-action@v1
with:
api_key: ${{ secrets.TINIFY_API_KEY }}
mode: write
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: compress images with Tinify.dev"
The paths filter keeps the workflow from spending operations on pushes
that touch no images.
Optional: fail PRs that ship uncompressed images
The action exposes outputs, so a hard gate is three lines:
- uses: tinify-dev/compress-action@v1
id: tinify
with:
api_key: ${{ secrets.TINIFY_API_KEY }}
mode: check
- if: steps.tinify.outputs.files_changed != '0'
run: |
echo "Images could save ${{ steps.tinify.outputs.total_saved_bytes }} bytes."
exit 1
Inputs worth tuning
| Input | Default | Notes |
|---|---|---|
patterns | **/*.{png,jpg,jpeg,webp,avif} | Narrow this to your asset directories to save operations |
mode | check | write rewrites files that get smaller |
min_savings_bytes | 128 | Raise it to skip trivial rewrites that churn git history |
quality_mode | API default (balanced) | best_quality or lossless; lossless is rejected for JPEG |
fail_on_error | true | Set false to tolerate individual file failures |
Raising min_savings_bytes is the underrated one. Rewriting a file to
save 200 bytes costs a commit, a diff, and a cache invalidation; a
threshold of a few kilobytes keeps the history quiet.
Other CI systems: the API directly
The action is a thin, honest wrapper - the underlying API works from any CI. With the @tinify-dev/client SDK (source):
import { TinifyClient } from "@tinify-dev/client";
import { writeFile } from "node:fs/promises";
const client = new TinifyClient({ apiKey: process.env.TINIFY_API_KEY });
const result = await client.compress("./hero.png");
if (result.data.optimized !== false) {
const blob = await client.download(result);
await writeFile("./hero.png", new Uint8Array(await blob.arrayBuffer()));
}
Or with nothing but curl:
curl -X POST https://api.tinify.dev/api/v1/images/compress \
-H "Authorization: Bearer $TINIFY_API_KEY" \
-F "file=@hero.png"
The JSON response includes result_bytes, optimized, and a
download_url that expires after two hours - download within the job.
Replicate the two rules that make the GitHub Action safe: skip the write
when optimized is false, and skip it when the saving is too small to
justify a commit.
Costs at CI scale
Operations are counted per processed file. The free tier covers 500 per
month; paid plans are $9/month for 2,000 operations and $29/month for
10,000, with flat overage - the pricing page has the exact
rates. One habit keeps usage low: compress only changed files. Scoped
paths triggers and narrow patterns mean a mature repo consumes
operations only when images actually move.
If CI automation is more than you need, the web compressor handles up to 50 files per batch without an account - sometimes a bookmarked page is the right amount of infrastructure.