Steven
Steven10 phút đọc

Đưa Bản Phát Hành Vào CI, Hai Lần

GeekBye v1.8.4 chuyển các bản build phát hành cho macOS và Windows vào CI. Điều changelog không nói là chúng tôi đã thử việc này một lần trước đó, xóa nó đi một tháng sau vì chi phí runner, và chỉ khiến nó trụ được ở lần thứ hai — bởi tới khi ấy release script đã chạy được bằng tay rồi.

Kỹ thuật
CI/CD
Desktop
Bản phát hành GeekBye
Đưa Bản Phát Hành Vào CI, Hai Lần

Changelog của v1.8.4 có một dòng nghe như thuần túy việc nhà: "Các bản build phát hành giờ đến từ CI cho cả macOS và Windows." Nó đọc lên như loại việc xảy ra một lần, gọn ghẽ, rồi chẳng ai nghĩ tới nữa. Nó không phải vậy. Đội ngũ đã từng đưa bản phát hành vào CI một lần — và giật nó ra. Bản phát hành này là lần thử thứ hai, và lý do nó trụ được mới chính là bài học kỹ thuật thật sự, nên hãy kể theo thứ tự.

Lần thử đầu tiên, và vì sao nó chết

Quay về tháng Mười 2025, có một release.yml. Nó được kích bằng tag push — đẩy một tag v*.*.* là một macOS runner sẽ khởi lên, build, và publish. Nó tự làm phần thiết lập ký bằng tay: một bước decode chứng chỉ ký, tạo và mở khóa một keychain tạm, import cert vào để codesign tìm thấy nó, và một bước dọn dẹp tương ứng ở cuối. Nó chạy được. Nó cũng chỉ dành cho macOS — các bản phát hành Windows hoàn toàn không nằm trong CI.

Ngày 4 tháng Mười Một, nó bị xóa. Thông điệp commit thẳng thắn một cách bất thường về lý do: "remove GitHub Actions release workflow to conserve minutes / macOS runners cost 10x multiplier (100 billed minutes per 10-minute release). Releases will be done locally." Đó là toàn bộ câu chuyện của lần thử đầu tiên gói gọn trong một câu. GitHub tính giờ runner macOS gấp mười lần Linux, và một bản phát hành kích bằng tag nghĩa là mỗi tag đơn lẻ — kể cả những tag dùng một lần rồi bỏ, những tag đánh lại, những tag "ối, quên bump phiên bản" — đều âm thầm ngốn một trăm phút tính tiền. Tự động hóa mà bạn không kiểm soát là tự động hóa tiêu tiền trong lúc bạn ngủ. Nên việc phát hành quay về máy Mac của một developer trong ba tháng rưỡi kế tiếp.

Nơi những bài toán khó thực sự được giải

Đây là phần khiến lần thử thứ hai chạy được, và nó xảy ra hoàn toàn bên ngoài CI. Trong ba tháng rưỡi phát hành tại chỗ ấy, scripts/release.js — cái script Node bình thường mà một developer chạy bằng tay — đã hấp thụ mọi bài toán đóng gói mà ứng dụng từng gặp, mỗi lần một cách sửa:

  • Khôi phục white-label. Đây là một codebase white-label build cả GeekBye lẫn Pavleur từ một nguồn, nghĩa là một lần phát hành sẽ tráo các trường trong package.json và asset icon theo sản phẩm đích. Hai cách sửa dạy cho script biết đặt mọi thứ về chỗ cũ sau đó, để build Pavleur không để lại cây git của bạn nhem nhuốc danh tính của Pavleur.
  • Windows dual-arch. Một cách sửa để build cả installer Windows x64 lẫn arm64 thay vì một cái.
  • Cách lách cho Windows không ký. electron-builder mới hơn phớt lờ cái config flag lẽ ra để tắt việc ký Windows, nên script học cách ép một bản build không ký bằng cách đặt CSC_IDENTITY_AUTO_DISCOVERY=false trong environment thay thế — một nếp gấp bạn chỉ tìm ra khi đâm vào nó.
  • Tự động phát hiện việc công chứng. Script học cách nhìn environment của mình rồi quyết định: có chứng danh để ký không? Ký. Có cả chứng danh công chứng của Apple không? Công chứng. Không có cái nào? Build không ký. Không có flag nào phải nhớ; sự hiện diện của các secret chính là cấu hình.

Chẳng cái nào trong đó hào nhoáng cả. Tất cả đều là loại việc mà, nếu nó làm bạn bất ngờ bên trong một CI runner, sẽ ngốn của bạn một giờ push-chờ-hỏng-đọc-log mỗi lần thử — với mức tính tiền 10x. Giải trên một chiếc Mac mà bạn đang ngồi trước mặt, mỗi cái tốn một phút.

Lần thử thứ hai: sáu mươi sáu dòng

