Skip to content

Commit 41c8495

Browse files
takemi-ohamaclaude
andcommitted
feat(build): build/rebuild/up の再ビルド仕様を統一 (i07)
キャッシュの扱いを3モードに整理: - devbase build : キャッシュビルド (常に実行) - devbase build --no-cache: 無条件で base/project とも no-cache - devbase build --expires=n: n日未満は再ビルドせず、n日以上のみ no-cache (親イメージ FROM devbase-* は作成日で独立判定) devbase rebuild を build --expires=7 のシノニムに、devbase up の自動準備を その rebuild 相当に集約。共通リゾルバ _build_with_expires に一本化した。 - container.py: _build_with_expires / _build_resolved / _resolve_dev_service を 追加。cmd_rebuild は素の compose build --no-cache をやめ期限判定へ。cmd_build に --no-cache / --expires を追加。_ensure_images (up) を共通リゾルバへ委譲。 - cli.py: build parser に --no-cache / --expires[=DAYS] を追加。 - bin/devbase: build --expires は作成日判定のため Python(project build)へ委譲。 - docs/CHANGELOG/issues/i07.md: 3モード仕様を反映。 - tests: _build_with_expires/_build_resolved のテスト追加、--base-cache 幻引数の 否定アサーションを削除。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f2037c3 commit 41c8495

9 files changed

Lines changed: 683 additions & 87 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,24 @@
1111
ローカル / WSL(Windows 側)/ VS Code Remote-SSH 統合ターミナル(手元クライアント側)
1212
を自動判別し、素の SSH では手元で実行するコマンドを提示します。エディタは
1313
`DEVBASE_EDITOR`(既定 `code`)で変更可能。詳細: `docs/user/environment-variables.md`
14+
- **`devbase build``--expires[=DAYS]` を追加**しました (i07)。イメージ作成日が
15+
DAYS 日(既定 7、`DEVBASE_IMAGE_MAX_AGE_DAYS` で上書き可)以上のときのみ no-cache で
16+
再ビルドし、未満なら再ビルドしません(既存イメージを使用)。親イメージ(`FROM devbase-*`)の
17+
作成日は独立して判定します。`devbase build``--no-cache` も明示フラグとして整理しました。
1418

1519
### Changed
20+
- **`build` / `rebuild` / `up` の再ビルド仕様を統一**しました (i07)。キャッシュの
21+
扱いを 3 モード(既定=キャッシュビルド / `--no-cache`=無条件 no-cache / `--expires=N`=
22+
期限切れ時のみ no-cache・期限内は再ビルドしない)に整理し、`devbase rebuild`
23+
`devbase build --expires=7` のシノニムに、`devbase up` の自動準備をその `rebuild` 相当に
24+
集約しました。`devbase rebuild` は従来の素の `docker compose build --no-cache` をやめ、
25+
devbase-base の 2 段ビルドと期限判定(期限内はスキップ)を行うようになりました。`devbase up`
26+
の「7 日未満は再ビルドしない」挙動は従来どおり維持されます。
27+
- **`devbase up` の自動再ビルドで base イメージの日付判定を分離**しました。
28+
プロジェクトイメージが閾値(既定 7 日)超過で `--no-cache` 再ビルドされる際、
29+
ベースの作成日を独立して判定し、ベースが閾値内(新しい)であればベースを no-cache で
30+
作り直さず、プロジェクトイメージのみ no-cache で再ビルドします。ベースが古い、または
31+
判定できない場合はベースも含めて no-cache で再ビルドします。
1632
- **シェル有効化を `bin/rc` の source に統一**しました (PLAN31_1)。`devbase init` 後に
1733
いま開いているシェルへ devbase(PATH / 補完)を即時適用するには
1834
`. ~/devbase/bin/rc`(= `source ~/devbase/bin/rc`)を使います。`bin/rc` は自身の

