Skip to content

Commit decf80f

Browse files
takemi-ohamaclaude
andcommitted
feat(container): 公開イメージの touch-file ベース定期 pull を追加
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>
1 parent 23e4e9e commit decf80f

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ projects/*
1010
!projects/.gitkeep
1111
.env.sources.yml
1212
issues/
13+
.cache/

docs/user/cli-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ devbase up
105105
- `build:` 定義あり、イメージ未存在 → `devbase build` を自動実行
106106
- `build:` 定義あり、イメージが7日以上古い → `devbase build --no-cache` で再ビルド
107107
- `image:` のみ(公開イメージ)、未存在 → `docker pull` を自動実行
108+
- `image:` のみ、前回 pull から7日以上経過 → `docker pull` で再取得
109+
(前回 pull 日時は `${DEVBASE_ROOT}/.cache/pulls/<image>` の touch-file mtime で判定)
108110
- 閾値は `DEVBASE_IMAGE_MAX_AGE_DAYS` 環境変数で上書き可能(既定 7、不正値は警告して既定値)
109111

110112
### `devbase container down`

lib/devbase/commands/container.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import subprocess
77
import sys
8+
import time
89
from datetime import datetime, timezone
910
from pathlib import Path
1011
from typing import Optional
@@ -443,11 +444,11 @@ def _ensure_images() -> bool:
443444
- Image missing + has build: → run `devbase build`
444445
- Image missing + image-only (no build:) → run `docker pull`
445446
- Image present and >= threshold days old + has build:
446-
→ rebuild with `--no-cache`
447-
- Image present + image-only → nothing to do
448-
(Created reflects upstream build time, not local pull time, so we
449-
cannot derive a meaningful freshness signal here. Users who want
450-
public images refreshed should run `docker pull` explicitly.)
447+
→ rebuild with `--no-cache` (uses image 'Created' timestamp)
448+
- Image present + image-only + last-pull >= threshold days old
449+
→ run `docker pull` (uses local touch-file mtime, since image
450+
'Created' reflects upstream build time and is not a meaningful
451+
local-freshness signal)
451452
- Otherwise: nothing to do
452453
453454
Returns True on success or no-op, False on failure.
@@ -495,14 +496,28 @@ def _ensure_images() -> bool:
495496
logger.info("Running 'devbase container build' to create it...")
496497
return _run_build()
497498
logger.info("Container image '%s' not found, pulling...", image_name)
498-
return _run_pull(image_name)
499+
ok = _run_pull(image_name)
500+
if ok:
501+
_mark_pulled(image_name)
502+
return ok
499503

500-
# Image-only services: 'Created' reflects upstream build time, not
501-
# local pull time, so age-based re-pull is not meaningful. Skip.
504+
max_age = _image_max_age_days()
505+
506+
# Image-only services: use local touch-file mtime, since image
507+
# 'Created' reflects upstream build time, not local pull time.
502508
if not has_build:
503-
return True
509+
pull_age = _pull_age_days(image_name)
510+
if pull_age is None or pull_age < max_age:
511+
return True
512+
logger.info(
513+
"Image '%s' last pulled %d days ago (>= %d days threshold), re-pulling...",
514+
image_name, pull_age, max_age
515+
)
516+
ok = _run_pull(image_name)
517+
if ok:
518+
_mark_pulled(image_name)
519+
return ok
504520

505-
max_age = _image_max_age_days()
506521
age_days = _get_image_age_days(inspect.stdout)
507522
if age_days is None or age_days < max_age:
508523
return True
@@ -578,6 +593,38 @@ def _run_pull(image_name: str) -> bool:
578593
return False
579594

580595

596+
def _pull_marker_path(image_name: str) -> Optional[Path]:
597+
"""Path of the touch-file recording the last pull time of `image_name`.
598+
599+
Returns None when DEVBASE_ROOT is not set so callers can no-op safely.
600+
"""
601+
devbase_root = os.environ.get('DEVBASE_ROOT')
602+
if not devbase_root:
603+
return None
604+
safe = re.sub(r'[^A-Za-z0-9._-]', '_', image_name)
605+
return Path(devbase_root) / '.cache' / 'pulls' / safe
606+
607+
608+
def _pull_age_days(image_name: str) -> Optional[int]:
609+
"""Days since the last successful pull of `image_name`. None if never."""
610+
marker = _pull_marker_path(image_name)
611+
if marker is None or not marker.exists():
612+
return None
613+
return int((time.time() - marker.stat().st_mtime) / 86400)
614+
615+
616+
def _mark_pulled(image_name: str) -> None:
617+
"""Touch the marker file to record a successful pull."""
618+
marker = _pull_marker_path(image_name)
619+
if marker is None:
620+
return
621+
try:
622+
marker.parent.mkdir(parents=True, exist_ok=True)
623+
marker.touch()
624+
except OSError as e:
625+
logger.warning("Could not write pull marker for '%s': %s", image_name, e)
626+
627+
581628
def _update_scale_in_env(new_scale: int) -> bool:
582629
"""Update CONTAINER_SCALE value in env file"""
583630
env_file = Path('./env')

0 commit comments

Comments
 (0)