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
159 changes: 133 additions & 26 deletions containers/base/entrypoint.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,133 @@

set -e

# ===================================================================
# PLAN32: 複数リポジトリの clone / workspace 生成
# ===================================================================
# ホスト側 (devbase up) が projects/<name>/project.yml を正規化し、clone プランを
# base64 のレコード列 (DEVBASE_REPOS) としてコンテナへ渡す。ここでは 1 行ずつ読んで clone
# するだけなので、コンテナイメージへ YAML/JSON パーサ依存を増やさずに済む。
#
# DEVBASE_REPOS : base64 の行区切りレコード。1 行 = url / dir / branch / init を
Comment thread
takemi-ohama marked this conversation as resolved.
# US (0x1f) 区切りで並べたもの。branch は空可、init は 1/0。
# 行区切りは LF で末尾にも LF が付く (符号化側の契約:
# lib/devbase/project/config.py の encode_repo_plan)
# DEVBASE_PRIMARY_DIR : 起動後に cd する /work 配下のディレクトリ名
# DEVBASE_WORKSPACE : 書き出す *.code-workspace の絶対パス (複数 repo 時)
# DEVBASE_WORKSPACE_B64 : その中身 (base64 JSON)
#
# 関数定義だけを読み込みたいテストからは
# `DEVBASE_ENTRYPOINT_LIB_ONLY=1 . entrypoint.sh` で source する。

# clone プランを復号して 1 行 1 repo で出力する (未設定なら何も出さない)。
devbase_repo_plan_lines() {
[ -n "${DEVBASE_REPOS:-}" ] || return 0
printf '%s' "$DEVBASE_REPOS" | base64 -d
}

# clone プランの各リポジトリを <work_root>/<dir> へ clone する。
#
# 個々の失敗 (clone / checkout / init.sh) は warning に留めて次の repo へ進む。
# 1 つ落ちただけでコンテナが起動しないと、他リポジトリでの作業まで止まるため。
devbase_clone_repos() {
local work_root="${1:-/work}"
local plan url dir branch init target cloned

if ! plan="$(devbase_repo_plan_lines 2>/dev/null)"; then
echo "Warning: Failed to decode DEVBASE_REPOS (skipping repository setup)"
return 0
fi
if [ -z "$plan" ]; then
echo "No repositories configured (DEVBASE_REPOS is empty)"
return 0
fi

mkdir -p "$work_root"
local extra index=0
# フィールド区切りは US (0x1f)。タブだと IFS の空白扱いで連続する区切りが 1 つに
# 畳まれ、branch 未指定 (空フィールド) の行で init の値がずれる。
while IFS=$'\x1f' read -r url dir branch init extra; do
# 末尾の空行 (符号化側が付ける末尾改行) は読み飛ばす
[ -n "$url$dir$branch$init$extra" ] || continue
index=$((index + 1))
if [ -z "$url" ] || [ -z "$dir" ] || [ -n "$extra" ] ||
{ [ "$init" != "1" ] && [ "$init" != "0" ]; }; then
echo "Warning: Ignoring malformed clone plan entry (line $index)"
continue
fi
target="$work_root/$dir"

cloned=0
if [ -d "$target/.git" ]; then
echo "Repository already exists: $dir"
else
echo "Cloning repository: $url -> $target"
if ! git clone "$url" "$target"; then
echo "Warning: Failed to clone repository: $url"
continue
fi
cloned=1
fi

# checkout は clone 直後だけ。既存 clone に対して毎回実行すると、コンテナ内で
# 作業ブランチへ切り替えたユーザが再起動のたびに引き戻されてしまう。
# 失敗したら意図しない branch で init.sh を走らせないよう この repo は打ち切る。
if [ "$cloned" = "1" ] && [ -n "$branch" ]; then
if ! git -C "$target" checkout "$branch"; then
Comment thread
takemi-ohama marked this conversation as resolved.
echo "Warning: Failed to checkout branch '$branch' in $dir (skipping)"
continue
fi
fi
Comment thread
takemi-ohama marked this conversation as resolved.

if [ "$init" = "1" ] && [ -f "$target/init.sh" ]; then
echo "Running init.sh in $dir"
(cd "$target" && ./init.sh) || echo "Warning: init.sh failed in $dir"
fi
done <<EOF
$plan
EOF
}

# 複数 repo をまとめて開くための *.code-workspace を書き出す。
#
# 中身はホスト側で組み立てて base64 で渡ってくるので、ここでは復号して置くだけ。
# JSON の組み立て (エスケープ) をシェルでやらない分、壊れにくい。
devbase_write_workspace() {
[ -n "${DEVBASE_WORKSPACE:-}" ] || return 0
[ -n "${DEVBASE_WORKSPACE_B64:-}" ] || return 0

local dest="$DEVBASE_WORKSPACE"
mkdir -p "$(dirname "$dest")"
if printf '%s' "$DEVBASE_WORKSPACE_B64" | base64 -d > "$dest.tmp" 2>/dev/null; then
mv "$dest.tmp" "$dest"
echo "Workspace file written: $dest"
else
rm -f "$dest.tmp"
echo "Warning: Failed to write workspace file: $dest"
fi
}