bin/devbase

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ export DEVBASE_ROOT
6464
cmd_build() {
6565
echo "=== Building devbase images ==="
6666

67+
# devbase build modes:
68+
# --no-cache : rebuild base and project without cache
69+
# --project-no-cache : prepare base with cache, rebuild project without cache
70+
# The latter is used by the automatic staleness refresh path after it has
71+
# already determined that the base image is still fresh.
72+
local project_no_cache=0
73+
local -a build_args=()
74+
local _arg
75+
for _arg in "$@"; do
76+
case "$_arg" in
77+
--project-no-cache) project_no_cache=1 ;;
78+
*) build_args+=("$_arg") ;;
79+
esac
80+
done
81+
set -- "${build_args[@]}"
82+
6783
# Helper: Check if Dockerfile uses devbase-* base images
6884
check_base_image_dependency() {
6985
local dockerfile_path="$1"
@@ -118,11 +134,43 @@ cmd_build() {
118134
fi
119135
fi
120136

121-
# --no-cache specified: always rebuild base first
137+
local base_image_name=$(check_base_image_dependency "$dockerfile_path")
138+
local base_image_to_build="${base_image_name:-devbase-base}"
139+
140+
if [ "$project_no_cache" = "1" ]; then
141+
echo ""
142+
local -a base_cached_args=()
143+
local _ba
144+
for _ba in "$@"; do
145+
if [ "$_ba" = "--no-cache" ]; then
146+
continue
147+
fi
148+
base_cached_args+=("$_ba")
149+
done
150+
echo "[1/2] Building ${base_image_to_build} (using cache)..."
151+
if ! build_base_image "$base_image_to_build" "${base_cached_args[@]}"; then
152+
exit 1
153+
fi
154+
155+
echo ""
156+
echo "[2/2] Building project image without cache..."
157+
if docker compose build "${DEV_SERVICE_NAME:-dev}" --no-cache "$@"; then
158+
echo ""
159+
echo "✓ All images built successfully"
160+
else
161+
echo ""
162+
echo "✗ Failed to build project image"
163+
exit 1
164+
fi
165+
return
166+
fi
167+
168+
# --no-cache specified: rebuild base first without cache, then rebuild
169+
# project without cache.
122170
if [[ "$*" == *"--no-cache"* ]]; then
123171
echo ""
124-
echo "[1/2] Building devbase-base..."
125-
if ! build_base_image "devbase-base" "$@"; then
172+
echo "[1/2] Building ${base_image_to_build}..."
173+
if ! build_base_image "$base_image_to_build" "$@"; then
126174
exit 1
127175
fi
128176

@@ -140,7 +188,6 @@ cmd_build() {
140188
fi
141189

142190
# Normal build: check if project uses devbase-* base image
143-
local base_image_name=$(check_base_image_dependency "$dockerfile_path")
144191
if [ -n "$base_image_name" ]; then
145192
echo ""
146193
echo "[1/2] Project uses ${base_image_name}, building base image..."
@@ -358,7 +405,24 @@ case "$_resolved_cmd" in
358405
init|status|project|container|ct|env|plugin|pl|snapshot|ss|up|down|login|ps|scale|rebuild|list)
359406
run_python "${_resolved_cmd}" "${_DEVBASE_ARGS[@]}" ;;
360407
# Shell-implemented commands
361-
build) cmd_build "${_DEVBASE_ARGS[@]}" ;;
408+
#
409+
# build: 既定 / --no-cache / <image> は shell の cmd_build (devbase-base の
410+
# 2 段ビルド) で処理する。--expires はイメージ作成日の判定が必要で、shell では
411+
# RFC3339 日付パースが非可搬なため Python (project build) へ委譲する
412+
# (i07: build --expires=N / rebuild / up が共通の期限リゾルバを使う)。
413+
build)
414+
_has_expires=0
415+
for _ba in "${_DEVBASE_ARGS[@]}"; do
416+
case "$_ba" in
417+
--expires|--expires=*) _has_expires=1 ;;
418+
esac
419+
done
420+
if [ "$_has_expires" = 1 ]; then
421+
run_python project build "${_DEVBASE_ARGS[@]}"
422+
else
423+
cmd_build "${_DEVBASE_ARGS[@]}"
424+
fi
425+
;;
362426
# Help and unknown
363427
-h|--help|help|"") run_python "--help" ;;
364428
*) echo "Error: unknown command '$1'" >&2; exit 1 ;;

docs/user/cli-reference.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ graph TD
5555

5656
> **Note:** `logs` はトップレベルシノニムを持ちません。`devbase project logs` を使用してください。
5757
>
58-
> **`build` の転送先について:** `devbase build` は他のショートカットのように `project` グループ
59-
> (Python 実装)へ転送されるのではなく、`bin/devbase` のシェル実装 `cmd_build` に直接委譲されます。
60-
> base イメージの段階ビルド等を CWD で行う必要があるためで、`devbase project build` とは実装経路が
61-
> 異なります(名前指定はラッパーの `cd` で解決)。挙動上の入出力は同等ですが、実装は別物です。
58+
> **`build` の転送先について:** `devbase build`(既定 / `--no-cache` / `<image>`)は他の
59+
> ショートカットのように `project` グループ(Python 実装)へ転送されるのではなく、`bin/devbase`
60+
> シェル実装 `cmd_build` に直接委譲されます。base イメージの段階ビルド等を CWD で行う必要があるため
61+
> です(名前指定はラッパーの `cd` で解決)。ただし `devbase build --expires[=DAYS]` のみ、作成日の
62+
> 判定が必要なため例外的に Python 経路(`project build`)へ委譲されます。挙動上の入出力は同等です。
6263
6364
### ユニークプレフィックスマッチング
6465

