让自己变得多余
来源:古生物学家曾经以为它的屁股里有个大脑。
2015年12月,我在寻找静态分析工具(static analysis,静态分析),以便将其集成到trivago的 CI 流程中。我们的想法是自动检测典型的编程错误。这种做法相当常见,而且有许多很有帮助的工具可以满足需求。
于是我开始寻找工具列表……
令我惊讶的是,我找到的唯一一个列表在 Wikipedia 上——而且已经过时了。在 GitHub 上也没有这样的项目,而大多数现代静态分析工具都托管在那里。
我没有多想,打开编辑器,把最初调研时找到的几个工具记了下来。然后,我把这个列表推送到了 GitHub。
我将这个项目命名为Awesome Static Analysis。
两年后,这个列表已经壮大了不少。截至目前,它有 75 位贡献者、277 个派生仓库,收获了超过 2,000 个 star。(感谢大家的支持!)(2018年5月更新:91 位贡献者、363 个派生仓库、超过 3,000 个 star。)(2025年10月更新:316 位贡献者、1,400 个派生仓库、超过 14,000 个 star。)
每周大约有 1,000 名独立访客找到这个列表。无论如何都算不上多,但我觉得自己有责任让它保持最新,因为它已经成为许多人的重要信息来源。
现在,列表中大约有 300 个静态分析工具。从 Ada 到 TypeScript,应有尽有。尤其让我感到振奋的是,现在工具作者们自己也会创建 pull request 来添加他们的工具!
不过有一个问题:pull request 列表越来越长,因为我正忙于处理其他事情。

增加贡献者
我总是努力把经常贡献代码的人变成团队成员。我的朋友兼同事 Andy Grunwald(安迪·格伦瓦尔德),以及 Ouroboros Chrysopoeia(衔尾蛇炼金术),都是非常宝贵的协作者。每当有时间,他们就会帮我筛选新的 PR。
但我们还是面对现实吧:检查 pull request 是一项枯燥的手工任务。每个新工具都需要检查的内容,大致可以总结为:
- 符合格式规则
- 项目 URL 可访问
- 许可证标注正确
- 每个分类中的工具按字母顺序排列
- 描述不能太长
我想我们该如何处理这份检查清单已经很明显了:自动化它!
用于检查代码检查器的 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();
}这让我可以专注于真正的分析代码,而那部分读起来相当无聊。它机械地检查上面提到的内容,用任何语言都能编写。如果你想看看(甚至想参与贡献!),可以查看代码仓库。
与 GitHub 通信
分析代码完成后,我有了一个在本地运行、等待传入 pull request 的机器人。
但我该如何与 GitHub 通信呢?我了解到,应该使用 Status API,向 /repos/mre/awesome-static-analysis/statuses/:sha 发送 POST 请求
(:sha 是指向 pull request 的 HEAD 的提交 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 负载。
这最终成了一个问题: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 将它们连接起来。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 进来时,你就能看到那个小机器人开始忙碌了!

结果与未来计划
我对自己选择的工具非常满意:afterparty 帮我省去了大量手工工作,而 zeit 让部署变得非常容易。
这种感觉就像是增强版的 Amazon Lambda。
如果你查看我的机器人的代码和提交记录,就能看到我一路走来的各种小失误,直到最终把一切调整妥当。事实证明,解析人类可读的文本很繁琐。
因此,我考虑过把分析工具列表转换成类似 YAML 的结构化格式。这样会大大简化解析工作,而且还能获得一份机器可读的工具列表,可供其他项目使用。
2018年5月更新
在参加维也纳的 WeAreDevelopers 大会期间(推荐参加),我把 CI 流程从 zeit.co 迁移到了 Travis CI。原因是我希望将 lint 代码放在项目旁边,这极大地简化了工作。最重要的是,我不再需要处理 Web 请求的代码,因为 Travis 会负责这些事情。如果你愿意,可以比较旧版本和新版本。
2025年10月更新
从那时起,这个项目几乎所有方面都发生了变化。现在我使用 GitHub Actions 来运行 CI 检查,而 README.md 则完全根据每个工具对应的 YAML 文件自动生成。我们支持特殊的元数据字段,例如附加资源列表(教程、视频等),或工具提供的付费方案列表。现在我们有了一个漂亮的网站,列出了 700 多个工具,并允许人们投票。网站使用 Next.js 构建,并采用 Algolia 进行搜索。我们现在还有不少赞助商,帮助我们支付托管和其他费用。看到项目只要投入一些关爱和时间就能不断成长,真是不可思议!我希望这能激励你开始自己的开源项目!这并没有那么难,而且会带来难以置信的成就感。
随机一篇博客