Skip to content

feat(container): 古いイメージの自動リフレッシュと公開イメージのpull対応#7

Merged
takemi-ohama merged 7 commits intomainfrom
feat-rebuild-stale-images
May 2, 2026
Merged

feat(container): 古いイメージの自動リフレッシュと公開イメージのpull対応#7
takemi-ohama merged 7 commits intomainfrom
feat-rebuild-stale-images

Conversation

@takemi-ohama
Copy link
Copy Markdown
Contributor

@takemi-ohama takemi-ohama commented May 1, 2026

Pull Request

概要

`devbase up` 実行時のイメージ事前チェックを強化し、以下を自動化:

  1. イメージが古い場合 (7日以上経過) は自動的に再ビルド: build context があるイメージは `docker compose build --no-cache` で再構築
  2. 公開イメージ (image: のみ) の取得・更新を明示化: 不在時は `docker pull`、古い場合も `docker pull` で再取得

合わせて以下も対応:

  • `bin/devbase` の `cmd_build --no-cache` から `docker builder prune -af` を削除(自動 `--no-cache` 時に他プロジェクトの buildx キャッシュを巻き添えで削除する副作用を排除)
  • README の「公式レジストリ」表記を撤去 (v2.2.0 で公式概念は廃止済)

関連 Issue

  • Closes #

変更点

`lib/devbase/commands/container.py`

  • `_IMAGE_MAX_AGE_DAYS = 7` を新設

  • `_ensure_images()` を 4 ケースに拡張:

    image: build: イメージ存在 年齢 動作
    × - `devbase build`
    < 7日 何もしない
    ≥ 7日 `devbase build --no-cache` (新規)
    × - `docker pull` (新規)
    < 7日 何もしない
    ≥ 7日 `docker pull` (新規)
  • 追加関数:

    • `_get_image_age_days(inspect_json)`: `docker image inspect` の `Created` (RFC3339, ナノ秒精度) を Python 3.10 互換に正規化して経過日数を返す
    • `_run_pull(image_name)`: `docker pull ` 実行
  • `_run_build(no_cache: bool = False)`: `bash bin/devbase build --no-cache` を透過

`bin/devbase` (cmd_build の副作用除去)

`--no-cache` 検知時に行っていた `docker builder prune -af` を削除:

  • 理由: `--no-cache` フラグだけでその build はキャッシュを使わない。`prune -af` は冗長かつホスト全体(他プロジェクトを含む)の buildx キャッシュを削除する副作用があった
  • 影響: 本 PR で導入した自動 `--no-cache` 再ビルド (7日経過時) で他プロジェクトの buildx キャッシュが意図せず消える事態を回避
  • 挙動変更: 手動 `devbase build --no-cache` も「現在の build のみ no-cache」となり最小驚きの原則に整合
  • ステップ番号を `[1/3]-[3/3]` から `[1/2]-[2/2]` に整合化

ディスククリーンアップが必要な場合は別コマンド(将来 `devbase clean` 等)で明示的に行う方針。

`README.md`

  • クイックスタートの「公式は自動登録済み」を「サンプルレジストリ `devbasex/devbase-samples` は自動登録済み」に修正

動作確認

  • `devbase up` で初回ビルドが従来通り動作する
  • 7日経過したイメージで `devbase up` を実行すると `--no-cache` 再ビルドが走る
  • その際、ホスト上の他プロジェクトの buildx キャッシュが消えていない
  • `compose.yml` で `image: postgres:16` のみ指定したサービスで `devbase up` 実行時に `docker pull` が走る (初回 / 7日後)
  • `docker image inspect` の `Created` フィールドのパースが macOS / Linux 両方で動く
  • イメージが新しい (< 7日) 場合に余計な build/pull が走らない
  • 手動 `devbase build --no-cache` も従来どおり動作する(prune が走らないだけで、対象イメージは no-cache でビルドされる)

やらないこと

  • 閾値 7 日のユーザー設定可 (環境変数化など) は今回見送り。将来必要なら別 PR で
  • ベースイメージ (devbase-base) の独立した年齢判定は実装しない (`--no-cache` 時に Bash の `cmd_build` が連鎖再構築するため)

takemi-ohama and others added 4 commits May 2, 2026 08:26
v2.2.0 で公式レジストリ固定の概念は廃止されているため、
クイックスタートのコメントを「サンプルレジストリ devbasex/devbase-samples は自動登録済み」に修正。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
devbase up 時のイメージ事前チェック (_ensure_images) を強化:

