All guides

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 via optimized: 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

InputDefaultNotes
patterns**/*.{png,jpg,jpeg,webp,avif}Narrow this to your asset directories to save operations
modecheckwrite rewrites files that get smaller
min_savings_bytes128Raise it to skip trivial rewrites that churn git history
quality_modeAPI default (balanced)best_quality or lossless; lossless is rejected for JPEG
fail_on_errortrueSet 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.