Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

### Changed
- **`build` / `rebuild` / `up` の再ビルド仕様を統一**しました (i07)。キャッシュの
扱いを 3 モード(既定=キャッシュビルド / `--no-cache`=無条件 no-cache / `--expires=N`=
期限切れ時のみ no-cache・期限内は再ビルドしない)に整理し、`devbase rebuild` を
`devbase build --expires=7` のシノニムに、`devbase up` の自動準備をその `rebuild` 相当に
集約しました。`devbase rebuild` は従来の素の `docker compose build --no-cache` をやめ、
devbase-base の 2 段ビルドと期限判定(期限内はスキップ)を行うようになりました。`devbase up`
の「7 日未満は再ビルドしない」挙動は従来どおり維持されます。
- **`devbase up` の自動再ビルドで base イメージの日付判定を分離**しました。
プロジェクトイメージが閾値(既定 7 日)超過で `--no-cache` 再ビルドされる際、
ベースの作成日を独立して判定し、ベースが閾値内(新しい)であればベースを no-cache で
作り直さず、プロジェクトイメージのみ no-cache で再ビルドします。ベースが古い、または
判定できない場合はベースも含めて no-cache で再ビルドします。
- **シェル有効化を `bin/rc` の source に統一**しました (PLAN31_1)。`devbase init` 後に
いま開いているシェルへ devbase(PATH / 補完)を即時適用するには
`. ~/devbase/bin/rc`(= `source ~/devbase/bin/rc`)を使います。`bin/rc` は自身の
Expand Down
74 changes: 69 additions & 5 deletions bin/devbase
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ export DEVBASE_ROOT
cmd_build() {
echo "=== Building devbase images ==="

# devbase build modes:
# --no-cache : rebuild base and project without cache
# --project-no-cache : prepare base with cache, rebuild project without cache
# The latter is used by the automatic staleness refresh path after it has
# already determined that the base image is still fresh.
local project_no_cache=0
local -a build_args=()
local _arg
for _arg in "$@"; do
case "$_arg" in
--project-no-cache) project_no_cache=1 ;;
*) build_args+=("$_arg") ;;
esac
done
set -- "${build_args[@]}"

# Helper: Check if Dockerfile uses devbase-* base images
check_base_image_dependency() {
local dockerfile_path="$1"
Expand Down Expand Up @@ -118,11 +134,43 @@ cmd_build() {
fi
fi

# --no-cache specified: always rebuild base first
local base_image_name=$(check_base_image_dependency "$dockerfile_path")
local base_image_to_build="${base_image_name:-devbase-base}"

if [ "$project_no_cache" = "1" ]; then
echo ""
local -a base_cached_args=()
local _ba
for _ba in "$@"; do
if [ "$_ba" = "--no-cache" ]; then
continue
fi
base_cached_args+=("$_ba")
done
echo "[1/2] Building ${base_image_to_build} (using cache)..."
if ! build_base_image "$base_image_to_build" "${base_cached_args[@]}"; then
exit 1
fi

echo ""
echo "[2/2] Building project image without cache..."
if docker compose build "${DEV_SERVICE_NAME:-dev}" --no-cache "$@"; then
echo ""
echo "✓ All images built successfully"
else
echo ""
echo "✗ Failed to build project image"
exit 1
fi
return
fi

# --no-cache specified: rebuild base first without cache, then rebuild
# project without cache.
if [[ "$*" == *"--no-cache"* ]]; then
echo ""
echo "[1/2] Building devbase-base..."
if ! build_base_image "devbase-base" "$@"; then
echo "[1/2] Building ${base_image_to_build}..."
if ! build_base_image "$base_image_to_build" "$@"; then
exit 1
fi

Expand All @@ -140,7 +188,6 @@ cmd_build() {
fi

# Normal build: check if project uses devbase-* base image
local base_image_name=$(check_base_image_dependency "$dockerfile_path")
if [ -n "$base_image_name" ]; then
echo ""
echo "[1/2] Project uses ${base_image_name}, building base image..."
Expand Down Expand Up @@ -358,7 +405,24 @@ case "$_resolved_cmd" in
init|status|project|container|ct|env|plugin|pl|snapshot|ss|up|down|login|ps|scale|rebuild|list)
run_python "${_resolved_cmd}" "${_DEVBASE_ARGS[@]}" ;;
# Shell-implemented commands
build) cmd_build "${_DEVBASE_ARGS[@]}" ;;
#
# build: 既定 / --no-cache / <image> は shell の cmd_build (devbase-base の
# 2 段ビルド) で処理する。--expires はイメージ作成日の判定が必要で、shell では
# RFC3339 日付パースが非可搬なため Python (project build) へ委譲する
# (i07: build --expires=N / rebuild / up が共通の期限リゾルバを使う)。
build)
_has_expires=0
for _ba in "${_DEVBASE_ARGS[@]}"; do
case "$_ba" in
--expires|--expires=*) _has_expires=1 ;;
esac
done
if [ "$_has_expires" = 1 ]; then
run_python project build "${_DEVBASE_ARGS[@]}"
else
cmd_build "${_DEVBASE_ARGS[@]}"
fi
;;
# Help and unknown
-h|--help|help|"") run_python "--help" ;;
*) echo "Error: unknown command '$1'" >&2; exit 1 ;;
Expand Down
40 changes: 29 additions & 11 deletions docs/user/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ graph TD

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

