optimise-github-actions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOptimise GitHub Actions
优化GitHub Actions
Billed time and wall-clock time are different numbers, and most CI waste lives between them. A 5-way matrix that finishes in 2 minutes bills 10 or more. A 5-second job bills a whole minute. A merge to main can re-run every suite the pull request already passed. So measure first, then cut the biggest items, and keep the checks that guard what you ship.
Speed and cost usually move together, but not always. Caches and removing duplicate work help both. Splitting a long job into parallel shards makes CI faster and more expensive, because each shard pays setup again. Merging small jobs does the reverse. Ask which one the user cares about, or say which way each change goes.
计费时长与墙钟时间是两个不同的指标,绝大多数CI浪费都存在于两者之间。一个5路matrix任务2分钟就能跑完,但计费时长可达10分钟甚至更多。一个仅运行5秒的任务会按整整一分钟计费。合并到主分支时可能会重新运行PR已通过的所有测试套件。因此要先衡量,再砍掉最大的浪费项,同时保留守护发布质量的必要检查。
速度与成本通常同向变化,但并非总是如此。缓存和减少重复工作对两者都有帮助。将一个长任务拆分为并行分片会让CI更快,但成本更高,因为每个分片都需要重新执行初始化步骤。合并小型任务则效果相反。要询问用户更关注哪一方面,或说明每个调整的影响方向。
1. Measure
1. 衡量
Run the script in this skill's folder:
bash
node <skill-dir>/scripts/measure.mjs OWNER/REPO --days 14 --out jobs.jsonIt needs with read access and nothing more. It prints:
gh- total billed minutes, a monthly estimate, the minutes lost to rounding, and cancelled and failed minutes
- one table by workflow and event, and one by job with the share of runs each job ran in
- median and p90 wall-clock time of successful runs
- how many runs each PR branch starts
jobs.jsonTo see where time goes inside a job, print the step timings of one typical run:
bash
gh api repos/OWNER/REPO/actions/runs/RUN_ID/jobs --paginate \
-q '.jobs[] | .name, (.steps[] | select(.started_at) | " \((.completed_at|fromdate)-(.started_at|fromdate))s \(.name)")'The org plan sets the included minutes and whether merge queue is available. Check it with .
gh api orgs/ORG -q .plan.name运行此技能目录下的脚本:
bash
node <skill-dir>/scripts/measure.mjs OWNER/REPO --days 14 --out jobs.json仅需要拥有读取权限的工具,无需其他依赖。脚本会输出:
gh- 总计费分钟数、月度预估、因向上取整损失的分钟数,以及已取消和失败的分钟数
- 按工作流和事件分类的表格,以及按任务分类的表格(含每个任务的运行占比)
- 成功运行的中位数和p90墙钟时间
- 每个PR分支触发的运行次数
jobs.json要查看任务内部的时间消耗,可输出一次典型运行的步骤耗时:
bash
gh api repos/OWNER/REPO/actions/runs/RUN_ID/jobs --paginate \
-q '.jobs[] | .name, (.steps[] | select(.started_at) | " \((.completed_at|fromdate)-(.started_at|fromdate))s \(.name)")'组织套餐决定了包含的免费分钟数以及是否支持合并队列。可通过查看套餐类型。
gh api orgs/ORG -q .plan.name2. Learn what each job protects
2. 明确每个任务的防护范围
Read these before you propose anything.
- Each workflow's triggers, , path filters and
concurrencygraph. The longestneeds:chain is the critical path, and only a shorter critical path makes CI faster.needs: - Required status checks. Run , read each ruleset by id, and run
gh api repos/OWNER/REPO/rulesets. If you rename or remove a required check, every merge blocks.gh api repos/OWNER/REPO/branches/main/protection - Whether "require branches to be up to date" is on. The API calls it . It decides how much a run on main still catches after a merge.
strict - Tests that parse the workflow files. Find them with . Many repos assert the CI layout in unit tests, and those tests fail on the first change.
grep -rln "workflows/" --include='*.test.*' . - What deploys use, such as image tags, release artifacts or a "CI passed" status. CI must still build and check everything a deploy relies on.
在提出任何调整建议前,请先了解以下信息:
- 每个工作流的触发器、配置、路径过滤器和
concurrency依赖图。最长的needs:链就是关键路径,只有缩短关键路径才能真正加速CI。needs: - 必需的状态检查。运行,按ID读取每个规则集,再运行
gh api repos/OWNER/REPO/rulesets。如果重命名或删除了某个必需检查,所有合并都会被阻塞。gh api repos/OWNER/REPO/branches/main/protection - 是否开启了「要求分支保持最新」。API中该配置名为,它决定了合并后主分支的运行还能发现多少问题。
strict - 解析工作流文件的测试。用查找这类测试。很多仓库会在单元测试中校验CI配置结构,一旦修改工作流这些测试就会失败。
grep -rln "workflows/" --include='*.test.*' . - 部署依赖的内容,比如镜像标签、发布制品或「CI通过」状态。CI必须仍会构建和检查部署依赖的所有内容。
3. Find the waste
3. 定位浪费点
Rank by the numbers from step 1. Each pattern below says how to spot it and how to fix it.
Main re-runs the pull request's suites. The same jobs show up under and . If every merge comes from a PR that already passed, keep a fast check and the build and publish steps on push, and drop the heavy suites. Tell the user what they give up. With off, a merge result nobody tested can break main, and the next PR will show it.
pushpull_requeststrictPath filters that match almost every PR. A job that ran in 80 to 100% of PR runs has a filter that filters nothing. Replay the filter over real PR file lists from and count which pattern matches most often. The usual causes are a manifest or lockfile that every PR touches for a version bump, a broad in a narrow job's rule, and the workflow file itself. To spot a version-only bump, compare the file at the merge base and at the head with the fields removed. Let docs-only changes skip CI with .
gh api repos/OWNER/REPO/pulls/N/filessrc/**versionpaths-ignoreMany small jobs. Every job pays for checkout, toolchain and dependency install, often 30 to 60 seconds, and then rounds up to a whole minute. If rounding adds more than about 10%, the jobs are too small. Merge matrix legs whose work takes about as long as their setup into one job with sequential steps, and put on each step so every case still reports when one fails. Keep long legs parallel. Move one-step jobs into a neighbour job that already checks out the code.
if: ${{ !cancelled() }}Slow dependency installs. Compare the install step with the rest of the job in the step timings. Turn on the setup action's cache, for example in or in , or use keyed on the lockfile. Install only the packages the job uses. Keep , the default, unless the job reads git history.
cache: npmactions/setup-nodecache: pipactions/setup-pythonactions/cachefetch-depth: 1Every push to a PR runs everything. Many runs per branch and many cancelled minutes mean thrown-away work. Key per PR and set . Skip the heavy suites on drafts with , and add to the types so they run when the PR is ready. This saves nothing if the team never opens drafts, so check before you promise it.
concurrencycancel-in-progress: trueif: ... && !github.event.pull_request.draftready_for_reviewpull_requestHung jobs. A job with no can run for 6 hours. Look for jobs whose longest run is far above their average, and set a timeout a little above their normal p90.
timeout-minutesDocker image builds. Watch for long build steps and a long step, which is the cache export. Install dependencies before . Avoid across many scopes, because the repo's 10 GB cache then evicts itself. Look for PR builds that repeat what main builds anyway. A good trade is to build images on PRs only when the Dockerfile, a lockfile or a file the Dockerfile copies by name changes, and to test the exact image main publishes before you tag it for release. Then nothing ships untested.
Post Set up Docker BuildxCOPY . .cache-to: mode=maxWindows and macOS runners. Windows bills 2 times the Linux rate and macOS 10 times. Run them only for platform-specific code, and don't run them again on main.
Bots and schedules. Copilot code review and Dependabot run under the event, and cron workflows under . Both use the same minutes. Check the cron frequency and how often people re-request reviews.
dynamicscheduleSelf-hosted runners. Jobs on an existing self-hosted group leave the bill. Before you move any job, the person who runs those machines has to confirm capacity, architecture (arm64 or x64) and access to package registries. Suggest the move, and wait for that answer.
Merge queue. It runs the full suite once per merge instead of once per PR push, which is often the biggest single saving. Private repositories need GitHub Enterprise Cloud for it, so check the plan first.
按照第1步得到的数据排序。以下每种模式都会说明如何识别以及如何修复。
主分支重复运行PR的测试套件
相同的任务同时出现在和事件中。如果每次合并都来自已通过检查的PR,那么可以在push事件中保留快速检查、构建和发布步骤,去掉重型测试套件。要告知用户这样做的代价。如果关闭了模式,未经过测试的合并结果可能会破坏主分支,问题会在下一个PR中暴露。
pushpull_requeststrict几乎匹配所有PR的路径过滤器
如果一个任务在80%到100%的PR运行中都被执行,说明它的路径过滤器根本没起到过滤作用。可以用获取真实PR的文件列表,回放过滤器规则,统计哪个模式最常被匹配。常见原因包括:每个PR都会因为版本更新而修改的清单或锁文件、范围狭窄的任务却用了宽泛的规则,以及工作流文件本身的修改。要识别仅版本号变更的提交,可以对比合并基点和HEAD处的文件(移除字段后)。可以用让仅文档变更的提交跳过CI。
gh api repos/OWNER/REPO/pulls/N/filessrc/**versionpaths-ignore大量小型任务
每个任务都需要付出代码检出、工具链安装和依赖安装的时间成本,通常为30到60秒,而且计费会向上取整到整分钟。如果取整损失的时间占比超过约10%,说明任务拆分得太小了。可以将那些工作时长和初始化时长差不多的matrix分支合并为一个包含顺序步骤的任务,并在每个步骤上添加,这样即使某个用例失败,其他用例仍会执行并上报结果。长耗时的matrix分支仍保持并行。将单步骤任务合并到已经完成代码检出的相邻任务中。
if: ${{ !cancelled() }}依赖安装缓慢
在步骤耗时中对比安装步骤和任务其他部分的时长。开启setup动作的缓存功能,比如中的或中的,或者使用以锁文件为key的。仅安装任务所需的依赖包。保持默认的配置,除非任务需要读取git历史。
actions/setup-nodecache: npmactions/setup-pythoncache: pipactions/cachefetch-depth: 1PR的每次推送都会运行所有任务
每个分支有大量运行记录、且有很多已取消的分钟数,意味着存在大量无效工作。按PR设置并发组,并开启。通过让草稿PR跳过重型测试套件,并在事件类型中添加,这样当PR转为就绪状态时会自动运行测试。如果团队从不创建草稿PR,这个优化不会有任何效果,因此在承诺收益前要先确认。
concurrencycancel-in-progress: trueif: ... && !github.event.pull_request.draftpull_requestready_for_review任务挂起
没有设置的任务最多可能运行6小时。找出最长运行时间远高于平均水平的任务,将超时时间设置为略高于其正常p90耗时。
timeout-minutesDocker镜像构建
留意耗时较长的构建步骤和步骤(该步骤用于导出缓存)。在之前安装依赖。避免在多个范围中使用,因为仓库的10GB缓存会因此频繁自我淘汰。找出那些重复构建主分支已经构建过内容的PR构建。一个不错的折中方案是:仅当Dockerfile、锁文件或Dockerfile指定复制的文件发生变更时,才在PR中构建镜像;并且在为镜像打发布标签前,直接测试主分支构建的镜像。这样就能确保所有发布内容都经过测试。
Post Set up Docker BuildxCOPY . .cache-to: mode=maxWindows与macOS运行器
Windows运行器的计费是Linux的2倍,macOS是10倍。仅在测试平台相关代码时使用这些运行器,且不要在主分支上重复运行。
机器人与定时任务
Copilot代码审查和Dependabot在事件下运行,cron工作流在事件下运行。两者都会消耗计费分钟数。要检查cron的执行频率,以及用户重新请求审查的频率。
dynamicschedule自托管运行器
在现有自托管运行器组上运行的任务不会产生计费。在迁移任何任务之前,必须由管理这些机器的人员确认容量、架构(arm64或x64)以及对包注册表的访问权限。可以提出迁移建议,等待对方确认。
合并队列
合并队列会在每次合并时运行一次完整测试套件,而不是每次PR推送都运行,这通常是最大的单项成本节省。私有仓库需要GitHub Enterprise Cloud套餐才能使用该功能,因此要先确认套餐类型。
4. Estimate before you edit
4. 编辑前预估收益
Replay each change over the measured data. Multiply removed jobs by their billed minutes, compare filter selection rates before and after, add up the setup that merging saves, and compare the critical path before and after. Show a table with the change, minutes saved, time saved and risk, and let the user pick. Keep the estimates low. Mark a saving that depends on how the team works, such as drafts, as unmeasured.
基于测量数据回放每个调整的效果。将被移除的任务数量乘以其计费分钟数,对比调整前后的过滤器命中率,累加合并任务节省的初始化时间,并对比调整前后的关键路径。用表格展示每个调整、节省的分钟数、节省的时间和风险,让用户选择。预估要保守。对于依赖团队工作习惯的节省(比如草稿PR优化),标记为未测量。
5. Change the workflows safely
5. 安全修改工作流
- Keep each change small. Add a comment in the workflow that says why, so the next person doesn't undo it.
- Update the tests that assert the workflow layout. Add a test only for behaviour that can break, like the version-only filter or drafts skipping heavy suites. Don't add tests that repeat the YAML.
- Lint with , downloading a release binary if it's missing, and run the repo's CI tests locally.
actionlint - Push and watch the PR. A workflow change usually starts every job, so its first run costs the most.
- 每次修改的范围要小。在工作流中添加注释说明修改原因,避免后续人员撤销优化。
- 更新校验工作流结构的测试。仅为可能出问题的行为添加测试,比如仅版本变更的过滤规则、草稿PR跳过重型套件等。不要添加重复YAML配置的测试。
- 用进行语法检查,如果缺少该工具可下载发布二进制文件,并在本地运行仓库的CI测试。
actionlint - 推送代码并观察PR情况。工作流变更通常会触发所有任务运行,因此第一次运行的成本最高。
Report
报告
Start with the numbers, then the plan.
markdown
undefined先展示数据,再给出方案。
markdown
undefinedWhere the minutes and time go (last N days)
Where the minutes and time go (last N days)
Total billed, monthly estimate, included minutes if known, median and p90 run time.
| Area | Minutes | Share | (by workflow and event)
| Job | Share | (top 5 jobs)
Total billed, monthly estimate, included minutes if known, median and p90 run time.
| Area | Minutes | Share | (by workflow and event)
| Job | Share | (top 5 jobs)
Why
Why
Numbered causes, each tied to a number from the data.
Numbered causes, each tied to a number from the data.
Changes
Changes
| # | Change | Saves (min) | Saves (time) | Risk |
| # | Change | Saves (min) | Saves (time) | Risk |
Not done
Not done
What you skipped and why, such as required check names, runner capacity nobody confirmed, or plan limits.
undefinedWhat you skipped and why, such as required check names, runner capacity nobody confirmed, or plan limits.
undefined