Khi CI trở lại ở v1.8.4, workflow dài 66 dòng, và phẩm chất định nghĩa nó là nó làm ít đến mức nào. Commit mô tả nó một cách mộc mạc: "Manual workflow_dispatch trigger that builds both platforms in parallel, reusing existing release.js script. macOS builds are signed and notarized, Windows builds are unsigned." Mỗi quyết định thiết kế trong đó là một vết sẹo từ lần thử đầu:

  • workflow_dispatch, không phải kích bằng tag. Bạn khởi một bản phát hành bằng cách bấm "Run workflow." Một con người canh cổng mỗi phút macOS tính tiền. Bài toán chi phí giết chết phiên bản đầu được giải chỉ bằng cách đơn giản là không tự động hóa cái trigger — chỗ duy nhất mà tự động hóa đang chủ động gây hại.
  • Một input product. Dispatch nhận một dropdown — geekbye hoặc pavleur — nên cùng một workflow ship được cả hai thương hiệu. Đường nối white-label chạy tuốt ra tới cái nút phát hành.
  • Hai job song song. build-mac trên macos-latest, build-win trên windows-latest, chạy cùng lúc. Windows trong CI thật sự là điều mới ở đây; workflow tháng Mười chưa từng build nó.
  • Không có logic ký trong YAML. Đây chính là toàn bộ điểm cốt lõi. Không có màn tung hứng keychain, không có bước import cert, không có dọn dẹp. Job Mac chạy node scripts/release.js <product> --publish và job Windows chạy đúng thứ đó với --no-sign. Mọi thứ workflow đầu tiên từng làm bằng tay trong YAML nay sống trong cái script vốn đã chạy được. Workflow là một bộ điều phối, không phải một phần triển khai.

Hai nền tảng muốn những thứ trái ngược, và các job phản ánh điều đó một cách trung thực. macOS build dưới hardened runtime, được ký và công chứng qua đường built-in của electron-builder (@electron/notarize), với chứng danh cấp từ secret của repository và được script tự phát hiện. Windows build installer NSIS không ký cho x64 và arm64. Một workflow, hai job, hai khái niệm "xong" hoàn toàn khác nhau.

Bằng chứng nằm ở điều đã không xảy ra

Đây là cách bạn biết thứ tự là đúng: sau hai lần chỉnh nhỏ cùng ngày đặt tên cho các lần chạy và sửa tên một token secret, release.yml không bị đụng tới lần nào nữa suốt bốn tháng. Không hotfix lỗi ký. Không cuống cuồng vì bị từ chối công chứng. Không hoảng loạn "native binary biến mất trên runner." Với một đường ống ký-code-và-công-chứng — thể loại CI khét tiếng nhất về chuyện quăng quật — bốn tháng im lặng gần như là chuyện chưa từng nghe.

Nó im ắng vì tiếng ồn đã xảy ra ở đâu đó rẻ hơn rồi. Những cuộc chiến thường diễn ra trong một tab log CI, ở mức tính tiền 10x, mỗi lần một cú force-push, đã diễn ra trên máy Mac của các developer trong thời kỳ tại chỗ. CI không phải là nơi việc đóng gói được debug, bởi việc đóng gói đã được debug rồi. Đó là luận điểm gói trong một dòng: chỉ chuyển bản build ra khỏi máy Mac sau khi máy Mac của bạn đã thôi làm bạn bất ngờ.

Mối kết dính duy nhất vẫn có thể cắn

Nó không hoàn toàn không có điểm yếu, và điều đó đáng nêu tên vì nó tinh vi. Các Swift binary mà ứng dụng macOS phụ thuộc vào — OCR, chụp màn hình, các transcriber — được commit vào git. Nhưng bản phát hành không ship các bản sao đã commit; bản build biên dịch lại chúng trên runner. Và một cổng check-swift-version.js sẽ làm cả bản phát hành thất bại cứng nếu trình biên dịch không phải phiên bản đã ghim. Ở bản phát hành này, cái ghim đó là Swift 6.2.x, và chẳng có gì trong workflow cài đặt nó — job chỉ đơn giản tin rằng Swift mặc định của macos-latest sẽ khớp. Cái ngày GitHub đẩy image runner của họ vượt qua cái ghim, bản phát hành dừng lại, không phải vì có gì sai với ứng dụng, mà vì đường ống bị kết dính một cách lặng lẽ vào một image máy nó không kiểm soát. Đó là chi tiết triển khai duy nhất mà workflow mỏng này đã không đẩy xuống thành thứ nó sở hữu.

Nửa còn lại của bản phát hành: payload nhỏ hơn