# primary リポジトリのディレクトリへ移動する (ログイン直後の作業場所)。
devbase_enter_primary_dir() {
local work_root="${1:-/work}"
local target="$work_root/${DEVBASE_PRIMARY_DIR:-}"

if [ -z "${DEVBASE_PRIMARY_DIR:-}" ]; then
return 0
fi
if [ -d "$target" ]; then
cd "$target"
echo "Current directory: $(pwd)"
else
echo "Warning: Primary directory does not exist: $target"
fi
}

# テストは関数定義だけを使う (source 時のみ有効な return で以降を読み飛ばす)。
if [ -n "${DEVBASE_ENTRYPOINT_LIB_ONLY:-}" ]; then
return 0 2>/dev/null || exit 0
fi

# Setup authentication credentials from environment variables
USERNAME="${USERNAME:-ubuntu}"

Expand DownExpand Up@@ -261,32 +388,12 @@ done
echo "AI agent settings symlinks setup completed"
# ========================================

# Git operations (optional, don't fail if they error)
if [ -n "$GIT_USER" ] && [ -n "$GIT_REPO" ]; then
# Clone repository only if it doesn't exist
GIT_HOST="${GIT_HOST:-github.com}"
if [ ! -d "$GIT_REPO" ]; then
echo "Cloning repository: $GIT_HOST/$GIT_USER/$GIT_REPO"
git clone "https://$GIT_HOST/$GIT_USER/$GIT_REPO.git" || echo "Warning: Failed to clone repository"
else
echo "Repository already exists: $GIT_REPO"
fi
# Run init.sh from cloned repository root if it exists
[ -f "$GIT_REPO/init.sh" ] && (cd "$GIT_REPO" && ./init.sh) || true
fi

# Move to repository directory if it exists
echo "Current directory before cd: $(pwd)"
if [ -n "$GIT_REPO" ]; then
echo "GIT_REPO=$GIT_REPO"
if [ -d "$GIT_REPO" ]; then
echo "Directory $GIT_REPO exists, changing to it"
cd "$GIT_REPO"
echo "Current directory after cd: $(pwd)"
else
echo "Directory $GIT_REPO does not exist in $(pwd)"
fi
fi
# Repository setup (PLAN32: 1 project = 複数リポジトリ)
# 個々の失敗はコンテナ起動を止めない (関数内で warning 扱い)。
DEVBASE_WORK_ROOT="${DEVBASE_WORK_ROOT:-/work}"
devbase_clone_repos "$DEVBASE_WORK_ROOT"
devbase_write_workspace
devbase_enter_primary_dir "$DEVBASE_WORK_ROOT"

# Signal that entrypoint setup is complete
touch /tmp/entrypoint-ready
Expand Down
25 changes: 17 additions & 8 deletions issues/PLAN32_multi-repo-project.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,8 +73,9 @@
| --- | --- | --- | --- |
| 設定ファイル名 `project.yml` (repos + devbase 設定を集約) | `repos` に加え `scale` / `open_editor` / `work_dir` を持つ | **採用** | issue の「`CONTAINER_SCALE` は yaml が相応しい」に沿う。設定の置き場所が 1 つに決まり「どっちに書くか」の迷いが消える |
| 設定ファイル名 `repos.yml` (repo 定義のみ) | scale 等は `env` に残す | 不採用 | devbase 設定とコンテナ環境変数が `env` に同居したままで、issue の振り分け要求を半分しか満たさない |
| repo リストの transport: **base64 TSV** を `DEVBASE_REPOS` で渡す | `url\tdir\tbranch\tinit` の行を base64 化 | **採用** | entrypoint 側が `base64 -d` + `while read` だけで解釈でき、jq/python への依存を増やさない (lfm など base 非継承イメージでも安全)。base64 なので compose の `$` 展開・改行事故も起きない |
| transport: base64 JSON + jq | entrypoint で `jq` パース | 不採用 | `jq` は base image にはあるが lfm 系の派生イメージで保証できない。TSV で足りる |
| repo リストの transport: **base64 の US 区切りレコード列** を `DEVBASE_REPOS` で渡す | `url<US>dir<US>branch<US>init` の行 (`<US>` = 0x1f) を base64 化 | **採用** | entrypoint 側が `base64 -d` + `IFS=$'\x1f' read` だけで解釈でき、jq/python への依存を増やさない (lfm など base 非継承イメージでも安全)。base64 なので compose の `$` 展開・改行事故も起きない |
| transport: base64 JSON + jq | entrypoint で `jq` パース | 不採用 | `jq` は base image にはあるが lfm 系の派生イメージで保証できない。US 区切りの平テキストで足りる |
| transport: 区切り文字にタブ (当初案) | `url\tdir\tbranch\tinit` の行を base64 化 | 不採用 | タブは IFS の**空白扱い**で連続する区切りが 1 つに畳まれる。`branch` 未指定 (空フィールド) の行で `init` の値がずれるため、`IFS=$'\t' read` で素直に 4 列として読めない。非空白の US (0x1f) なら空フィールドがそのまま残る |
| transport: `project.yml` を bind mount して entrypoint で解釈 | コンテナ内で YAML を読む | 不採用 | project ディレクトリは現在マウントしていない。マウント経路の追加とコンテナ内 YAML パーサ依存の 2 つを同時に増やす |
| workspace ファイル: **host で JSON を組み立て base64 で渡し entrypoint が書き出す** | `DEVBASE_WORKSPACE_B64` | **採用** | 生成ロジックを Python 側 (テスト可能) に置ける。entrypoint は `base64 -d > file` の 1 行 |
| workspace ファイル: entrypoint で shell 組み立て | printf で JSON を書く | 不採用 | テストできない場所にエスケープ処理を置くことになる |
Expand All@@ -88,7 +89,7 @@
| project | `projects/<name>/` 1 ディレクトリ = 1 compose プロジェクト = 1 dev コンテナ (群) |
| repo | project が `/work` 配下へ clone する git リポジトリ。今回から**複数** |
| primary repo | ログイン時の `cd` 先、および repo 1 件時にエディタが開くフォルダ。既定は `repos` の先頭 |
| clone プラン | `project.yml` を正規化した内部表現。base64 TSV で `DEVBASE_REPOS` としてコンテナへ渡る |
| clone プラン | `project.yml` を正規化した内部表現。base64 の US 区切りレコード列として `DEVBASE_REPOS` でコンテナへ渡る |
| plugin repo | project 定義を配布するリポジトリ (`volareinc/devbase-ext` 等)。`projects/*` はここへの symlink |

