Skip to content

CI/CD của chúng tôi đã thay đổi thế nào khi agent lập trình tiếp quản

Agent lập trình giờ viết và ship phần lớn thay đổi của AnyRouter. Sau năm tháng dùng GitHub Actions, chúng tôi đã đổi thiết kế CI/CD bốn lần trong một tháng. Chúng tôi chuyển từ GitHub Actions sang CI chạy local trên một máy, rồi sang PR có review gate, rồi sang vài PR lớn, và giờ là commit thẳng lên main. Bài này đi qua từng giai đoạn với các con số chúng tôi đã đo, các lỗi tìm thấy trên đường đi, và các quy tắc chúng tôi đưa cho agent hiện nay.


Agent làm thay đổi điểm nghẽn

Phần lớn thay đổi của AnyRouter giờ do agent lập trình viết, kiểm tra và ship. Chúng push và deploy nhiều lần mỗi ngày. Khi viết code nhanh lên, pipeline xung quanh trở thành thứ mọi người phải chờ.

Chúng tôi đã chạy CI bằng GitHub Actions trong năm tháng. Rồi chúng tôi đổi thiết kế CI/CD bốn lần trong một tháng, từ ngày 4 tháng 10 đến ngày 3 tháng 11. Mỗi thay đổi sửa một điểm nghẽn và cho chúng tôi thấy điểm nghẽn tiếp theo. Bài này đi qua các giai đoạn theo thứ tự, với ngày tháng và các commit hoặc PR tạo ra từng thay đổi.

Mọi con số ở đây được đo vào ngày 3 tháng 11 năm 2026 từ lịch sử git, GitHub, log deploy và một script đo lead time. Chỗ nào số liệu có khoảng trống hoặc lưu ý, chúng tôi nói ngay bên cạnh.

Giai đoạn 1: GitHub Actions và preview deploy

