Agent push cả ngày, nên CI là chỗ phải chờ
Agent lập trình viết và merge phần lớn thay đổi của AnyRouter. Một agent push, chờ CI, sửa phần bị lỗi, rồi push lại. Khi viết code nhanh lên mà CI không nhanh lên, CI là thứ mọi người phải chờ.
CI của chúng tôi không chạy trên GitHub. Chúng tôi đã tắt CI của GitHub Actions để tiết kiệm tiền, và mọi check chạy bằng pnpm local-ci trên một máy có 16 core và 15 GB bộ nhớ. Nhiều agent dùng chung máy đó. Bài đầu tiên của loạt bài này mô tả vòng lặp đó.
Bài này bắt đầu từ bài viết How I made our CI 20x faster của Owen Gretzinger. Tóm tắt trong một dòng, Owen audit xem thời gian CI đi đâu rồi sửa từng nguyên nhân, như runner chậm, linter chậm, cache trượt và cách cô lập test. Có ý tưởng hợp với chúng tôi, có ý tưởng không. Chúng tôi nói rõ ở phần dưới.
Audit trước: chờ, chuẩn bị và chạy
Trước khi đổi gì, chúng tôi đọc 50 log local CI gần nhất, từ 23 đến 26 tháng 10. Mỗi lần chạy được chia thành ba phần.
- Chờ. Thời gian chờ máy hoặc chờ khóa. Trung bình là 0 giây.
- Chuẩn bị. Thời gian tạo một bản repo sạch và các dependency. Trung bình là 52 giây.
- Chạy. Thời gian bên trong các check. Trung bình là 326 giây, trung vị là 20 giây, và lần lâu nhất mất khoảng 2.900 giây.
Khoảng cách giữa trung bình và trung vị là con số hữu ích nhất. Một nửa số lần chạy xong các check trong 20 giây. Vài lần chạy typecheck khoảng 20 app nối tiếp nhau đã kéo trung bình lên. Vậy vấn đề không nằm ở mọi lần chạy. Nó nằm ở phần đuôi nặng, và phần đuôi nặng là typecheck.
Những thứ chúng tôi tưởng là chậm thì lại không chậm. Lint bằng Biome mất khoảng 1 giây. Chỉ 1 trong 50 lần chạy chạy toàn bộ bộ test, vì local CI chỉ chạy các test liên quan tới file thay đổi. Chờ máy không tốn gì. Giới hạn là bộ nhớ, không phải số core.
Log cũng cho thấy vài chi phí nhỏ hơn.
- Bước build ảnh trượt cache ở 19 trên 21 lần chạy. Mỗi lần chạy xóa các file không được git theo dõi để bắt đầu sạch, và việc đó xóa khoảng 1.200 ảnh social card được sinh ra. Vì vậy lần nào cũng phải vẽ lại.
- Một thay đổi ở lockfile tạo ra một bản làm việc mới cho CI, không có trạng thái build TypeScript. Log có 11 khóa lockfile trong 6 ngày, và khởi động lạnh cho mọi chương trình typecheck mất khoảng 5 phút.
- File setup của test nạp Testing Library và React DOM vào mọi file test, kể cả các file không dùng DOM. Phần đó chiếm 38 tới 42 phần trăm thời gian của một lô nhỏ test node.
Cùng một check chạy hai lần
Rồi chúng tôi tìm ra vấn đề lớn nhất, và nó đến từ một bản sửa. Ngày 1 tháng 11, chúng tôi đổi pnpm typecheck ở thư mục gốc để nó kiểm tra mọi package và app. Trước đó, lỗi kiểu trong vài file test có thể lọt vào main. Thay đổi đó đúng. Nhưng local CI vẫn chạy typecheck riêng cho API, server core, UI và từng app sau bước gốc. Kết quả là 29 chương trình TypeScript mỗi lần chạy, khoảng 26 trong số đó bị kiểm tra hai lần, tất cả nối tiếp nhau. Một lần chạy warm mất 162 giây.
Không có gì lỗi, nên không có gì báo cho chúng tôi biết. Một check bị lặp thì vẫn xanh và chậm. Giờ khi local CI lên kế hoạch typecheck gốc, nó lên đúng tập mà pnpm typecheck bao phủ và chạy mỗi chương trình một lần. Nó đọc tập đó từ package.json của từng package, nên một app mới được nhận ra mà không cần danh sách viết tay.
Chạy song song, nhưng có ngân sách bộ nhớ
Bài đầu tiên của loạt bài nói rằng local CI chạy các check lần lượt vì máy có 15 GB bộ nhớ và không chịu được build song song. Điều đó đúng với một số job cố định. Trên máy này, ba chương trình TypeScript lạnh khởi động cùng lúc đã bị trình xử lý hết bộ nhớ kill khi còn khoảng 1,5 GB trống.
Vì vậy chúng tôi không chọn một con số job. Một script mới khởi động chương trình lớn nhất trước. Nó chỉ khởi động chương trình tiếp theo khi bộ nhớ đỉnh của chương trình đó vừa với bộ nhớ trống, sau khi trừ phần tăng còn lại của các chương trình đang chạy và giữ 2 GB dự phòng. Nó học bộ nhớ đỉnh của mỗi chương trình từ các lần chạy trước, và giả định 2,8 GB cho chương trình chưa gặp.
Một chương trình bị kill mà không có lỗi kiểu sẽ chạy lại một lần, một mình. Nếu nó lại bị kill, lần chạy được báo là hết bộ nhớ, không phải check thất bại. Một agent thấy typecheck thất bại sẽ đi tìm lỗi kiểu. Một agent thấy hết bộ nhớ thì biết là cần chờ rồi thử lại.
Các lần chạy warm luôn còn ít nhất 2,5 GB trống. Trong lần chạy song song lạnh, không chương trình nào bị kill.
Những gì khác chúng tôi đã đổi
- 15 guard nhẹ, như schema drift và ranh giới import giữa các app, giờ chạy 4 cái một lúc. Chúng chỉ đọc file, nên chạy cạnh nhau là an toàn. Các check nặng và các lần chạy test vẫn giữ khóa riêng.
- Một guard từng parse mọi file TypeScript để tìm tham chiếu tới issue. Giờ nó bỏ qua những file không có
#theo sau là chữ số. - Một bản làm việc CI mới bắt đầu với trạng thái build TypeScript và cache của bản được dùng gần nhất. TypeScript kiểm tra lại mọi file đã thay đổi, và chúng tôi đã xác nhận một lỗi kiểu được cài vào vẫn làm check thất bại trên trạng thái được sao chép.
- Social card đã render được lưu trong một cache nằm ngoài các file bị bước dọn dẹp xóa, theo khóa đường dẫn và nguồn. Card không dùng trong 14 ngày sẽ bị xóa.
- Testing Library chỉ được nạp trong các test có DOM.
- Một hook pre-push chạy Biome trên các file thay đổi và chỉ báo lỗi. Agent biết về lỗi lint trước khi push, không phải sau khi CI chạy.
Đây là các con số, mỗi số đo trước và sau trên cùng một commit. Mỗi dòng ghi rõ số lần chạy.
| Check | Trước | Sau | Thay đổi |
|---|---|---|---|
| Typecheck, 29 chương trình, warm (trung vị của 3) | 162 s | 79 s | -51% |
| Typecheck, 29 chương trình, lạnh (1 lần mỗi bên) | 402 s | 256 s | -36% |
| 15 guard (trung vị của 3, máy đang bận) | 35 s | 14 s | -60% |
| Riêng guard tham chiếu issue (trung vị của 3) | 13,9 s | 5,0 s | -64% |
| Build ảnh sau khi dọn sạch (2 lần mỗi bên) | 100 s | 60 s | -40% |
| 20 file test node (trung vị của 3) | 4,86 s | 3,58 s | -26% |
| 12 file test DOM (2 lần mỗi bên) | 16,5 tới 17,6 s | 16,7 s | không đổi |
| Biome pre-push trên 13 file thay đổi | không có | 0,1 s | check mới |
Chạy mỗi chương trình một lần, vẫn lần lượt, đưa typecheck warm từ 162 xuống 111 giây. Chạy chúng song song đưa xuống 79. Vậy khoảng 60 phần trăm mức cải thiện đến từ việc bỏ phần lặp, phần còn lại đến từ chạy song song.
Đây là số liệu theo từng check. Chúng tôi chưa đo trung bình của cả một lần chạy local CI sau thay đổi. 50 log tiếp theo sẽ cho thấy điều đó.
Một danh sách rỗng có thể nghĩa là tất cả
Có một cái bẫy đáng một mục riêng. Local CI chỉ chạy các test liên quan tới file thay đổi. Một script thường tạo danh sách đó từ git diff, như T=$(git diff --name-only main | grep test), rồi chạy vitest run $T.
Nếu diff trả về rỗng, ví dụ vì git pull thất bại và HEAD không đổi, lệnh trở thành vitest run không có file nào. Với Vitest, điều đó nghĩa là chạy toàn bộ bộ test. Trên máy của chúng tôi, toàn bộ bộ test có nguy cơ bị trình xử lý hết bộ nhớ kill. Chúng tôi đã dừng một lần chạy như vậy sau 600 giây.
Guard chỉ là một dòng. Kiểm tra danh sách trước khi chạy, bằng [ -n "$T" ] && vitest run $T. Owen nêu một ý tương tự về Nx. Một quy tắc bỏ qua việc cần có test riêng, vì một lần bỏ qua bị hỏng sẽ trông như xanh hoặc chạy tất cả.
Ý tưởng nào hợp với chúng tôi
Bài của Owen liệt kê chín thay đổi. Đây là cách mỗi thay đổi áp vào hệ thống của chúng tôi.
| Ý tưởng | Với chúng tôi | Trạng thái |
|---|---|---|
| Runner nhanh hơn | Local CI chờ 0 s. Nhưng các workflow release trên GitHub chờ một runner self-hosted không bao giờ tới, và 146 trên 146 lần chạy bị hủy. Chúng tôi chuyển chúng sang runner tiêu chuẩn của GitHub. | Đã làm |
| Linter nhanh hơn | Chúng tôi đã lint bằng Biome, khoảng 1 s. | Không cần đổi |
| Typechecker nhanh hơn | Trình biên dịch TypeScript viết bằng Go, tsgo. | Tiếp theo |
| Chia test lên máy lớn hơn | Chúng tôi có một máy và không trả tiền cho CI. Thêm máy thì tốn tiền. | Không làm |
| Không nạp lại app cho mỗi file test | Vitest isolate: false. Test DOM của chúng tôi không nhanh hơn, nên phần khởi động test là chỗ cần xem. | Tiếp theo |
| Sửa phần chuẩn bị | Bản làm việc được gieo sẵn và cache card sống qua bước dọn dẹp. | Đã làm |
| Chạy check song song | Typecheck theo ngân sách bộ nhớ, guard 4 cái một lúc. | Đã làm |
| Chỉ chạy những gì thay đổi chạm tới | Đã làm từ trước. Thêm guard cho danh sách rỗng. | Đã làm |
| Check trước mỗi lần push | Biome chỉ báo lỗi trong hook pre-push. | Đã làm |
Đội của Owen trả tiền để có tốc độ. Bài của Owen cho biết hóa đơn CI tăng từ $230 lên $4.300 mỗi tháng và cho rằng đáng giá. Ràng buộc của chúng tôi khác. CI chạy trên một máy chúng tôi đã có sẵn, nên chúng tôi tìm thời gian đang bị lãng phí trước khi tìm thời gian có thể mua.
Prompt cho agent lập trình của bạn
Bạn có thể đưa prompt này cho agent lập trình của mình. Nó yêu cầu cùng một cách audit và cùng các quy tắc chúng tôi đã theo.
Audit CI của repo này và làm nó nhanh hơn. Không bao giờ bỏ hay nới lỏng một check.
1. Lấy 50 lần chạy CI gần nhất. Chia thời gian của mỗi job thành chờ, chuẩn bị và chạy.
2. Báo cáo trung bình, trung vị và lớn nhất của mỗi phần. Nếu trung bình cao hơn trung vị nhiều, tìm các lần chạy chậm gây ra điều đó.
3. Liệt kê 5 job chậm nhất và mỗi job tốn thời gian vào việc gì.
4. Kiểm tra các nguyên nhân thường gặp: hàng chờ runner chậm, linter chậm, typechecker chậm, quá ít core cho test, nạp lại app cho mỗi file test, cache trượt hoặc bị bước dọn dẹp xóa, việc chạy nối tiếp có thể chạy song song, check trên code mà thay đổi không chạm tới, và check không bao giờ chạy trước khi push.
5. Tìm check chạy hai lần, và danh sách file rỗng bị hiểu thành "chạy tất cả".
6. Chạy song song trong một ngân sách bộ nhớ. Báo job bị kill vì bộ nhớ là hết bộ nhớ, không phải check thất bại.
7. Đo mỗi thay đổi trước và sau trên cùng một commit, trung vị của 3 lần. Không bỏ test hay hạ ngưỡng để làm đẹp con số.
8. Không đưa vào thay đổi tốn tiền (runner trả phí, máy lớn hơn, thêm shard). Liệt kê chúng cho người quyết định, kèm thời gian tiết kiệm và chi phí.
9. Mở một PR với bảng trước và sau, và các lệnh để tái hiện kết quả.Bài liên quan
How agents ship AnyRouter: the merge and deploy loop
Coding agents write, review, merge and deploy most AnyRouter changes. In the week of 19 to 25 October 2026 that loop merged 346 pull requests. This first post in the series shows the loop: a signed local CI gate, one reviewer per commit, one merge script, and gated deploys about once an hour.
How agents deploy AnyRouter: one Worker at a time
AnyRouter runs as a fleet of Cloudflare Workers, and agents deploy it about once an hour from more than one machine. Each Worker now records the commit it runs, a deploy ships only the Workers whose code changed since that commit, and a checkout that is older than production cannot replace it. This post shows how that works and what it still costs.
Gửi request đầu trong hai phút
Bắt đầu miễn phí với key của bạn, hoặc nạp rồi trả theo token. Go: $4 credit/tháng — $2/tháng, hoặc miễn phí khi góp một provider key.
Bắt đầu miễn phí