diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94477ec4..ec4ca337 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -151,6 +151,12 @@ jobs: # 読み取り専用ジョブのため checkout の認証情報をディスクに残さない(サプライチェーン保護)。 persist-credentials: false + # 領域跨ぎの SSoT drift 検知(env_keys.py↔docker-compose / errors.py↔errorCodes.ts)。 + # grep/sed/comm のみに依存し(ripgrep は使わない)nix 非経由で直接実行する。 + # 重い依存インストール前に置いて fail-fast にする。 + - name: Lint SSoT (env names / error codes) + run: bash scripts/lint-env-keys.sh + - name: Install uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: diff --git a/Makefile b/Makefile index dc6a41ec..12dd3dd8 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ setup install-hooks install-backend install-web generate-keys \ dev dev-build dev-down dev-amd64 dev-amd64-build dev-web preview-web dev-proxy dev-proxy-only stripe-webhook \ test test-backend test-web \ - lint lint-backend lint-web lint-web-messages lint-fix \ + lint lint-backend lint-web lint-web-messages lint-env-keys lint-fix \ format format-check \ ci \ dupe-check dupe-check-html dupe-clean \ @@ -42,6 +42,7 @@ help: @echo " lint-backend Backend: ruff check" @echo " lint-web Frontend: eslint" @echo " lint-web-messages Frontend: setError等にリテラル日本語が渡っていないか検知" + @echo " lint-env-keys env名/エラーコードの SSoT drift を検知 (env_keys.py↔compose, errors.py↔errorCodes.ts)" @echo " lint-fix リント自動修正 (ruff + eslint)" @echo " format Prettier で整形" @echo " format-check Prettier チェック" @@ -140,7 +141,7 @@ test-backend: test-web: nix develop --command bash -c "cd web && npm test" -lint: lint-backend lint-web lint-web-messages +lint: lint-backend lint-web lint-web-messages lint-env-keys lint-backend: nix develop --command bash -c "cd backend && .venv/bin/python -m ruff check app tests alembic_migrations" @@ -153,6 +154,12 @@ lint-web: lint-web-messages: nix develop --command bash scripts/lint-web-messages.sh +# env 名 / エラーコードの SSoT drift を検知。 +# env_keys.py↔docker-compose.yml、errors.py↔errorCodes.ts の集合一致を検証する。 +# grep/sed/comm のみに依存(ripgrep 不要)。他 lint と揃えて nix wrap で実行する。 +lint-env-keys: + nix develop --command bash scripts/lint-env-keys.sh + lint-fix: nix develop --command bash -c "cd backend && .venv/bin/python -m ruff check --fix app tests alembic_migrations" cd web && npm run lint:fix diff --git a/backend/app/core/env_keys.py b/backend/app/core/env_keys.py index c1b75211..1f569386 100644 --- a/backend/app/core/env_keys.py +++ b/backend/app/core/env_keys.py @@ -24,6 +24,13 @@ 4. `docker-compose.yml` の environment ブロックを追従 5. `docs/api.md` の環境変数表を更新 +## drift 検知(自動) + +本モジュールの定数値が `docker-compose.yml` の environment にすべて存在するかは +`scripts/lint-env-keys.sh`(`make lint-env-keys` / CI の test-backend ジョブ)で機械検証する。 +rename / 追加で compose 追従を忘れると lint が落ちる。ローカル開発で意図的に注入しない +定数(起動時内部フラグ等)は同スクリプトの `COMPOSE_ALLOWLIST` に明示的に追記する。 + ## 関連ドキュメント - 環境変数一覧と用途: `docs/api.md`「環境変数」セクション diff --git a/backend/app/core/errors.py b/backend/app/core/errors.py index dba4f16d..c909cb79 100644 --- a/backend/app/core/errors.py +++ b/backend/app/core/errors.py @@ -18,6 +18,10 @@ class ErrorCode(str, Enum): 1. 本 enum に値を追加 2. ``web/src/constants/errorCodes.ts:ERROR_CODES`` に文字列を追加 3. ``web/src/constants/errorMessages.ts:ERROR_CONFIG`` にメッセージと recovery を追加 + + 本 enum と ``ERROR_CODES`` の集合一致は ``scripts/lint-env-keys.sh`` + (``make lint-env-keys`` / CI の test-backend ジョブ)で機械検証する。 + FE 側の型縛りは FE 内で完結し BE 起点の追加漏れを拾えないため、それを補う。 """ # 認証 diff --git a/scripts/lint-env-keys.sh b/scripts/lint-env-keys.sh new file mode 100755 index 00000000..315e7f35 --- /dev/null +++ b/scripts/lint-env-keys.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# 領域跨ぎの SSoT drift を検知する。 +# +# 背景: +# 環境変数名やエラーコードは「正本(backend)を変えたのに downstream(compose / FE)の +# 追従を忘れる」事故が起きやすい。これらは言語境界(Python / YAML / TS)上リテラルの +# 複製を消せないため、複製を消す代わりに「複製が正本と一致しているか」を機械検証する。 +# +# 検証内容: +# (1) env 名: backend/app/core/env_keys.py の定数値(= 実 env 名)が +# docker-compose.yml の environment ブロックにすべて存在するか +# (ALLOWLIST で起動時内部フラグ等を除外)。rename / 削除時の同期忘れを CI で止める。 +# (2) エラーコード: backend/app/core/errors.py の ErrorCode 値集合と +# web/src/constants/errorCodes.ts の ERROR_CODES が完全一致するか。 +# FE 側の型検査(Record)は FE 内で完結するため、 +# BE が新コードを追加して FE 未反映の drift は型エラーにならない。それを補う。 +# +# 正本: +# - env 名: backend/app/core/env_keys.py +# - エラーコード: backend/app/core/errors.py の ErrorCode +# +# 詳細: .claude/rules/common/duplication.md / backend/app/core/env_keys.py の docstring +set -euo pipefail + +cd "$(dirname "$0")/.." + +ENV_KEYS="backend/app/core/env_keys.py" +COMPOSE="docker-compose.yml" +ERRORS_PY="backend/app/core/errors.py" +ERROR_CODES_TS="web/src/constants/errorCodes.ts" + +# docker-compose に注入しない env_keys 定数(設定ではなくランタイム内部フラグ)。 +# APP_BOOTSTRAPPED: backend/scripts/entrypoint.sh が export する起動ガードで、 +# 外部から注入する設定 env ではないため compose には現れない。 +COMPOSE_ALLOWLIST=$(printf '%s\n' \ + "APP_BOOTSTRAPPED" \ + | sort -u) + +fail=0 + +# ── (1) env_keys.py ⊆ docker-compose.yml ────────────────────────────────── +# 抽出は grep -E / sed -E / awk のみで行う(PCRE / ripgrep に依存しない)。 +# ripgrep は flake.nix の devshell にも GitHub ランナーにも入っていないため。 +# +# env_keys.py からは定数の「文字列値」(= 実 env 名)を取る。symbol 名ではなく値が +# downstream の env 名になるため、`NAME = "VALUE"` の VALUE 側を比較対象にする +# (symbol だけ rename しても誤検知せず、値だけ変えた drift も取りこぼさない)。 +env_names=$(grep -E '^[A-Z_]+[[:space:]]*=[[:space:]]*"[^"]+"' "$ENV_KEYS" \ + | sed -E 's/^[A-Z_]+[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/' | sort -u) + +# docker-compose.yml からは api サービスの environment ブロック内の env 名だけを取る。 +# 全 YAML の大文字キーを拾うと、他サービス(libsql の SQLD_NODE 等)の名前で +# 「api に無い env 名」を誤って pass させてしまう(検知したい drift を隠す)。 +compose_names=$(awk ' + /^ [a-z_]+:[[:space:]]*$/ { in_api = ($0 ~ /^ api:[[:space:]]*$/) } + in_api && /^ environment:[[:space:]]*$/ { in_env = 1; next } + in_api && in_env && /^ [^[:space:]]/ { in_env = 0 } + in_api && in_env && /^ [A-Z_]+:/ { + name = $0; sub(/^[[:space:]]+/, "", name); sub(/:.*/, "", name); print name + } +' "$COMPOSE" | sort -u) + +# 正本から allowlist を除いた「compose に存在すべき env 名」 +expected_in_compose=$(comm -23 <(printf '%s\n' "$env_names") <(printf '%s\n' "$COMPOSE_ALLOWLIST")) +missing_in_compose=$(comm -23 <(printf '%s\n' "$expected_in_compose") <(printf '%s\n' "$compose_names")) + +if [ -n "$missing_in_compose" ]; then + echo "ERROR: 次の env 名が $ENV_KEYS にあるが $COMPOSE の environment に未注入です:" >&2 + printf ' - %s\n' $missing_in_compose >&2 + echo "" >&2 + echo "$ENV_KEYS の定数を rename / 追加したら $COMPOSE も追従してください。" >&2 + echo "ローカル開発で意図的に不要な場合は scripts/lint-env-keys.sh の COMPOSE_ALLOWLIST に追記。" >&2 + echo "" >&2 + fail=1 +fi + +# ── (2) errors.py ErrorCode == errorCodes.ts ERROR_CODES ────────────────── +be_codes=$(grep -E '^[[:space:]]+[A-Z_]+[[:space:]]*=[[:space:]]*"[A-Z_]+"' "$ERRORS_PY" \ + | sed -E 's/.*=[[:space:]]*"([A-Z_]+)".*/\1/' | sort -u) +fe_codes=$(grep -E '^[[:space:]]+"[A-Z_]+",' "$ERROR_CODES_TS" \ + | sed -E 's/.*"([A-Z_]+)".*/\1/' | sort -u) + +be_only=$(comm -23 <(printf '%s\n' "$be_codes") <(printf '%s\n' "$fe_codes")) +fe_only=$(comm -13 <(printf '%s\n' "$be_codes") <(printf '%s\n' "$fe_codes")) + +if [ -n "$be_only" ] || [ -n "$fe_only" ]; then + echo "ERROR: ErrorCode の集合が BE と FE で一致しません。" >&2 + if [ -n "$be_only" ]; then + echo " $ERRORS_PY にあるが $ERROR_CODES_TS に無い:" >&2 + printf ' - %s\n' $be_only >&2 + fi + if [ -n "$fe_only" ]; then + echo " $ERROR_CODES_TS にあるが $ERRORS_PY に無い:" >&2 + printf ' - %s\n' $fe_only >&2 + fi + echo "" >&2 + echo "errors.py の ErrorCode を正本に、errorCodes.ts と errorMessages.ts を追従してください。" >&2 + echo "" >&2 + fail=1 +fi + +if [ "$fail" -ne 0 ]; then + exit 1 +fi + +echo "lint-env-keys: OK(env 名 / ErrorCode の SSoT drift なし)"