返回文章列表

在 GitHub Actions 里压缩图片

PiPic 团队
指南
npx @pipic/cli ./assets --replace --json
✓ compressed · committed back to the PR

有人往 assets/ 里丢了一张 4 MB 的头图,没人注意到,直到两周后 Lighthouse 分数掉了 下来。解决办法不是往 code review 清单里再加一条,而是在流水线里加一步——让图片在发布 之前就先瘦下来。

完整的工作流就这些:

# .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 过滤器是在干实事的。没有它,这个 job 会在每个 pull request 上都跑一遍, 而每一次运行都会走完整个 assets/ 目录——为什么这件事要紧,下面会说。

令牌

CI 里没有浏览器,所以 pipic login 这条路走不通。去账号页 建一个令牌,存成名为 PIPIC_TOKEN 的仓库 secret,CLI 会自己从环境变量里读到它—— 它的优先级高于凭据文件,所以根本不存在"要登录"这件事。

注意 pipic token create故意把你送到网页上去建,而不是在终端里直接铸一个出来。 一份被偷走的 CI 凭据,不该有能力再铸出新的凭据。

那个不能省的 flag

--replace 是就地覆盖,-o <dir> 是写到别处。两者互斥,而且必须二选一—— npx @pipic/cli ./assets 两个都不带的话,会在发出任何一个请求之前就以用法错误退出。

在一个要把结果提交回去的工作流里,你要的是 --replace:工作区里的文件就是你接下来要 提交的文件。

读懂输出

--json 会为每个文件打印一行 NDJSON 对象。在工作流里你多半不需要解析它—— git diff --quiet 已经告诉你有没有东西变了——但它会留在 job 日志里,而那正是某次运行 出乎你意料时你要去翻的地方:

{"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,几乎总是意味着 secret 没送到这一步——env: 块里打错了字,或者这个 secret 对来自 fork 的 pull request 不可见。

配额这笔账

这是最容易让人栽跟头的地方,所以直说:每月额度算的是请求数,不是变化的文件数。 CLI 交给服务端的每一张图片都要算一次,包括那些原样返回"已经够小了"的。

本地没有任何"这张压过了"的记忆。你把它指向一个目录,它就处理那个目录里的全部文件, 每一次运行都是。

所以一个在每个 pull request 上都走一遍 200 张图 assets/ 目录的 job,每跑一次就花掉 200 次额度——而免费额度是每月 100 次。两个可操作的结论:

  • paths 过滤器收紧。 它能让这个 job 在没有图片变动时压根不跑,而那是大多数 时候的情况。
  • 把命令指向尽可能窄的目录。 如果改动都发生在 ./assets/heroes,就别写 ./assets

如果你想让这个 job 只碰这个 pull request 里真正改动过的图片,那就把它们逐个传进去, 而不是甩给它一个目录:

      - 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

这就把"每次运行的固定成本"变成了"与 diff 成正比的成本",而在大多数仓库上,那不过是 寥寥几个文件。

退出码 4 表示这个月的额度用完了。不会自动扣任何费用—— Pro 把额度提到 5,000,而在 pipic.cc 网页上压缩无论如何都是 免费且不限量的。

这个工作流不做什么

它不会卡住 pull request。没有"只检查、未优化就失败"这种模式——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
          }

这是拿便利换控制权:没有任何东西会以作者的名义被提交,但总得有个人去把这活干了。

拿你自己的图片试试
免费,无需注册,绝不留存。
开始压缩