1. build: 定義あり / イメージが7日以上古い場合
   → devbase build --no-cache で再ビルド
2. image: のみ (build: なし) / イメージが存在しない場合
   → docker pull で取得
3. image: のみ / イメージが7日以上古い場合
   → docker pull で再取得

実装:
- _IMAGE_MAX_AGE_DAYS = 7 を定数化
- _get_image_age_days(): docker image inspect の Created を解析
  Docker のナノ秒精度 RFC3339 を Python 3.10 の fromisoformat 互換形式に正規化
- _run_pull(image_name): docker pull の実行
- _run_build(no_cache=False): bash の cmd_build に --no-cache を透過

Bash 側 cmd_build は既に --no-cache をサポート済みのため変更不要。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
--no-cache フラグだけでビルド時にキャッシュは使われないため、
docker builder prune -af は冗長。さらに、prune は

  - 未使用キャッシュ (-a) を含むホスト全体の buildx キャッシュを削除
  - 他プロジェクトの build キャッシュも巻き添えに

する副作用があり、特に PR #7 で導入した自動 --no-cache 再ビルド
(7日経過時) では意図せぬ全消去を引き起こす。

最小驚きの原則に従い、--no-cache はその build のみに作用するよう
修正。ステップ番号を [1/3]-[3/3] から [1/2]-[2/2] に整合化。

ディスク容量整理が必要な場合は別コマンド (devbase clean 等) で
明示的に行うべき。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #7 の self-review で挙げた MED/LOW 指摘を反映:

- [MED] datetime / timezone / re を関数内 import からモジュール先頭へ移動
  Python 標準ライブラリの軽量モジュールを関数内 import するのは非慣用。

- [MED] 閾値を環境変数 DEVBASE_IMAGE_MAX_AGE_DAYS で上書き可能に
  _image_max_age_days() ヘルパを追加。不正値 (非数値・負数) は
  警告ログを出してデフォルト 7 にフォールバック。

- [LOW] RFC3339 タイムスタンプ整形を re.sub で簡潔化
  split('.') + rfind('+/-') の手作業ロジックを 1 行の正規表現に。
  ナノ秒/マイクロ秒/ミリ秒/小数なし/オフセット付き全パターン動作確認済み。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@takemi-ohama
Copy link
Copy Markdown
Contributor Author

レビュー指摘対応サマリー (commit 9010cbd)

self-review で挙げた 4 件のうち、3 件を対応・1 件を見送り。

# 重要度 指摘 対応 コメント
1 MED datetime import を関数内→モジュール先頭 ✅ 対応 re も合わせて先頭 import に統一
2 MED 閾値を環境変数で上書き可能に ✅ 対応 DEVBASE_IMAGE_MAX_AGE_DAYS を新設。不正値は警告→デフォルト
3 LOW RFC3339 整形を re.sub で簡潔化 ✅ 対応 re.sub(r'(\.\d{6})\d+', r'\1', ts) の 1 行に
4 LOW timezone 無し timestamp の暗黙フォールスルー ⏭️ 見送り Docker は常に timezone 付きを返却。except Exception で安全側に倒れているため挙動上問題なし

自己テスト結果

nanoseconds with Z:       837 days
microseconds with Z:      837 days
milliseconds with Z:      837 days
no fraction with Z:       837 days
with offset (+09:00):     837 days
3-day-old:                3 days
10-day-old:               10 days
default threshold:        7
override 14:              14
override 0:               0
invalid 'abc':            7 (warning logged)
invalid '-3':             7 (warning logged)

CI 再実行結果

全 5 チェック PASS(Ruff lint / ShellCheck / Python syntax 3.10-3.12)。

修正ファイル

  • lib/devbase/commands/container.py (+36 -20)

Codex 第二意見レビューで指摘された 4 件に対応:

[1.1 HIGH] 公開イメージの定期 re-pull を廃止
  docker image inspect の Created はイメージのビルド日時で
  ローカル pull 日時ではないため、age 比較で再 pull すると
  毎回 pull がループする欠陥があった。image-only サービス
  (build: なし) は不在時のみ pull し、定期再 pull は行わない
  方針に変更。「image: のみで定期 pull が必要」というユース
  ケースが出てきたら別 PR で digest 比較等を実装する。

