From 9c8ef88c0fd271b8aa9cf8ff0702cbe484f10d68 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Mon, 22 Jun 2026 21:29:19 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(backend):=20Python=20=E3=82=92=203.13?= =?UTF-8?q?=20=E3=81=AB=E5=9B=BA=E5=AE=9A=E3=81=97=20libsql=20segfault=20?= =?UTF-8?q?=E3=82=92=E5=9B=9E=E9=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renovate が base image を python:3.14 に上げた結果、libsql-experimental (sqlalchemy-libsql 経由)が 3.14 未対応でマイグレーション時に segfault (exit 139)し api が起動ループしていた。 - backend/Dockerfile: builder/runtime を python:3.13-slim に戻す - .github/renovate.json5: dockerfile の python を <3.14 に制限し再発防止 (upstream 対応待ち: tursodatabase/libsql-experimental-python#106) Co-Authored-By: Claude Opus 4.8 --- .github/renovate.json5 | 10 ++++++++++ backend/Dockerfile | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) 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/backend/Dockerfile b/backend/Dockerfile index 6cc9b9cd..a7ff3d4b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,7 @@ # ============================================================================ # Build stage: libsql-experimental など Rust ビルドが必要な wheel を作成する # ============================================================================ -FROM python:3.14-slim@sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061 AS builder +FROM python:3.13-slim@sha256:c33f0bc4364a6881bed1ec0cc2665e6c53c87a43e774aaeab88e6f17af105e4f AS builder WORKDIR /build @@ -27,7 +27,7 @@ RUN pip wheel --no-cache-dir --wheel-dir=/build/wheels -r requirements.txt # ============================================================================ # Final stage: 実行に必要なシステムライブラリと wheel のみを含む軽量イメージ # ============================================================================ -FROM python:3.14-slim@sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061 +FROM python:3.13-slim@sha256:c33f0bc4364a6881bed1ec0cc2665e6c53c87a43e774aaeab88e6f17af105e4f ARG APP_VERSION=dev ENV APP_VERSION=$APP_VERSION From eb05f29ff6340de113621b38e230e6d470c65262 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Mon, 22 Jun 2026 21:56:03 +0900 Subject: [PATCH 2/4] =?UTF-8?q?arm=E3=81=A8amd=E3=81=AE=E5=B7=AE=E5=88=86?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 10 +++++++++- docker-compose.amd64.yml | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docker-compose.amd64.yml diff --git a/Makefile b/Makefile index 1127be98..dc6a41ec 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: help \ setup install-hooks install-backend install-web generate-keys \ - dev dev-build dev-down dev-web preview-web dev-proxy dev-proxy-only stripe-webhook \ + 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 \ format format-check \ @@ -27,6 +27,8 @@ help: @echo " dev docker-compose で API を起動" @echo " dev-build 再ビルドして起動" @echo " dev-down docker-compose を停止" + @echo " dev-amd64 本番と同じ amd64 で API を起動 (native 不具合の再現用 / Mac では低速)" + @echo " dev-amd64-build amd64 で再ビルドして起動" @echo " dev-web Frontend 開発サーバーを起動 (Vite / localhost:5173)" @echo " preview-web ビルド済みを wrangler でローカル提供 (HMR なし / localhost:8788)" @echo " stripe-webhook Stripe Webhook を localhost:8000 へ転送 (要 stripe login / whsec を .env へ)" @@ -103,6 +105,12 @@ dev-build: dev-down: docker compose down +dev-amd64: + docker compose -f docker-compose.yml -f docker-compose.amd64.yml up + +dev-amd64-build: + docker compose -f docker-compose.yml -f docker-compose.amd64.yml up --build + stripe-webhook: nix develop --command stripe listen --forward-to localhost:8000/api/billing/webhook diff --git a/docker-compose.amd64.yml b/docker-compose.amd64.yml new file mode 100644 index 00000000..3984d1f2 --- /dev/null +++ b/docker-compose.amd64.yml @@ -0,0 +1,15 @@ +# 本番(Cloud Run / CI = linux/amd64)と同じ CPU アーキテクチャで api を動かすための +# オーバーレイ。ベースの docker-compose.yml に重ねて使う: +# +# docker compose -f docker-compose.yml -f docker-compose.amd64.yml up --build +# (make dev-amd64 / make dev-amd64-build が上記をラップする) +# +# 目的: ネイティブ拡張(libsql-experimental / WeasyPrint 等)が絡む不具合を、 +# 本番と同じ amd64 上で再現・検証するため。 +# 注意: Apple Silicon では Rosetta/qemu エミュレーションで動くため遅い。 +# 日常開発はベースのまま(ホスト native = arm64)の方が速い。 +# +# libsql / redis は HTTP / TCP 接続でありアーキ非依存のため、api だけを amd64 に固定する。 +services: + api: + platform: linux/amd64 From 32f27ae6d9891ee60b5f6dcb126aa86842e774f5 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Mon, 22 Jun 2026 22:09:57 +0900 Subject: [PATCH 3/4] =?UTF-8?q?test(ci):=20=E3=82=B3=E3=83=B3=E3=83=86?= =?UTF-8?q?=E3=83=8A=E8=B5=B7=E5=8B=95=E3=82=B9=E3=83=A2=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0=E3=81=A8=E3=83=8D?= =?UTF-8?q?=E3=82=A4=E3=83=86=E3=82=A3=E3=83=96=E6=A4=9C=E8=A8=BC=E6=96=B9?= =?UTF-8?q?=E9=87=9D=E3=81=AE=E6=98=8E=E6=96=87=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-backend(pytest) は標準 SQLite で動き本番イメージの起動経路 (libsql ネイティブ / alembic / uvicorn 起動)を検証しないため、Python 3.14 bump 時の起動 segfault を CI ですり抜けていた。実イメージ build → 実 libSQL 起動 → /health で起動成功を検証する smoke-backend ジョブを追加する。 - .github/workflows/ci.yml: smoke-backend ジョブ追加(amd64 ランナー) - CLAUDE.md / rules/backend/{test,database}.md: ネイティブ・コンテナ起動は smoke-backend で検証する方針を明文化(旧「検証されない」記述を更新) Co-Authored-By: Claude Opus 4.8 --- .claude/CLAUDE.md | 1 + .claude/rules/backend/database.md | 4 +- .claude/rules/backend/test.md | 4 ++ .github/workflows/ci.yml | 65 +++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) 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..5352be14 100644 --- a/.claude/rules/backend/test.md +++ b/.claude/rules/backend/test.md @@ -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/workflows/ci.yml b/.github/workflows/ci.yml index ad3d84df..5e76434a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -285,6 +285,71 @@ 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 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - 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 < Date: Mon, 22 Jun 2026 22:16:49 +0900 Subject: [PATCH 4/4] =?UTF-8?q?review(ci):=20CodeRabbit=20=E6=8C=87?= =?UTF-8?q?=E6=91=98=E5=AF=BE=E5=BF=9C=EF=BC=88smoke=20checkout=20?= =?UTF-8?q?=E3=81=AEcredential=20=E7=84=A1=E5=8A=B9=E5=8C=96=E3=83=BBdocs?= =?UTF-8?q?=E6=95=B4=E5=90=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci.yml: smoke-backend の checkout に persist-credentials: false を付与 (push しないジョブのため認証情報を保持しない / codegen-drift と同方針) - rules/backend/test.md: マイグレーション追加トリガーの「make test-backend で確認」を smoke-backend / database.md 手順に整合(pytest は migration を通らない旨と矛盾しないよう修正) Co-Authored-By: Claude Opus 4.8 --- .claude/rules/backend/test.md | 2 +- .github/workflows/ci.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.claude/rules/backend/test.md b/.claude/rules/backend/test.md index 5352be14..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` を必ず回す ## 実行コマンド diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e76434a..9380c755 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -300,7 +300,10 @@ jobs: steps: - name: Checkout + # スモークテストは repo への push をしないため認証情報を保持しない(サプライチェーン保護)。 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false - name: 起動用の一時シークレットを生成 (.env) run: |