一覧SE-07-1 · ビルドとCI

ビルドの再現性とキャッシュ

成果物の差を入力・キャッシュ・包装時の情報へさかのぼり、再現に必要な条件を説明します。

説明

同じソースから同じ成果物を得るには、ソース本文だけでなく、依存する処理、設定、ツール、アーカイブの時刻情報なども条件に含める必要があります。キャッシュのキーから入力が漏れると、成功したように見えて古い出力を再利用する場合があります。

短い見出しをHTMLへ変換するPythonのビルドを使います。依存ファイルのハッシュ確認、入力不足のキャッシュ、依存と設定を含めたキャッシュを順に試します。最後は同じHTMLをZIPへ入れ、時刻の違いがアーカイブのバイト列を変えることを確認します。

成果物の差を入力・キャッシュ・包装時の情報へさかのぼり、再現に必要な条件を説明します。

1ソース・設定・依存の固定

見出し本文はhello、表示の接頭辞はLessonです。このビルドのdeps.lock.jsonは、依存ファイルのパスと内容のSHA-256を記録したものです。

cat src/title.txt見出し本文を読む cat config.jsonビルド設定を読む cat deps.lock.json固定する依存ファイルの識別子を読む

依存はvendor/formatter.pyです。ロック記録を置くだけでなく、ビルド時に内容のハッシュと照合します。この演習のロック形式は、この小さなビルド専用です。

2ソースだけをキーにしたキャッシュ

まず不十分なキャッシュ方式でビルドします。source方式はsrc/title.txtだけからキーを作ります。

python3 build.py --cache sourceソース本文だけをキャッシュキーに使う cat dist/index.html生成したHTMLの内容を読む

初回はmissでHTMLを生成します。表示したSHA-256は成果物そのもののバイト列から求めた値です。キーが一致することと、成果物が要求どおり新しいことは別に確認します。

3同じ入力なら再利用する

入力を変えずにもう一度ビルドします。

python3 build.py --cache source同じキーで再ビルドする

hitになり、成果物のSHA-256も同じです。この場合は入力が同じなので、再利用した内容と生成すべき内容が一致しています。

4依存の変更をハッシュ照合で検出

formatterを大文字化する版へ替えます。ロック記録はまだ元の内容を指しています。

cp formatter-v2.py vendor/formatter.py依存処理を変更する python3 build.py --cache source固定したハッシュと照合する

dependency hash mismatchで停止します。依存の内容が記録と違うため、キャッシュの有無を調べる前に拒否されました。

5依存を更新しても古いキャッシュが返る

新しい依存に対応するロック記録へ更新します。本文hello自体は変えていません。

cp deps-v2.lock.json deps.lock.json更新した依存をロック記録へ反映する python3 build.py --cache source本文しか見ないキーで再利用する cat dist/index.html

依存は大文字化する版ですが、古いhelloのHTMLがhitで返ります。依存のハッシュ検証は通っても、キャッシュキーに依存の変更を含めなかったため、出力が古いままです。

6キャッシュを使わず結果を比較

現在の入力から作り直し、内容を比較します。

python3 build.py --no-cache保存済みの出力を使わず生成する cat dist/index.html現在の依存で生成した内容を読む

今度はHELLOです。成果物のSHA-256も変わります。依存を更新した後に本来得られる出力と、キャッシュが返した出力の不一致を確認できました。

7依存・設定・ツールを含むキーへ

full方式は本文、設定、依存とロック、ビルドスクリプト、Pythonの版をキーに含めます。続けて2回実行します。

python3 build.py --cache fullこのビルドが使う入力全体でキーを作る python3 build.py --cache full

初回miss、2回目hitで同じ成果物になります。必要な入力を含めたキーによって、現在の条件に対応する出力を再利用できます。別の環境でも再現するには、実際に出力へ影響する他の条件も調べる必要があります。

8設定変更でも再生成する

接頭辞をTopicへ変更します。本文と依存ファイルは同じです。

cp config-v2.json config.json表示の接頭辞を変更する python3 build.py --cache full設定変更を含めたキーでビルドする cat dist/index.html

設定もキーに含むのでmissになり、Topic: HELLOへ変わります。ソースコードだけでなく、設定値も成果物を変える入力です。

9ZIPの時刻だけでバイト列が変わる

同じHTMLを、ZIP内の時刻だけ変えて二つのアーカイブにします。

