calibreapp/image-actions and the pull request that shrinks your images
A Github Action that automatically compresses JPEGs, PNGs, WebPs & AVIFs in Pull Requests.
At a glance
- What is it?
- Image Actions is a GitHub Action that compresses JPEG, PNG, WebP and AVIF files and commits the results back into an open pull request. The design is small and opinionated, its sharp based compression is close to the same work a local build step would do, and it has one hard limitation, it cannot write to a forked repository.
- Who is it for?
- Adopt calibreapp/image-actions if your repository stores images in version control and you want every contributor to get compressed assets without changing their local workflow. Do not adopt it if your images are generated at build time, if the visual fidelity of an asset matters more than its weight, or if your pull requests regularly come from forks, since the action cannot write there without a personal access token.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The action edits the branch, not a build artifact
Image Actions is a GitHub Action built by Calibre, a web performance monitoring service. Its job is narrow: for every pull request, it compresses JPEGs, PNGs, WebPs and AVIFs and commits the optimised versions back to the branch, then posts a summary comment describing what changed. The README describes it as running on demand or on a schedule, and notes that it supports GitHub Enterprise.
The interesting design decision is where the compression happens. Most teams compress images in a build step, in a pre-commit hook, or in a dedicated asset pipeline. This action instead runs in the pull request, on the GitHub runner, and writes the result into the branch the author is proposing to merge. The author sees the smaller files in their own diff before review, and the review conversation happens over the compressed version rather than over a preview.
The cost of that choice is that the action needs write access to your repository, and it needs to run on a runner that can execute it. It also means the compression is a commit in your history rather than a transformation at deploy time, so the savings are permanent in the tree and visible in every future clone.
The technology underneath is sharp, which the README links to libvips as the underlying compression engine, with the compression described as fast, efficient and near-lossless. AVIF support is the most recent addition of substance, arriving with the 1.4.0 release in August 2025.
The workflow file is the whole installation
There is nothing to install. You add a workflow file to your repository, and the README gives the exact contents to copy. The trigger list decides which pull requests wake the action up, limited by path to the image extensions you care about, and a workflow_dispatch entry adds a manual trigger:
name: Compress Images
on:
pull_request:
paths:
- '**.jpg'
- '**.jpeg'
- '**.png'
- '**.webp'
- '**.avif'
workflow_dispatch:
jobs:
build:
if: github.event.pull_request.head.repo.full_name == github.repository
name: calibreapp/image-actions
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Compress PR Images
uses: calibreapp/image-actions@mainTwo lines in that file carry the weight. The if condition restricts the job to pull requests whose head branch lives in the same repository as the destination, which is how the action avoids touching anything it has no permission to touch. And permissions: write-all grants the job the token scope it needs to push a commit back to the branch.
Pointing at calibreapp/image-actions@main means you run whatever is on the main branch at the moment the workflow runs. Pinning a release tag is the safer choice if your team cares about reproducibility, and releases exist for that purpose, with 1.5.0 published on 2026-05-18 and 1.4.1 on 2025-09-10.
Quality settings are strings, and the defaults are opinionated
Every compression knob is passed through the with block, and most of them are strings, which is a small friction point worth knowing before you write your first workflow. The README lists the options with their ranges and defaults:
with:
jpegQuality: '85'
jpegProgressive: false
pngQuality: '80'
webpQuality: '85'
avifQuality: '75'jpegQuality, pngQuality, webpQuality and avifQuality are integers from 1 to 100 stored as strings, defaulting to 85, 80, 85 and 75 respectively. jpegProgressive is a boolean, default false, and turns on progressive rendering for JPEGs.
Those defaults encode an editorial decision about how much quality you are willing to trade for size, and they are applied without asking. A team with a large archive of screenshots that are already well compressed will see little change, while a team with photographic hero images will notice the difference immediately and should probably start by raising jpegQuality rather than by accepting the default and regretting it later. The same applies to AVIF, where 75 is a fairly aggressive starting point for a format that is still relatively new.
Paths can be excluded with ignorePaths, which takes comma separated minimatch globs:
with:
ignorePaths: 'node_modules/**,build'A 5 percent and 1 KB threshold decides what gets committed
By default the action commits an optimised image only when it is at least 5 percent smaller than the original and the reduction is at least 1 KB. Both thresholds are configurable, minPctChange and minAbsChange, and both take a numeric value represented as a string, with defaults of '5' and '1024':
with:
minPctChange: '2.5'
minAbsChange: '10000'The README explains why you would want to raise them, and the reasoning is good. Loosening the percentage guard makes it easier for the same WebP file to be compressed again on the next run, because re-encoding an already optimised file often still shaves off a little. The absolute guard does the same job for small PNGs, where a proportional change is easy to hit but the absolute saving is negligible and the churn in git history is not.
This is a nice piece of engineering for a small tool, and it also tells you something about how the action is expected to be used. It is not a one-off cleanup tool but a repeating process, and the thresholds are there to keep that process from producing an endless series of near-identical commits.
There is also a compressOnly option, taking a boolean that defaults to false. With it set to true the action skips the commit and the summary comment, which is what makes the scheduled and manual patterns work.
Forked pull requests are the wall you will hit
By default GitHub Actions cannot alter forked repositories, so Image Actions only works for pull requests whose branch is in the same repository as the destination branch. The README is upfront about this and lists workarounds, the first being to replace the default GITHUB_TOKEN with a personal access token that has the necessary permissions.
This is the sharpest limitation of the project, and it is not a bug so much as a consequence of how GitHub models trust. A workflow that runs code from a fork runs untrusted code, and the platform does not hand out write tokens to it. Any action in this category, not just this one, has the same problem.
The second workaround is the one that requires no extra credential. Setting compressOnly to true makes the action compute the optimised images and expose the results without committing them, which is exactly what the scheduled workflow pattern relies on. In that pattern the action runs with compressOnly, and a separate step checks whether the markdown output is non-empty before opening a pull request with the create-pull-request action from peter-evans:
- name: Create New Pull Request If Needed
if: steps.calibre.outputs.markdown != ''
uses: peter-evans/create-pull-request@v4
with:
title: Compressed Images Nightly
branch-suffix: timestamp
commit-message: Compressed Images
body: ${{ steps.calibre.outputs.markdown }}The pattern is worth copying even if you never schedule anything, because it separates the detection of what could be compressed from the act of writing it, and it works the same way for forks.
A Docker image and a TypeScript entrypoint for the runner
The action.yml at the root declares the action, and the implementation is TypeScript under src/ with entrypoint.ts as the entry point, as the package manifest also records. The dependencies tell you what it actually does: sharp for the image work, glob for finding files, ejs for the summary comment templates in markdown-templates/, p-throttle for pacing, and the GitHub packages @actions/core, @actions/github and @octokit/action for talking to the platform.
Because the action runs on a GitHub runner, it ships as a container. The Dockerfile starts from node:lts-alpine, installs with npm ci, copies the sources and the markdown templates, and starts the action with node using the experimental strip types flag so the TypeScript entry point runs without a separate build step:
ENTRYPOINT ["node", "--experimental-strip-types", "/usr/local/src/image-actions/entrypoint.ts"]The development setup is equally container aware. The test script points vitest at a fixture directory by setting GITHUB_WORKSPACE to __tests__/test-images, there is a docker-compose.test.yml for running the suite inside a container, and docker:build tags an image as image-actions:latest. The check script chains the typecheck, lint, test and container test into one command:
npm run checkThe repository also carries a CODE_OF_CONDUCT, a CONTRIBUTING file, a .prettierignore and an eslint config, which is the expected shape for a project inviting outside contributions.
Two licences in the same repository, and releases every few months
There is an inconsistency worth flagging. The repository is licensed under GPL-3.0, and the LICENSE file and the README badge both say so. The package manifest, however, declares ISC. Those are very different terms, and the GPL-3.0 one is the stricter of the two, with copyleft obligations on redistribution of modified versions.
For most consumers of a GitHub Action the practical question is what happens when you use it in your own repository, and the honest answer is that the documentation does not resolve which licence governs the action code. The README points at the LICENSE file, so a team that cares should read that file rather than the package manifest, and anyone shipping a modified version of the action into a closed source product should get the question answered before doing so.
On maintenance, the repository is not archived and the last push was on 2026-10-12. The release history is steady rather than fast, with 1.5.0 on 2026-05-18, 1.4.1 on 2025-09-10 and 1.4.0, the release that added AVIF, on 2025-08-28. That is a project that changes when it needs to rather than on a fixed schedule, and it is a reasonable signal of the maintenance budget behind a small but useful action.
Editorial conclusion
Adopt calibreapp/image-actions if your repository stores images in version control and you want every contributor to get compressed assets without changing their local workflow. Do not adopt it if your images are generated at build time, if the visual fidelity of an asset matters more than its weight, or if your pull requests regularly come from forks, since the action cannot write there without a personal access token. Verify three things first: that the quality settings suit your assets, because the defaults are 85 for JPEG and WebP, 80 for PNG and 75 for AVIF, and that your repository accepts commits from the action, which needs permissions write-all. The last push was on 2026-10-12 and the newest release is 1.5.0 from 2026-05-18, so pin the release tag rather than main.
Frequently asked questions
What does the calibreapp/image-actions action do to my pull requests?
It compresses JPEG, PNG, WebP and AVIF files, commits the optimised versions back to the pull request branch and posts a summary comment. By default it only processes new or updated images, while manual, scheduled and API failure runs process all images.
How do I add the image compression action to my repository?
Create .github/workflows/calibreapp-image-actions.yml with a pull_request trigger filtered by image paths, a workflow_dispatch trigger, the condition that the head repository equals the current repository, permissions: write-all, a checkout step and a step using calibreapp/image-actions@main.
Why did the action not commit anything for my image?
By default a commit only happens when the optimised file is at least 5 percent smaller and the saving is at least 1 KB. You can lower or raise those with the minPctChange and minAbsChange options.
Does calibreapp/image-actions work on pull requests from forks?
Not by default, because GitHub Actions cannot write to a forked repository. The README lists a personal access token as one workaround, and the compressOnly option as another, since it computes the results without committing them.
Can I change the compression quality in calibreapp/image-actions?
Yes, through the with block using jpegQuality, jpegProgressive, pngQuality, webpQuality and avifQuality, the quality values stored as strings, plus ignorePaths for minimatch globs to exclude. The defaults are 85, false, 80, 85 and 75.
What licence is calibreapp/image-actions released under?
The repository and its README badge state GPL-3.0, while the package manifest declares ISC. The two do not agree, so check the LICENSE file in the repository before redistributing a modified version.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/calibreapp-image-actions)Was this useful?