[1.2 HIGH] DEV_SERVICE_NAME 連動の build に修正
  Python 側 _ensure_images() は get_dev_service_name() で対象
  サービスを解決していたが、Bash 側 cmd_build は
  'docker compose build dev' を固定実行していた。
  DEV_SERVICE_NAME != dev なプロジェクトで自動再ビルド経路に
  入ると存在しないサービスをビルドしようとして失敗する回帰。
  Bash 側を 'docker compose build "${DEV_SERVICE_NAME:-dev}"'
  に変更してチェックと実行のサービス名解決を整合化。

[1.4 MED] エラーメッセージを build/pull 双方を案内する文面に
  _ensure_images() 失敗時に常に "devbase container build" を
  案内していたが、image-only サービスでは build は不適切。
  build と docker pull の両方を案内する文面に変更。

[2.1 MED] cli-reference.md に自動 build/pull 動作を明記
  devbase up が条件次第で build/pull を自動実行する旨と、
  DEVBASE_IMAGE_MAX_AGE_DAYS 環境変数の存在を追記。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@takemi-ohama
Copy link
Copy Markdown
Contributor Author

Codex 第二意見レビュー 対応サマリー (commit 23e4e9e)

外部 AI (Codex / gpt-5.4) で独立レビューを実施し、HIGH 3 件・MED 9 件・LOW 3 件の指摘を取得。本コミットでは HIGH 2 件と MED 2 件 (実質 [4.1] は [1.2] と同一) に対応。

対応した指摘

# 重要度 指摘 対応
1.1 HIGH 公開イメージの定期 re-pull が Created 依存で機能せず、毎回 pull ループになる image-only サービスは不在時のみ pull、定期再 pull は廃止。設計変更で根本対処
1.2 HIGH Python 側 get_dev_service_name() と Bash 側 docker compose build dev 固定の不整合で DEV_SERVICE_NAME != dev 環境が壊れる Bash の cmd_builddocker compose build "${DEV_SERVICE_NAME:-dev}" に修正 (2 箇所)
1.4 MED エラーメッセージが常に devbase build 案内 (image-only 構成で誤案内) build と docker pull の両方を案内する文面に変更
2.1 MED docs に新挙動の記載がない cli-reference.mddevbase container up 節に自動 build/pull の挙動と DEVBASE_IMAGE_MAX_AGE_DAYS を追記
4.1 HIGH (1.2 の後方互換性視点での再掲) 1.2 と同時に解消

見送った指摘とその理由

# 重要度 指摘 理由
1.3 MED compose config 失敗時のフォールバック 既存の保守的 fallback。build: 必須プロジェクトが大半なので一旦維持
2.2 MED UX: every up may trigger pull 1.1 の修正で自然解消
3.1 MED build / pull の閾値分離 1.1 で pull 側の閾値判定自体を廃止したため不要
3.2 MED Python/Bash 責務分離 大規模リファクタになるため別 PR
3.3 LOW _ensure_images() の戻り値型 1.4 でメッセージ改善した範囲で十分。型変更は別 PR
4.2 MED up がネットワーク依存になる 1.1 で公開イメージの自動 pull がほぼなくなり影響軽減
6.1-6.3 MED-LOW 自動テスト追加 別 PR で _get_image_age_days 等の pure 関数からテスト導入
7 (追加提案) - digest 比較、devbase images refresh コマンド等 必要が出てきたら別 PR

設計判断のポイント

[1.1] への対応として「image-only の定期 pull 機能自体を削除」という選択をしました。Codex は「pull 日時を別保存する」「digest 比較する」を提案しましたが:

  • 現時点で必要性が確認されていない (公開イメージの自動更新を頻繁に求められるユースケースが具体的に出ていない)
  • 中途半端な実装より「やらない」のほうが Codex 指摘の 「意味論が異なる 2 種を 1 閾値で扱う」(3.1) も同時に解消できる
  • 必要になったら明示的に devbase images refresh 等のコマンドで導入する余地を残せる

CI 結果

全 5 チェック PASS (Ruff lint / ShellCheck / Python syntax 3.10-3.12)

修正ファイル

  • lib/devbase/commands/container.py (+18 -8)
  • bin/devbase (+2 -2)
  • docs/user/cli-reference.md (+5 -0)

Codex review [1.1] 対応で削除した「公開イメージの定期再 pull」を、
touch-file 方式で再実装。

