Making Myself Obsolete

Matthias Endler

自分を不要にする

原文は Matthias Endler により に公開されました。 このブログを購読する

ステゴサウルスにも1億5000万年前には良い時代があった。
ステゴサウルスにも1億5000万年前には良い時代があった。
出典:かつて古生物学者は、ステゴサウルスには尻に脳があると考えていた。

2015年12月、trivagoのCIプロセスに組み込むための静的解析ツールを探していた。典型的なプログラミングミスを自動で検出したいというのが狙いだった。ごく一般的なことだし、要件に合う便利なツールは世の中にたくさんある。

そこで、ツールのリストを探してみた……

驚いたことに、見つかったリストはWikipediaにあるものだけ——しかも内容は古かった。ほとんどのモダンな静的解析ツールがホストされているGitHubには、そういったプロジェクトが存在しなかったのだ。

深く考えず、エディタを開いて、最初のリサーチで見つけたツールをいくつか書き出した。そして、そのリストをGitHubにプッシュした。

プロジェクト名はAwesome Static Analysisと名付けた。

2年が経つと、リストはかなり成長していた。これまでに75人のコントリビューター、277のフォーク、そして2,000以上のスターを獲得した。(応援ありがとう!)(2018年5月追記:91人のコントリビューター、363フォーク、3,000スター超)(2025年10月追記:316人のコントリビューター、1,400フォーク、14,000スター超)

毎週およそ1,000人のユニークビジターがこのリストを見つけている。決して多いとは言えないが、多くの人にとって不可欠な情報源になったため、最新の状態に保つ責任を感じている。

現在では約300の静的解析ツールが掲載されている。AdaからTypeScriptまで何でも揃っている。特に励みになるのは、ツール作者自身が自らのツールを追加するプルリクエストを送ってくれるようになったことだ!

ただ、ひとつ問題があった。他のことで忙しくしている間に、プルリクエストのリストがどんどん長くなっていったのだ。

awesome-static-analysisのGitHubプルリクエスト一覧
awesome-static-analysisのGitHubプルリクエスト一覧

コントリビューターを増やす

僕は常連のコントリビューターにはチームメンバーになってもらうようにしている。友人であり同僚のAndy Grunwaldや、Ouroboros Chrysopoeiaは貴重な協力者だ。彼らが時間を見つけては、新しいPRの選別を手伝ってくれている。

でも正直なところ、プルリクエストのチェックは退屈な手作業だ。新しいツールごとに確認すべきことは、こんな感じにまとめられる。

  • フォーマットルールが守られているか
  • プロジェクトのURLにアクセスできるか
  • ライセンス表記が正しいか
  • 各セクションのツールがアルファベット順に並んでいるか
  • 説明が長すぎないか

このチェックリストをどうすべきかは明らかだろう――自動化だ!

リンターをlintするリンター

それなら、解析ツールのリストをチェックする解析ツールを作ってみたらどうだろう!メタっぽく聞こえるが、実際はとてもシンプルな話だ。

プルリクエストが来るたびにBotを起動し、上記のルールをチェックして結果を返すようにする。

最初のステップは、CIサーバーを構築するためのGitHubドキュメントを読むことだった。

せっかくなので、BotはRustで作ってみることにした。Rust向けで最も人気のあるGitHubクライアントはgithub-rs(現在は非推奨)とhubcapsの2つだった。どちらもなかなか良さそうだったが、そのときafterpartyという「Github webhook server」を見つけた。

サンプルコードは素晴らしかった。

#[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とのやり取り

解析コードが完成すると、手元で動き、プルリクエストを待ち受けるBotができた。

でも、どうやってGitHubと通信すればいいのか?調べてみると、Status APIを使って、POSTリクエストを/repos/mre/awesome-static-analysis/statuses/:sha
(:shaはプルリクエストのHEADを指すコミットID)に送ればいいことがわかった。

{
  "state": "success",
  "description": "The build succeeded!"
}

既存のRust用GitHubクライアントを使ってもよかったが、プルリクエストのステータスを更新するシンプルな関数を自分で書くことにした。

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トークンを渡し、reqwestライブラリを使ってJSONペイロードをPOSTリクエストとして送っている。

これが最終的に問題になった。afterpartyはhyperのバージョン0.9を使っていたのに対し、reqwestは0.11を使っていたのだ。残念ながら、これら2つのバージョンは異なるビルドのopenssl-sysバインディングに依存している。これはよく知られた問題で、解決するにはコンフリクトを解消するしかない。

しばらく行き詰まっていたが、afterpartyをhyper 0.10にアップグレードするオープンプルリクエストがあるのを見つけた。

そこでCargo.tomlの中で、afterpartyのバージョンをそのプルリクエストのバージョンに固定した。

[dependencies]
afterparty = { git = "https://github.com/ms705/afterparty" }

これでビルドが通り、ようやく先に進むことができた。

デプロイ

Botをホストする場所が必要だった。

できれば無料がいい。非営利のオープンソースプロジェクトなので。また、バイナリを実行できるプロバイダーである必要があった。

しばらくの間、zeitというプロダクトを追いかけていた。どんなDockerコンテナでもnowという直観的なコマンドラインインターフェースで実行できる。

サイトでデモを見た瞬間に惚れ込んだので、ぜひ試してみたかった。

そこでプロジェクトにマルチステージ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を2つに分割し、両者を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として追加することだった。

これで、新しいプルリクエストが来るたびに、あの小さなBotが動き出すのが見られる!

Botによってチェックされた、成功したプルリクエスト

成果と今後の展望

使ったツール選びにはとても満足している。afterpartyは多くの手作業から解放してくれたし、zeitはデプロイを超簡単にしてくれた。まるでAmazon Lambdaの強化版のようだ。

Botのコードコミットを見れば、すべてがうまくいくまでにどれだけ小さな失敗を重ねたかがわかる。人が読むためのテキストをパースするのは骨が折れる。そこで、解析ツールのリストをYAMLのような構造化されたフォーマットに変えようかと考えている。そうすればパースが大幅に簡単になるし、他のプロジェクトでも使える機械可読なツールリストが手に入るという利点もある。

2018年5月追記

ウィーンで開催されたWeAreDevelopersカンファレンスに参加している間(おすすめだ)、CIパイプラインをzeit.coからTravis CIに移行した。lint用のコードをプロジェクトの隣に置きたかったからで、これで物事が大幅にシンプルになった。何より、Travisがやってくれるので、ウェブリクエストを扱うコードが不要になった。よければ、旧バージョン新バージョンを比べてみてほしい。

2025年10月追記

それ以来、プロジェクトのほぼすべてが変わった。現在はGithub ActionsでCIチェックを実行し、README.mdは各ツールのYAMLファイルから完全に自動生成されている。追加リソース(チュートリアル、動画など)のリストや、ツールが提供する有料プランのリストといった特別なメタデータフィールドにも対応している。今ではおしゃれなウェブサイトもあり、700以上のツールを掲載して投票もできるようになっている。サイトはNext.jsで作られ、検索にはAlgoliaを使っている。さらに、ホスティングなどの費用を賄ってくれるスポンサーもたくさんついた。少し愛情と時間をかければプロジェクトがどれだけ成長するか、驚くばかりだ。これがあなた自身のオープンソースプロジェクトを始めるきっかけになれば嬉しい!それほど難しくないし、信じられないほどやりがいがあるよ。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント