From 31151ce0edc549cadf5014b925bc0a9caed84568 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 17 Jul 2026 00:26:38 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(backend):=20=E6=9C=AC=E7=95=AA?= =?UTF-8?q?=E3=82=A4=E3=83=A1=E3=83=BC=E3=82=B8=E3=82=92=20Nix=20build=20?= =?UTF-8?q?=E5=8C=96=E3=81=97=20ADR-0021=20=E3=82=92=20Accepted=20?= =?UTF-8?q?=E3=81=B8=E6=98=87=E6=A0=BC=EF=BC=88Phase=203=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backend/Dockerfile の pip wheel 経路を全廃し、builder stage(nixos/nix)内で flake の backend-runtime(uv2nix build。devshell と同一の Python 環境 + WeasyPrint ライブラリ + cacert)を nix build して final stage(debian:12-slim) へ closure コピーする構成に変更。dev / CI / 本番の 3 経路のビルド機構を flake + uv.lock に一元化する。 - flake.nix: packages.backend-env / backend-runtime を出力に追加。 WeasyPrint ライブラリは lib.getLib で lib output を明示(glib の default output は bin のため)。cacert を同梱し SSL_CERT_FILE で参照(debian slim は ca-certificates 非同梱で libsql/rustls が起動時に失敗する) - aarch64-linux のみ libsql-experimental が sdist(Rust)ビルドとなるため、 Rust ツールチェーン等を override で注入(本番 amd64 は wheel のみで純粋) - build context をリポジトリルートへ変更(flake.nix 参照のため)。 docker-compose.yml / deploy.yml を追従し、ルートに .dockerignore を新設 - deploy-backend の timeout を 20→30 分に拡大(Nix レイヤ構築時間を考慮) - ADR-0021 を Accepted へ昇格(実装追記付き)。索引・ADR-0017(uv 非管理 前提)・ADR-0014(Renovate manager)・rules/backend/python.md を追従 検証: Apple Silicon で docker build → compose up → /health 200 を実機確認 (libsql Rust ビルド・alembic 適用・WeasyPrint import を通過)。make ci green。 Co-Authored-By: Claude Fable 5 --- .claude/rules/backend/python.md | 4 +- .dockerignore | 12 +++ .github/workflows/deploy.yml | 7 +- .github/workflows/test.yml | 11 ++- backend/Dockerfile | 96 ++++++++++--------- docker-compose.yml | 5 +- .../0014-renovate-dependency-automation.md | 7 +- ...utation-testing-and-slack-notifications.md | 7 +- docs/adr/0021-nix-managed-python-env.md | 9 +- docs/adr/README.md | 5 +- flake.nix | 45 +++++++++ 11 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 .dockerignore diff --git a/.claude/rules/backend/python.md b/.claude/rules/backend/python.md index 066c6aca..1b40f11f 100644 --- a/.claude/rules/backend/python.md +++ b/.claude/rules/backend/python.md @@ -33,5 +33,5 @@ paths: ## システムパッケージと Dockerfile -- Pythonライブラリがシステムパッケージ(C ライブラリ等)に依存する場合、ローカル環境(`flake.nix` の `devShells.default.packages`)と本番イメージ(`backend/Dockerfile` の `apt-get install`)の両方に追加すること -- `flake.nix` だけ更新して Dockerfile を忘れると Cloud Run デプロイで import エラーになる +- Python ライブラリがシステムパッケージ(C ライブラリ等)に依存する場合、`flake.nix` の **devshell(`devShells.default.packages`)と本番ランタイム(`backendRuntime` の `paths`)の両方**に追加すること(ADR-0021 Phase 3 で本番イメージも flake の Nix build に一元化済み。Dockerfile 側に apt はもう無い) +- devshell だけ更新して `backendRuntime` を忘れると、ローカルのテストは通るのに Cloud Run / smoke-backend で import エラーになる(逆も然り)。CI の `smoke-backend` が本番イメージの起動でこれを検知する diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6fa100e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# backend/Dockerfile の build context はリポジトリルート(flake.nix 参照のため / ADR-0021 Phase 3)。 +# ホワイトリスト方式: イメージビルドに必要なファイルだけを context に含める。 +* +!flake.nix +!flake.lock +!backend/pyproject.toml +!backend/uv.lock +!backend/alembic.ini +!backend/alembic_migrations +!backend/app +!backend/scripts +backend/app/**/__pycache__ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d401556a..63f9b4db 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -88,7 +88,9 @@ jobs: deploy-backend: runs-on: ubuntu-latest - timeout-minutes: 20 + # イメージビルドが Dockerfile 内 nix build(ADR-0021 Phase 3)となり、 + # docker layer cache が無い環境では構築に数分かかるため余裕を持たせる + timeout-minutes: 30 env: PROJECT_ID: ${{ inputs.project_id }} REGION: ${{ inputs.region }} @@ -121,7 +123,8 @@ jobs: - name: Build and push image run: | IMAGE=${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO }}/${{ env.SERVICE }} - docker build -t ${IMAGE}:${{ github.sha }} -t ${IMAGE}:latest --build-arg APP_VERSION=${{ env.APP_VERSION }} backend/ + # build context はリポジトリルート(Dockerfile が flake.nix を参照するため / ADR-0021 Phase 3) + docker build -f backend/Dockerfile -t ${IMAGE}:${{ github.sha }} -t ${IMAGE}:latest --build-arg APP_VERSION=${{ env.APP_VERSION }} . docker push ${IMAGE}:${{ github.sha }} docker push ${IMAGE}:latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dcdc432f..c97a83c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -268,11 +268,12 @@ jobs: fi # コンテナ起動スモークテスト。 - # test-backend は uv + 標準 SQLite で動くため、本番イメージ(Dockerfile)の - # ビルド・libsql ネイティブドライバ・alembic マイグレーション実行・uvicorn 起動という - # 実行パスを一度も通らない。実際にイメージを build → libsql を立てて起動し、 - # /health(DB 接続を検証する)が 200 を返すことを確認することで、 - # 「ビルドは通るが起動時にネイティブ拡張が segfault する」種の不具合を CI で検知する。 + # test-backend は devshell + 標準 SQLite で動くため、本番イメージ(Dockerfile 内の + # nix build / ADR-0021 Phase 3)の実イメージ組み立て・libsql ネイティブドライバ・ + # alembic マイグレーション実行・uvicorn 起動という実行パスを一度も通らない。 + # 実際にイメージを build → libsql を立てて起動し、/health(DB 接続を検証する)が + # 200 を返すことを確認することで、「ビルドは通るが起動時にネイティブ拡張が + # segfault する」種の不具合を CI で検知する。 # (例: Python 3.14 への bump で libsql-experimental が segfault した事象) smoke-backend: runs-on: ubuntu-latest diff --git a/backend/Dockerfile b/backend/Dockerfile index d7a96229..be7b5335 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,42 +1,45 @@ # ============================================================================ -# Build stage: libsql-experimental など Rust ビルドが必要な wheel を作成する +# 本番イメージ(ADR-0021 Phase 3: Nix build 化) +# +# build context は「リポジトリルート」であること(flake.nix を参照するため): +# docker build -f backend/Dockerfile . +# (docker-compose.yml / deploy.yml / smoke-backend はこの前提で呼び出す) +# +# 依存の SSoT は backend/pyproject.toml + uv.lock(Phase 0)。devshell(flake.nix の +# uv2nix build)と同じ backend-runtime 環境をイメージ内で nix build し、 +# dev / CI / 本番の 3 経路のビルド機構を flake に一元化する。 # ============================================================================ -FROM python:3.13-slim@sha256:eb43ff125d8d58d7449dcba7d336c23bcac412f526d861db493b9994d8010280 AS builder + +# ============================================================================ +# Build stage: uv2nix で Python 依存 + WeasyPrint ライブラリの closure を構成する +# ============================================================================ +FROM nixos/nix:2.33.0@sha256:081b65e50a5c4e6ef4a9094a462da3b83ff76bfec70236eb010047fcee36e11c AS builder WORKDIR /build -# Rust + C リンカ + cmake + tcl が必要なのは libsql-experimental -# (aarch64-linux + cp313 の wheel が PyPI に無いためソースから build される) -# - build-essential: gcc / cc (Rust の linker) -# - cmake: libsql-ffi が同梱する sqlite3mc (SQLite3MultipleCiphers) のビルドに必要 -# - tcl: SQLite 系のビルドスクリプトが要求するケースあり -# - curl: puccinialin が rustup を取得する際に使う -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - cmake \ - tcl \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# 依存の SSoT は pyproject.toml + uv.lock(ADR-0021 Phase 0)。 -# 本番ビルドの pip wheel → pip install 経路は変えず、lock から requirements.txt を -# ビルド時に生成して従来経路へ渡す(pip の Nix build 化は Phase 3 で行う)。 -# uv 自体も供給網方針(P7)に合わせて == で固定する。 -# --no-hashes: final stage はローカルビルドした wheel を install するため、 -# sdist のハッシュ検証を要求すると不一致で失敗する -COPY pyproject.toml uv.lock ./ -RUN pip install --no-cache-dir uv==0.8.17 \ - && uv export --frozen --no-emit-project --no-hashes \ - --format requirements-txt --output-file requirements.txt - -# wheel を /build/wheels に集約しておき、final stage で再利用する -RUN pip wheel --no-cache-dir --wheel-dir=/build/wheels -r requirements.txt +# flake と lock 一式のみコピーする(app コードの変更で Nix build レイヤの +# docker cache が無効化されないよう、コード COPY は final stage 側で行う) +COPY flake.nix flake.lock ./ +COPY backend/pyproject.toml backend/uv.lock ./backend/ + +# --option sandbox false: コンテナ内では Nix サンドボックス(user namespace)を +# 作れないため無効化する。wheel の取得は fixed-output derivation(ハッシュ検証付き) +# なので再現性は lock で担保される。aarch64-linux のみ libsql-experimental が +# sdist(Rust)ビルドとなり crates.io へのネットワーク取得を伴う +# (従来の pip wheel ビルドと同等の妥協。本番 = amd64 は wheel のみで純粋)。 +RUN nix --extra-experimental-features "nix-command flakes" \ + build --option sandbox false .#backend-runtime -o /build/runtime + +# ランタイム closure を /out に集約し、実体へのパスを控える(final stage へのコピー用) +RUN mkdir -p /out/nix/store \ + && cp -a $(nix --extra-experimental-features "nix-command flakes" path-info -r /build/runtime) /out/nix/store/ \ + && readlink /build/runtime > /out/runtime-path # ============================================================================ -# Final stage: 実行に必要なシステムライブラリと wheel のみを含む軽量イメージ +# Final stage: ランタイム closure とアプリコードのみを含む軽量イメージ # ============================================================================ -FROM python:3.13-slim@sha256:eb43ff125d8d58d7449dcba7d336c23bcac412f526d861db493b9994d8010280 +FROM debian:12-slim@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818 ARG APP_VERSION=dev ENV APP_VERSION=$APP_VERSION @@ -48,22 +51,23 @@ ENV TURSO_DATABASE_URL="" WORKDIR /app -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ - libpango-1.0-0 libpangoft2-1.0-0 libpangocairo-1.0-0 \ - libglib2.0-0 libgobject-2.0-0 libffi-dev libcairo2 \ - && rm -rf /var/lib/apt/lists/* - -# builder stage でビルドした wheel と、lock から生成した requirements.txt を流用する -COPY --from=builder /build/wheels /wheels -COPY --from=builder /build/requirements.txt ./ -RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \ - && rm -rf /wheels - -COPY alembic.ini ./ -COPY alembic_migrations ./alembic_migrations -COPY app ./app -COPY scripts ./scripts +# Nix build 済みのランタイム closure(python / uvicorn / alembic / WeasyPrint ライブラリ)。 +# /runtime は store パスへの symlink で、ENV から固定パスで参照できるようにする +COPY --from=builder /out/nix/store /nix/store +COPY --from=builder /out/runtime-path /tmp/runtime-path +RUN ln -s "$(cat /tmp/runtime-path)" /runtime && rm /tmp/runtime-path + +ENV PATH="/runtime/bin:$PATH" +# WeasyPrint が共有ライブラリを発見できるよう動的リンカーのパスを設定(devshell と同じ構成) +ENV LD_LIBRARY_PATH="/runtime/lib" +# libsql ドライバ(rustls)と curl が CA 証明書を発見できるようにする +# (debian:12-slim は ca-certificates 非同梱。バンドルは flake の cacert 由来) +ENV SSL_CERT_FILE="/runtime/etc/ssl/certs/ca-bundle.crt" + +COPY backend/alembic.ini ./ +COPY backend/alembic_migrations ./alembic_migrations +COPY backend/app ./app +COPY backend/scripts ./scripts RUN chmod +x ./scripts/entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index 5a53f30d..041f9025 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,9 @@ services: api: - build: ./backend + # build context はリポジトリルート(Dockerfile が flake.nix を参照するため / ADR-0021 Phase 3) + build: + context: . + dockerfile: backend/Dockerfile container_name: devforge-api restart: unless-stopped depends_on: diff --git a/docs/adr/0014-renovate-dependency-automation.md b/docs/adr/0014-renovate-dependency-automation.md index a22b8298..4938b875 100644 --- a/docs/adr/0014-renovate-dependency-automation.md +++ b/docs/adr/0014-renovate-dependency-automation.md @@ -28,9 +28,10 @@ Dependabot / Renovate のいずれも未導入だったため、依存更新を - 設定の正本は `.github/renovate.json5`(コメントを日本語で残すため JSON5)。 - 対象エコシステム: github-actions / pip(requirements) / npm / terraform / docker・docker-compose / nix の 6 種。 + - **更新(2026-07-16 / ADR-0021)**: backend の依存が pyproject + `uv.lock` 管理へ移行したため、pip(requirements) manager は pep621 manager(`uv lock` で lockfile 追従)へ置き換えた。 - 固定方式は維持する: - github-actions は `pinDigests: true` で digest 固定 + `# v4` コメントを継続。 - - pip は `rangeStrategy: "pin"` で `==` 固定を維持。 + - pip(現 pep621)は `rangeStrategy: "pin"` で `==` 固定を維持。 - docker は `docker:pinDigests` で digest 固定。 - nix は `flake.lock` の locked input を追従。 - `vulnerabilityAlerts` を優先起票し、pip-audit の後追いを Renovate の先回りで補強する。 @@ -67,3 +68,7 @@ Dependabot / Renovate のいずれも未導入だったため、依存更新を - 設定: `.github/renovate.json5` - 関連方針: `.claude/rules/common/duplication.md`(環境変数・バージョン固定の SSoT) - Renovate ドキュメント: https://docs.renovatebot.com/ + +--- + +> **追記(2026-07-16)**: ADR-0021 の Accepted 昇格に伴い、対象 manager の pip(requirements) → pep621 置き換えを反映した(設定の正本は `.github/renovate.json5`)。 diff --git a/docs/adr/0017-mutation-testing-and-slack-notifications.md b/docs/adr/0017-mutation-testing-and-slack-notifications.md index 72b38aff..7504f049 100644 --- a/docs/adr/0017-mutation-testing-and-slack-notifications.md +++ b/docs/adr/0017-mutation-testing-and-slack-notifications.md @@ -20,6 +20,7 @@ Accepted | web | Stryker(`@stryker-mutator/core` + `@stryker-mutator/vitest-runner`) | 既存 vitest(`vite.config.ts` 内蔵設定)をそのまま利用 | - **backend は uv 管理ではない**(pyproject に `[project]` なし・lockfile は `requirements.txt`)ため、mutmut も既存慣例どおり `requirements.txt` にバージョン固定で追加する。 + - **更新(2026-07-16 / ADR-0021)**: backend は PEP 621 + `uv.lock` 管理へ移行済み。mutmut は `backend/pyproject.toml` の `[project.dependencies]` で `==` 固定し、実体は Nix devshell(uv2nix build)が提供する。 - Stryker の TS checker は使わない(tsconfig が project-references + noEmit 構成のため)。ランナーは vitest-runner(peer: `vitest >=2.0.0`、vitest 4 対応確認済み)。 ### 対象スコープ(決定論的ビジネスロジックに限定) @@ -86,7 +87,7 @@ mutmut は `mutants/` に app / tests / pyproject.toml をコピーし pytest - baseline が安定したら `MUTATION_SCORE_THRESHOLD` を実測に合わせて調整し、`thresholds.break`(Stryker)/ CI ゲート化(mutmut)へ移行する。 - 通知チャンネルが増えたら Incoming Webhook 方式から Slack App(Bot トークン + チャンネル指定)へ移行する。 -- backend が uv 管理(PEP 621 + uv.lock)へ移行した場合、mutmut は dev dependency group(`uv add --dev`)へ移す。 +- ~~backend が uv 管理(PEP 621 + uv.lock)へ移行した場合、mutmut は dev dependency group(`uv add --dev`)へ移す。~~ → ADR-0021 Phase 0 で移行済み(dependency group ではなく `[project.dependencies]` に一本化。uv2nix の mkVirtualEnv が default 依存のみを build するため)。 ## 関連リンク @@ -96,3 +97,7 @@ mutmut は `mutants/` に app / tests / pyproject.toml をコピーし pytest - `docs/development.md`「ミューテーションテスト」節(ローカル実行・Secrets 登録手順) - ADR-0014(Renovate。Action の SHA ピン運用) - mutmut: https://mutmut.readthedocs.io/ / Stryker: https://stryker-mutator.io/docs/ + +--- + +> **追記(2026-07-16)**: ADR-0021(backend Python 環境の Nix フルマネージド化)の Accepted 昇格に伴い、本文中の「uv 非管理・requirements.txt が lockfile」という前提記述を更新した。 diff --git a/docs/adr/0021-nix-managed-python-env.md b/docs/adr/0021-nix-managed-python-env.md index 889127f0..c65e6212 100644 --- a/docs/adr/0021-nix-managed-python-env.md +++ b/docs/adr/0021-nix-managed-python-env.md @@ -2,7 +2,7 @@ ## ステータス -Proposed +Accepted ## 関連 ADR @@ -81,3 +81,10 @@ Proposed - 本番パリティ検証: `.github/workflows/test.yml` の `smoke-backend` ジョブ - uv2nix: https://github.com/pyproject-nix/uv2nix - 関連 ADR: [ADR-0017](./0017-mutation-testing-and-slack-notifications.md) / [ADR-0014](./0014-renovate-dependency-automation.md) / [ADR-0007](./0007-openapi-typescript-codegen.md) + +--- + +> **実装追記(2026-07-16)**: Phase 0(PR #499)/ Phase 1・2(PR #500)/ Phase 3 を実装し Accepted へ昇格。 +> Phase 3 は dockerTools(pure Nix イメージ)ではなく **Nix-in-Dockerfile 方式**(builder stage の `nixos/nix` イメージ内で `nix build .#backend-runtime` → final stage へ closure コピー)を採用した。macOS ローカルでは linux イメージを `nix build` できず、`make dev-build`(docker compose)の開発フローを維持するため。build context はリポジトリルートへ変更(flake.nix 参照のため)。 +> 既知の妥協: aarch64-linux(Apple Silicon のローカル Docker)のみ `libsql-experimental` が sdist(Rust)ビルドとなり、`--option sandbox false` の下で crates.io 取得にネットワークを使う(従来 pip 経路と同等)。本番 = amd64 は全依存 wheel のため lock のハッシュ検証で純粋性が保たれる。smoke-backend は `docker compose up --build` の構図を維持したまま新経路(Nix build イメージ)を検証している。 + diff --git a/docs/adr/README.md b/docs/adr/README.md index 3528aa40..cf0dc037 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -25,6 +25,7 @@ | [ADR-0018](./0018-github-resume-draft-generation.md) | GitHub 連携データからの経歴書ドラフト生成 | LLM / Agent | 構造はルールベース・自然文だけ LLM のハイブリッド。何も永続化せず PDF プレビューのみ返す | | [ADR-0019](./0019-tdd-for-logic-layer.md) | 決定論的ロジック層への TDD(テスト駆動開発)導入 | 開発プロセス / 品質 | mutation 対象と同一スコープに red→green→refactor を必須化。テスト随伴を lint-tdd で機械検証 | | [ADR-0020](./0020-async-resume-draft-generation.md) | 経歴書ドラフト生成の非同期化と最小永続化 | LLM / Agent | ドラフト生成を独立の非同期タスク化。payload だけを連携ドメインに最小永続化し DL 時に再レンダリング | +| [ADR-0021](./0021-nix-managed-python-env.md) | backend Python 環境の Nix フルマネージド化(.venv 廃止) | 開発プロセス / 品質 | 依存 SSoT を pyproject + uv.lock に一本化し、devshell / CI / 本番イメージの 3 経路を uv2nix(flake)で統一 | ## 全 ADR 一覧 @@ -53,7 +54,7 @@ | [ADR-0018](./0018-github-resume-draft-generation.md) | GitHub 連携データからの経歴書ドラフト生成 | Accepted | LLM / Agent | 関連: 0010(不変条件を継承し適用範囲を拡張)、0012(課金配線)、0013・0015(プロバイダ)、0016(データ供給源)、0020(非同期化・最小永続化で更新) | P4・P5 | | [ADR-0019](./0019-tdd-for-logic-layer.md) | 決定論的ロジック層への TDD(テスト駆動開発)導入 | Accepted | 開発プロセス / 品質 | 関連: 0017(対象スコープの正本を共有)、0007(drift の機械検知パターン) | P3・P5 | | [ADR-0020](./0020-async-resume-draft-generation.md) | 経歴書ドラフト生成の非同期化と最小永続化 | Accepted | LLM / Agent | 関連: 0018(同期実装を更新)、0010(不変条件を継承)、0012(課金をタスク側へ移設)、0016(データ供給源) | P1・P4・P5 | -| [ADR-0021](./0021-nix-managed-python-env.md) | backend Python 環境の Nix フルマネージド化(.venv 廃止) | Proposed | 開発プロセス / 品質 | 関連: 0017(uv 非管理前提を更新)、0014(依存固定・Renovate manager)、0007(Nix devshell 規約) | P7・P3・P6 | +| [ADR-0021](./0021-nix-managed-python-env.md) | backend Python 環境の Nix フルマネージド化(.venv 廃止) | Accepted | 開発プロセス / 品質 | 関連: 0017(uv 非管理前提を更新)、0014(依存固定・Renovate manager)、0007(Nix devshell 規約) | P7・P3・P6 | ## テーマ別の決定系統 @@ -110,7 +111,7 @@ graph LR D0017 -.->|"uv 非管理前提を更新"| D0021 ``` -「正本を 1 つに定め、複製との乖離は機械で検知する」(0007)、「依存は固定し、追従は自動化する」(0014)、「テストの検出力自体を計測する」(0017)、「テストを先に書くプロセスを機械ゲートで支える」(0019。0017 の事後計測と対になる事前プロセス)という、**プロダクト機能ではなく開発体験そのものへの投資**の系統。`docs/metrics/ai-friendliness.md` はこの系統の効果を月次で観測するダッシュボード。0021(Proposed)は 0014 の依存固定運用を継承しつつ、backend の Python 環境を Nix でフルマネージド化して `.venv` を廃止する方向を提案している(0017 の「uv 非管理・requirements.txt が lockfile」前提を将来更新する)。 +「正本を 1 つに定め、複製との乖離は機械で検知する」(0007)、「依存は固定し、追従は自動化する」(0014)、「テストの検出力自体を計測する」(0017)、「テストを先に書くプロセスを機械ゲートで支える」(0019。0017 の事後計測と対になる事前プロセス)という、**プロダクト機能ではなく開発体験そのものへの投資**の系統。`docs/metrics/ai-friendliness.md` はこの系統の効果を月次で観測するダッシュボード。0021 は 0014 の依存固定運用を継承しつつ、backend の Python 環境を Nix でフルマネージド化して `.venv` を廃止した(devshell / CI / 本番 Dockerfile の 3 経路を flake + uv.lock に一元化。0017 の「uv 非管理・requirements.txt が lockfile」前提も更新済み)。 ## 運用 diff --git a/flake.nix b/flake.nix index 412114fc..ccf3f31b 100644 --- a/flake.nix +++ b/flake.nix @@ -52,17 +52,62 @@ # wheel 優先: uv.lock に記録された wheel をそのまま使い、sdist ビルドの # ツールチェーン差異(Rust / cmake 等)を持ち込まない pyprojectOverlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; }; + + # sdist ビルドが必要なパッケージへの個別対応(ADR-0021 Phase 3)。 + # libsql-experimental は aarch64-linux の wheel が PyPI に無く、maturin(Rust)での + # sdist ビルドになるため、Rust ツールチェーン等を build 入力へ注入する。 + # 他プラットフォームは wheel が選択されるため注入しない(devshell の閉包を汚さない)。 + # crates.io の取得ネットワークは Dockerfile 側の `nix build --option sandbox false` が + # 許可する(従来の pip sdist ビルドと同等の妥協。amd64 本番経路は wheel なので純粋)。 + pyprojectOverrides = final: prev: + lib.optionalAttrs (system == "aarch64-linux") { + libsql-experimental = prev.libsql-experimental.overrideAttrs (old: { + nativeBuildInputs = + (old.nativeBuildInputs or [ ]) + ++ final.resolveBuildSystem { maturin = [ ]; } + ++ (with pkgs; [ rustc cargo cmake tcl pkg-config cacert ]); + # nix build 中は HOME が書き込み不可のため、cargo のレジストリキャッシュを + # ビルド用一時領域へ向ける + preBuild = '' + export CARGO_HOME="$TMPDIR/cargo" + ''; + }); + }; + pythonSet = (pkgs.callPackage pyproject-nix.build.packages { python = pkgs.python313; # Python 3.13(Dockerfile / requires-python 準拠) }).overrideScope (lib.composeManyExtensions [ pyproject-build-systems.overlays.default pyprojectOverlay + pyprojectOverrides ]); # 全依存入りの virtualenv(devshell の python / pytest / ruff / alembic の実体) backendEnv = pythonSet.mkVirtualEnv "devforge-backend-env" workspace.deps.default; + + # --- 本番イメージ用ランタイム環境(ADR-0021 Phase 3) --- + # backend/Dockerfile の builder stage が `nix build .#backend-runtime` で参照する。 + # devshell と同じ backendEnv + WeasyPrint ネイティブライブラリを 1 つの環境に束ね、 + # イメージ側は PATH=/runtime/bin と LD_LIBRARY_PATH=/runtime/lib を張るだけにする。 + # curl は旧イメージ(apt install curl)とのパリティ維持(デバッグ・疎通確認用)。 + # cacert は libsql ドライバ(rustls)が要求する CA 証明書ストア。イメージ側は + # SSL_CERT_FILE=/runtime/etc/ssl/certs/ca-bundle.crt で参照する + # (debian:12-slim は ca-certificates 非同梱のため必須)。 + # weasyPrintLibs は lib.getLib で lib output を明示する(glib 等は default output が + # bin のため、そのまま渡すと libgobject-2.0.so 等が /runtime/lib に入らない。 + # devshell の makeLibraryPath と同じ解決)。 + backendRuntime = pkgs.buildEnv { + name = "devforge-backend-runtime"; + paths = [ backendEnv pkgs.curl pkgs.cacert ] ++ (map lib.getLib weasyPrintLibs); + }; in { + # 本番イメージ(backend/Dockerfile)から nix build で参照する出力(ADR-0021 Phase 3) + packages = { + backend-env = backendEnv; + backend-runtime = backendRuntime; + }; + devShells.default = pkgs.mkShell { packages = [ # --- Python (Backend) --- From fcddcb2b553110088c022c4c86753162116423e0 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 17 Jul 2026 00:35:49 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20.dockerignore=20=E3=82=92=E8=A6=AA?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E5=86=8D?= =?UTF-8?q?=20include=20=E3=81=AE=E6=A8=99=E6=BA=96=E5=BD=A2=E3=81=AB?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=EF=BC=88=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E6=8C=87=E6=91=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BuildKit では旧形式でも動作するが、除外ディレクトリ配下の re-include を解釈しない ビルダー実装があるため、!backend/ → backend/* → 個別 re-include の標準形にする。 Co-Authored-By: Claude Fable 5 --- .dockerignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 6fa100e3..bc25f9f6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,17 @@ # backend/Dockerfile の build context はリポジトリルート(flake.nix 参照のため / ADR-0021 Phase 3)。 # ホワイトリスト方式: イメージビルドに必要なファイルだけを context に含める。 +# `!backend/` で親ディレクトリの走査を許可してから `backend/*` で中身を再除外し、 +# 必要なサブパスだけを個別に re-include する(除外ディレクトリ配下の re-include は +# ビルダー実装によって解釈されない場合があるため、この標準形にしておく)。 * !flake.nix !flake.lock +!backend/ +backend/* !backend/pyproject.toml !backend/uv.lock !backend/alembic.ini !backend/alembic_migrations !backend/app !backend/scripts -backend/app/**/__pycache__ +**/__pycache__ From 8df219f95508c229ac322cd9db6d5912b0a69088 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 17 Jul 2026 00:44:35 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs(adr):=20=E6=97=A7=E5=89=8D=E6=8F=90?= =?UTF-8?q?=E3=81=AE=E7=8F=BE=E5=9C=A8=E5=BD=A2=E8=A8=98=E8=BF=B0=E3=81=AB?= =?UTF-8?q?=E3=80=8C=E5=B0=8E=E5=85=A5=E5=BD=93=E6=99=82=E3=80=8D=E3=83=A9?= =?UTF-8?q?=E3=83=99=E3=83=AB=E3=82=92=E4=BB=98=E4=B8=8E=EF=BC=88=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0014 / 0017 の requirements.txt 前提の記述が更新追記と矛盾して読めるため、 履歴であることを明示する(原文は書き換えず、履歴保持の ADR 規約に沿う)。 Co-Authored-By: Claude Fable 5 --- docs/adr/0014-renovate-dependency-automation.md | 6 +++--- docs/adr/0017-mutation-testing-and-slack-notifications.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/adr/0014-renovate-dependency-automation.md b/docs/adr/0014-renovate-dependency-automation.md index 4938b875..96432ca6 100644 --- a/docs/adr/0014-renovate-dependency-automation.md +++ b/docs/adr/0014-renovate-dependency-automation.md @@ -8,7 +8,7 @@ Accepted DevForge は依存をすべて「固定」運用している。GitHub Actions は SHA(digest) ピン留め (`uses: actions/checkout@34e1148... # v4`)、Python は `backend/requirements.txt` で -`==` 完全固定、infra プロバイダは `~>` 制約 + `.terraform.lock.hcl`、Nix は `flake.lock` +`==` 完全固定(導入当時。現在は `pyproject.toml` + `uv.lock` / ADR-0021)、infra プロバイダは `~>` 制約 + `.terraform.lock.hcl`、Nix は `flake.lock` で固定している。これはサプライチェーン攻撃(レンジ内 yank / 侵害バージョンの混入)に 対して安全な一方で、**固定したバージョンを追従する仕組みが無い**という弱点があった。 @@ -26,8 +26,8 @@ Dependabot / Renovate のいずれも未導入だったため、依存更新を 「提案(PR 起票)」だけを自動化する。 - 設定の正本は `.github/renovate.json5`(コメントを日本語で残すため JSON5)。 -- 対象エコシステム: github-actions / pip(requirements) / npm / terraform / - docker・docker-compose / nix の 6 種。 +- 対象エコシステム: github-actions / pip(requirements)(導入当時。現在は pep621) / npm / + terraform / docker・docker-compose / nix の 6 種。 - **更新(2026-07-16 / ADR-0021)**: backend の依存が pyproject + `uv.lock` 管理へ移行したため、pip(requirements) manager は pep621 manager(`uv lock` で lockfile 追従)へ置き換えた。 - 固定方式は維持する: - github-actions は `pinDigests: true` で digest 固定 + `# v4` コメントを継続。 diff --git a/docs/adr/0017-mutation-testing-and-slack-notifications.md b/docs/adr/0017-mutation-testing-and-slack-notifications.md index 7504f049..b21dd97b 100644 --- a/docs/adr/0017-mutation-testing-and-slack-notifications.md +++ b/docs/adr/0017-mutation-testing-and-slack-notifications.md @@ -16,10 +16,10 @@ Accepted | 領域 | ツール | 実行系 | |---|---|---| -| backend | mutmut 3.x(`requirements.txt` で固定) | pytest を `mutants/` 内で in-process 実行 | +| backend | mutmut 3.x(導入当時は `requirements.txt` で固定。現在は `pyproject.toml` の `[project.dependencies]` / ADR-0021) | pytest を `mutants/` 内で in-process 実行 | | web | Stryker(`@stryker-mutator/core` + `@stryker-mutator/vitest-runner`) | 既存 vitest(`vite.config.ts` 内蔵設定)をそのまま利用 | -- **backend は uv 管理ではない**(pyproject に `[project]` なし・lockfile は `requirements.txt`)ため、mutmut も既存慣例どおり `requirements.txt` にバージョン固定で追加する。 +- **backend は uv 管理ではない**(導入当時。pyproject に `[project]` なし・lockfile は `requirements.txt`)ため、mutmut も既存慣例どおり `requirements.txt` にバージョン固定で追加する。 - **更新(2026-07-16 / ADR-0021)**: backend は PEP 621 + `uv.lock` 管理へ移行済み。mutmut は `backend/pyproject.toml` の `[project.dependencies]` で `==` 固定し、実体は Nix devshell(uv2nix build)が提供する。 - Stryker の TS checker は使わない(tsconfig が project-references + noEmit 構成のため)。ランナーは vitest-runner(peer: `vitest >=2.0.0`、vitest 4 対応確認済み)。