GitHub Actions で画像を圧縮する
誰かが assets/ に 4 MB のヒーロー画像を置き、誰も気づかないまま二週間後に
Lighthouse のスコアが落ちる。直し方はコードレビューのチェックリストを増やすことでは
なく、公開される前に画像を小さくするステップをパイプラインに入れることだ。
これが全体像:
# .github/workflows/optimize-images.yml
name: Optimize images
on:
pull_request:
paths:
- 'assets/**.png'
- 'assets/**.jpg'
- 'assets/**.jpeg'
- 'assets/**.webp'
- 'assets/**.avif'
permissions:
contents: write
jobs:
compress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Compress
env:
PIPIC_TOKEN: ${{ secrets.PIPIC_TOKEN }}
run: npx @pipic/cli ./assets --replace --json
- name: Commit if anything shrank
run: |
if git diff --quiet; then
echo "Nothing to commit — images were already optimal."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "chore: compress images"
git push
ここで paths フィルターは実際に仕事をしている。これがないとジョブはすべての
プルリクエストで走り、毎回 assets/ ディレクトリ全体を歩くことになる——それがなぜ
問題なのかは後述する。
トークン
CI にブラウザはないので pipic login は選択肢に入らない。
アカウントページでトークンを作り、PIPIC_TOKEN という名前
のリポジトリシークレットとして保存すれば、CLI が環境変数から拾ってくれる——認証情報
ファイルより優先されるので、ログインという手順自体が存在しない。
pipic token create があえてターミナルで発行せずウェブページへ誘導するのは意図的だ。
盗まれた CI 認証情報が、新しい認証情報を発行できてはならない。
省略できないフラグ
--replace はその場で上書きし、-o <dir> は別の場所へ書き出す。両者は排他で、
どちらか一方が必須だ——npx @pipic/cli ./assets をどちらもなしで実行すると、
リクエストを一つも送る前に使い方エラーで終了する。
結果をコミットし返すワークフローで欲しいのは --replace のほうだ。作業ツリーの
ファイルが、そのままこれからコミットするファイルになる。
結果を読む
--json はファイルごとに NDJSON オブジェクトを 1 行ずつ出力する。ワークフローの中では
たいていパースする必要はない——変化があったかどうかは git diff --quiet が既に教えて
くれる——が、ジョブログには残る。想定外の結果が出たときに見るのはそこだ:
{"file":"assets/hero.png","status":"ok","before":2411233,"after":486201,"saved":1925032}
{"file":"assets/logo.webp","status":"skipped","before":18944,"after":18944,"saved":0,"error":"not smaller — original kept"}
skipped は失敗ではない。結果が実際に小さくなったときだけ書き戻すので、すでに最適化
済みの画像は理由付きで skipped を報告する——そして git diff はそのファイルについて
きれいなままだ。
作り込むべきは終了コードのほうだ:
| コード | 意味 | ワークフローでは |
|---|---|---|
| 0 | すべて成功 | ステップは通る |
| 1 | 一部のファイルが失敗 | ステップは失敗——どれかはログで確認 |
| 2 | 使い方エラー | コマンドが誤り。ワークフローを直す |
| 3 | サインインが必要 | PIPIC_TOKEN が無い・誤り・失効 |
| 4 | 月間クォータ超過 | 下記参照 |
CI での 3 はほぼ必ず、シークレットがそのステップに届いていないという意味だ——
env: ブロックのタイプミスか、fork からのプルリクエストで参照できないシークレットか。
クォータの計算
ここでつまずく人が多いので率直に書く:月間の割り当ては、変化したファイル数ではなく リクエスト数を数える。 CLI がサーバーに渡した画像は、「もう十分小さい」と返ってきた ものも含めて 1 回ずつ消費する。
以前に何を圧縮したかというローカルの記憶はない。ディレクトリを指定すれば、そのディレ クトリの中身を毎回すべて処理する。
つまり 200 枚の assets/ フォルダをすべてのプルリクエストで歩くジョブは、1 回の実行で
月間割り当てを 200 消費する——無料枠は月 100 だ。実務的な結論は二つ:
pathsフィルターは絞る。 画像に変更がないときにジョブが走らなくなる。そして それが大半の場合だ。- コマンドは可能な限り狭いディレクトリに向ける。 変更が集中しているのが
./assets/heroesなら、./assetsよりそちらを指す。
そのプルリクエストで変更された画像だけを対象にしたいなら、ディレクトリを渡すのでは なく明示的にファイルを渡す:
- name: Compress changed images
env:
PIPIC_TOKEN: ${{ secrets.PIPIC_TOKEN }}
run: |
FILES=$(git diff --name-only --diff-filter=d origin/${{ github.base_ref }}... \
-- 'assets/**.png' 'assets/**.jpg' 'assets/**.jpeg' 'assets/**.webp' 'assets/**.avif')
if [ -z "$FILES" ]; then
echo "No image changes."
exit 0
fi
echo "$FILES" | xargs npx @pipic/cli --replace --json
これで実行ごとの固定コストが、差分に比例するコストに変わる。ほとんどのリポジトリでは 数ファイル分でしかない。
終了コード 4 はその月の割り当てを使い切ったという意味だ。自動的な課金は一切ない—— Pro は割り当てを 5,000 に引き上げるが、pipic.cc 上での 圧縮はどちらにしても無料・無制限のままだ。
このワークフローがしないこと
プルリクエストをブロックはしない。「チェックだけして未最適化なら失敗」というモードは
ない——CLI は圧縮するだけで、その結果をどう扱うかはあなたが決める。厳格なゲートが
欲しいなら形はこうなる:圧縮を実行し、git diff が空でなければステップを失敗させ、
最適化済みのファイルは作者自身にコミットさせる。
- name: Fail if images weren't optimised
run: |
git diff --quiet || {
echo "::error::These images can be compressed further. Run pipic locally and commit."
git diff --stat
exit 1
}
これは利便性と引き換えに制御を得る取引だ。作者の名前で何かがコミットされることはない が、代わりに誰かがその作業をしに行く必要がある。