让自己变得不再必要
原文由 Matthias Endler 于 发布,订阅该博客
来源:古生物学家曾以为它屁股里长着大脑。
2015年12月,我在为trivago的 CI 流程寻找静态分析工具。想法是自动检测常见的编程错误。这本来是很常见的需求,市面上也有很多好用的工具可以满足。
于是我去找了一份工具清单……
让我意外的是,我找到的唯一一份清单就在维基百科上——而且已经过时了。而在托管着大多数现代静态分析工具的 GitHub 上,却没有这样的项目。
我没多想,就打开编辑器,把初步调研找到的几个工具记了下来,然后把这份清单推到了 GitHub 上。
我把这个项目叫做Awesome Static Analysis。
两年过去,这份清单已经壮大了不少。到当时为止,它已有 75 位贡献者、277 个 fork,收获了超过 2000 个 star。(感谢大家的支持!)(2018年5月更新:91 位贡献者、363 个 fork,超过 3000 个 star。)(2025年10月更新:316 位贡献者、1400 个 fork,超过 14000 个 star。)
每周大约有 1000 名独立访客会找到这份清单。数量算不上多,但因为它已经成为许多人重要的信息来源,我觉得有义务保持它的更新。
它现在收录了约 300 款静态分析工具,从 Ada 到 TypeScript 应有尽有。尤其让我感到鼓舞的是,现在工具的作者们会主动提交 pull request 来添加自己的工具!
不过,有一个问题:因为我忙于其他事情,pull request 列表越积越长。

添加协作者
我一向会尝试把经常贡献的人发展成团队成员。我的朋友兼同事Andy Grunwald以及Ouroboros Chrysopoeia都是非常宝贵的协作者,他们一有时间就会帮我筛选新的 PR。
但说实话,检查 pull request 是一项枯燥的手工活。每个新工具需要检查的内容可以归纳如下:
- 符合格式规范
- 项目链接可访问
- 许可证标注正确
- 每个分类下的工具按字母顺序排列
- 描述不要过长
接下来该怎么做,想必已经很明显了:把它自动化!
给 Linter 做检查的 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();
}这让我可以专注于实际的分析代码,那部分读起来相当枯燥。它只是机械地检查上面提到的那些项,用任何语言都能写。如果你想看看(甚至想贡献代码),可以去这个仓库瞧瞧。
与 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绑定。这是一个众所周知的问题,唯一的解决办法就是解决冲突。
我卡了一段时间,后来发现有一个将 afterparty 升级到 hyper 0.10 的待处理 pull request。
于是我在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。原因是,我想把检查代码直接放在项目旁边,这样能大大简化事情。最重要的是,我不再需要处理网络请求的代码了,因为 Travis 会搞定这些。如果你愿意,可以对比一下旧版和新版。
2025年10月更新
从那以后,这个项目的大部分内容都变了。我现在使用 GitHub Actions 来运行 CI 检查,README.md完全由每个工具对应的 YAML 文件自动生成。我们还支持一些特殊的元数据字段,比如附加资源列表(教程、视频等)或工具提供的付费方案列表。我们现在有了一个精美的网站,在上面列出了 700 多款工具,并允许大家投票。网站使用 Next.js 构建,并用 Algolia 实现搜索。我们现在也有了一批赞助者,他们帮我们支付托管和其他费用。看到一个项目只要投入热爱和时间就能成长到这个地步,真是令人惊叹!希望这能激励你也开启自己的开源项目!这并不难,而且回报巨大。
随机一篇博客
评论
登录后参与讨论