@@ -165,9 +166,13 @@ devbase up [name]
165166

166167
- 起動時にスナップショットを自動作成(新世代 or 差分追加)
167168
- `CONTAINER_SCALE` の値に基づいてコンテナ数を決定
168-
- イメージの自動準備:
169+
- イメージの自動準備`devbase up``devbase rebuild``devbase build --expires=7` 相当を実行):
169170
- `build:` 定義あり、イメージ未存在 → `devbase build` を自動実行
170-
- `build:` 定義あり、イメージが7日以上古い → `devbase build --no-cache` で再ビルド
171+
- `build:` 定義あり、イメージ存在 → プロジェクトイメージの作成日で再ビルドの要否を判定:
172+
- 7日未満 → 再ビルドしない(既存イメージをそのまま使用)
173+
- 7日以上 + ベースが閾値内=新しい → プロジェクトのみ no-cache(ベースはキャッシュ)
174+
- 7日以上 + ベースが古い/判定不能 → ベースも含めて no-cache
175+
- ベースイメージ `FROM devbase-*` の作成日はプロジェクトと独立して判定します
171176
- `image:` のみ(公開イメージ)、未存在 → `docker pull` を自動実行
172177
- `image:` のみ、前回 pull から7日以上経過 → `docker pull` で再取得
173178
(前回 pull 日時は `${DEVBASE_ROOT}/.cache/pulls/<image>` の touch-file mtime で判定)
@@ -261,21 +266,34 @@ devbase project scale adminer 3
261266

262267
### `devbase project build`
263268

264-
コンテナイメージをビルドします。
269+
コンテナイメージをビルドします。キャッシュの扱いは 3 モードあります。
265270

266271
```
267272
devbase project build [image]
268-
devbase build [image]
273+
devbase build [image] [--no-cache | --expires[=DAYS]]
269274
```
270275

