讓自己變得不再被需要
原文由 Matthias Endler 于 發布,訂閱此部落格
來源:古生物學家曾以為牠的屁股裡有顆腦。
2015 年 12 月,我正在為 trivago 的 CI 流程尋找靜態分析工具。想法是自動偵測常見的程式錯誤。這是很普遍的需求,市面上也有很多好用的工具可以做到。
於是我開始尋找工具清單……
令我意外的是,我找到的唯一一份清單竟然在維基百科上——而且已經過時了。在大多數現代靜態分析工具所在的 GitHub 上,反而沒有這樣的專案。
沒有想太多,我就打開編輯器,把初步研究找到的幾個工具記下來,然後把這份清單推上了 GitHub。
我把這個專案命名為 Awesome Static Analysis。
兩年過去,這份清單已經成長了不少。目前已有 75 位貢獻者、277 次 fork,並獲得了超過 2,000 顆星。(感謝大家的支持!)(2018 年 5 月更新:91 位貢獻者、363 次 fork,超過 3,000 顆星。)(2025 年 10 月更新:316 位貢獻者、1,400 次 fork,超過 14,000 顆星。)
每週約有 1,000 名不重複訪客瀏覽這份清單。數量不算多,但我覺得有責任持續維護更新,因為它已經成為許多人重要的資訊來源。
現在清單上列有約 300 個靜態分析工具。從 Ada 到 TypeScript 應有盡有。特別讓我感到振奮的是,現在連工具的作者們自己都會發 pull request 來新增他們的工具!
不過有一個問題:由於我忙於其他事情,pull request 的清單越積越長。

增加協作者
我總是試著把固定的貢獻者拉進來成為團隊成員。我的朋友兼同事 Andy Grunwald 以及 Ouroboros Chrysopoeia 都是非常有價值的合作夥伴。他們一有時間就會幫我篩選新的 PR。
但老實說,檢查 pull request 是一件枯燥又手動的工作。每個新工具需要檢查的項目,大致可以歸納如下:
- 符合格式規範
- 專案網址可正常連線
- 授權標示正確
- 各分類下的工具依字母順序排列
- 描述不會過長
想必很明顯我們該怎麼處理這份檢查清單:把它自動化!
幫 linter 做 lint 的 linter
何不寫一個分析工具,來檢查我們的分析工具清單呢!聽起來很後設,但其實相當單純。
每當有新的 pull request,我們就會觸發機器人,讓它檢查上述規則並回覆結果。
第一步是閱讀關於打造 CI 伺服器的 GitHub 文件。
只是好玩,我想用 Rust 來打造這個機器人。Rust 上兩個最受歡迎的 GitHub 客戶端是 github-rs(現已棄用)和 hubcaps。兩者看起來都不錯,但後來我找到了 afterparty,一個「GitHub webhook 伺服器」。
範例看起來很棒:
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate afterparty;
extern crate hyper;
use afterparty::{Delivery, Hub};
use hyper::Server;
pub fn main() {
env_logger::init().unwrap();
let addr = format!("0.0.0.0:{}", 4567);
let mut hub = Hub::new();
hub.handle("pull_request", |delivery: &Delivery| {
match delivery.payload {
Event::PullRequest { ref action, ref sender, .. } => {
// TODO: My code here!
println!("sender {} action {}", sender.login, action)
}
_ => (),
}
});
let srvc = Server::http(&addr[..])
.unwrap()
.handle(hub);
println!("listening on {}", addr);
srvc.unwrap();
}這讓我可以專注在真正的分析程式碼上,讀起來其實相當無聊。它只是機械式地檢查上述項目,用任何語言都能寫。如果你想看看(甚至想貢獻!),可以去看看這個 repo。
與 GitHub 對話
完成分析程式碼後,我就有了一個在本地端運行的機器人,等著接收新進的 pull request。
但要怎麼跟 GitHub 溝通呢?我發現應該使用 Status API 並對 POST 請求到 /repos/mre/awesome-static-analysis/statuses/:sha
(:sha 是指向該 pull request HEAD 的 commit ID):
{
"state": "success",
"description": "The build succeeded!"
}我本來可以使用現有的 Rust GitHub 客戶端之一,但我決定自己寫一個簡單的函式來更新 pull request 的狀態。
fn set_status(status: Status, desc: String, repo: &str, sha: &str) -> Result<reqwest::Response> {
let token = env::var("GITHUB_TOKEN")?;
let client = reqwest::Client::new();
let mut params = HashMap::new();
params.insert("state", format!("{}", status));
params.insert("description", desc);
println!("Sending status: {:#?}", params);
let status_url = format!("https://api.github.com/repos/{}/statuses/{}", repo, sha);
println!("Status url: {}", status_url);
Ok(client
.request(
reqwest::Method::Post,
&format!(
"{}?access_token={}",
status_url,
token,
),
)
.json(¶ms)
.send()?)
}可以看到,我從環境變數傳入 GitHub token,然後使用 reqwest 函式庫以 POST 請求發送 JSON payload。
最後這卻成了問題:afterparty 使用的是 hyper 0.9 版,而 reqwest 用的是 0.11 版。不幸的是,這兩個版本依賴了不同編譯版本的 openssl-sys 綁定。這是個有名的問題,唯一的解法就是解決這個衝突。
我卡關了一陣子,後來發現已經有一個開放的 pull request 要把 afterparty 升級到 hyper 0.10。
所以我在 Cargo.toml 裡,把 afterparty 的版本鎖定為該 pull request 的版本:
[dependencies]
afterparty = { git = "https://github.com/ms705/afterparty" }這樣就修好了建置,我終於可以繼續前進。
部署
我需要一個地方來託管這個機器人。
最好是免費的,因為這是個非營利的開源專案。而且服務商必須能執行二進位檔。
有一段時間,我一直在關注一個叫 zeit 的產品。它能透過一個名為 now 的直覺式命令列介面來執行任何 Docker 容器。
我在網站上第一次看到他們的展示就愛上了,所以想試試看。
於是我在專案中加入了一個多階段 Dockerfile:
FROM rust as builder
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN cargo build --release
FROM debian:stretch
RUN apt update \
&& apt install -y libssl1.1 ca-certificates \
&& apt clean -y \
&& apt autoclean -y \
&& apt autoremove -y
COPY --from=builder target/release/check .
EXPOSE 4567
ENTRYPOINT ["./check"]
CMD ["--help"]第一階段會建置出靜態二進位檔,第二階段則在容器啟動時執行它。結果行不通,因為 zeit 還不支援多階段建置。
變通方法是把 Dockerfile 拆成兩個,再用 Makefile 把它們串起來。Makefiles 可是很強大的,你知道吧?
有了這些,我就備齊了部署所需的所有環節。
# Build Rust binary for Linux
docker run --rm -v $(CURDIR):/usr/src/ci -w /usr/src/ci rust cargo build --release
# Deploy Docker images built from the local Dockerfile
now deploy --force --public -e GITHUB_TOKEN=${GITHUB_TOKEN}
# Set domain name of new build to `check.now.sh`
# (The deployment URL was copied to the clipboard and is retrieved with pbpaste on macOS)
now alias `pbpaste` check.now.sh以下是在使用 now 部署時的輸出:
> Deploying ~/Code/private/awesome-static-analysis-ci/deploy
> Ready! https://deploy-sjbiykfvtx.now.sh (copied to clipboard) [2s]
> Initializing…
> Initializing…
> Building
> ▲ docker build
Sending build context to Docker daemon 2.048 kBkB
> Step 1 : FROM mre0/ci:latest
> latest: Pulling from mre0/ci
> ...
> Digest: sha256:5ad07c12184755b84ca1b587e91b97c30f7d547e76628645a2c23dc1d9d3fd4b
> Status: Downloaded newer image for mre0/ci:latest
> ---> 8ee1b20de28b
> Successfully built 8ee1b20de28b
> ▲ Storing image
> ▲ Deploying image
> ▲ Container started
> listening on 0.0.0.0:4567
> Deployment complete!最後一步是在 awesome-static-analysis 的專案設定中,將 check.now.sh 加為 webhook。
現在,每當有新的 pull request 進來,你就會看到那個小機器人動起來!

