|
| 1 | +# Design: PyPI Publish Readiness |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +insight-blueprint を PyPI に公開可能にするためのパッケージング整備と CI/CD パイプライン構築。変更対象は Python ソースコード(`__init__.py` のみ)、プロジェクトメタデータ(`pyproject.toml`)、GitHub Actions ワークフロー、およびプロジェクトルートのドキュメント。アプリケーションのビジネスロジックには一切変更を加えない。 |
| 6 | + |
| 7 | +## Steering Document Alignment |
| 8 | + |
| 9 | +### Technical Standards (tech.md) |
| 10 | + |
| 11 | +- **Build System**: hatchling (Python wheel) + Vite (frontend bundle) — 既存構成をそのまま活用 |
| 12 | +- **Package Management**: uv — `uv build` でビルド、`uv sync` で開発インストール |
| 13 | +- **Distribution**: tech.md に「PyPI パッケージ(`pip install insight-blueprint`)」と明記済み。本設計はその実現 |
| 14 | +- **CI**: GitHub Actions — 既存 `ci.yml` を拡張し、新規 `publish.yml` を追加 |
| 15 | + |
| 16 | +### Project Structure (structure.md) |
| 17 | + |
| 18 | +- 変更ファイルは全てプロジェクトルートまたは `.github/` 配下。`src/insight_blueprint/` 内の変更は `__init__.py` と `py.typed` の2ファイルのみ |
| 19 | +- structure.md の依存方向ルール(`server.py/web.py → core/ → storage/ → models/`)に影響なし |
| 20 | + |
| 21 | +## Code Reuse Analysis |
| 22 | + |
| 23 | +### Existing Components to Leverage |
| 24 | + |
| 25 | +- **hatch artifacts config**: `pyproject.toml` の `[tool.hatch.build.targets.wheel] artifacts` が既に `static/**` を含めるよう設定済み。変更不要 |
| 26 | +- **Vite build config**: `frontend/vite.config.ts` の `outDir: "../src/insight_blueprint/static"` が既にビルド出力を正しい場所に配置。変更不要 |
| 27 | +- **CI workflow**: `.github/workflows/ci.yml` の `python` / `frontend` ジョブ構成を `build-check` ジョブの依存元として再利用 |
| 28 | + |
| 29 | +### Integration Points |
| 30 | + |
| 31 | +- **pyproject.toml**: version フィールドが唯一のソース。`__init__.py` がこれを参照 |
| 32 | +- **.gitignore**: `src/insight_blueprint/static/` が既に除外済み。hatch artifacts がこれを wheel に含める仕組みは変更不要 |
| 33 | + |
| 34 | +## Architecture |
| 35 | + |
| 36 | +本設計は3つの独立したコンポーネントで構成される。相互依存はない。 |
| 37 | + |
| 38 | +```mermaid |
| 39 | +graph TD |
| 40 | + subgraph "L1: Package Metadata" |
| 41 | + A1[py.typed marker] |
| 42 | + A2[version single source] |
| 43 | + A3[CHANGELOG.md] |
| 44 | + A4[classifiers update] |
| 45 | + end |
| 46 | +
|
| 47 | + subgraph "L2: Publish Workflow" |
| 48 | + B1[publish.yml] --> B2[build job] |
| 49 | + B2 --> B3[tag-version check] |
| 50 | + B2 --> B4[frontend build] |
| 51 | + B4 --> B5[uv build] |
| 52 | + B5 --> B6[wheel verification] |
| 53 | + B6 --> B7[twine check] |
| 54 | + B7 --> B8[upload artifact] |
| 55 | + B8 --> B9[publish job] |
| 56 | + B9 --> B10[PyPI upload via OIDC] |
| 57 | + end |
| 58 | +
|
| 59 | + subgraph "L3: CI Wheel Check" |
| 60 | + C1[ci.yml build-check job] |
| 61 | + C2[python job] --> C1 |
| 62 | + C3[frontend job] --> C1 |
| 63 | + C1 --> C4[build wheel + verify] |
| 64 | + end |
| 65 | +``` |
| 66 | + |
| 67 | +## Components and Interfaces |
| 68 | + |
| 69 | +### Component 1: Package Metadata (L1) |
| 70 | + |
| 71 | +- **Purpose:** PyPI 公開に必要なメタデータの整備。py.typed マーカー、version 一元化、CHANGELOG、classifiers |
| 72 | +- **Files:** |
| 73 | + - `src/insight_blueprint/py.typed` — 新規作成(空ファイル) |
| 74 | + - `src/insight_blueprint/__init__.py` — version 取得方法の変更 |
| 75 | + - `pyproject.toml` — classifiers 追加 |
| 76 | + - `CHANGELOG.md` — 新規作成 |
| 77 | +- **Dependencies:** なし |
| 78 | +- **Reuses:** 既存の pyproject.toml 構造 |
| 79 | + |
| 80 | +#### `__init__.py` version 設計 |
| 81 | + |
| 82 | +```python |
| 83 | +"""insight-blueprint: MCP server for analysis design management.""" |
| 84 | + |
| 85 | +from importlib.metadata import PackageNotFoundError, version |
| 86 | + |
| 87 | +try: |
| 88 | + __version__ = version("insight-blueprint") |
| 89 | +except PackageNotFoundError: |
| 90 | + __version__ = "0.0.0+unknown" |
| 91 | +``` |
| 92 | + |
| 93 | +**設計判断(Codex レビュー反映):** `importlib.metadata.version()` は editable install (`uv sync`) でも動作するが、ソースから直接実行された場合(未インストール状態)に `PackageNotFoundError` が発生する。fallback を設けることで、開発中の `python -m insight_blueprint` でもクラッシュしない。fallback 値 `"0.0.0+unknown"` は PEP 440 互換のローカルバージョン識別子。 |
| 94 | + |
| 95 | +#### classifiers 追加 |
| 96 | + |
| 97 | +```toml |
| 98 | +classifiers = [ |
| 99 | + # ... existing ... |
| 100 | + "Intended Audience :: Science/Research", |
| 101 | + "Topic :: Scientific/Engineering :: Information Analysis", |
| 102 | +] |
| 103 | +``` |
| 104 | + |
| 105 | +### Component 2: Publish Workflow (L2) |
| 106 | + |
| 107 | +- **Purpose:** tag push で PyPI への自動 publish を実行する GitHub Actions ワークフロー |
| 108 | +- **Files:** `.github/workflows/publish.yml` — 新規作成 |
| 109 | +- **Dependencies:** GitHub Environment `pypi`、PyPI Trusted Publisher 設定(手動、ワークフロー外) |
| 110 | +- **Reuses:** 既存 CI の Node.js / uv セットアップパターン |
| 111 | + |
| 112 | +#### ワークフロー構造 |
| 113 | + |
| 114 | +```yaml |
| 115 | +name: Publish to PyPI |
| 116 | +on: |
| 117 | + push: |
| 118 | + tags: ["v*"] |
| 119 | + |
| 120 | +jobs: |
| 121 | + build: |
| 122 | + runs-on: ubuntu-latest |
| 123 | + steps: |
| 124 | + # 1. Checkout |
| 125 | + # 2. Tag-version consistency check |
| 126 | + # 3. Frontend build (Node.js + npm ci + npm run build) |
| 127 | + # 4. Python build (uv build) |
| 128 | + # 5. Wheel verification (static assets existence) |
| 129 | + # 6. twine check (metadata validation) |
| 130 | + # 7. Upload artifact |
| 131 | + |
| 132 | + publish: |
| 133 | + needs: build |
| 134 | + runs-on: ubuntu-latest |
| 135 | + environment: pypi |
| 136 | + permissions: |
| 137 | + id-token: write |
| 138 | + steps: |
| 139 | + # 1. Download artifact |
| 140 | + # 2. PyPI upload (Trusted Publisher OIDC) |
| 141 | +``` |
| 142 | + |
| 143 | +#### Tag-Version Consistency Check(Codex レビュー反映) |
| 144 | + |
| 145 | +```bash |
| 146 | +TAG=${GITHUB_REF#refs/tags/v} |
| 147 | +PKG=$(python -c " |
| 148 | +import tomllib |
| 149 | +with open('pyproject.toml', 'rb') as f: |
| 150 | + print(tomllib.load(f)['project']['version']) |
| 151 | +") |
| 152 | +if [ "$TAG" != "$PKG" ]; then |
| 153 | + echo "ERROR: Tag v$TAG does not match pyproject.toml version $PKG" |
| 154 | + exit 1 |
| 155 | +fi |
| 156 | +``` |
| 157 | + |
| 158 | +**設計判断:** tag `v0.2.0` と pyproject.toml `version = "0.2.0"` の一致を強制する。不一致は即座に fail。これにより、version 更新忘れ / tag typo を publish 前に検出する。 |
| 159 | + |
| 160 | +#### Wheel Verification |
| 161 | + |
| 162 | +```python |
| 163 | +import zipfile, sys |
| 164 | +from pathlib import Path |
| 165 | + |
| 166 | +whl = next(Path("dist").glob("*.whl")) |
| 167 | +with zipfile.ZipFile(whl) as zf: |
| 168 | + names = zf.namelist() |
| 169 | + |
| 170 | + # Check required static assets |
| 171 | + has_html = any("static/index.html" in n for n in names) |
| 172 | + has_js = any("static/assets/" in n and n.endswith(".js") for n in names) |
| 173 | + |
| 174 | + if not (has_html and has_js): |
| 175 | + print("ERROR: Frontend assets missing from wheel") |
| 176 | + for n in sorted(names): |
| 177 | + if "static" in n: |
| 178 | + print(f" found: {n}") |
| 179 | + sys.exit(1) |
| 180 | + |
| 181 | + static_count = sum(1 for n in names if "static/" in n) |
| 182 | + print(f"OK: {static_count} static files verified in wheel") |
| 183 | +``` |
| 184 | + |
| 185 | +**設計判断(Codex レビュー反映):** 存在確認に加えて `twine check dist/*` を別ステップで実行し、メタデータの妥当性(long_description のレンダリング等)も検証する。サイズ閾値は Vite の圧縮・コード分割で変動するため入れない(Codex の警告に従い)。 |
| 186 | + |
| 187 | +#### Action Pinning(Codex レビュー反映) |
| 188 | + |
| 189 | +publish.yml は供給チェーン攻撃のリスクが高いため、主要 Action を SHA pin する: |
| 190 | + |
| 191 | +- `actions/checkout` → SHA pin |
| 192 | +- `actions/setup-node` → SHA pin |
| 193 | +- `astral-sh/setup-uv` → SHA pin |
| 194 | +- `actions/upload-artifact` / `download-artifact` → SHA pin |
| 195 | +- `pypa/gh-action-pypi-publish` → SHA pin |
| 196 | + |
| 197 | +ci.yml の既存ジョブは major tag (`@v6`) のまま据え置き(変更スコープを限定)。 |
| 198 | + |
| 199 | +#### Trusted Publisher 設定(手動、ワークフロー外) |
| 200 | + |
| 201 | +PyPI 側で一度だけ設定する: |
| 202 | + |
| 203 | +``` |
| 204 | +PyPI → Your projects → insight-blueprint → Publishing |
| 205 | + → Add a new pending publisher |
| 206 | + Owner: etoyama |
| 207 | + Repository: insight-blueprint |
| 208 | + Workflow: publish.yml |
| 209 | + Environment: pypi |
| 210 | +``` |
| 211 | + |
| 212 | +**注意事項(Codex レビュー反映):** |
| 213 | +- workflow filename は正確に `publish.yml` と一致させること(`release.yml` 等にリネームすると OIDC 失敗) |
| 214 | +- environment 名も正確に `pypi` と一致させること |
| 215 | +- 初回は PyPI 上でパッケージが存在しない状態から pending publisher を登録する |
| 216 | + |
| 217 | +### Component 3: CI Wheel Check (L3) |
| 218 | + |
| 219 | +- **Purpose:** PR ごとに wheel が正しくビルドできることを検証し、壊れたパッケージングが main にマージされることを防ぐ |
| 220 | +- **Files:** `.github/workflows/ci.yml` — `build-check` ジョブ追加 |
| 221 | +- **Dependencies:** 既存の `python` / `frontend` ジョブ |
| 222 | +- **Reuses:** publish.yml の wheel verification ロジック(同一のインライン Python スクリプト) |
| 223 | + |
| 224 | +#### ジョブ構造 |
| 225 | + |
| 226 | +```yaml |
| 227 | +build-check: |
| 228 | + runs-on: ubuntu-latest |
| 229 | + needs: [python, frontend] |
| 230 | + steps: |
| 231 | + # 1. Checkout |
| 232 | + # 2. Setup Node.js + npm ci + npm run build (frontend) |
| 233 | + # 3. Setup uv + uv build |
| 234 | + # 4. Verify wheel contents (same script as publish.yml) |
| 235 | +``` |
| 236 | + |
| 237 | +**設計判断(Codex レビュー反映):** 全 PR で実行する。path filter は導入しない(初期段階で実行時間は 1-2 分程度、問題になってから path filter に移行)。 |
| 238 | + |
| 239 | +## Data Models |
| 240 | + |
| 241 | +本設計にデータモデルの変更はない。アプリケーションの models/ 層には一切変更を加えない。 |
| 242 | + |
| 243 | +## Error Handling |
| 244 | + |
| 245 | +### Error Scenarios |
| 246 | + |
| 247 | +1. **Tag-version mismatch** |
| 248 | + - **Handling:** build job の tag-version check ステップで即座に fail |
| 249 | + - **User Impact:** maintainer に「Tag vX.Y.Z does not match pyproject.toml version X.Y.Z」エラーメッセージ。push し直しが必要 |
| 250 | + |
| 251 | +2. **Frontend assets missing from wheel** |
| 252 | + - **Handling:** wheel verification ステップで fail。見つかった static ファイルの一覧を出力 |
| 253 | + - **User Impact:** maintainer に「Frontend assets missing from wheel」エラー。`poe build-frontend` の実行漏れ or Vite 設定の問題 |
| 254 | + |
| 255 | +3. **Trusted Publisher OIDC failure** |
| 256 | + - **Handling:** PyPI upload ステップで 403 エラー |
| 257 | + - **User Impact:** maintainer に PyPI 側の Trusted Publisher 設定の確認を促す。workflow filename / environment 名の不一致が最も多い原因 |
| 258 | + |
| 259 | +4. **twine check failure** |
| 260 | + - **Handling:** twine check ステップで fail。メタデータの問題箇所を出力 |
| 261 | + - **User Impact:** pyproject.toml の description / classifiers / URLs の修正が必要 |
| 262 | + |
| 263 | +5. **importlib.metadata.version() fallback** |
| 264 | + - **Handling:** `PackageNotFoundError` を catch し `"0.0.0+unknown"` を返す |
| 265 | + - **User Impact:** 未インストール状態での実行時にバージョンが `0.0.0+unknown` と表示される。正常動作には影響なし |
| 266 | + |
| 267 | +## Testing Strategy |
| 268 | + |
| 269 | +### Unit Testing |
| 270 | + |
| 271 | +- **`__init__.py` version fallback**: `importlib.metadata.version` を mock して `PackageNotFoundError` を raise させ、`__version__` が `"0.0.0+unknown"` になることを検証 |
| 272 | +- **`__init__.py` version normal**: mock なしで `__version__` が pyproject.toml の version と一致することを検証 |
| 273 | + |
| 274 | +### Integration Testing |
| 275 | + |
| 276 | +- **Wheel build verification**: `uv build` で wheel を生成し、zipfile で static assets の存在を確認する pytest テスト |
| 277 | +- **py.typed inclusion**: wheel 内に `py.typed` が含まれることを確認 |
| 278 | + |
| 279 | +### CI/CD Testing |
| 280 | + |
| 281 | +- **publish.yml**: 直接テストは困難(PyPI upload は本番操作)。tag-version check と wheel verification は CI の build-check ジョブで間接的にカバー |
| 282 | +- **ci.yml build-check**: PR を出すことで自動実行される |
| 283 | + |
| 284 | +## Release Procedure (Reference) |
| 285 | + |
| 286 | +ワークフロー利用者(maintainer)向けのリリース手順: |
| 287 | + |
| 288 | +``` |
| 289 | +1. pyproject.toml の version を更新(例: "0.1.0" → "0.2.0") |
| 290 | +2. CHANGELOG.md に [0.2.0] セクションを追加 |
| 291 | +3. git add pyproject.toml CHANGELOG.md |
| 292 | +4. git commit -m "chore: release v0.2.0" |
| 293 | +5. git tag v0.2.0 |
| 294 | +6. git push && git push --tags |
| 295 | +7. → publish.yml が自動発火 |
| 296 | + → tag-version check → frontend build → uv build → wheel verify → twine check → PyPI upload |
| 297 | +8. PyPI で公開を確認 |
| 298 | +``` |
0 commit comments