背景
サイトを毎日自動でビルドしてデプロイしています。
デプロイの前には検査(preflight と呼んでいます)を通していて、そこに1つ検査を足しました。
きっかけは、去年から何度も踏んでいる形の事故です。
「本番は直っているのに、リポジトリは直っていない」というやつ…
どういう状態か
先日、コミットの中身を追っていたら、3つのサイトでこうなっていました。
- manga-deals の
build.js… 終了したセールにnoindexを付ける修正が未コミット - anime-trend の
build.js… 配信サービスへの検索リンクを足す修正が未コミット - cve-watch の
build.js… データの表記ゆれを正規化する修正が未コミット(375行)
どれも Issue としては「対応済み・クローズ」になっていました。
本番の HTML を見ると、たしかに直っています。
なのにリポジトリの HEAD には入っていない。
理由は単純で、デプロイが「作業ツリーの成果物をそのまま本番にする」操作だからです。
私の自動運用は、生成物である public/ はコミット対象に入れていますが、
それを作った build.js はコミット対象に入れていません(無人で動くものにコードを勝手にコミットさせたくないので)。
すると public/ だけが git に入り、build.js は作業ツリーに残る。
結果として、次に clean な作業ツリーから git clone してビルドすると、その修正が消えて本番が退行する。
しかもコマンドは全部成功し、警告も出ません。
なぜ既存の検査で拾えなかったか
preflight には「作業ツリーが origin/<branch> より遅れていないか」という検査が既にありました。
これは古い作業ツリーからデプロイして、公開直後の記事を本番から消した事故(実際に10分ほど404にしました)の再発防止です。
つまり見ている向きが逆でした。
あちらは「遅れ」、今回のは「未コミットの先行」。
足した検査
下記が本体です。
r.site はサイトのキー、r.repo はそのリポジトリのパスで、preflight が最初に解決しています。
function checkUncommitted(r) {
let rules;
try {
const cfg = loadScopes();
rules = (cfg.scopes || {})[r.site] || cfg.defaultSiteAllow || [];
} catch (e) {
return fail("uncommitted", `commit-scopes.json を読めませんでした: ${e.message}`);
}
let entries;
try {
entries = parseStatusZ(run("git", ["status", "--porcelain=v1", "-z", "-uall"], r.repo, 60000));
} catch (e) {
return fail("uncommitted", `未コミットの変更を確認できませんでした: ${e.message}`);
}
const ahead = findUncommittedAhead(entries, rules);
if (ahead.length === 0) return pass("uncommitted", "コミット待ちの変更なし");
return warn("uncommitted", `未コミットの変更 ${ahead.length}件が deploy に含まれます …`);
}
loadScopes() が読む commit-scopes.json は、日次ランがコミットしてよいパスの一覧です(data/ と public/ など)。
findUncommittedAhead は git status の結果のうち、その一覧に 入っていない 変更だけを返します。
この絞り込みが大事なところで、data/ や public/ の生成物では鳴りません。
それらは毎回変わるのが正常で、警告にすると「preflight はいつも黄色」になって誰も読まなくなるからです。
git status --porcelain=v1 -z を使っているのは、パスに空白や日本語が入っても壊れないようにするためです。
-z は各エントリを NUL 区切りで出すので、行分割の心配が要りません。
fail ではなく warn にした理由
最初は fail(デプロイ中止)にしようとして、やめました。
この状態の直し方はデプロイの中止ではなく、コミットです。
そしてコミットできるのは人だけです(無人ランはコードを自動コミットしない、というのが元の方針なので)。
ここで止めると、無人で動くバッチが「誰も直せない理由」で毎日落ちます。
記事が0本の日を作るくらいなら、警告を出して進むほうがましだと判断しました。
ただし検査そのものが実行できなかったときは fail のままです。
commit-scopes.json が読めない、git status が失敗する、といった場合ですね。
取得失敗を「問題なし」と読み替えるのが、これまでの誤診のほぼ全ての原因だったので、そこだけは譲っていません。
出口を作る
warn は放っておくと消えます。
実際、ビルドとデプロイが1コマンドになっているラッパでは、成功時に preflight:ok の一行しか記録が残っていませんでした。
なのでゲートの戻り値に warned[] を足して、呼び出し側の記録に1行出すようにしました。
steps.push("preflight:ok");
for (const w of gate.warned || []) steps.push(`preflight:warn(${w.name})`);
警告が出た回だけ1行増える形です。
通常の記録の形は変えたくなかったので、これくらいがちょうどいい気がします。
テストは node --test test/preflight.test.js で 40 pass / 0 fail でした。
おまけに、この修正を入れたその日のランで、検査したかった状態が実際に2件見つかりました。
この preflight の修正自体と、別サイトのファクトチェックの修正が、どちらも未コミットで作業ツリーに残っていたのです…
どちらもその場でコミットしました。
対象にしているサイトのひとつは https://manga.autoarticles.net です。
デプロイを自動化していて、かつ生成物だけをコミットしている人は、一度 git status をデプロイ直前に見てください。
そこに build.js が居たら、あなたの本番はリポジトリより先に進んでいます。
本記事はAI補助で執筆した、個人開発の紹介記事です。