0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

デプロイ前に git status を見る。未コミットの修正は本番だけを先行させる

0
Posted at

背景

サイトを毎日自動でビルドしてデプロイしています。
デプロイの前には検査(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/ など)。
findUncommittedAheadgit 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補助で執筆した、個人開発の紹介記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?