実装:
- _pull_marker_path(image): \${DEVBASE_ROOT}/.cache/pulls/<safe-name>
  image 名の '/' や ':' を '_' に置換してファイル名安全化
- _pull_age_days(image): touch-file mtime からの経過日数 (None=未存在)
- _mark_pulled(image): pull 成功時に marker を touch
- _ensure_images() の image-only 分岐:
  - 不在 → pull → mark
  - 前回 pull から N 日経過 → pull → mark
  - それ以外 → no-op

メリット:
- JSON parse 不要 (pathlib + time だけ)
- 中断耐性 (壊れた JSON が残らない)
- 手動リセット容易 (rm .cache/pulls/foo で次回再 pull)
- 約 30 行で完結

副次:
- .gitignore に .cache/ を追加
- cli-reference.md に touch-file 仕様を追記

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@takemi-ohama
Copy link
Copy Markdown
Contributor Author

追加対応: touch-file ベースで公開イメージの定期 pull を再実装 (commit decf80f)

前コミット (23e4e9e) では Codex 指摘 [1.1] への対応として「公開イメージの定期 pull 機能自体を削除」しましたが、ユーザーフィードバックで touch-file 方式なら簡潔に実装できるとの指摘があり、機能を復活させました。

実装方針

pull 成功時に touch-file を ${DEVBASE_ROOT}/.cache/pulls/<safe-name> に作成し、次回以降は touch-file の mtime から経過日数を判定。

追加関数(合計約30行)

関数 役割
_pull_marker_path(image_name) marker ファイルのパスを返す。image:tag/ :_ に置換してファイル名安全化
_pull_age_days(image_name) (time.time() - mtime) / 86400 で経過日数を返す。未存在は None
_mark_pulled(image_name) pull 成功時に marker を touch(mkdir parents=True)

_ensure_images() の image-only 分岐

if not has_build:
    pull_age = _pull_age_days(image_name)
    if pull_age is None or pull_age < max_age:
        return True
    # pull → mark
    ok = _run_pull(image_name)
    if ok:
        _mark_pulled(image_name)
    return ok

採用理由(JSON / digest 比較ではなく touch-file)

  • 依存ゼロ: pathlib + time だけ。JSON parse 不要
  • クラッシュ耐性: 中断しても壊れた JSON が残らない
  • 手動リセット容易: rm .cache/pulls/foo で次回再 pull
  • DEVBASE_ROOT 未設定でも安全: marker None フォールバック、no-op
  • 約30行で完結

副次変更

  • .gitignore: .cache/ を追加
  • cli-reference.md: touch-file 仕様を追記

動作テスト結果

nginx:1.25                  -> marker: nginx_1.25
mysql/mysql-server:8.0      -> marker: mysql_mysql-server_8.0
ghcr.io/foo/bar:v1          -> marker: ghcr.io_foo_bar_v1
never pulled:    age=None
just pulled:     age=0
10 days ago:     age=10  (mtime を10日前に書き換えて検証)
no DEVBASE_ROOT: marker=None, age=None  (no-op 安全)

CI 結果

全 5 チェック PASS (Ruff lint / ShellCheck / Python syntax 3.10-3.12)

Codex 指摘との対応関係

Codex 指摘 当初対応 最終対応
[1.1] HIGH: Created 依存で pull がループ 機能削除 touch-file 方式で再実装
[3.1] MED: build/pull の閾値を 1 つで扱う意味論 削除で解消 統一閾値のまま (touch-file ベース pull は build と異なる対象だが、ユーザー側からは「7 日経過で更新」という単一 mental model)

[3.1] については、内部実装は build / pull で別判定 (_get_image_age_days vs _pull_age_days) を行いつつ、ユーザー API (DEVBASE_IMAGE_MAX_AGE_DAYS) は単一閾値のまま維持しています。将来 DEVBASE_BUILD_MAX_AGE_DAYS / DEVBASE_PULL_MAX_AGE_DAYS に分離する余地は残しています。

修正ファイル

  • lib/devbase/commands/container.py (+57 -10)
  • docs/user/cli-reference.md (+2 -0)
  • .gitignore (+1 -0)

[HIGH] 既存 image-only サービスで marker bootstrap

