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
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 チェック"
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions backend/app/core/env_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`「環境変数」セクション
Expand Down
4 changes: 4 additions & 0 deletions backend/app/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 起点の追加漏れを拾えないため、それを補う。
"""

# 認証
Expand Down
106 changes: 106 additions & 0 deletions scripts/lint-env-keys.sh
Original file line number Diff line number Diff line change
@@ -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<ErrorCodeKey,...>)は 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 なし)"
Loading