python3 package.py --year 2025 --output a.zip2025年の時刻情報で格納する python3 package.py --year 2026 --output b.zip2026年の時刻情報で格納する python3 compare.py本文・時刻・全バイト列を比較する

展開した本文は同じですが、アーカイブ全体は一致しません。ZIPには時刻などのメタデータも保存されるため、中身のHTMLが同じでもファイル全体のハッシュは変わります。

10アーカイブの作成条件もそろえる

a.zipと同じ時刻・名前・属性でc.zipを作ります。

python3 package.py --year 2025 --output c.zip同じ包装条件で再作成する python3 -c "from pathlib import Path; print(Path('a.zip').read_bytes()==Path('c.zip').read_bytes()二つのアーカイブ全体を比較する)"

Trueになり、a.zipとc.zipのSHA-256も一致します。入力の固定だけでなく、成果物へ含めるメタデータをそろえることも再現可能なビルドの条件です。

まとめ

依存のハッシュ確認とキャッシュの正しさは別です。キーから依存が漏れると古い出力を返します。

キャッシュキーには、このビルドのソース・設定・依存・スクリプト・ツール版など、出力を変える入力を含めます。

同じ本文でもZIPの時刻が異なるとファイル全体は変わります。包装条件も固定して再現性を確認します。

操作履歴と解説を振り返れます。

シェル / Python 3

1
cat src/title.txt cat config.json cat deps.lock.json
 hello 
{"prefix":"Lesson: "}
{
  "path": "vendor/formatter.py",
  "sha256": "9d57f8f90a514713754ce616007c19c5f27f014203dc6c49dd803ba9743f3498"
}
2
python3 build.py --cache source cat dist/index.html
cache=miss mode=source
artifact_sha256=5abb02a5c64b0c55590b225c2dbe3ffca1b5150a770a4749b894ecf494401390
<!doctype html><html lang="en"><title>Lesson: hello</title><h1>Lesson: hello</h1></html>
3
python3 build.py --cache source
cache=hit mode=source
artifact_sha256=5abb02a5c64b0c55590b225c2dbe3ffca1b5150a770a4749b894ecf494401390
4
cp formatter-v2.py vendor/formatter.py python3 build.py --cache source
dependency hash mismatch: vendor/formatter.py
5
cp deps-v2.lock.json deps.lock.json python3 build.py --cache source cat dist/index.html
cache=hit mode=source
artifact_sha256=5abb02a5c64b0c55590b225c2dbe3ffca1b5150a770a4749b894ecf494401390
<!doctype html><html lang="en"><title>Lesson: hello</title><h1>Lesson: hello</h1></html>
6
python3 build.py --no-cache cat dist/index.html
cache=miss mode=disabled
artifact_sha256=03d694e5e73ccb3333df413a0ec6b5ab32d82b3c389e4c856d55c83d798a979a
<!doctype html><html lang="en"><title>Lesson: HELLO</title><h1>Lesson: HELLO</h1></html>
7
python3 build.py --cache full python3 build.py --cache full
cache=miss mode=full
artifact_sha256=03d694e5e73ccb3333df413a0ec6b5ab32d82b3c389e4c856d55c83d798a979a
cache=hit mode=full
artifact_sha256=03d694e5e73ccb3333df413a0ec6b5ab32d82b3c389e4c856d55c83d798a979a
8
cp config-v2.json config.json python3 build.py --cache full cat dist/index.html
cache=miss mode=full
artifact_sha256=998fb1e334cce4204b3d2f514fa28415b6c3cd66e38ab529ebb0c3998a7d2403
<!doctype html><html lang="en"><title>Topic: HELLO</title><h1>Topic: HELLO</h1></html>
9
python3 package.py --year 2025 --output a.zip python3 package.py --year 2026 --output b.zip python3 compare.py
a.zip sha256=d74a6a92f241771131d77bbe306eb41360c0e551821a6586e273bb9ea83896fc
b.zip sha256=da7ddee7462eef7b5cc8b42a9f39c68b42a417477391e549f53e97a343bf8306
payload_equal=True
timestamps=(2025, 1, 1, 0, 0, 0),(2026, 1, 1, 0, 0, 0)
archive_equal=False
10
python3 package.py --year 2025 --output c.zip python3 -c "from pathlib import Path; print(Path('a.zip').read_bytes()==Path('c.zip').read_bytes())"
c.zip sha256=d74a6a92f241771131d77bbe306eb41360c0e551821a6586e273bb9ea83896fc
True

演習完了