diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d76e2298..e698ff6e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -160,6 +160,7 @@ Claude Code は `/model` コマンドを自分では実行できないため、 過去の手戻り・障害から導いた再発防止ルール。**領域固有の項目は各 scoped rule に集約済み**(対象パス編集時に自動ロードされる)。ここには領域横断(常に効かせたい)ものだけを残す。 - **テストで DB をモックしない**: 統合テストは実 DB(テスト用 SQLite セッション)に当てる。モック/本番乖離でマイグレーション失敗を見落とした実績がある。 +- **ネイティブ/コンテナ起動はスモークテストで必ず検証する**: pytest(`make test-backend`)は標準 SQLite で動き、本番イメージのビルド・libsql ネイティブドライバ・alembic マイグレーション・uvicorn 起動の実行パスを通らない。この穴は CI の `smoke-backend` ジョブ(本番イメージ build → 実 libSQL 起動 → `/health` 200 を検証)で塞ぐ。backend の Dockerfile / 依存 / 起動経路を変えたら smoke-backend が green であることを確認する(Python 3.14 bump で libsql が起動時 segfault した事象の再発防止)。詳細: `.claude/rules/backend/test.md` / `database.md` - **新規ブランチは `origin/main` 起点で切る**: リリース前は全てを `main` にマージする運用。以前は `origin/dev` 起点だったが dev 環境作業の名残で、現在は廃止。 領域別の再発防止ルールは各 scoped rule に集約(対象パス編集時に自動ロード): diff --git a/.claude/rules/backend/database.md b/.claude/rules/backend/database.md index 82888846..aadd8e4a 100644 --- a/.claude/rules/backend/database.md +++ b/.claude/rules/backend/database.md @@ -18,9 +18,9 @@ paths: - **ALTER COLUMN(型・制約変更)**: libSQL は非対応。`batch_alter_table`(テーブル再作成)で行う - **DROP COLUMN は原則 `op.drop_column` を直接使う**: SQLite/libSQL 3.35+ は `ALTER TABLE ... DROP COLUMN` をサポートする。インデックス・FK・制約のない素のカラムはこれで消せる(テーブル再作成不要) - **FK 参照される親テーブルに `batch_alter_table` を使わない**: batch は「新テーブル作成 → 旧テーブル DROP → リネーム」で動くため、`users` のように子テーブルから FK 参照される親を batch で触ると、旧テーブル DROP 時に libSQL(`foreign_keys=ON`)で `FOREIGN KEY constraint failed` になる。標準 SQLite ドライバは `foreign_keys` がデフォルト OFF で通ってしまい差異を見落とすので注意。子テーブル(他から参照されない)の `drop_column` でのみ batch は安全 -- **マイグレーションは `make test-backend` では検証されない**: テストは `conftest.py` の `Base.metadata.create_all` でスキーマを作るため alembic を通らない。migration の upgrade/downgrade は **実 libSQL に対して**確認すること: +- **マイグレーション/libsql ネイティブ経路は `make test-backend`(pytest)では通らないが、CI の `smoke-backend` ジョブで検証する**: pytest は `conftest.py` の `Base.metadata.create_all` でスキーマを作るため alembic を通らず、libsql ネイティブドライバの実行パスも踏まない。これを補うため CI は本番イメージを build → 実 libSQL を起動 → alembic 適用 → `/health`(DB 接続を検証)が 200 を返すことを確認する(`.github/workflows/ci.yml` の `smoke-backend`)。ローカルで個別に migration の upgrade/downgrade を確認したい場合は **実 libSQL に対して**行うこと: - offline SQL の事前確認: `nix develop --command bash -c "cd backend && TURSO_DATABASE_URL='file:///tmp/x.db' .venv/bin/python -m alembic upgrade : --sql"` - - 実適用: docker stack を起動(`make dev-build`)し `docker compose logs api` で適用成功を確認する + - 実適用: docker stack を起動(`make dev-build` / 本番 arch で再現したいときは `make dev-amd64-build`)し `docker compose logs api` で適用成功を確認する - 失敗した `batch_alter_table` が残す `_alembic_tmp_` テーブルは `turso db shell http://localhost:8080 "DROP TABLE IF EXISTS _alembic_tmp_
"` で掃除する ## Turso (libSQL) 接続方式 diff --git a/.claude/rules/backend/test.md b/.claude/rules/backend/test.md index a3585cae..44b48d31 100644 --- a/.claude/rules/backend/test.md +++ b/.claude/rules/backend/test.md @@ -13,7 +13,7 @@ paths: - **既存エンドポイントの契約変更**: ステータスコード / レスポンス body / 副作用が変わる場合、既存テストの assert を見直す(旧契約を固定化したテストが残ると意図が後退する) - **リポジトリ層・サービス層のロジック変更**: 該当ユニットテスト(`tests/test_.py` / `tests/services/`)を更新 - **タスクハンドラの追加・変更**: `tests/test_worker_extended.py` または `tests/test_worker_timeout.py` に状態遷移(`processing` → `completed` / `dead_letter` / `retrying`)のテストを追加 -- **マイグレーション追加**: 実 DB に対する upgrade/downgrade が通ることを `make test-backend` で確認 +- **マイグレーション追加**: upgrade/downgrade は `make test-backend`(pytest)では検証されない(後述「pytest が通らない経路」参照)。実 libSQL に対する適用は CI の `smoke-backend` で確認され、ローカルでは `database.md` の手順(offline SQL / `make dev-build`)で確認する - **暗号化・認証関連**: `tests/test_auth.py` / `tests/test_encryption.py` / `tests/test_oauth_flow.py` を必ず回す ## 実行コマンド @@ -27,6 +27,10 @@ make test-backend # 全テスト nix develop --command bash -c "cd backend && .venv/bin/python -m pytest tests/test_worker_extended.py -q" ``` +### pytest が通らない経路(コンテナ起動スモーク) + +pytest は標準 SQLite + `Base.metadata.create_all` で動くため、**本番イメージのビルド・libsql ネイティブドライバ・alembic マイグレーション・uvicorn 起動の実行パスは検証しない**。この経路は CI の `smoke-backend` ジョブ(本番イメージ build → 実 libSQL 起動 → `/health` 200)でカバーする。backend の Dockerfile・依存・起動経路(`scripts/entrypoint.sh` / `app/db/bootstrap.py`)を変えたら smoke-backend が green であることを確認すること。ローカルで本番 arch(amd64)の起動を再現したいときは `make dev-amd64-build`。 + ## OK 基準(達成条件) 以下をすべて満たして初めて「テスト OK」と判定する: diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ea96cec0..4311849c 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -65,6 +65,16 @@ matchPackageNames: ["ghcr.io/tursodatabase/libsql-server"], pinDigests: true, }, + { + // backend の Python ベースイメージは 3.14 未満に固定する。 + // libsql-experimental(sqlalchemy-libsql 経由)が Python 3.14 未対応で、 + // 3.14 にするとマイグレーション時にネイティブ拡張が segfault する(exit 139)。 + // upstream 対応待ち: tursodatabase/libsql-experimental-python#106。 + // 3.13.x の patch 追従と digest 更新は許可し、3.14 への bump のみ抑止する。 + matchManagers: ["dockerfile"], + matchPackageNames: ["python"], + allowedVersions: "<3.14", + }, { // Nix: flake.lock の locked input(nixpkgs unstable / flake-utils)を追従。 // Mend hosted app では nix manager が動作する。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad3d84df..9380c755 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -285,6 +285,74 @@ jobs: exit 1 fi + # コンテナ起動スモークテスト。 + # test-backend は uv + 標準 SQLite で動くため、本番イメージ(Dockerfile)の + # ビルド・libsql ネイティブドライバ・alembic マイグレーション実行・uvicorn 起動という + # 実行パスを一度も通らない。実際にイメージを build → libsql を立てて起動し、 + # /health(DB 接続を検証する)が 200 を返すことを確認することで、 + # 「ビルドは通るが起動時にネイティブ拡張が segfault する」種の不具合を CI で検知する。 + # (例: Python 3.14 への bump で libsql-experimental が segfault した事象) + smoke-backend: + runs-on: ubuntu-latest + needs: detect-changes + timeout-minutes: 20 + if: needs.detect-changes.outputs.app == 'true' + + steps: + - name: Checkout + # スモークテストは repo への push をしないため認証情報を保持しない(サプライチェーン保護)。 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: 起動用の一時シークレットを生成 (.env) + run: | + # 起動検証専用の使い捨て鍵。永続化せず CI 実行限りで破棄する。 + PRIV="$(openssl genrsa 2048 2>/dev/null)" + PUB="$(printf '%s' "$PRIV" | openssl rsa -pubout 2>/dev/null)" + # docker-compose の .env は複数行値を扱えないため、PEM を \n エスケープした + # 1 行値にする(settings.get_jwt_private_key が \n をアンエスケープする)。 + PRIV_ESC="$(printf '%s' "$PRIV" | awk 'BEGIN{ORS="\\n"}{print}')" + PUB_ESC="$(printf '%s' "$PUB" | awk 'BEGIN{ORS="\\n"}{print}')" + # Fernet 鍵 = 32 バイトの urlsafe base64 + FERNET="$(openssl rand -base64 32 | tr '+/' '-_')" + cat > .env <