Making Myself Obsolete

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にアクセスできること
  • ライセンス表記が正しいこと
  • 各セクションのツールがアルファベット順に並んでいること
  • 説明が長すぎないこと

このチェックリストをどうすべきかは明らかでしょう。自動化するのです!

リンターをリントするリンター

ならば、解析ツールのリストをチェックする解析ツールを作ってみてはどうでしょう。一見かなりメタに聞こえますが、実際はとてもシンプルです。

プルリクエストが来るたびに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というプロダクトに注目していました。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を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のようです。

コードコミット履歴を見れば、すべてがうまくいくまでにどれだけ細かな試行錯誤があったかがわかります。結論として、人間が読むためのテキストをパースするのは面倒なのです。
そこで、解析ツールのリストをYAMLのような構造化フォーマットに変換することを考えました。そうすればパースが大幅に簡単になるうえ、他のプロジェクトでも利用できる機械可読なツールリストが手に入るというメリットもあります。

2018年5月追記

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

2025年10月追記

あの頃から、このプロジェクトのほぼすべてが変わりました。現在はGitHub ActionsでCIチェックを実行し、README.mdは各ツールのYAMLファイルから完全に自動生成されています。追加リソース(チュートリアル、動画など)の一覧や、ツールが提供する有料プランの一覧といった特別なメタデータフィールドにも対応しています。おしゃれなWebサイトもでき、700以上のツールを掲載して投票もできるようになっています。サイトはNext.jsで構築し、検索にはAlgoliaを使っています。さらに、ホスティングなどの費用を支援してくれるスポンサーも増えました。愛情と時間をかければプロジェクトがここまで成長するのを見るのは、本当に驚きです。この話が、あなた自身のオープンソースプロジェクトを始めるきっかけになれば嬉しいです。決して難しくはなく、得られるものはとても大きいのです!

原文は Matthias Endler により に公開されました。

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