### ユニークプレフィックスマッチング

Expand Down Expand Up @@ -165,9 +166,13 @@ devbase up [name]

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

### `devbase project build`

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

```
devbase project build [image]
devbase build [image]
devbase build [image] [--no-cache | --expires[=DAYS]]
```

| モード | 子イメージ | 親イメージ(`FROM devbase-*`) |
|--------|-----------|-------------------------------|
| `devbase build` | キャッシュがあれば使う | キャッシュがあれば使う |
| `devbase build --no-cache` | 無条件で no-cache | 無条件で no-cache |
| `devbase build --expires[=DAYS]` | DAYS 日以上古ければ no-cache、未満なら再ビルドしない | 親の作成日で独立に同判定 |

| パラメータ | 必須 | 説明 |
|-----------|------|------|
| `image` | いいえ | ビルドするイメージ名(省略時は全イメージ) |
| `image` | いいえ | 単体ビルドするイメージ名(`$DEVBASE_ROOT/containers/<image>` を直接ビルド。省略時は compose イメージ) |
| `--no-cache` | いいえ | base / project とも無条件でキャッシュ無視 |
| `--expires[=DAYS]` | いいえ | 作成日が DAYS 日以上のときのみ no-cache 再ビルド、未満なら再ビルドしない(既定 7、`DEVBASE_IMAGE_MAX_AGE_DAYS` で上書き可)。`--no-cache` とは併用しません |

> **`--no-cache` / `--expires` は compose ビルド(`image` 省略時)に適用されます。** `image` 指定の
> 単体ビルドでは `--no-cache` のみ反映され、`--expires` は対象外です。`--expires` 付きビルドは
> 作成日判定のため Python 経路(`project build`)で処理されます。

### `devbase project rebuild`

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

