ルールはあるのに、誰も守ってない。CIを入れたら初日から真っ赤になった

自分のプロジェクトのドキュメントに、ルールはありました。

「1つの文書は1つの責務」「45行を超えたら分割」 一昨日の自分が、たしかにそう書いてます。

で、実際どうだったか。

  • README(そのフォルダの目次)が無いフォルダ、37個中25個
  • 開発の行動ルールを置いてるフォルダ、11ファイル入ってるのに目次ゼロ
  • 45行ルールを破ってる文書、47本
  • 最大のやつ、1203行

ルールは、書いただけでは守られません。 当たり前ですね。当たり前なんですが、自分で書いたルールを自分で破ってる文書を47本も並べられると、さすがに笑えなくなってきます。 (一昨日の自分は、たぶん本気で守るつもりだった)

まず規約そのものを書き直したら、150行を超えた

やることは決まってました。ルールを機械に強制させる。CIで落とす。 そのためには、まず規約自体をちゃんと書く必要がある。

README必須、フォルダ構成の決まり、更新義務、CI の中身……全部足していったら、規約ファイルが150行を超えました。

45行ルールを、規約ファイルが破ってます。

ここで「規約は特別だから例外にしよう」とやると、どうなるか。 「規約は例外」という前例が、その日から永久に残ります。 以後だれかが45行を超えるたびに、「規約だって超えてるじゃん」と言える。言えてしまう。

なので分割しました。規約を8ファイルに割って、全部45行以内に収めた。 自分のルールを自分で守れないなら、そのルールは最初から要らないので。

CIを入れた瞬間、既存の63件が牙を剥いてくる

さて本題。ルールを検査するスクリプトを書いて、CIに繋ぎます。

走らせると、当然こうなる。

違反 63 件

全部落ちます。真っ赤です。 これから触るファイルも、何ヶ月も触ってない古いファイルも、平等に赤い。

ここで人類には2つの選択肢が与えられます。

  1. 全部直してからCIを入れる(63件、最大1203行のやつ含む)
  2. とりあえず例外リストを作って、既存の63件を黙らせる

2番、やりたくなりませんか。私はなりました。 .lintignore みたいなファイルに63行並べて、「あとで消す」ってコメントを添えるやつです。

その例外リスト、いつ消すんですか

冷静に考えると、あれは第二の正本なんですよね。

ルールの本体はルール文書に書いてある。 でも「実際に何が免除されてるか」は例外リストにしか書いてない。 2つのファイルが、別々に育っていく。

そして例外リストは、絶対に消えません。 消すインセンティブが誰にも無いので。減るとしたら偶然です。 (「あとで消す」の「あとで」が来たプロジェクトを、私は見たことがない)

じゃあ1番(全部直してからCI)はどうか。これも罠で、直してる最中はCIが無いんです。 一番ドキュメントを触ってる期間に、ゲートが効いてない。

「1200行のtypoを直した人」に、分割を強制していいのか

ここで手が止まりました。

仮に、1203行の文書のtypoを1文字直すPRを出したとします。 boy-scout rule(触ったところは綺麗にして帰る)を素直に実装すると、そのPRは「この文書は45行を超えてます」で落ちます。

typo1文字直そうとした人に、1203行の分割を要求する。

そんなCIがあったら、どうなるか。 誰もtypoを直さなくなります。 壊れた窓を見なかったことにするのが、合理的な行動になる。ルールが、改善を罰する装置になってしまう。

じゃあ何を怒るべきか。答えは「そのPRが持ち込んだものだけ」でした。

  • 新しく作ったファイルが45行超 → 落とす(今から負債を増やすな)
  • 既存の1203行を触った → 落とさない(それはあなたの責任じゃない)
  • ただし既存の1203行に書き足して1250行にした → 落とす(悪化はさせるな)

「新規か既存か」じゃなくて「増えたか」で見る。 これ、例外リストが要らないんです。gitに全部書いてあるので。

比較元(マージ先のブランチ)と比べて、そのファイルが伸びたかどうか。 それだけで判定できる。第二の正本が生まれない。

自分で書いたルール文書が、自分のlintに落とされた

この設計をルール文書に書き足してたら、CIが落ちました。

LONG  docs/rules/doc-ci.md: 本文 46 行 > 45

私です。犯人は私です。 「既存の文書に書き足して伸ばしたら落とす」を実装した本人が、その仕様を説明する文章を書き足して伸ばして、落ちてます。

正直、笑いました。そして直しました(44行に削った) 設計が意図通り動いてる証拠が、まさか自分の首で取れるとは思ってなかったです。 感心しました。よくできてる、と。 止められるべき人間を、正しく止めている。 しかもその人間が作者だからといって、一切手加減しない。 自分が例外にならないことを、自分の首で確かめられたわけです。

ファイルを移動しただけで「新規追加」扱いされる