成果與未來計畫
我對自己選擇的工具非常滿意:afterparty 幫我省去了大量手動工作,而 zeit 讓部署變得非常簡單。
感覺就像是Amazon Lambda 的強化版。
如果你去看我這個機器人的程式碼和 commit 紀錄,可以看到我一路上的各種小失誤,直到最後才把一切調整到位。結果發現,解析人類可讀的文字其實很繁瑣。
因此我開始考慮把分析工具清單轉成像 YAML 這樣的結構化格式。這會大幅簡化解析,也帶來額外的好處——得到一份可供其他專案使用的機器可讀工具清單。
2018 年 5 月更新
在參加維也納的 WeAreDevelopers 研討會期間(很推薦),我把 CI 流程從 zeit.co 搬到了 Travis CI。原因是,我想把 lint 的程式碼放到專案旁邊,這讓事情簡化了許多。首先,我不再需要處理網路請求的程式碼,因為 Travis 會幫忙處理。如果你有興趣,可以比較一下舊版和新版。
2025 年 10 月更新
從那之後,這個專案的幾乎一切都改變了。我現在使用 GitHub Actions 來執行 CI 檢查,而 README.md 則完全是從每個工具的 YAML 檔案自動產生的。我們支援特殊的詮釋資料欄位,像是額外資源清單(教學、影片等)或工具提供的付費方案清單。我們現在還有了一個精美的網站,在上面列出全部 700 多個工具,並讓大家可以投票。網站是用 Next.js 打造的,並使用 Algolia 提供搜尋功能。我們現在也有了一批贊助者,幫我們支付託管和其他開銷。看到一個專案只要用心投入時間就能成長到這種程度,真的很不可思議!希望這能激勵你也開啟自己的開源專案!這並不難,而且回報無比豐厚。
隨機一篇部落格
留言
登入後參與討論