```
devbase project rebuild [name]
Expand Down
129 changes: 129 additions & 0 deletions issues/i07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@

# devbase build / devbase up の仕様再考

`devbase build` と `devbase up` の build 機構が混乱してきたので、再ビルドの意味を
統一する。中心となるのは「キャッシュをどう扱うか」を以下の 3 モードに整理し、
`rebuild` / `up` をその上に集約することである。

## 用語
- **親 image (base image)**: `FROM devbase-*` で参照されるベースイメージ
(`devbase-base` など)。
- **子 image (project image)**: compose の build 対象となるプロジェクトイメージ。

## build の 3 モード

| 指定 | 子 image | 親 image |
|---|---|---|
| `devbase build` | キャッシュビルド(常に実行) | キャッシュビルド(常に実行) |
| `devbase build --no-cache` | 無条件で no-cache | 無条件で no-cache |
| `devbase build --expires=n` | n 日以上古ければ no-cache、未満なら再ビルドしない | 親の作成日で独立に同判定 |

- `--expires=n` の `n` のデフォルトは 7。
- `--expires=n` で **project が n 日未満なら再ビルドを実行しない**(既存イメージを
そのまま使う)。n 日以上のときだけ no-cache で作り直す。
- `--expires=n` における親 image の判定は、**親 image 自身の作成日**で行う
(親が n 日以上古ければ親も no-cache、未満なら親はキャッシュ利用)。
つまり「子は古いが親は新しい」場合、親はキャッシュを使い子だけ no-cache で
作り直す。
- `--no-cache` と `--expires` は併用しない (意味が矛盾するため)。`--no-cache` は
無条件、`--expires` は条件付き、と明確に分ける。

## rebuild / up

- `devbase rebuild` は `devbase build --expires=7` のシノニム
(デフォルトの 7 日で判定する)。
- `devbase up` 時は、`devbase rebuild` 相当の build を実行する。

## 補足: image-only サービス (公開イメージ) の扱い

`build:` を持たず `image:` のみのサービスについては、作成日 (`Created`) が
upstream のビルド時刻であってローカルの鮮度を表さないため、`--expires` 判定は
**前回 pull 日時** (touch-file の mtime) を基準とし、n 日以上経過していれば
`docker pull` で再取得する。

---

# 実装プラン

## 現状の構造 (混乱の原因)

再ビルドの概念が 3 つの別実装に散らばっている:

1. **shell `cmd_build`** (`bin/devbase`): base 検出 + 2 段ビルドの機械的処理。
現状フラグは `--no-cache` (両方 no-cache) / `--project-no-cache` (base は
cache・project は no-cache)。
2. **Python `cmd_rebuild`** (`container.py`): 素の `docker compose build --no-cache`。
base 処理も期限判定もしない → 仕様から最も乖離。
3. **Python `_ensure_images` → `_rebuild_if_stale` → `_base_image_is_fresh`**
(`up` 経路): 作成日による期限判定 + base 独立判定を持ち、結果に応じて shell
`cmd_build` を `--no-cache` / `--project-no-cache` で呼ぶ。

期限判定 (RFC3339 作成日のパース、base の独立判定) は **③ に既に実装済み**。
shell では日付パースが困難なため、判定は Python に集約し、shell は機械的 3 モード
に徹する、という方針で統一する。

## 設計方針

- **shell `cmd_build`** は機械的な 3 モードのみを担う (現状維持):
デフォルト (cache) / `--no-cache` (両方 no-cache) / `--project-no-cache`
(base cache・project no-cache)。`--expires` の日付判定は持たせない。
- **`--expires=n` の解決は Python に集約**する。project image / base image の
作成日を inspect し、上表のどのモードで shell を呼ぶかを決める共通リゾルバを
用意し、`build --expires` / `rebuild` / `up` の 3 経路から再利用する。
既存の `_rebuild_if_stale` / `_base_image_is_fresh` / `_get_image_age_days` を
この共通リゾルバに一般化する。

## 変更点

### 1. 共通リゾルバの導入 (`container.py`)
- `_rebuild_if_stale` を一般化した `_resolve_build_mode(expires, image_name,
inspect_json, dev_service)` を作り、戻り値に応じて `_run_build()` を呼ぶ。
- project が期限内 → no-op (cache 利用、再ビルド不要)
- project が期限超過 + base 期限内 → `_run_build(project_no_cache=True)`
- project が期限超過 + base 期限超過/判定不能 → `_run_build(no_cache=True)`

### 2. `cmd_rebuild` を `build --expires=7` のシノニムに (`container.py`)
- 現状の `docker compose build --no-cache` 直呼びを廃止。
- compose config から dev サービスを解決し、共通リゾルバを `expires=7`
(= `_image_max_age_days()`) で呼ぶ実装に置き換える。
- これにより base 2 段ビルドと期限判定が rebuild にも効くようになる。

### 3. `devbase build` に `--expires` / `--no-cache` を追加
- **CLI**: `_add_build_subparser` (`cli.py`) に `--no-cache` / `--expires N`
(`nargs='?'`, default なし、`--expires` 単独時は 7) を追加。
- **shell ルーティング** (`bin/devbase`): top-level `build` は現状 shell `cmd_build`
へ直行している。`--expires` 付き呼び出しは日付判定が必要なため Python 経路へ
振り分ける (例: `build --expires` を検出したら `run_python build ...` に回す)。
`--no-cache` / 引数なしは従来どおり shell `cmd_build` で機械処理してよい。
※ build の CWD 依存 (PLAN06) は、Python が `bash bin/devbase build` を
呼び戻す既存の `_run_build` 経路で担保されている (wrapper が既に cd 済み)。

### 4. `up` 経路を rebuild に集約 (`container.py`)
- `_ensure_images` の「build 定義あり」分岐 (`_rebuild_if_stale`) を、2 で作る
rebuild 相当 (= 共通リゾルバ) に寄せる。`up` = `rebuild` 相当を実体化する。
- ただし `up` は rebuild に無い責務 (イメージ未存在時の build/pull、image-only
サービスの期限切れ pull) も持つため、それらは `_ensure_images` 側に残す。
「build 定義あり + イメージ存在」のケースのみ共通リゾルバへ委譲する。

### 5. ドキュメント / テスト
- `docs/user/cli-reference.md`: build の 3 モード表、`rebuild` = `build
--expires=7`、`up` = `rebuild` 相当を反映。
- `CHANGELOG.md`: 仕様統一を追記。
- テスト:
- `test_base_image_staleness.py` の `--base-cache` 否定アサーション 3 箇所
(存在しない引数名の見張り) を削除し、`--project-no-cache` の肯定検証に統一。
- `--expires` の境界 (n 日未満=cache / n 日以上=no-cache)、`rebuild` =
`build --expires=7` のシノニム、`up` 経路が共通リゾルバを通ることのテストを追加。

## 影響範囲
- `bin/devbase` (cmd_build ルーティング)
- `lib/devbase/cli.py` (build parser に `--no-cache` / `--expires`)
- `lib/devbase/commands/container.py` (共通リゾルバ・cmd_rebuild・_ensure_images)
- `docs/user/cli-reference.md`, `CHANGELOG.md`
- `tests/cli/test_base_image_staleness.py`

## 進め方 (推奨ステップ)
1. 共通リゾルバ抽出 + `cmd_rebuild` 置換 (内部のみ、CLI 変更なし) — テスト先行。
2. `build --expires` / `--no-cache` の CLI + shell ルーティング追加。
3. `up` 経路 (`_ensure_images`) をリゾルバへ集約。
4. docs / CHANGELOG / テスト整理。
Loading
Loading