Skip to content

Commit ecb74d3

Browse files
etoyamaclaude
andauthored
feat: add PyPI publish readiness (#41)
* docs: add pypi-publish-readiness spec (requirements, design, test-design, tasks) Spec for PyPI publication readiness: package metadata fixes (py.typed, version single source, CHANGELOG, classifiers), publish workflow (tag push trigger, Trusted Publisher OIDC), and wheel integrity verification (CI + local). Codex-reviewed design with 11 implementation tasks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add PyPI publish readiness (version, metadata, CI/CD, verification) - Version single-source-of-truth via importlib.metadata with fallback - PEP 561 py.typed marker and scientific classifiers - CHANGELOG.md in Keep a Changelog format - publish.yml: tag-triggered PyPI publish with Trusted Publisher (OIDC), all actions SHA-pinned, build/publish job separation - ci.yml: build-check job for wheel integrity on every PR - scripts/verify_wheel.py: wheel content verification (frontend assets + py.typed) - scripts/check_tag_version.py: git tag ↔ pyproject.toml version consistency - poe tasks: verify-wheel, check-tag-version, release-dry-run - sdist artifacts config to fix static files in sdist-based wheel builds - 18 new tests (725 total), team-review S-01 fix (command injection prevention) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: ensure module reload in version fallback tests via try/finally Assertion failure in TestVersionFallback left __version__ stuck at "0.0.0+unknown", potentially polluting subsequent tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ec27963 commit ecb74d3

17 files changed

Lines changed: 1596 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,32 @@ jobs:
4545
- name: Build
4646
run: npm run build
4747
working-directory: frontend
48+
49+
build-check:
50+
needs: [python, frontend]
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@v6
54+
55+
- uses: actions/setup-node@v6
56+
with:
57+
node-version: "22"
58+
cache: npm
59+
cache-dependency-path: frontend/package-lock.json
60+
61+
- name: Build frontend
62+
run: npm ci && npm run build
63+
working-directory: frontend
64+
65+
- uses: astral-sh/setup-uv@v7
66+
with:
67+
enable-cache: true
68+
69+
- name: Build package
70+
run: uv build
71+
72+
- name: Verify wheel contents
73+
run: python scripts/verify_wheel.py
74+
75+
- name: Check distribution metadata
76+
run: uvx --from twine twine check dist/*

.github/workflows/publish.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
12+
13+
- name: Check tag-version consistency
14+
env:
15+
TAG_NAME: ${{ github.ref_name }}
16+
run: python scripts/check_tag_version.py --tag "$TAG_NAME"
17+
18+
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
19+
with:
20+
node-version: "22"
21+
cache: npm
22+
cache-dependency-path: frontend/package-lock.json
23+
24+
- name: Build frontend
25+
run: npm ci && npm run build
26+
working-directory: frontend
27+
28+
- uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
29+
with:
30+
enable-cache: true
31+
32+
- name: Build package
33+
run: uv build
34+
35+
- name: Verify wheel contents
36+
run: python scripts/verify_wheel.py
37+
38+
- name: Check distribution metadata
39+
run: uvx --from twine twine check dist/*
40+
41+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
42+
with:
43+
name: dist
44+
path: dist/
45+
46+
publish:
47+
needs: build
48+
runs-on: ubuntu-latest
49+
environment: pypi
50+
permissions:
51+
id-token: write
52+
steps:
53+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
54+
with:
55+
name: dist
56+
path: dist/
57+
58+
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
59+
with:
60+
packages-dir: dist/
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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

Comments
 (0)