Bản phát hành chở theo một tính năng không liên quan đáng nhắc, vì nó là một ví dụ sạch sẽ của việc sửa độ trễ bằng phép trừ. Hành động Assist Me đang gửi cho backend nhiều context hơn nó cần ở mỗi request. Một commit cắt hai thứ: lịch sử transcript gửi kèm giảm từ 30 mục gần nhất xuống 15 mục gần nhất, và các file context hồ sơ của người dùng — được tiêm vào system prompt ở mỗi lần gọi — bị giới hạn cứng ở 4.000 ký tự kèm một dấu cắt bớt. Ít thứ để serialize hơn, ít thứ để upload hơn, ít thứ để model đọc trước khi bắt đầu trả lời hơn, và đó chính là chỉ số mà commit này tối ưu: time-to-first-token. Vì sự trung thực: commit này tuyên bố có tăng tốc nhưng không đo nó — không có con số trước/sau nào trong lịch sử, nên hãy xem nó như một sự cắt tỉa có lập luận vững chứ không phải một chiến thắng đã được benchmark.

Ba điều bản phát hành này dạy chúng tôi

  1. CI là một lớp bọc mỏng đặt trên một script vốn đã chạy được. Workflow 66 dòng không có logic ký của riêng nó; nó gọi một script mà ba tháng phát hành tại chỗ đã debug xong. Hãy đặt cơ chế vào một thứ bạn chạy được bằng tay, và để CI chỉ quyết định khi nào chạy nó.
  2. Tự động hóa công việc, không nhất thiết là cái trigger. Lần thử đầu chết vì các bản phát hành kích bằng tag tự động ngốn phút runner tính tiền 10x. workflow_dispatch thủ công giữ lại phần tự động hóa và loại bỏ phần đang tiêu tiền — đôi khi con người trong vòng lặp mới chính là tính năng.
  3. Debug ở nơi lặp lại rẻ. Mỗi cuộc chiến đóng gói được giải trên máy Mac của một developer là một cuộc chiến không bao giờ xảy ra trong một tab log CI với giá gấp mười lần. Chuyển ra khỏi Mac là bước cuối, không phải bước đầu.

Về chương trước trong câu chuyện v1, xem Ship Ba Mươi Ngôn Ngữ Mà Không Có Lưới An Toàn (v1.8.3); và cho toàn bộ vòng cung, xem giải phẫu việc xuất xưởng phần mềm đến độ hoàn hảo.

Bài Viết Liên Quan

Ba Động Từ Giữ Cho Web Audio Sống
Steven
Steven11 phút đọc

Ba Động Từ Giữ Cho Web Audio Sống

Hai bản point release của GeekBye, cách nhau hai tháng và nằm trong hai file khác nhau, đã dạy code âm thanh của chúng tôi cùng một bài học từ hai đầu đối nghịch: thôi coi AudioContext của browser như một thứ dùng-rồi-bỏ. Một bản học được cách resume() một context mà macOS đã lặng lẽ suspend giữa lúc ghi; bản kia học được cách suspend() thay vì close() để những session nối tiếp nhau thôi đâm sầm vào cái trần khoảng-sáu-context của Chromium. resume, suspend, close — đó là toàn bộ cốt truyện.

Kỹ thuật
Audio
Desktop
Phân Biệt Một Cuộc Gọi Với Một App Đang Mở
Steven
Steven11 phút đọc

Phân Biệt Một Cuộc Gọi Với Một App Đang Mở

GeekBye có thể nhận ra bạn vừa vào một cuộc họp video và đề nghị ghi lại nó chỉ bằng một cú nhấp. Việc phát hiện hóa ra là nửa dễ — một Swift binary đọc tiêu đề cửa sổ mỗi mười giây. Nửa khó là độ chính xác: không phát hỏa khi Zoom chỉ đang mở, không nhắc cho một cuộc họp bạn đã đang ghi, và không tắt tiếng cái mic trong cuộc gọi bạn thực sự đang ngồi. Ba bản phát hành, và mỗi bản là một guard phải học cách không tự đánh bại chính mình.

Kỹ thuật
macOS
Desktop
Gỡ Backend Ra Khỏi Đường Đi Của Upload
Steven
Steven10 phút đọc

Gỡ Backend Ra Khỏi Đường Đi Của Upload

GeekBye quay lại màn hình của bạn và lưu video vào Google Drive của bạn. Phiên bản đầu tiên đẩy mọi bản ghi qua chính máy chủ của GeekBye trên đường tới đó; một bản phát hành sau, tập tin đi thẳng từ máy của bạn tới Drive, và backend bị giáng xuống thành thứ chỉ giữ một con trỏ. Phần thú vị là cái phiên bản «trực tiếp, có thể tiếp tục» ấy thực ra chứa ít code đến mức nào — vì khả năng tiếp tục đến từ việc xóa đi một proxy, chứ không phải từ việc viết ra một cái.

Kỹ thuật
Kiến trúc
Desktop