## 不変条件
Expand DownExpand Up@@ -139,15 +140,23 @@ repos:
- `dir` 重複はエラー。`primary: true` が 2 件以上はエラー。`repos` が空はエラー。
- 未知キーはエラー (typo を黙って無視しない)。

wire format (`DEVBASE_REPOS`, base64 TSV / 1 行 1 repo, タブ区切り):
wire format (`DEVBASE_REPOS`, base64 / 1 行 1 repo, US (0x1f) 区切り):

```
https://github.com/volareinc/carmo.git<TAB>carmo<TAB>main<TAB>1
https://gitlab.com/uttaro_dev/uttarov2.git<TAB>system<TAB><TAB>0
https://github.com/volareinc/carmo.git<US>carmo<US>main<US>1
https://gitlab.com/uttaro_dev/uttarov2.git<US>system<US><US>0
```

- フィールド区切りは **US = unit separator (0x1f)**。非空白なので `IFS=$'\x1f' read -r url dir branch init`
がそのまま 4 列として読め、`branch` 未指定 (空フィールド) の行でも `init` の値がずれない。
タブだと IFS の空白扱いで連続する区切りが 1 つに畳まれるため採用しない。
- 行区切りは LF。**末尾にも LF を付ける**。`while read` は EOF 直前の改行なし行を読み捨てる実装が
あるため、末尾 LF が無いと最後の行 (repo 1 件ならその唯一の行) が丸ごと落ちる。
- 各フィールドは符号化側で検証済み。空白・制御文字 (US / LF を含む) は通さないので、
エスケープ規則は持たない。

列: `url`, `dir`, `branch` (空可), `init` (`1`/`0`)。primary は別変数 `DEVBASE_PRIMARY_DIR` で渡す
(TSV の列を増やさず、entrypoint の `cd` 先判定を単純に保つ)。
(列を増やさず、entrypoint の `cd` 先判定を単純に保つ)。

## 修正対象

Expand DownExpand Up@@ -179,7 +188,7 @@ plugin リポジトリ (別 PR):

- **対象ファイル:** `lib/devbase/project/config.py`, `tests/project/test_config.py`
- **変更内容:** `ProjectConfig` / `RepoSpec` dataclass、YAML 読み込み・`defaults` 継承・正規化・検証、
`encode_repo_plan()` / `decode_repo_plan()` (base64 TSV)。`project.yml` 不在・不正時は移行手順を含む
`encode_repo_plan()` / `decode_repo_plan()` (base64 / US 区切り)。`project.yml` 不在・不正時は移行手順を含む
`ConfigError` を送出。この PR では**呼び出し元を差し替えない** (挙動変更なし)。
- **満たす受け入れ条件:** AC6 の一部 (エラー文言)、AC2/AC3 のデータ表現
- **進め方:** テスト駆動。正常系 (defaults 継承 / dir 明示 / host 混在 / primary 指定) と
Expand Down
Empty file addedtests/containers/__init__.py
Empty file.
Loading