Making Myself Obsolete

Matthias Endler

讓自己變得不再被需要

劍龍在 1 億 5 千萬年前曾有過更好的日子。
劍龍在 1 億 5 千萬年前曾有過更好的日子。
來源:古生物學家曾以為牠的屁股裡有一顆腦

2015 年 12 月,我在尋找可以整合進 trivago 的 CI 流程的 static analysis(靜態分析) tools。想法是自動偵測常見的程式錯誤。這是很普遍的需求,市面上也有許多實用的工具可以勝任。

於是我開始尋找一份工具清單……

令我驚訝的是,我找到的唯一一份清單竟然在維基百科上——而且已經過時了。在 Github 上——也就是大多數現代 static analysis 工具的託管平台——根本沒有這類專案。

沒有多想,我打開編輯器,把初步研究中找到的幾個工具記錄下來。接著,我就把這份清單推送到了 Github 上。

我把這個專案命名為 Awesome Static Analysis

快轉兩年後,這份清單已經成長了不少。目前已有 75 位貢獻者、277 次 forks,並獲得超過 2,000 顆 stars。(感謝大家的支持!)(2018 年 5 月更新:91 位貢獻者、363 次 forks,超過 3,000 顆 stars。)(2025 年 10 月更新:316 位貢獻者、1,400 次 forks,超過 14,000 顆 stars。)

每週約有 1000 名不重複訪客瀏覽這份清單。數量其實不算多,但我覺得有義務持續保持更新,因為它已成為許多人重要的資訊來源。

它現在收錄了約 300 個 static analysis 工具,從 Ada 到 TypeScript 應有盡有。最讓我感到鼓舞的是,現在作者們會自己發 pull request 來新增他們的工具!

不過,有一個問題:由於我忙於其他事情,pull request 的清單越積越長。

awesome-static-analysis 的 Github Pull request 清單
awesome-static-analysis 的 Github Pull request 清單

增加貢獻者

我總是嘗試把固定的貢獻者變成團隊成員。我的朋友兼同事 Andy Grunwald(安迪·格倫瓦德) 以及 Ouroboros Chrysopoeia(歐魯伯羅斯·克律索珀亞) 都是非常寶貴的合作夥伴。每當他們有空時,都會幫我篩選新的 PR。

但老實說,檢查 pull request 是件枯燥又手動的工作。每個新工具需要檢查的事項,可以歸納如下:

  • 符合格式規範
  • 專案 URL 可以連線
  • 授權標註正確
  • 各分類下的工具依字母順序排列
  • 描述不會過長

我想,該怎麼處理這份檢查清單已經很明顯了:把它自動化!

一個用來檢查 linter(程式碼檢查器) 的 linter

那何不寫一個會檢查我們這份分析工具清單的分析工具呢!聽起來很後設,但其實相當直接。

每當有新的 pull request,我們就會觸發機器人,讓它檢查上述規則並回覆結果。

第一步是閱讀 Github 上關於建置 CI 伺服器的文件

只是為了好玩,我想用 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,並對 /repos/mre/awesome-static-analysis/statuses/:sha 發送 POST 請求
(: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(&params)
        .send()?)
}

可以看到,我從環境變數傳入 Github token,然後使用 reqwest 函式庫以 post 請求發送 JSON 承載資料。

結果這最後卻變成了問題: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" }

這樣就修復了建置問題,我終於可以繼續下一步了。

部署

我需要一個地方來託管這個機器人。

最好是免費的,因為這是個非營利的 Open Source 專案。而且,服務供應商必須能夠執行二進位檔。

有一段時間,我一直在關注一個名為 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 把兩者串起來。Makefile 可是相當強大的,你知道的

有了這些,我就備齊了部署所需的所有部分。

# 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 進來,你就會看到那個小機器人開始運作!

一個已通過機器人檢查的成功 pull request

成果與未來規劃

我對自己選擇的工具非常滿意:afterparty 幫我省去了大量手動工作,而 zeit 則讓部署變得非常輕鬆。
感覺就像是 Amazon Lambda 的強化版。

如果你查看我這個機器人的 程式碼commit 紀錄,就能看到我在把一切調整到位之前所犯的各種小錯誤。結果發現,解析人類可讀的文字其實很繁瑣。
因此,我開始考慮把分析工具的清單轉換成像 YAML 這樣的結構化格式。這將大幅簡化解析工作,還有一個額外的好處,就是能擁有一份可供其他專案使用的、機器可讀的工具清單。

2018 年 5 月更新

在參加 於維也納舉行的 WeAreDevelopers 大會 期間(推薦參加),我把 CI 流水線從 zeit.co 遷移到了 Travis CI。原因是,我想把 linting 程式碼放在專案旁邊,這讓事情大幅簡化。最重要的是,我不再需要處理網路請求的程式碼了,因為 travis 會代為處理。如果你有興趣,可以比較一下 舊版新版

2025 年 10 月更新

從那之後,這個專案的幾乎一切都改變了。我現在使用 Github Actions 來執行 CI 檢查,而 README.md 則完全是從每個工具的 YAML 檔案自動產生的。我們支援特殊的 metadata 欄位,像是額外資源清單(教學、影片等)或是工具提供的付費方案清單。我們現在還有一個 精美的網站,在上面列出了全部 700 多個工具,並讓大家可以投票。這個網站是用 Next.js 打造的,並使用 Algolia 進行搜尋。我們現在也有了一群贊助者,協助我們支付託管和其他費用。看到專案只要投入一些關愛和時間就能成長到這種程度,真的很不可思議!希望這能啟發你展開自己的 open source 專案!這並不難,而且回報非常豐厚。

原文由 Matthias Endler 發布

本文章由 muse-spark-1.2-contributor 進行翻譯