276+
| モード | 子イメージ | 親イメージ(`FROM devbase-*`|
277+
|--------|-----------|-------------------------------|
278+
| `devbase build` | キャッシュがあれば使う | キャッシュがあれば使う |
279+
| `devbase build --no-cache` | 無条件で no-cache | 無条件で no-cache |
280+
| `devbase build --expires[=DAYS]` | DAYS 日以上古ければ no-cache、未満なら再ビルドしない | 親の作成日で独立に同判定 |
281+
271282
| パラメータ | 必須 | 説明 |
272283
|-----------|------|------|
273-
| `image` | いいえ | ビルドするイメージ名(省略時は全イメージ) |
284+
| `image` | いいえ | 単体ビルドするイメージ名(`$DEVBASE_ROOT/containers/<image>` を直接ビルド。省略時は compose イメージ) |
285+
| `--no-cache` | いいえ | base / project とも無条件でキャッシュ無視 |
286+
| `--expires[=DAYS]` | いいえ | 作成日が DAYS 日以上のときのみ no-cache 再ビルド、未満なら再ビルドしない(既定 7、`DEVBASE_IMAGE_MAX_AGE_DAYS` で上書き可)。`--no-cache` とは併用しません |
287+
288+
> **`--no-cache` / `--expires` は compose ビルド(`image` 省略時)に適用されます。** `image` 指定の
289+
> 単体ビルドでは `--no-cache` のみ反映され、`--expires` は対象外です。`--expires` 付きビルドは
290+
> 作成日判定のため Python 経路(`project build`)で処理されます。
274291
275292
### `devbase project rebuild`
276293

277-
キャッシュを使わずにコンテナイメージを再ビルドします(`docker compose build --no-cache`)。
278-
`build` と異なり Python 実装で完結するため、トップレベルショートカット `devbase rebuild` を持ちます。
294+
`devbase build --expires=7` のシノニムです(既定 7 日)。プロジェクトイメージが 7 日以上古ければ
295+
no-cache で再ビルドし、未満なら再ビルドしません(既存イメージを使用)。親イメージ(`FROM devbase-*`)の
296+
作成日は独立して判定します。トップレベルショートカット `devbase rebuild` を持ちます。
279297

280298
```
281299
devbase project rebuild [name]

issues/i07.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
2+
# devbase build / devbase up の仕様再考
3+
4+
`devbase build``devbase up` の build 機構が混乱してきたので、再ビルドの意味を
5+
統一する。中心となるのは「キャッシュをどう扱うか」を以下の 3 モードに整理し、
6+
`rebuild` / `up` をその上に集約することである。
7+
8+
## 用語
9+
- **親 image (base image)**: `FROM devbase-*` で参照されるベースイメージ
10+
(`devbase-base` など)。
11+
- **子 image (project image)**: compose の build 対象となるプロジェクトイメージ。
12+
13+
## build の 3 モード
14+
15+
| 指定 | 子 image | 親 image |
16+
|---|---|---|
17+
| `devbase build` | キャッシュビルド(常に実行) | キャッシュビルド(常に実行) |
18+
| `devbase build --no-cache` | 無条件で no-cache | 無条件で no-cache |
19+
| `devbase build --expires=n` | n 日以上古ければ no-cache、未満なら再ビルドしない | 親の作成日で独立に同判定 |
20+
21+
- `--expires=n``n` のデフォルトは 7。
22+
- `--expires=n`**project が n 日未満なら再ビルドを実行しない**(既存イメージを
23+
そのまま使う)。n 日以上のときだけ no-cache で作り直す。
24+
- `--expires=n` における親 image の判定は、**親 image 自身の作成日**で行う
25+
(親が n 日以上古ければ親も no-cache、未満なら親はキャッシュ利用)。
26+
つまり「子は古いが親は新しい」場合、親はキャッシュを使い子だけ no-cache で
27+
作り直す。
28+
- `--no-cache``--expires` は併用しない (意味が矛盾するため)。`--no-cache`
29+
無条件、`--expires` は条件付き、と明確に分ける。
30+
31+
## rebuild / up
32+
33+
- `devbase rebuild``devbase build --expires=7` のシノニム
34+
(デフォルトの 7 日で判定する)。
35+
- `devbase up` 時は、`devbase rebuild` 相当の build を実行する。
36+
37+
## 補足: image-only サービス (公開イメージ) の扱い
38+
39+
`build:` を持たず `image:` のみのサービスについては、作成日 (`Created`) が
40+
upstream のビルド時刻であってローカルの鮮度を表さないため、`--expires` 判定は
41+
**前回 pull 日時** (touch-file の mtime) を基準とし、n 日以上経過していれば
42+
`docker pull` で再取得する。
43+
44+
---
45+
46+
# 実装プラン
47+
48+
## 現状の構造 (混乱の原因)
49+
50+
再ビルドの概念が 3 つの別実装に散らばっている:
51+
52+
1. **shell `cmd_build`** (`bin/devbase`): base 検出 + 2 段ビルドの機械的処理。
53+
現状フラグは `--no-cache` (両方 no-cache) / `--project-no-cache` (base は
54+
cache・project は no-cache)。
55+
2. **Python `cmd_rebuild`** (`container.py`): 素の `docker compose build --no-cache`
56+
base 処理も期限判定もしない → 仕様から最も乖離。
57+
3. **Python `_ensure_images``_rebuild_if_stale``_base_image_is_fresh`**
58+
(`up` 経路): 作成日による期限判定 + base 独立判定を持ち、結果に応じて shell
59+
`cmd_build``--no-cache` / `--project-no-cache` で呼ぶ。
60+
61+
期限判定 (RFC3339 作成日のパース、base の独立判定) は **③ に既に実装済み**
62+
shell では日付パースが困難なため、判定は Python に集約し、shell は機械的 3 モード
63+
に徹する、という方針で統一する。
64+
65+
## 設計方針
66+
67+
- **shell `cmd_build`** は機械的な 3 モードのみを担う (現状維持):
68+
デフォルト (cache) / `--no-cache` (両方 no-cache) / `--project-no-cache`
69+
(base cache・project no-cache)。`--expires` の日付判定は持たせない。
70+
- **`--expires=n` の解決は Python に集約**する。project image / base image の
71+
作成日を inspect し、上表のどのモードで shell を呼ぶかを決める共通リゾルバを
72+
用意し、`build --expires` / `rebuild` / `up` の 3 経路から再利用する。
73+
既存の `_rebuild_if_stale` / `_base_image_is_fresh` / `_get_image_age_days`
74+
この共通リゾルバに一般化する。
75+
76+
## 変更点
77+
78+
### 1. 共通リゾルバの導入 (`container.py`)
79+
- `_rebuild_if_stale` を一般化した `_resolve_build_mode(expires, image_name,
80+
inspect_json, dev_service)` を作り、戻り値に応じて `_run_build()` を呼ぶ。
81+
- project が期限内 → no-op (cache 利用、再ビルド不要)
82+
- project が期限超過 + base 期限内 → `_run_build(project_no_cache=True)`
83+
- project が期限超過 + base 期限超過/判定不能 → `_run_build(no_cache=True)`
84+
85+
### 2. `cmd_rebuild``build --expires=7` のシノニムに (`container.py`)
86+
- 現状の `docker compose build --no-cache` 直呼びを廃止。
87+
- compose config から dev サービスを解決し、共通リゾルバを `expires=7`
88+
(= `_image_max_age_days()`) で呼ぶ実装に置き換える。
89+
- これにより base 2 段ビルドと期限判定が rebuild にも効くようになる。
90+
91+
### 3. `devbase build``--expires` / `--no-cache` を追加
92+
- **CLI**: `_add_build_subparser` (`cli.py`) に `--no-cache` / `--expires N`
93+
(`nargs='?'`, default なし、`--expires` 単独時は 7) を追加。
94+
- **shell ルーティング** (`bin/devbase`): top-level `build` は現状 shell `cmd_build`
95+
へ直行している。`--expires` 付き呼び出しは日付判定が必要なため Python 経路へ
96+
振り分ける (例: `build --expires` を検出したら `run_python build ...` に回す)。
97+
`--no-cache` / 引数なしは従来どおり shell `cmd_build` で機械処理してよい。
98+
※ build の CWD 依存 (PLAN06) は、Python が `bash bin/devbase build`
99+
呼び戻す既存の `_run_build` 経路で担保されている (wrapper が既に cd 済み)。
100+
101+
### 4. `up` 経路を rebuild に集約 (`container.py`)
102+
- `_ensure_images` の「build 定義あり」分岐 (`_rebuild_if_stale`) を、2 で作る
103+
rebuild 相当 (= 共通リゾルバ) に寄せる。`up` = `rebuild` 相当を実体化する。
104+
- ただし `up` は rebuild に無い責務 (イメージ未存在時の build/pull、image-only
105+
サービスの期限切れ pull) も持つため、それらは `_ensure_images` 側に残す。
106+
「build 定義あり + イメージ存在」のケースのみ共通リゾルバへ委譲する。
107+
108+
### 5. ドキュメント / テスト
109+
- `docs/user/cli-reference.md`: build の 3 モード表、`rebuild` = `build
110+
--expires=7``up` = `rebuild` 相当を反映。
111+
- `CHANGELOG.md`: 仕様統一を追記。
112+
- テスト:
113+
- `test_base_image_staleness.py``--base-cache` 否定アサーション 3 箇所
114+
(存在しない引数名の見張り) を削除し、`--project-no-cache` の肯定検証に統一。
115+
- `--expires` の境界 (n 日未満=cache / n 日以上=no-cache)、`rebuild` =
116+
`build --expires=7` のシノニム、`up` 経路が共通リゾルバを通ることのテストを追加。
117+
118+
## 影響範囲
119+
- `bin/devbase` (cmd_build ルーティング)
120+
- `lib/devbase/cli.py` (build parser に `--no-cache` / `--expires`)
121+
- `lib/devbase/commands/container.py` (共通リゾルバ・cmd_rebuild・_ensure_images)
122+
- `docs/user/cli-reference.md`, `CHANGELOG.md`
123+
- `tests/cli/test_base_image_staleness.py`
124+
125+
## 進め方 (推奨ステップ)
126+
1. 共通リゾルバ抽出 + `cmd_rebuild` 置換 (内部のみ、CLI 変更なし) — テスト先行。
127+
2. `build --expires` / `--no-cache` の CLI + shell ルーティング追加。
128+
3. `up` 経路 (`_ensure_images`) をリゾルバへ集約。
129+
4. docs / CHANGELOG / テスト整理。

lib/devbase/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ def _add_build_subparser(sub):
137137
"""
138138
p = sub.add_parser('build', help='Build container images')
139139
p.add_argument('image', nargs='?', default=None, help='Image name')
140+
p.add_argument('--no-cache', action='store_true',
141+
help='Rebuild base and project images without cache')
142+
# `--expires` 単独 (値なし) は const=-1 を渡し、cmd_build 側で既定日数
143+
# (_image_max_age_days, 環境変数 DEVBASE_IMAGE_MAX_AGE_DAYS 既定 7) に解決する。
144+
p.add_argument('--expires', nargs='?', type=int, const=-1, default=None,
145+
metavar='DAYS',
146+
help='Rebuild without cache only if the image is older than '
147+
'DAYS days (default 7). Base image is judged independently.')
140148

141149

142150
def _add_container_parser(subparsers):

0 commit comments

Comments
 (0)