GitHub Action

set it up once.
forget about it.

a GitHub Action that automatically optimizes images on every PR. detects changed files, converts in-place, commits them back if you want. you literally don't have to think about images again.

what it does

1detects which image files (.jpg, .jpeg, .png, .webp, .avif) were added or changed in the PR.
2converts them in-place using npx leano — no Docker, no pre-installed tools.
3reports savings via action outputs and summary logs.
4optionally commits the optimized images back to your branch. you can just merge.

quick start — copy and paste this

# .github/workflows/optimize-images.yml
name: Optimize Images

on:
  pull_request:
    paths:
      - '**.jpg'
      - '**.jpeg'
      - '**.png'
      - '**.webp'
  push:
    branches: [master]
    paths:
      - '**.jpg'
      - '**.jpeg'
      - '**.png'
      - '**.webp'

jobs:
  optimize:
    runs-on: ubuntu-latest
    # contents: write is only needed when commit-back: true
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Optimize images
        uses: meowbeen/leano-action@v1
        with:
          format: webp
          quality: 82
          commit-back: true
          token: ${{ secrets.GITHUB_TOKEN }}

inputs

InputDefaultDescription
formatwebpOutput format: webp | avif | both
quality80Compression quality (1–100)
losslessfalseEnable lossless compression
max-widthMaximum output width in pixels
max-heightMaximum output height in pixels
paths.Comma-separated directories to process
changed-onlytrueOnly process images changed in this PR or push
commit-backfalsePush a bot commit with optimized images back to the branch
commit-messagechore: optimize images [leano]Commit message when commit-back is true
token${{ github.token }}GitHub token — only needed when commit-back: true

outputs

OutputDescription
files-convertedNumber of image files converted
bytes-savedTotal bytes saved
savings-percentOverall size reduction as a percentage

using the outputs:

- uses: actions/checkout@v4
  with:
    fetch-depth: 2

- name: Optimize images
  id: optimize
  uses: meowbeen/leano-action@v1

- name: Print savings
  run: |
    echo "Converted: ${{ steps.optimize.outputs.files-converted }} files"
    echo "Saved:     ${{ steps.optimize.outputs.bytes-saved }} bytes"
    echo "Savings:   ${{ steps.optimize.outputs.savings-percent }}%"

more examples

convert changed files and commit them back

jobs:
  optimize:
    permissions:
      contents: write   # needed for commit-back

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: meowbeen/leano-action@v1
        with:
          format: webp
          quality: 80
          changed-only: true
          commit-back: true
          token: ${{ secrets.GITHUB_TOKEN }}

scan a full directory on every push

- uses: meowbeen/leano-action@v1
  with:
    format: avif
    quality: 70
    paths: public/images,assets
    changed-only: false
    commit-back: true
    commit-message: 'chore(images): convert to AVIF'
    token: ${{ secrets.GITHUB_TOKEN }}

stuff worth knowing

use fetch-depth: 2, not 0

push events diff HEAD~1..HEAD, which needs exactly 2 commits. full history (depth 0) can add 30–60s on large repos. not worth it.

commit-back defaults to false

when you turn it on, a bot commit appears in your PR history. add a path filter on your on: trigger so the bot commit doesn't re-trigger the action and loop forever.

paths works differently depending on changed-only

changed-only: true — paths is a scope filter. only changed images inside those dirs are processed.
changed-only: false — paths is the scan target. full recursive convert of everything there.

prefer running it yourself?

the CLI does the same thing from your terminal — no CI required.

View CLI docs