Ngày 23 tháng 5 năm 2026, chúng tôi dựng pipeline quen thuộc (#321). Mỗi pull request chạy CI trên GitHub Actions và có một preview deploy. Merge vào main thì deploy từ GitHub Actions.

Agent pushes a branch

Pull request

GitHub Actions CI

Preview deploy

Merge

GitHub Actions deploy

Giai đoạn 1. Mỗi lần push từ mỗi agent chạy cả pipeline trên GitHub.

Cách này hợp với một đội người. Nó không hợp với một đội agent. Một agent push, chờ CI, sửa phần lỗi rồi push lại. Mỗi lần push đó chạy toàn bộ pipeline. Chúng tôi không đo hàng đợi vào thời điểm đó, nhưng việc chờ đợi rất dễ thấy.

Nó cũng tốn tiền. Repo của chúng tôi là private. GitHub ghi rõ rằng Actions trên repo private đi kèm một lượng sử dụng mỗi tháng, và sau đó bạn trả theo phút. Runner tiêu chuẩn do GitHub cung cấp chỉ miễn phí cho repo public. Agent push cả ngày, nên chúng tôi dự đoán sẽ phải trả cho phần lớn số phút đó. Chúng tôi không đo chi phí này.

Giai đoạn 2: CI chạy local trên một máy riêng

Ngày 4 tháng 10, chúng tôi tắt CI của GitHub Actions (#4452). Chỉ các workflow release còn chạy ở đó. Các check đó giờ chạy bằng pnpm local-ci trên một máy riêng, và lần chạy đó ký một commit status lên PR. Agent cũng phát triển và deploy từ máy đó.

signs a status

Agent lane in a worktree

Pull request

pnpm local-ci on the dedicated machine

merge-pr.sh

pnpm ops deploy from local main

Only the Workers that changed

Giai đoạn 2. Check và deploy chuyển sang một máy mà các agent dùng chung.

Một máy thì có giới hạn riêng. Nó có 15 GB bộ nhớ, và nhiều agent dùng chung. Ba thay đổi giúp nó chạy được.

  • Deploy theo từng Worker (10 tháng 10). AnyRouter chạy trên 19 Cloudflare Worker. Một lần deploy giờ chỉ ship các Worker có code thay đổi kể từ lần deploy trước của chính Worker đó. Mỗi phiên bản Worker được đóng dấu bằng commit mà nó được build từ đó, nên lần deploy sau biết cần diff từ đâu.
  • Một khóa heavy (12 tháng 10, #5026). Cài đặt, build, typecheck và chạy test dùng chung một khóa, chỉ áp dụng trong các pha nặng của chúng. Trước đó, build song song làm máy hết bộ nhớ và trình xử lý hết bộ nhớ đã kill các phiên agent.
  • Một script merge (14 tháng 10, #5492). merge-pr.sh trở thành cách duy nhất để PR của một lane được merge, với cùng các check mỗi lần.

Chúng tôi đã viết về deploy theo từng Worker và về việc làm local CI nhanh hơn trong các bài trước. Bài này nói về luồng xung quanh chúng.

Giai đoạn 3: review gate, rồi ít PR hơn và lớn hơn

Local CI loại bỏ chi phí. Nó không loại bỏ thời gian chờ. Vì vậy ngày 28 tháng 10 chúng tôi đổi cổng merge (#6840). Một PR được merge sau một lần review độc lập, gồm nhãn review:approved cộng một status đã ký, không chạy CI theo từng PR. Test chạy một lần trên main sau một loạt merge, và lỗi được sửa tiến lên. Một PR không có review sau 2 giờ bị đánh dấu là stale.

Rồi khối lượng đuổi kịp chúng tôi. Trong tuần 26 tháng 10, có 821 PR được merge. Mỗi PR có một agent reviewer riêng, một worktree riêng và một lần cài dependency riêng. Một reviewer chỉ thấy một PR nhỏ thì không thấy được nó tương tác thế nào với PR bên cạnh. Review từng PR ở khối lượng đó không hợp lý.

Số liệu lead time cho thấy cái giá. Trong tuần đó, PR trung vị đi từ lúc mở đến lúc live trong 1.4 giờ, nhưng 10 phần trăm chậm nhất mất 22.2 giờ hoặc hơn. Phần lớn cái đuôi đó nằm trước lúc merge, không phải giữa lúc merge và lúc deploy. Biểu đồ lead time nằm ở phần số liệu bên dưới.

Ngày 3 tháng 11, phiên manager đang chạy 8 lane cùng lúc. Chúng xếp hàng sau các lần chạy test của nhau trên khóa heavy trong 15 phút hoặc hơn. Hai lane sửa cùng các file và phải chờ PR của nhau. Vì vậy chúng tôi đổi quy tắc (#7798).

  • Tối đa 2 lane implementation cùng lúc. Mọi thứ khác chờ trong hàng đợi.
  • Lane được cố định theo vùng code, như catalog và pricing, hoặc UI trang model. Một yêu cầu mới vào lane sở hữu vùng đó.
  • Một lane gom các thay đổi trong hàng đợi thành một thay đổi lớn hơn với một lần review.

Lane: catalog and pricing

Stacked commits

Lane: model pages UI

Stacked commits

One PR

One PR

One independent review per PR

Merge, then one deploy for the batch

Giai đoạn 3. Hai lane, mỗi lane gom các thay đổi của vùng mình sau một lần review.

Ý tưởng này một phần đến từ bài của Anthropic Building agents: when to fold subagents into your main agent loop. Lời khuyên là chỉ giữ subagent cho công việc song song trong ngữ cảnh mới, cho tình trạng phình ngữ cảnh đã đo được, hoặc khi một agent gặp khó với công cụ hay chỉ dẫn của nó. Còn lại thì gộp vào vòng lặp chính. Mỗi lần chuyển giao giữa các agent làm mất chi tiết, và mỗi agent thêm vào làm tăng độ trễ. Chúng tôi đã thấy cả hai.

Giai đoạn 4: không PR, commit trên main

PR lớn vẫn có một vòng đi về. Ngày 3 tháng 11, một bản sửa test một dòng vẫn cần một worktree, một lần cài đặt, một PR, một reviewer và một lần merge trước khi đến lần deploy mà nó đang chặn. Vì vậy chúng tôi bỏ PR (78675034b).

Giờ một agent làm việc trên main, hoặc trong một worktree của lane push lên main. Nó review và check local, commit mỗi bước chạy được như một checkpoint, rebase rồi push. Lần push là bản sao lưu và là trạng thái chung giữa các máy. Deploy chạy từ main local.

Edit on main

Local checks: diff read, vitest, biome, typecheck

git commit as a checkpoint

git pull --rebase, then fast-forward push

Deploy from local main

Review the pushed range, fix forward with new commits

Giai đoạn 4. Một commit là checkpoint, một lần push là bản sao lưu, và review đọc dải commit đã push.

Review không biến mất. Sau khi một dải commit được đưa lên, một reviewer đọc git diff <last-reviewed>..<head> và liệt kê vấn đề. Với thay đổi nhỏ, reviewer đó là phiên chính. Billing, pricing, auth, secrets, migration và routing vẫn có một reviewer độc lập trước khi push. Mỗi bản sửa được đưa lên như một commit mới. Chúng tôi không bao giờ amend hay force-push một commit đã review, nên dấu vết review vẫn dễ đọc.

PR của release-please là loại PR duy nhất còn lại. Chủ dự án merge chúng bằng tay.

Những điều khác chúng tôi tìm ra trên đường đi

Đo từng giai đoạn làm lộ ra những vấn đề mà không check nào báo. Mọi check đều qua trong khi mỗi vấn đề này làm chúng tôi mất thời gian hoặc để lọt một lỗi.

  • Một vòng deploy tự nuôi chính nó. Các file chưa được theo dõi nằm rải rác trong checkout dùng để deploy khiến mọi lần deploy đóng dấu phiên bản Worker của nó là +dirty (chẩn đoán của manager đếm được 5 file như vậy). Một lần deploy không thể diff từ một dấu dirty, nên lần deploy sau build lại cả đội Worker, và lại đóng dấu +dirty vì các file vẫn còn đó. Cả 51 lần deploy kể từ 1 tháng 11 đều là deploy toàn bộ. Các file chưa được theo dõi nằm rải rác không còn đánh dấu một lần deploy là dirty nữa (2ee30c20f).
  • Một cache không bao giờ trúng. Lần deploy bỏ qua typecheck nếu cùng commit đó đã qua. Cache đó coi các file chưa được theo dõi nằm rải rác là một cây dirty, nên nó trúng 0 trên 51 lần. Typecheck mất trung bình 197 giây mỗi lần deploy. Đó là cùng một nguyên nhân gốc với vòng lặp ở trên. Giờ cache bỏ qua các file chưa được theo dõi mà TypeScript không bao giờ đọc (fbe45259b).
  • Một lần chạy smoke quá sớm sau deploy. Ngày 3 tháng 11 (đã đo), lần chạy smoke production đầu tiên ngay sau một lần deploy đã thất bại 52 trên 115 check với 503 database_unavailable. Chạy lại vài phút sau thì qua cả 72 check không bị bỏ qua. Các lỗi đến từ lần deploy mới, không phải từ một bug. Giờ chúng tôi chờ trước khi chạy smoke sau deploy.
  • Vitest không kiểm tra kiểu. Vitest bỏ kiểu đi và chạy code. Check của một lane chỉ chạy Vitest, và một test có lỗi kiểu đã lọt vào main. Nó lộ ra ở lúc deploy, vốn dừng ở lỗi kiểu đầu tiên. Giờ mọi lane chạy pnpm typecheck ở thư mục gốc trước khi push và đọc exit code thật của nó.
  • Một guard chỉ cho phép fast-forward trên main. Ngày 29 tháng 10, worktree của một lane bị xóa trong khi lane vẫn đang dùng. Lệnh tiếp theo của nó nối một cd thất bại, nên một lệnh git push --force-with-lease chạy từ một checkout cũ của main. main lùi lại khoảng một phút (#7089). Lane đã khôi phục nó và không mất commit nào. Khi cho phép push thẳng lên main, chúng tôi giữ lại bài học đó. Hook pre-push cho phép push fast-forward và từ chối mọi lần push ghi đè hoặc xóa main (62d85ea2b).
  • Hash của commit là danh tính. Mỗi phiên bản Worker trên Cloudflare mang anyr-sha=<commit>. Một trường đó điều khiển diff deploy theo từng Worker, việc kiểm tra rollback ngăn một checkout cũ làm production lùi lại, và các số lead time trong bài này. Một chỉ số gắn với số PR sẽ hỏng ngày bạn ngừng mở PR. Một chỉ số gắn với commit thì không.
  • Tốc độ deploy, đang làm. Một lần deploy mất trung vị 602 giây trên 51 lần chạy đó. Hai bản sửa ở trên đã được đưa lên ngày 3 tháng 11, nên deploy theo từng Worker và cache typecheck có thể hoạt động trở lại. Chúng tôi cũng đang làm một build cache sống qua các lần deploy, và đang đánh giá tsgo, trình biên dịch TypeScript viết bằng Go. Chúng tôi chưa đo được kết quả nào trong số này.

Các quy tắc chúng tôi đưa cho agent

Đây là hợp đồng hiện tại, lấy từ các mục Main-only flow và Few lanes trong AGENTS.md của chúng tôi. Bạn có thể sao chép và đổi các lệnh cho repo của mình.

Lấy từ AGENTS.md, ngày 3 tháng 11 năm 2026
# How we land changes (main-only flow)
- No pull requests. Work on main, or in a lane worktree that pushes to main.
- Keep each commit small and on one topic. Stage exact paths. Never git add -A.
- Check locally before every commit:
  - read the whole diff
  - targeted vitest: scripts/heavy.sh light -- npx vitest run <files> --maxWorkers=2
  - biome on the changed files
  - root pnpm typecheck for TypeScript changes; read its real exit code
    (vitest does not type-check)
- Billing, pricing, auth, secrets, migrations, routing: one independent
  reviewer reads the diff before you push.
- git commit, git pull --rebase origin main, git push origin main.
  Never force-push main.
- A review of the pushed range lands its fixes as new commits.
  Never amend, rebase or force-push reviewed commits.

# Few lanes
- At most 2 implementation lanes at once. Queue the rest.
- Lanes are fixed by code area. A new request joins the lane that owns
  its area. It does not get a new worktree.
- Fold before you fan out. Spawn a subagent only for parallel
  fresh-context work, a black-box review, or measured context bloat.
- One heavy command at a time per machine (install, vitest, tsc, build).

Các con số

Các biểu đồ này dùng lịch sử git, GitHub và log deploy của chúng tôi. Log deploy chỉ bao gồm một máy, và thiếu vài tuần. Những tuần đó bị bỏ khỏi biểu đồ deploy, không được vẽ thành 0. Vì vậy số lần deploy là mức tối thiểu. Tuần 2 tháng 11 chưa đủ. Nó chạy đến ngày 3 tháng 11.

Commits on main vs PRs merged, per weekAug 3Aug 10Aug 17Aug 24Aug 31Sep 7Sep 14Sep 21Sep 28Oct 5Oct 12Oct 19Oct 26Nov 2*9008007006005004003002001000Count
Commit trên main (cột) và PR đã merge (đường) mỗi tuần, từ tuần 3 tháng 8. * Nov 2 là một tuần chưa đủ.

Vào tháng 8, các số đếm cho thấy nhiều commit trên main không đến từ một PR đã merge. Từ cuối tháng 9, gần như mọi commit là một PR squash-merge, và cả hai đường đạt đỉnh trong tuần 26 tháng 10 với 810 commit và 821 PR. Sau khi chuyển sang main-only, đường PR sẽ giảm trong khi commit tiếp tục.

Số lần deploy mỗi tuần

Các lần deploy trong log của máy này. Các tuần Sep 7, Sep 14 và Oct 19 không được ghi log và bị bỏ qua.

Aug 312
Aug 1059
Aug 1791
Aug 2450
Aug 319
Sep 2114
Sep 28168
Oct 545
Oct 1234
Oct 2614
Nov 2 (partial)37

Thời gian deploy trung vị mỗi tuần

p50 của các lần deploy thành công trong log của máy này. Cùng các tuần bị thiếu.

Aug 395 s
Aug 10438 s
Aug 17538 s
Aug 2443 s
Aug 31177 s
Sep 21172 s
Sep 28107 s
Oct 5883 s
Oct 12937 s
Oct 26794 s
Nov 2 (partial)562 s

Thời gian deploy tăng vào tháng 10. Deploy trở thành build đầy đủ của một đội 19 Worker đang lớn dần, cộng thêm một cổng typecheck. Dấu dirty ở trên khiến mọi lần deploy kể từ 1 tháng 11 đều là deploy đầy đủ.

Thời gian deploy đi đâu

Số giây trung bình mỗi bước trên 51 lần deploy thành công kể từ Nov 1. Tổng p50 602 s, p90 810 s.

deploy:spokes227 s
typecheck197 s
core-uploads124 s
prerender79 s
prerender:redeploy50 s
build49 s
assets42 s
build:docs41 s
deploy:docs40 s

Deploy các Worker spoke và typecheck cộng lại chiếm khoảng một nửa tổng các số trung bình của từng bước. Đó là hai thứ mà dấu dirty làm hỏng. Một dấu sạch cho phép deploy bỏ qua các Worker không đổi và bỏ qua một typecheck đã qua.

Lead time của luồng PR, từ đầu đến live

Từ lúc PR được mở đến phiên bản Worker đầu tiên được đóng dấu bằng commit đó hoặc commit sau nó. p50 và p90 mỗi tuần.

Oct 12 p502.1 h
Oct 12 p9010.7 h
Oct 19 p504.6 h
Oct 19 p9014.3 h
Oct 26 p501.4 h
Oct 26 p9022.2 h
Nov 2 p50 (partial)0.3 h
Nov 2 p90 (partial)1.6 h

Script scripts/delivery-metrics.ts của chúng tôi đo lead time theo từng commit. Một commit bắt đầu khi PR của nó được mở, hoặc khi nó được viết nếu không có PR. Nó live khi một phiên bản Worker được đóng dấu bằng commit đó, hoặc commit sau nó, được deploy. Bảng này chia luồng PR thành hai nửa của nó.

Luồng PR, tuầnCommitTừ đầu đến đã vào main p50 / p90Từ đã vào main đến live p50 / p90Từ đầu đến live p50 / p90
Oct 124120.6 h / 5.6 h0.8 h / 5.0 h2.1 h / 10.7 h
Oct 193443.0 h / 12.6 h0.8 h / 2.7 h4.6 h / 14.3 h
Oct 268190.2 h / 21.7 h0.5 h / 3.4 h1.4 h / 22.2 h
Nov 2 (chưa đủ tuần)83 (80 đã live)0.1 h / 0.4 h0.2 h / 0.8 h0.3 h / 1.6 h

Trong tuần 26 tháng 10, cái đuôi p90 nằm gần như hoàn toàn trước lúc merge, ở mức 21.7 giờ. Thời gian chờ nằm trước lúc merge, không phải giữa lúc merge và lúc deploy. Tuần 2 tháng 11 chưa đủ nhanh hơn nhiều, nhưng nó chỉ gồm 83 commit từ những ngày cuối của luồng PR. Đó không phải là kết quả của luồng main-only.

Những gì chúng tôi chưa biết

Luồng main-only mới một ngày tuổi. Khi chúng tôi đo, nó có 6 commit trên main và chưa commit nào được deploy. Chúng tôi chưa có số liệu sau thay đổi, nên chúng tôi không tuyên bố nó nhanh hơn.

Luồng main, tuầnCommitTừ đầu đến đã vào main p50 / p90Từ đầu đến live p50 / p90
Oct 126 (6 live)0.0 h / 0.0 h1.0 h / 2.5 h
Oct 261 (1 live)0.0 h / 0.0 h1.7 h / 1.7 h
Nov 26 (0 live)0.2 h / 0.7 hchưa có

Các dòng main trước đó là một nhúm commit trực tiếp được làm trong thời kỳ PR. Chúng quá ít để so sánh với bất cứ thứ gì.

Chúng tôi sẽ chạy lại npx tsx scripts/delivery-metrics.ts --since 2026-10-12 sau vài ngày của luồng mới và hai bản sửa deploy. Chúng tôi sẽ cập nhật bài này với các số liệu đó, kể cả những số trông không đẹp.

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í