もう1個踏みました。

フォルダ構成を直すので、当然 git mv でファイルを大量に移動します。 すると移動先のパスには、比較元にファイルが存在しない。

CIから見ると「新規追加されたファイル」です。 新規追加は容赦なく落とす仕様なので、移動しただけの1203行が、分割を要求されました。10件まとめて。

これ、rename(改名)を追いかける処理を入れて解決しました。 移動しただけのファイルは、移動前の行数と比べる。伸びてなければ落とさない。

構造を直すことが、無関係な分割を強制する。 実際にやってみるまで、まったく気づかなかったです。

秘密の除外を、パスでやりたくなる(そしてそれは間違いだった)

もう1つゲートを入れてました。秘密情報(APIキーとかトークン)がコミットに混ざってないか見るやつです。

これがドキュメントのPRで落ちました。中身はこの2つ。

  • Authorization: Bearer <トークン名> ← 山括弧の、どう見ても穴埋め
  • サンプルのID(末尾が固定のダミー値。誰がどう見ても例)

誤検知です。しかも私が新しく書いた行ですらない。何ヶ月も前からあった行が、ファイルを分割したせいで「新規追加行」として再検出されただけ。

で、ここでも誘惑が来ます。 「ドキュメントのフォルダを、まるごと検査対象から外せばいい」 一行です。一行で直る。

やらなくてよかったです。

というのも、この作業の途中で、本番で使っている署名用の秘密が、平文のままドキュメントに書かれているのを見つけてたんですよ。自分で書いて、自分で忘れてたやつです。 見つけた瞬間に思ったのは、一行で逃げなくて本当によかった、でした。

もしフォルダごと除外していたら。 その秘密は、検査対象の外に置かれて、静かに残り続けていました。

除外するのは「場所」じゃなくて「形」でした。 山括弧の穴埋めというだけを除外する。本物のキーは形が違うので、必ず引っかかる。

念のため、両方向を試しました。

  • 穴埋めのプレースホルダ → 検出されない(意図通り)
  • 試しに本物っぽいキーを置いてみる → 3つのルールが同時に反応して、ちゃんと落ちた

緑になったことより、こっちの確認のほうが大事です。 ゲートは「通ること」じゃなく「落ちるべきものが落ちること」を確かめないと、意味がない。

負債が0になった日、昇格は3行で済んだ

そこから、47本を分割していきました。63件 → 50 → 36 → 27 → 17 → 0。

最終的にドキュメントは488ファイル・99フォルダ。全フォルダに目次があって、全ファイルが45行以内(丸ごとコピペして使う台本とかは、理由を書いて例外にしてます。15本)

そして負債が0になったので、CIを「全ファイル・常時・容赦なし」に昇格させました。 その差分が、これです。

  • 環境変数に STRICT: '1' を足す
  • 移行期用の「触った分だけ検査」を消す

3行でした。

例外リストを作らなかったので、消すべき第二の正本が存在しなかった。 段階運用は最初から最後までgitの事実だけで回ってたので、畳むのにコストがかからなかった。

もしあの日、63行の例外リストを作っていたら。 今ごろ「このリスト、まだ要るんだっけ」を1行ずつ確かめてたはずです。たぶん3行では終わってない。

機械が守れるものと、守れないもの

最後に、正直な話をします。

このCIが保証してるのは構造だけです。 目次が漏れてないか。45行を超えてないか。リンクが切れてないか。新しい秘密が混ざってないか。

「書いてある中身が、実装と合っているか」は、機械には守れません。

実装を変えたらドキュメントも必須、みたいなゲートを作ることはできます。でもそれは推測でしかなくて、正当なPRを誤爆で落とすか、逆に「網羅した気になる偽の安心」を生むかのどちらかです。

なので鮮度は、ルールのほうに書きました。 ドキュメントを読んでズレに気づいたら、実機で裏が取れた明白な間違いだけ直す。裏が取れないなら本文は触らず、issueを立てて「怪しい」と印を付ける

推測で書き換えるのが最悪なんです。 間違いが「レビュー済み」の顔をして残るので。

このルール、作業中に2回発動しました。 1回はフォルダ構成の説明が実態とズレてたとき(直さずに起票した) もう1回は、文書を分割してる最中に「過去に却下した提案の、却下した理由」が消えかけてたのを見つけて、復元したとき。

なぜ却下したかの記録は、失うと復元できません。 そして失われると、同じ提案が半年後にまた出てきます。

締め

ルールを機械に守らせる話は、だいたい「どう検出するか」で盛り上がります。 でも実際に効いたのは、何を見逃すかの設計のほうでした。

見逃し方を雑にすると、ゲートは2通りに死にます。 全部落として無視されるか、例外リストが太って何も守らなくなるか。

「あとで消す」と書いた例外リストは、あとで消えない。 だったら、最初から作らないほうがいい。