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
4 changes: 2 additions & 2 deletions .claude/rules/backend/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` が本番イメージの起動でこれを検知する
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +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
**/__pycache__
7 changes: 5 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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

Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
96 changes: 50 additions & 46 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
5 changes: 4 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
13 changes: 9 additions & 4 deletions docs/adr/0014-renovate-dependency-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 / 侵害バージョンの混入)に
対して安全な一方で、**固定したバージョンを追従する仕組みが無い**という弱点があった。

Expand All @@ -26,11 +26,12 @@ 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` コメントを継続。
- pip は `rangeStrategy: "pin"` で `==` 固定を維持。
- pip(現 pep621)は `rangeStrategy: "pin"` で `==` 固定を維持。
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- docker は `docker:pinDigests` で digest 固定。
- nix は `flake.lock` の locked input を追従。
- `vulnerabilityAlerts` を優先起票し、pip-audit の後追いを Renovate の先回りで補強する。
Expand Down Expand Up @@ -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`)。
11 changes: 8 additions & 3 deletions docs/adr/0017-mutation-testing-and-slack-notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ 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 対応確認済み)。

### 対象スコープ(決定論的ビジネスロジックに限定)
Expand Down Expand Up @@ -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 するため)

## 関連リンク

Expand All @@ -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」という前提記述を更新した。
9 changes: 8 additions & 1 deletion docs/adr/0021-nix-managed-python-env.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## ステータス

Proposed
Accepted

## 関連 ADR

Expand Down Expand Up @@ -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 イメージ)を検証している。

Loading
Loading