PLAN67: base イメージに shellcheck を入れる (#249) - #252
Conversation
- 1 つ目の RUN の 1 回目の apt-get install の一覧へ shellcheck を足す - 版の確認の RUN へ shellcheck --version を足し、入れ損ないをビルドで止める - Dockerfile の形を固定する test_base_dockerfile_shellcheck.py を新設 - 利用者向け文書(イメージの詳細の表・Bash の静的検査の小節)と CHANGELOG - 実装計画 issues/PLAN67_base-shellcheck-impl.md Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add characterization tests for the amd64 browser branch, offline npm global replacement by the default user, and both apt package sets. Leave the Dockerfile unchanged. Item-Id: R1-001 Round: 1 Impl-Runtime: codex Impl-Model: default
改修計画 — devbasex/devbase #252
ラウンド 1(実装 codex)R1-001 —
|
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | kiro | 採用 | 1 |
なぜ: ブラウザの導入は amd64 のときだけ Google Chrome のリポジトリを足して BROWSER_PKG=google-chrome-stable にし、arm64 では chromium-browser のみになる分岐を持つ。この if [ "$arch" = "amd64" ] の分岐を固定するテストが無い。
手順: 1. 1 つ目の RUN 命令 (行継続を含む) を取り出す
2. if の中で amd64 のときだけ google-chrome のリポジトリ登録と BROWSER_PKG="google-chrome-stable" が現れることを assert する
3. apt-get install の一覧が chromium-browser と $BROWSER_PKG の両方を含み、arm64 でも chromium-browser が常に入ることを assert する
R1-002 — containers/base/Dockerfile#NPM_CONFIG_PREFIX
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| normal | integration | — | codex | 採用 | 1 |
なぜ: tests/containers 全体の npm・NPM_CONFIG_PREFIX・install -g の検索と既存のイメージ検査を確認したが、一般ユーザーによるグローバルパッケージ更新の検証はない。Dockerfile が root で導入した npm パッケージを既定の非 root ユーザーが sudo なしで更新し、PATH 経由で実行できる振る舞いを固定する。OS の所有権・権限、npm、PATH のつなぎ目なので、文字列検査やスタブだけでは代替できない。
手順: 1. 現行 Dockerfile からビルドした base イメージを用意し、使い捨てコンテナを既定ユーザー、entrypoint=/bin/bash、ネットワーク無効、ホストのホームや認証情報をマウントしない条件で起動する。Docker または対象イメージが利用できない場合だけ理由付きで skip する。
2. コンテナ内に、依存と install script を持たず、bin の codex が決まった識別値を返すローカルの @openai/codex パッケージを作り、npm pack で tarball にする。既存の root 導入パッケージを置き換えるために同じ名前を使うが、ネットワークや実際の AI サービスには接続しない。
3. UID が非 0 であることを確認し、sudo や prefix・PATH・所有権の変更をせず、npm install -g --offline --ignore-scripts --no-audit --no-fund を実行する。続けて非対話シェルから codex を PATH 経由で起動する。
4. 現行イメージで得たインストールと起動の終了コード、および fixture の識別値を観測して期待値として記録する。npm の表示文言、内部の配置パス、chmod/chown の手順は固定しない。失敗を観測した場合も成功と決めつけず、現状の制約として明記する。
5. 同じ観測を assertion にして tests/containers/test_base_image_npm_globals.py に追加し、コンテナは必ず破棄する。
R1-003 — containers/base/Dockerfile#RUN apt-get install
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| normal | unit | — | claude | 採用 | 1 |
なぜ: 1 つ目の RUN が apt-get install で入れるパッケージのうち固定されているのは #160 の 6 つと shellcheck だけで、locales / git / tmux / openssh-client / fonts-noto-cjk などの 1 回目の残りと、2 回目の docker-ce / terraform / gh / nodejs / chromium-browser は固定されていない。一覧を並べ替えたり分けたりすると、1 つ落ちても既存のテストは落ちない
手順: 1. コメント行を除いた Dockerfile の 1 つ目の RUN を行継続まで連結して取り出す
2. apt-get install の 1 回目と 2 回目の引数を、次の ; までの範囲でそれぞれ取り出し、オプション (- で始まる語) と行継続の \ を除いて語の集合にする
3. 1 回目の集合と 2 回目の集合を、現状の出力を実行して得た値とそれぞれ比較する ($BROWSER_PKG は語のまま集合に残す)
R1-004 — containers/base/Dockerfile#RUN aws-gcloud-uv-npm
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | claude | 採用 | 1 |
なぜ: アーキテクチャの分岐は bao の RUN だけが固定されており、session-manager-plugin の case (amd64→ubuntu_64bit / arm64→ubuntu_arm64 / それ以外で exit 1) と gcloud の case (x86_64→x86_64 / aarch64→arm / それ以外で exit 1) は固定されていない。この RUN を分けたりまとめたりしたときに、片方の対応や未対応時の停止が欠けても検出できない
手順: 1. コメント行を除いた Dockerfile から session-manager-plugin を入れる RUN を行継続まで連結して取り出す
2. ssm_arch の case について、amd64) と arm64) の枝がそれぞれ現状の値を代入し、) の枝が exit 1 することを確かめる
3. gcloud_arch の case について、x86_64) と aarch64) の枝が現状の値を代入し、) の枝が exit 1 することを確かめる
4. 取り出した値がダウンロード URL (${ssm_arch} / ${gcloud_arch}) で参照されていることを確かめる
R1-005 — containers/base/Dockerfile#RUN version-check
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| normal | unit | — | claude | 採用 | 1 |
なぜ: 版の確認の RUN で固定されているのは shellcheck --version の 1 つだけで、gh / node / npm / aws / gcloud / session-manager-plugin の 6 つは固定されていない。この RUN を組み替えると、入れ損ないをビルドで止める確認が黙って欠けても既存のテストは落ちない
手順: 1. コメント行を除いた Dockerfile を RUN の命令単位に分け、gh --version を含む RUN をちょうど 1 つ取り出す
2. RUN を外した本文を && で分けて前後の空白を落とし、コマンドの集合を作る
3. その集合が現状の 7 つ (gh / node / npm / aws / gcloud / session-manager-plugin / shellcheck の --version) と等しいことを比較する。順序は仕様ではないので集合で比べる
ラウンド 2(実装 kiro)
R2-001 — containers/base/Dockerfile#agy
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| error | unit | — | claude / codex | 検証中 | 1 |
なぜ: agy のインストーラは、curl の失敗を bash の成功が覆い隠さないように、パイプに直結せずファイルへ落としてから実行し、最後にバイナリの実体を --version で確かめている(コメントに理由がある)。この失敗の検出経路はどのテストにも固定されていない。ユーザー用ツールの RUN を整理するときに、ほかの行(uv / claude)と同じ curl ... | bash の形へ揃えてしまうと検出できない
手順: 1. ユーザー用ツールを導入するRUN全体をDockerfileから取得し、専用の作業ディレクトリとHOME、PATH上の外部コマンドスタブを使って/bin/shで実行する。curlはURL別のローカルfixtureを返し、先行するuvとClaudeの導入は成功させる。
2. agyについて、取得が非0終了する場合、取得したインストーラが非0終了する場合、インストーラが0終了しても実行ファイルを生成しない場合を独立に実行する。実行失敗はfixtureスクリプト自身から発生させる。
3. 現行の終了コードと、後続のKiroダウンロードおよびPlaywright導入の成果が作られないことを観測して固定する。外部コマンドの代替は成果を専用ディレクトリへ記録し、実ネットワーク・sudo・ホストの清掃処理は実行しない。内部呼び出し順やエラーメッセージの全文は固定しない。
R2-002 — containers/base/Dockerfile#bao
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| error | unit | — | codex / kiro | 検証中 | 1 |
なぜ: bao の RUN の case は amd64 / arm64 が選べることは test_both_architectures_are_selectable で固定されているが、未対応アーキテクチャで停止する枝 (*) echo ... exit 1) は固定されていない。ssm / gcloud の同種の error 枝は test_base_dockerfile_shellcheck.py の _case_branches で固定済みだが bao だけ欠けている。
手順: 1. _bao_run_block() を使い bao を入れる RUN の 1 命令分を取り出す
2. 行継続の \ を畳んでから、bao_arch の case を {パターン: 値} に写す
3. amd64 -> amd64、arm64 -> arm64 を確認する
4. *) の枝に exit 1 が含まれること、その直前に echo の停止メッセージがあることを確認する
5. 期待値は現行 Dockerfile を実行せず文言から読み取った現状値をそのまま置く
R2-003 — containers/base/Dockerfile#RUN user-config
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| normal | unit | — | claude | 未着手 | 0 |
なぜ: /.bashrc へ足す 2 行 (PATH と ai-cli-aliases.sh の source)、git の credential.helper、/.claude/settings.json の SessionStart フック(unset AWS_REGION)は、イメージ側の出力なのにどのテストにも固定されていない。test_entrypoint_ai_settings.py は仮の HOOKS 文字列を使っていて、Dockerfile が焼く中身は見ていない。フックの JSON はクォートが 3 重に入れ子になっていて、書き換えると壊れやすい
手順: 1. _run_blocks() から ~/.claude/settings.json を含む RUN をちょうど 1 つ取り出し、先頭の RUN を外す
2. tmp_path を HOME にして bash -c でその本文を実行する(ネットワークにも sudo にも依存しない)
3. $HOME/.bashrc の末尾 2 行が PATH の行と . /etc/devbase/ai-cli-aliases.sh であることを比べる
4. git config --global --get credential.helper が store を返すことを確かめる(HOME を差し替えた環境で実行する)
5. $HOME/.claude/settings.json を json.loads し、hooks.SessionStart[0].hooks[0] の type が command、command に unset AWS_REGION と $CLAUDE_ENV_FILE が含まれることを要点比較する
R2-004 — containers/base/Dockerfile#RUN user-setup
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| branch | unit | — | claude | 未着手 | 0 |
なぜ: ユーザー設定の RUN (groupadd / 条件付き useradd / usermod / sudoers) を固定するテストがない。if ! id -u $USERNAME の分岐で、既存ユーザーがいれば useradd を飛ばして usermod だけ行う。npm グループの GID は ARG NPM_GID=2000 に固定されていて、派生イメージ lfm が依存している。sudoers の 2 ファイルは 0440。どれも RUN を分けたりまとめたりするときに落としやすいが、どこにも固定されていない
手順: 1. 既存の _run_blocks() から useradd を含む RUN をちょうど 1 つ取り出す
2. ARG NPM_GID="2000" が Dockerfile にあり、その RUN に groupadd -g "$NPM_GID" npm があることを確かめる
3. 行継続を畳み、if ! id -u $USERNAME >/dev/null 2>&1; then から fi; までの本体が useradd だけで、usermod -aG users,docker,npm $USERNAME が if の外(fi の後)にあることを確かめる
4. /etc/sudoers.d/users と /etc/sudoers.d/$USERNAME の 2 ファイルが書かれ、両方が chmod 0440 の対象になっていることを集合で比べる
5. この RUN が npm 領域を作る RUN(install -d -m 2775)より前にあることを、_run_blocks() の並びで確かめる(npm グループが先に要る)
R2-005 — containers/base/Dockerfile#aws-cli
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| normal | unit | — | kiro | 検証中 | 1 |
なぜ: AWS CLI v2 のダウンロードは ssm / gcloud と異なり case を持たず $(uname -m) を URL へ直接埋める正常経路である。この代表経路が固定されていないため、RUN を組み替えたときに arch の埋め込みが黙って欠けても検出できない。
手順: 1. ssm_arch / gcloud_arch を持つ RUN の 1 命令分を取り出す (test_base_dockerfile_shellcheck.py の _aws_gcloud_run と同じ判定)
2. awscli-exe-linux-$(uname -m).zip を含む URL 文字列があることを確認する
3. unzip -q /tmp/awscliv2.zip と /tmp/aws/install が続くことを確認する
4. AWS CLI の download には case (amd64/arm64 の分岐) が無いことを、この URL 断片の周辺に別の case ブロックが挟まらない形で確認する
5. 期待値は現行 Dockerfile の文言から読み取った現状値をそのまま置く
見送った項目
| ラウンド | 対象 | 兆候・経路 | 理由 |
|---|---|---|---|
| 1 | containers/base/Dockerfile#gcloud |
branch | 1 ラウンドの採用上限 5 件を超えた |
| 1 | containers/base/Dockerfile#gcloud |
error | 1 ラウンドの採用上限 5 件を超えた |
| 1 | containers/base/Dockerfile#kirocli |
branch | 1 ラウンドの採用上限 5 件を超えた |
| 1 | containers/base/Dockerfile#session-manager-plugin |
branch | 1 ラウンドの採用上限 5 件を超えた |
| 1 | containers/base/Dockerfile#session-manager-plugin |
error | 1 ラウンドの採用上限 5 件を超えた |
| 2 | containers/base/Dockerfile#bao |
branch | 1 ラウンドの採用上限 5 件を超えた |
| 2 | containers/base/Dockerfile#claude-hooks |
normal | 1 ラウンドの採用上限 5 件を超えた |
| 2 | containers/base/Dockerfile#user-setup |
branch | 1 ラウンドの採用上限 5 件を超えた |
…-npm
session-manager-plugin と gcloud SDK のアーキテクチャ分岐を現状固定する。
ssm_arch の case (amd64→ubuntu_64bit / arm64→ubuntu_arm64 / *→exit 1) と
gcloud_arch の case (x86_64→x86_64 / aarch64→arm / *→exit 1) の各枝、および
選んだ値がダウンロード URL の ${ssm_arch} / ${gcloud_arch} で参照されることを
固定した。RUN を分けたりまとめたりしたときに片方の対応や未対応時の停止が
欠けても検出できる。対象コードは変更していない。
Item-Id: R1-004
Round: 1
Impl-Runtime: kiro
Impl-Model: default
版の確認の RUN のコマンド一式 (gh / node / npm / aws / gcloud / session-manager-plugin / shellcheck の --version) を集合で固定する。 RUN を組み替えたときに確認が黙って欠けても検出できるようにする。 Item-Id: R1-005 Round: 1 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add characterization tests for R2-001, R2-002, and R2-005 without changing the Dockerfile. Item-Id: R2-001 Round: 2 Impl-Runtime: codex Impl-Model: default
cross-refactoring のテスト整備ラウンドが --scope(containers/base/Dockerfile)を ファイル単位で読み、#249 と関係しない既存の命令(AWS CLI / gcloud / bao / agy / ブラウザ / npm のグローバル)の現状固定テストを 4 コミット積んだ。shellcheck の 2 か所は既存のテストが固定しているため、4 コミットをまとめて戻す。 Revert f2f2c81 8414aaa 38b1a52 a67ca10 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | agy | APPROVE
PLAN67(#249)の要件に従って base イメージへの shellcheck 追加、ビルド時検証、Dockerfile 構造の回帰テスト、および文書と CHANGELOG の更新が反映されており、修正を要する問題はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | kiro | REQUEST_CHANGES
PR #252 の main に対する正味変更(Dockerfile への shellcheck 追加、版確認 RUN への shellcheck --version、新規テスト、docs/CHANGELOG)は目的と一貫しており、派生イメージの継承関係(lfm/snapshot は base を継がない)や rebuild の挙動記述も既存コードと整合する。head の revert(4 コミット)は既存コメントの cross-refactoring 計画どおりの意図的なスコープ判断で、そこは指摘しない。修正提案は新規テストの 1 件の締まりの弱さのみ。
_first_apt_install が 2 回目の apt-get install の直前までを返しており、 locale-gen やリポジトリの設定に語が残るだけでも通ってしまっていた。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
🔧 /ndf:fix サマリ | round 1 | commit 5990a30対応件数: critical=0 / major=0 / minor=1(合計 1 件) |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | APPROVE
指定コミットの差分について、新規の修正指摘はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | kiro | APPROVE
PLAN67 (#249) の base イメージへの shellcheck 追加。変更は 1 回目の apt-get install への 1 パッケージ追加と版確認 RUN への shellcheck --version 追加に限定され、目的と変更範囲が一貫している。文字列固定テスト 4 件は tests/containers 全体 (197 件) と併せて緑で、5990a30 の範囲絞り込み (_first_apt_install を最初の ; まで) も妥当。docs の rebuild vs --no-cache の説明も rebuild=build --expires=7 の実装と整合。スナップショットの R1/R2 の特性化テスト群はプラン注記どおり e379a6c で戻され #249 の範囲外。修正を要する critical/major/minor の指摘は見当たらず、APPROVE とする。
🔧 /ndf:fix サマリ | commit (なし)対応件数: critical=0 / major=0 / minor=0(合計 0 件) |
PLAN67 の要求・設計・実装計画を issues/old/ へ移し、置き場所・入れ損ないの 止め方・版の扱い・伝播の規則・テスト観点を base-image-shellcheck.md として 確定仕様にした。利用者向けの小節から新しい仕様へリンクする。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
範囲外と判断し、#254(v3.7.0 のタグと GitHub Release が無い)として残した。 |
配布の記録段階: 本番(検証のチャネルが無いため main へのマージが本番への配布にあたる。2026-09-24 に head 1d4c3b4 に対して承認、2026-09-24 09:42 JST にマージ。マージコミット 676cba5) 形: パッケージ・プラグイン(利用者が 届き方: タグ・GitHub Release(v3.7.0 の分を含む)は今回行わない(#254 に残す)。 |
リリース後テスト対象の版: 3.7.0 + main@676cba5(2026-09-24 09:42 JST にマージ)
受け入れ条件 4・5(Dockerfile の文字列検査)はマージ前の全体テストと CI で確認済みのため対象外。 合否: 合格(受け入れ条件 1〜3・6 の 4 件を実施し全件合格。amd64 は #242 で保留) |
Summary
base イメージに
shellcheckを入れ、base と、base を継ぐ派生イメージ(general/phpなど)のコンテナで Bash スクリプトの静的検査と、言語サーバによる Bash の診断を使えるようにします。docs/specifications/base-image-shellcheck.md(確定仕様化で新設)issues/old/PLAN67_base-shellcheck.md(受け入れ条件 1〜9)issues/old/PLAN67_base-shellcheck-design.md(決定 1〜5、設計 PR 設計(PLAN67): base イメージに shellcheck を入れる (#249) #250 でマージ済み)issues/old/PLAN67_base-shellcheck-impl.mdCloses #249
変更点
containers/base/DockerfileRUNの 1 回目のapt-get installの一覧へshellcheckを足した(決定 1)。版の確認のRUNへ&& shellcheck --versionを足し、入れ損ないをビルドで止める(決定 2)tests/containers/test_base_dockerfile_shellcheck.py(新設)docs/user/container-operations.mdCHANGELOG.md[Unreleased]の### Addeddocs/specifications/base-image-shellcheck.md(新設)issues/PLAN67_*.md→issues/old/反映には
devbase build base --no-cacheと、使っている派生イメージの建て直しが要ります。 CI はイメージを建てないため、イメージの中の受け入れ条件は手元(arm64)で建てたイメージから採りました。Test plan
shellcheck --versionがexit=0、version: 0.11.0devbase-general/devbase-phpで同じくexit=0echo $fooの 1 行にSC2154/SC2086を出しexit=1uv run --locked pytest tests/containers/test_base_dockerfile_shellcheck.py -q→ 4 passed(Dockerfile の変更前は 2 failed / 2 passed を確認)。レビューの指摘で 1 回目の一覧の範囲を最初の;までに絞った(5990a30)後も 4 passed / exit=0libnuma1とshellcheckの 2 行。建てたイメージのInstalled-Sizeの合計 25058 KB(≤ 30720)uv run --locked pytest tests/ -q→ 3026 passed、exit=0devbase build base --no-cache(arm64)→exit=0(3 分 4 秒)。devbase build general --no-cache/devbase build php --no-cacheもexit=0devbase build base --no-cacheが要ることを書いた)受け入れ条件 6: 変更前のイメージ(c477ec7764c1、2026-09-23 08:10:59 +0900)
受け入れ条件 1・3・6: 建てた base(a94591db20a3、2026-09-24 08:07:03 +0900)
受け入れ条件 2: 派生イメージ(general 6a8ecc270f0a / php e2f9b087fa63)
検査の結果
uv run --locked pytest tests/containers/test_base_dockerfile_shellcheck.py -quv run --locked pytest tests/ -quvx ruff check tests/containers/test_base_dockerfile_shellcheck.pyenv -u DEVBASE_ROOT uv run --locked pytest tests/ -qdevbase build base/general/php --no-cache(arm64)pyproject.tomlと.coveragercに無し)uvx ruff check .(ruff 0.16.8)は 995 件を出すが、main(83c2c3b)でも同数で、本 PR の差分のファイルには出ない。CI の Ruff lint は successやらないこと
lfm/snapshotへの導入未確認のまま残ること
配布の記録
段階: 承認待ち(検証のチャネルが無く、
mainへのマージが本番への配布にあたる。未実施)版: 3.7.0 → 3.7.0 + main のマージコミット(版数は上げない。
[Unreleased]に積み、タグは溜まった分をまとめて切る)まとまり: PR #252
🤖 Generated with Claude Code