PR 適用前から手動 docker pull 済みの公開 image は marker が無く、
_pull_age_days() が永遠に None を返して定期 pull が発火しない不具合。
image-only かつ marker 不在のケースで marker を「now」で bootstrap し、
次回以降 threshold-based pull が正しく作動するよう修正。

option (a) (即 pull) ではなく option (b) (現時点を起点に記録) を選択。
理由: 初回 up での予期せぬネットワーク呼び出しを避け、
最大 7 日の更新遅延 (SLA 厳密ではない) を受容する方が UX として穏便。

[MED] marker filename の衝突を hash で防止

re.sub による単純置換は lossy で、
'a/b:c' と 'a_b/c' がともに 'a_b_c' に衝突する問題があった。
filename を `<sanitized[:60]>--<sha256[:12]>` 形式に変更し、
人間可読性と衝突耐性を両立。

[LOW] 未来 mtime のクランプ

時刻巻き戻しや手動 mtime 操作で marker mtime が未来を指すと、
時刻差が負値になり pull_age < max_age が常に成立して
refresh が永続抑止される問題があった。
負値検出時は warning ログを出して 0 を返すよう修正。

動作確認:
- collision: a/b:c, a_b/c, a:b/c が全て異なる hash で識別される
- bootstrap: marker 無し既存 image で初回 up 時に marker 作成、age=0
- future mtime: 5 日先の mtime → warning + age=0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@takemi-ohama
Copy link
Copy Markdown
Contributor Author

Codex 第二回 review 指摘 対応サマリー (commit eed868d)

外部 AI 第二意見 (Codex / gpt-5.4) の再レビューで挙がった HIGH 1 件・MED 1 件・LOW 1 件に対応。

対応した指摘

# 重要度 指摘 対応
1 HIGH PR 適用前から手動 docker pull 済みの公開 image は marker が無く _pull_age_days() = None で永遠に no-op、定期 pull が発火しない image-only かつ marker 不在のケースで marker を「now」で bootstrap。次回以降 threshold-based pull が正しく作動
2 MED re.sub の単純置換が lossy で a/b:ca_b/c がともに a_b_c に衝突 filename を <sanitized[:60]>--<sha256[:12]> 形式に変更。可読性と衝突耐性を両立
3 LOW 未来 mtime (時刻巻き戻し等) で pull_age が負値になり refresh が永続抑止 負値検出時に warning ログを出して 0 にクランプ

設計判断: bootstrap 方式

Codex は (a) 「marker 無し → 即 pull」を推奨しましたが、本実装では (b) 「marker 無し → now を記録、pull はしない」を採用しました。

理由:

  • (a) は既存ユーザの初回 up でいきなりネットワーク呼び出しが走り驚きが大きい
  • (b) は最大 7 日の更新遅延が出るが、SLA は厳密でない (`DEVBASE_IMAGE_MAX_AGE_DAYS` で短縮可)
  • 2 回目以降は (a)(b) 同じく正しく作動

動作確認

=== Collision test ===
  'a/b:c'                        -> a_b_c--3b07f80ca173
  'a_b/c'                        -> a_b_c--02d7306b94c0
  'a:b/c'                        -> a_b_c--fb7456513927
  'foo/bar:1.0'                  -> foo_bar_1.0--e97ebedfbb04
  'foo_bar_1.0'                  -> foo_bar_1.0--06a3d76edc2e

=== Long name (85 chars) ===
  marker: registry.example.com_very_long_path_to_some_deeply_nested_im--96656d7ca34b (74 chars)

=== Bootstrap test ===
  age before bootstrap: None
  age after bootstrap : 0

=== Future mtime clamp (5 days ahead) ===
  WARNING: Pull marker for 'test-bootstrap:1.0' has a future mtime (clock skew?); treating as 0 days
  age: 0

見送り

Codex 指摘 理由
ユニットテスト追加 (テスト戦略節) 別 PR で _get_image_age_days / _pull_age_days / _image_max_age_days 等の pure 関数からテストを導入予定 (元の方針継続)
symlink abuse のリスク 同一ユーザ権限を要するローカル自己攻撃で脅威モデル外、Codex も「低リスク」評価

CI 結果

全 5 チェック PASS (Ruff lint / ShellCheck / Python syntax 3.10-3.12)

修正ファイル

  • lib/devbase/commands/container.py (+34 -5)

累計

  • 7 commits / +215 -37 (4 ファイル)

@takemi-ohama takemi-ohama merged commit 58cfd99 into main May 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant