From edea51b2a14647aeaf160ee1b5030a904e87e17b Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 29 May 2026 20:50:53 +0900 Subject: [PATCH 1/4] openapi-typescript endpoint --- .github/workflows/ci.yml | 58 + .gitignore | 3 + Makefile | 14 +- backend/app/routers/auth/endpoints.py | 8 +- backend/app/routers/github_link.py | 12 +- backend/app/schemas/__init__.py | 3 +- backend/app/schemas/auth.py | 10 + backend/app/schemas/shared.py | 11 + backend/scripts/export_openapi.py | 47 + frontend/.prettierignore | 2 + frontend/eslint.config.js | 4 +- frontend/package-lock.json | 237 +++ frontend/package.json | 1 + frontend/scripts/gen-types.mjs | 34 + frontend/src/api/generated.ts | 2524 +++++++++++++++++++++++++ 15 files changed, 2955 insertions(+), 13 deletions(-) create mode 100644 backend/scripts/export_openapi.py create mode 100644 frontend/.prettierignore create mode 100644 frontend/scripts/gen-types.mjs create mode 100644 frontend/src/api/generated.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37c4caa1..b294172e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,6 +219,64 @@ jobs: working-directory: backend run: uv run pytest -q tests + # OpenAPI → TypeScript 型のドリフト検知 (ADR-0007)。 + # backend schema を変更したのに frontend/src/api/generated.ts を再生成して + # いない状態をビルドで落とす。エラーコードの型縛りと同じ思想。 + codegen-drift: + runs-on: ubuntu-latest + needs: detect-changes + timeout-minutes: 10 + if: needs.detect-changes.outputs.app == 'true' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + enable-cache: true + cache-dependency-glob: "backend/requirements.txt" + + - name: Setup Python + run: uv python install 3.13 + + - name: WeasyPrint 用システムライブラリのインストール + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libpango-1.0-0 libpangoft2-1.0-0 libpangocairo-1.0-0 \ + libglib2.0-0 libgobject-2.0-0 libffi-dev libcairo2 + + - name: Install backend dependencies + working-directory: backend + run: uv pip install -r requirements.txt + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + run: npm ci --prefix frontend + + - name: Export OpenAPI schema + working-directory: backend + run: uv run python scripts/export_openapi.py + + - name: Generate TypeScript types + run: node frontend/scripts/gen-types.mjs + + - name: Check generated types drift + run: | + if ! git diff --exit-code frontend/src/api/generated.ts; then + echo "::error::frontend/src/api/generated.ts が最新ではありません。'make codegen-types' を実行して再生成し、コミットしてください (ADR-0007)。" + exit 1 + fi + deploy-frontend: runs-on: ubuntu-latest needs: [detect-changes, test-frontend, test-backend] diff --git a/.gitignore b/.gitignore index fdd510dd..0d0c9759 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ backend/local.sqlite backend/local.sqlite-shm backend/local.sqlite-wal +# OpenAPI 型生成の中間生成物(ADR-0007)。コミット対象は frontend/src/api/generated.ts のみ。 +backend/openapi.json + # Env files .env frontend/.env.local diff --git a/Makefile b/Makefile index a144928e..ddcb5de5 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ ci \ dupe-check dupe-check-html dupe-clean \ build-frontend build-backend deploy-frontend \ - gen-redirects \ + gen-redirects codegen-types \ migrate migrate-create \ infra-fmt infra-fmt-check infra-validate-dev infra-validate-stg infra-validate-prod infra-validate \ clean @@ -53,6 +53,7 @@ help: @echo " build-backend Docker イメージビルド" @echo " deploy-frontend Cloudflare Pages へビルド&デプロイ (CLOUD_RUN_URL=... 指定可)" @echo " gen-redirects Cloudflare Pages 用 _redirects を生成 (CLOUD_RUN_URL=... 指定可)" + @echo " codegen-types OpenAPI から frontend 型 (src/api/generated.ts) を再生成 (ADR-0007)" @echo "" @echo "マイグレーション" @echo " migrate alembic upgrade head" @@ -181,6 +182,17 @@ deploy-frontend: gen-redirects: nix develop --command bash -c "cd frontend && CLOUD_RUN_URL='$(CLOUD_RUN_URL)' node scripts/gen-redirects.mjs" +# ------------------------------------------------------------------ # +# OpenAPI 型コード生成 (ADR-0007) +# ------------------------------------------------------------------ # + +# backend の FastAPI OpenAPI スキーマから frontend の型定義を生成する。 +# export_openapi.py で backend/openapi.json を出力し、gen-types.mjs で +# frontend/src/api/generated.ts を再生成する。backend app の import に +# WeasyPrint 等のネイティブ依存解決が必要なため Nix devshell 経由で実行する。 +codegen-types: + nix develop --command bash -c "set -e; cd backend && .venv/bin/python scripts/export_openapi.py && cd ../frontend && node scripts/gen-types.mjs" + # ------------------------------------------------------------------ # # マイグレーション # ------------------------------------------------------------------ # diff --git a/backend/app/routers/auth/endpoints.py b/backend/app/routers/auth/endpoints.py index 1271f628..e0acae49 100644 --- a/backend/app/routers/auth/endpoints.py +++ b/backend/app/routers/auth/endpoints.py @@ -22,7 +22,7 @@ from ...core.settings import get_callback_base_url from ...db import get_db from ...repositories import UserRepository -from ...schemas import GitHubCallbackRequest, TokenResponse +from ...schemas import GitHubCallbackRequest, GitHubLoginUrlResponse, TokenResponse from .github_auth import authenticate_github_user from .oauth_flow import ( begin_github_oauth, @@ -142,19 +142,19 @@ def me(request: Request, current_user=Depends(get_current_user)) -> TokenRespons ) -@router.get("/github/login-url") +@router.get("/github/login-url", response_model=GitHubLoginUrlResponse) @limiter.limit("10/minute") def github_login_url( request: Request, return_to: str | None = None, -) -> dict[str, str]: +) -> GitHubLoginUrlResponse: """GitHub OAuth 認可 URL と state を返す。 state はフロントが sessionStorage に保持し、コールバック時に CSRF 検証する。 """ frontend_url = resolve_frontend_url_from_request(request, return_to) authorization_url, state = begin_github_oauth(request, frontend_url) - return {"authorization_url": authorization_url, "state": state} + return GitHubLoginUrlResponse(authorization_url=authorization_url, state=state) @router.get("/github/login") diff --git a/backend/app/routers/github_link.py b/backend/app/routers/github_link.py index 063dc422..57e912f7 100644 --- a/backend/app/routers/github_link.py +++ b/backend/app/routers/github_link.py @@ -22,7 +22,7 @@ GitHubLinkRequest, ProgressResponse, ) -from ..schemas.shared import TaskStatusResponse +from ..schemas.shared import TaskAcceptedResponse, TaskStatusResponse from ..services.tasks import AsyncTaskCacheService, TaskType logger = logging.getLogger(__name__) @@ -98,7 +98,7 @@ def get_cache_status( ) -@router.post("/run", status_code=202) +@router.post("/run", response_model=TaskAcceptedResponse, status_code=202) @limiter.limit("5/minute") async def start_github_link( request: Request, @@ -124,7 +124,7 @@ async def start_github_link( # DB 最新状態を取得しつつ pending へアトミック遷移。進行中なら早期リターン if not service.try_reset_to_pending(): - return {"status": cache.status} + return TaskAcceptedResponse(status=cache.status) try: await service.dispatch( @@ -142,10 +142,10 @@ async def start_github_link( except Exception: _raise_dispatch_failed() - return {"status": "pending"} + return TaskAcceptedResponse(status="pending") -@router.post("/run/retry", status_code=202) +@router.post("/run/retry", response_model=TaskAcceptedResponse, status_code=202) @limiter.limit("5/minute") async def retry_github_link( request: Request, @@ -212,4 +212,4 @@ async def retry_github_link( except Exception: _raise_dispatch_failed() - return {"status": "pending"} + return TaskAcceptedResponse(status="pending") diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 14dc4243..dc6b33e1 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,6 +1,6 @@ """Pydantic スキーマ。""" -from .auth import GitHubCallbackRequest, TokenResponse, UserResponse +from .auth import GitHubCallbackRequest, GitHubLoginUrlResponse, TokenResponse, UserResponse from .blog import ( BlogAccountCreate, BlogAccountResponse, @@ -50,6 +50,7 @@ "Experience", "GitHubCallbackRequest", "GitHubLinkRequest", + "GitHubLoginUrlResponse", "GitHubLinkResponse", "MasterItem", "MasterItemCreate", diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index a288d1f5..9bcfd3af 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -16,3 +16,13 @@ class UserResponse(BaseModel): class GitHubCallbackRequest(BaseModel): code: str = Field(min_length=1) state: str = Field(min_length=1) + + +class GitHubLoginUrlResponse(BaseModel): + """GitHub OAuth 認可 URL と CSRF 検証用 state を返すレスポンス。 + + state はフロントが sessionStorage に保持し、コールバック時に CSRF 検証する。 + """ + + authorization_url: str + state: str diff --git a/backend/app/schemas/shared.py b/backend/app/schemas/shared.py index dac9c6af..636bb8f0 100644 --- a/backend/app/schemas/shared.py +++ b/backend/app/schemas/shared.py @@ -18,6 +18,17 @@ class TaskStatusResponse(BaseModel): error_code: str | None = None +class TaskAcceptedResponse(BaseModel): + """非同期タスクの受付応答(202 Accepted)。 + + ``POST /github/run`` / ``POST /github/run/retry`` など、 + バックグラウンドタスクを開始するエンドポイントで共通利用される受付レスポンス。 + 現在のタスクステータス(``pending`` 等)のみを返す。 + """ + + status: str + + class SubProgress(BaseModel): """ステップ内の細粒度な進捗(例: リポジトリ詳細取得ステップ)。""" diff --git a/backend/scripts/export_openapi.py b/backend/scripts/export_openapi.py new file mode 100644 index 00000000..6bc8a189 --- /dev/null +++ b/backend/scripts/export_openapi.py @@ -0,0 +1,47 @@ +"""FastAPI アプリの OpenAPI スキーマを JSON にダンプするスクリプト。 + +frontend の型生成(`openapi-typescript`)の入力となる `backend/openapi.json` を出力する。 +ADR-0007(OpenAPI → TypeScript 型コード生成)のパイプライン Phase 0 の一部。 + +使用例(必ず Nix devshell 経由で実行する。WeasyPrint 等のネイティブ依存解決のため): + nix develop --command bash -c "cd backend && .venv/bin/python scripts/export_openapi.py" + +出力先はデフォルトで `backend/openapi.json`。第 1 引数で変更可能。 + +注意: +- `app.main` を import するだけで FastAPI app は構築される(lifespan の bootstrap は実行されない)。 +- ENVIRONMENT 未設定時は local 扱いとなり、INTERNAL_SECRET 等の必須チェックは走らない。 +- diff の安定化のため `sort_keys=True` で出力する(openapi-typescript はキー順に依存しない)。 +""" + +import json +import os +import sys +from pathlib import Path + +# import 時の設定チェックを避けるため、未設定なら local 環境として扱う。 +os.environ.setdefault("ENVIRONMENT", "local") + +# scripts/ から見たプロジェクトルート(backend/)配下に openapi.json を出力する。 +_BACKEND_DIR = Path(__file__).resolve().parent.parent +_DEFAULT_OUTPUT = _BACKEND_DIR / "openapi.json" + +# backend ディレクトリを import パスに追加し、`app.main` を解決可能にする。 +sys.path.insert(0, str(_BACKEND_DIR)) + + +def main() -> None: + from app.main import app + + output_path = Path(sys.argv[1]) if len(sys.argv) > 1 else _DEFAULT_OUTPUT + schema = app.openapi() + # 末尾に改行を付け、エディタ・lint と整合させる。 + output_path.write_text( + json.dumps(schema, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(f"OpenAPI スキーマを書き出しました: {output_path}") + + +if __name__ == "__main__": + main() diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 00000000..fe57a7a3 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +# openapi-typescript の自動生成物(手編集禁止 / フォーマット対象外)。ADR-0007 参照。 +src/api/generated.ts diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 8bf77276..426bbfa3 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -5,7 +5,9 @@ import reactRefresh from "eslint-plugin-react-refresh"; import eslintConfigPrettier from "eslint-config-prettier"; export default tseslint.config( - { ignores: ["dist"] }, + // src/api/generated.ts は openapi-typescript の自動生成物(手編集禁止)。 + // lint 対象に含めると生成スタイルと衝突するため除外する(ADR-0007)。 + { ignores: ["dist", "src/api/generated.ts"] }, { files: ["src/**/*.{ts,tsx}"], extends: [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5c936a0a..8780e1e5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -38,6 +38,7 @@ "http-proxy-middleware": "^3.0.5", "jsdom": "^29.0.1", "msw": "^2.13.2", + "openapi-typescript": "^7.13.0", "playwright": "^1.56.0", "prettier": "^3.8.1", "typescript": "^5.7.2", @@ -2101,6 +2102,82 @@ "dev": true, "license": "MIT" }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.15", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.15.tgz", + "integrity": "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.1.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -3322,6 +3399,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -3339,6 +3426,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3703,6 +3800,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -3782,6 +3886,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5118,6 +5229,20 @@ "dev": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -5213,6 +5338,19 @@ "node": ">=8" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -5366,6 +5504,16 @@ "node": ">=8" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6031,6 +6179,40 @@ "wrappy": "1" } }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6101,6 +6283,37 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -6248,6 +6461,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -7677,6 +7900,13 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -8600,6 +8830,13 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5458b2e8..2377b59e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,6 +50,7 @@ "http-proxy-middleware": "^3.0.5", "jsdom": "^29.0.1", "msw": "^2.13.2", + "openapi-typescript": "^7.13.0", "playwright": "^1.56.0", "prettier": "^3.8.1", "typescript": "^5.7.2", diff --git a/frontend/scripts/gen-types.mjs b/frontend/scripts/gen-types.mjs new file mode 100644 index 00000000..0d663a2a --- /dev/null +++ b/frontend/scripts/gen-types.mjs @@ -0,0 +1,34 @@ +/** + * backend/openapi.json から OpenAPI 型定義(src/api/generated.ts)を生成するスクリプト。 + * + * ADR-0007(OpenAPI → TypeScript 型コード生成)のパイプライン。 + * 先に backend/scripts/export_openapi.py で openapi.json を出力しておくこと。 + * 通常は `make codegen-types`(Nix devshell 経由)から呼び出される。 + * + * 生成物の冒頭に「手編集禁止」バナーを付与し、誤編集を防ぐ。 + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import openapiTS, { astToString } from "openapi-typescript"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OPENAPI_PATH = resolve(__dirname, "../../backend/openapi.json"); +const OUTPUT_PATH = resolve(__dirname, "../src/api/generated.ts"); + +const BANNER = `/** + * 自動生成ファイル — 手編集禁止。 + * + * backend の FastAPI OpenAPI スキーマから openapi-typescript で生成される。 + * 再生成: \`make codegen-types\`(ADR-0007 参照)。 + * backend の Pydantic schema が DTO の Single Source of Truth であり、 + * このファイルはその機械的ミラー。直接編集しても次回生成で上書きされる。 + */ +`; + +const schema = JSON.parse(readFileSync(OPENAPI_PATH, "utf8")); +const ast = await openapiTS(schema); +const contents = BANNER + astToString(ast); +writeFileSync(OUTPUT_PATH, contents, "utf8"); +console.log(`型定義を生成しました: ${OUTPUT_PATH}`); diff --git a/frontend/src/api/generated.ts b/frontend/src/api/generated.ts new file mode 100644 index 00000000..8fb1f302 --- /dev/null +++ b/frontend/src/api/generated.ts @@ -0,0 +1,2524 @@ +/** + * 自動生成ファイル — 手編集禁止。 + * + * backend の FastAPI OpenAPI スキーマから openapi-typescript で生成される。 + * 再生成: `make codegen-types`(ADR-0007 参照)。 + * backend の Pydantic schema が DTO の Single Source of Truth であり、 + * このファイルはその機械的ミラー。直接編集しても次回生成で上書きされる。 + */ +export interface paths { + "/api/blog/accounts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Accounts + * @description 連携アカウント一覧を取得する。 + */ + get: operations["list_accounts_api_blog_accounts_get"]; + put?: never; + /** + * Add Account + * @description 連携アカウントを登録する。 + * 同じプラットフォームは1つまで。ユーザー存在チェックあり。 + */ + post: operations["add_account_api_blog_accounts_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/blog/accounts/{account_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete Account + * @description 連携アカウントを解除する。紐づく記事も削除される。 + */ + delete: operations["delete_account_api_blog_accounts__account_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/blog/accounts/{account_id}/sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Sync Account + * @description 外部 API からデータを取得して DB に保存する。 + */ + post: operations["sync_account_api_blog_accounts__account_id__sync_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/blog/accounts/{platform}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update Account + * @description 連携アカウントの username を更新し、同期状態を未同期に戻す。 + */ + patch: operations["update_account_api_blog_accounts__platform__patch"]; + trace?: never; + }; + "/api/blog/articles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Articles + * @description DB に保存済みの記事一覧を取得する。 + */ + get: operations["list_articles_api_blog_articles_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/blog/score": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Blog Score + * @description 保存済みの記事に対してスコアリングを実行する。 + */ + get: operations["get_blog_score_api_blog_score_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/github-link/cache": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Cache + * @description 保存済みの連携結果を取得する。 + */ + get: operations["get_cache_api_github_link_cache_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/github-link/cache/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Cache Status + * @description 連携ステータスを返す(軽量ポーリング用)。 + */ + get: operations["get_cache_status_api_github_link_cache_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/github-link/progress": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Link Progress + * @description GitHub 連携タスクの進捗を取得する(ポーリング用)。 + * + * Redis にデータがない場合(タスク未開始・Redis 障害)は step_index=0 のデフォルトを返す。 + */ + get: operations["get_link_progress_api_github_link_progress_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/github-link/run": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Start Github Link + * @description GitHub 連携パイプラインをバックグラウンドで開始する。 + */ + post: operations["start_github_link_api_github_link_run_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/github-link/run/retry": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Retry Github Link + * @description 失敗した GitHub 連携タスクを手動で再実行する。 + * + * ``dead_letter`` 状態のキャッシュのみ再実行可能。 + * ``retry_count`` を 0 にリセットし、ステータスを ``pending`` に戻して再ディスパッチする。 + */ + post: operations["retry_github_link_api_github_link_run_retry_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/master-data/qualification": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Items */ + get: operations["list_items_api_master_data_qualification_get"]; + put?: never; + /** Create Item */ + post: operations["create_item_api_master_data_qualification_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/master-data/qualification/{item_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update Item */ + put: operations["update_item_api_master_data_qualification__item_id__put"]; + post?: never; + /** Delete Item */ + delete: operations["delete_item_api_master_data_qualification__item_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/master-data/technology-stack": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Technology Stacks + * @description 技術スタックマスタ一覧を取得する(認証不要)。 + */ + get: operations["list_technology_stacks_api_master_data_technology_stack_get"]; + put?: never; + /** + * Create Technology Stack + * @description 技術スタックマスタを新規作成する(admin認証必須)。 + */ + post: operations["create_technology_stack_api_master_data_technology_stack_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/master-data/technology-stack/{item_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Update Technology Stack + * @description 技術スタックマスタを更新する(admin認証必須)。 + */ + put: operations["update_technology_stack_api_master_data_technology_stack__item_id__put"]; + post?: never; + /** + * Delete Technology Stack + * @description 技術スタックマスタを削除する(admin認証必須)。 + */ + delete: operations["delete_technology_stack_api_master_data_technology_stack__item_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/notifications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Notifications + * @description 最新30件の通知を取得する。 + */ + get: operations["list_notifications_api_notifications_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/notifications/read-all": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Mark All As Read + * @description 全通知を既読にする。 + */ + post: operations["mark_all_as_read_api_notifications_read_all_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/notifications/unread-count": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Unread Count + * @description 未読通知件数を返す。 + */ + get: operations["get_unread_count_api_notifications_unread_count_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/notifications/{notification_id}/read": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Mark As Read + * @description 指定された通知を既読にする。 + */ + patch: operations["mark_as_read_api_notifications__notification_id__read_patch"]; + trace?: never; + }; + "/api/resumes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create Resume */ + post: operations["create_resume_api_resumes_post"]; + /** Delete Resume */ + delete: operations["delete_resume_api_resumes_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/resumes/latest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Latest Resume */ + get: operations["get_latest_resume_api_resumes_latest_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/resumes/{resume_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Resume */ + get: operations["get_resume_api_resumes__resume_id__get"]; + /** Update Resume */ + put: operations["update_resume_api_resumes__resume_id__put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/resumes/{resume_id}/markdown": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Download Resume Markdown */ + get: operations["download_resume_markdown_api_resumes__resume_id__markdown_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/resumes/{resume_id}/pdf": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Download Resume Pdf */ + get: operations["download_resume_pdf_api_resumes__resume_id__pdf_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/github/callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Github Callback Redirect + * @description GitHub OAuth コールバックを処理し、フロントエンドへリダイレクトする。 + * + * 一部の CDN / リバースプロキシは 303 レスポンスの Set-Cookie を除去することがあるため、 + * 200 + HTML リダイレクトで Cookie を確実にセットする。 + */ + get: operations["github_callback_redirect_auth_github_callback_get"]; + put?: never; + /** + * Github Callback + * @description GitHub OAuth コードを受け取り、認証 Cookie を発行する。 + * + * state はフロントの sessionStorage で検証済みのためサーバー側では再検証しない。 + * redirect_uri は GitHub OAuth App の登録値 (`/github/callback`) と一致させる必要がある。 + */ + post: operations["github_callback_auth_github_callback_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/github/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Github Login + * @description GitHub OAuth 認可 URL へリダイレクトする。 + */ + get: operations["github_login_auth_github_login_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/github/login-url": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Github Login Url + * @description GitHub OAuth 認可 URL と state を返す。 + * + * state はフロントが sessionStorage に保持し、コールバック時に CSRF 検証する。 + */ + get: operations["github_login_url_auth_github_login_url_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Logout + * @description ログアウト処理。DB の refresh_jti を無効化し Cookie を削除する。 + * トークン解析が失敗した場合でも必ず Cookie を削除して 204 を返す。 + */ + post: operations["logout_auth_logout_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Me + * @description 現在のログインユーザー情報を返す。 + */ + get: operations["me_auth_me_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Refresh + * @description リフレッシュトークンで新しいアクセストークンを発行する。 + */ + post: operations["refresh_auth_refresh_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Healthcheck + * @description ヘルスチェック。Uptime Check から呼ばれる。DB 接続も検証する。 + */ + get: operations["healthcheck_health_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/internal/tasks/{task_type}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Handle Task + * @description Cloud Tasks コールバックまたはローカルテスト用エンドポイント。 + */ + post: operations["handle_task_internal_tasks__task_type__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** + * BlogAccountCreate + * @description ブログ連携アカウントの作成リクエスト。 + */ + BlogAccountCreate: { + /** + * Platform + * @enum {string} + */ + platform: "zenn" | "note" | "qiita"; + /** Username */ + username: string; + }; + /** + * BlogAccountResponse + * @description ブログ連携アカウントのレスポンス。 + */ + BlogAccountResponse: { + /** + * Created At + * Format: date-time + */ + created_at: string; + /** + * Id + * Format: uuid + */ + id: string; + /** Last Synced At */ + last_synced_at?: string | null; + /** Platform */ + platform: string; + /** Username */ + username: string; + }; + /** + * BlogAccountUpdate + * @description ブログ連携アカウントの更新リクエスト。 + */ + BlogAccountUpdate: { + /** Username */ + username: string; + }; + /** + * BlogArticleResponse + * @description ブログ記事のレスポンス。 + */ + BlogArticleResponse: { + /** + * Id + * Format: uuid + */ + id: string; + /** + * Likes Count + * @default 0 + */ + likes_count: number; + /** Platform */ + platform: string; + /** Published At */ + published_at?: string | null; + /** Summary */ + summary?: string | null; + /** Tags */ + tags?: string[]; + /** Title */ + title: string; + /** Url */ + url: string; + }; + /** + * BlogScoreArticleResponse + * @description 技術記事判定結果付きの記事情報。 + */ + BlogScoreArticleResponse: { + /** Id */ + id: string; + /** + * Is Tech + * @default false + */ + is_tech: boolean; + /** + * Likes Count + * @default 0 + */ + likes_count: number; + /** Published At */ + published_at?: string | null; + /** Tags */ + tags?: string[]; + /** Title */ + title: string; + /** Url */ + url: string; + }; + /** + * BlogScoreResponse + * @description ブログ統計サマリのレスポンス。 + */ + BlogScoreResponse: { + /** Articles */ + articles?: components["schemas"]["BlogScoreArticleResponse"][]; + /** + * Avg Likes + * @default 0 + */ + avg_likes: number; + /** + * Avg Monthly Posts + * @default 0 + */ + avg_monthly_posts: number; + /** + * Tech Article Count + * @default 0 + */ + tech_article_count: number; + /** + * Total Article Count + * @default 0 + */ + total_article_count: number; + }; + /** + * BlogSyncResponse + * @description ブログ同期結果のレスポンス。 + */ + BlogSyncResponse: { + /** Synced Count */ + synced_count: number; + /** Total Count */ + total_count: number; + }; + /** + * CachedGitHubLinkResponse + * @description DB に保存された連携結果を返す。 + */ + CachedGitHubLinkResponse: { + /** Error Code */ + error_code?: string | null; + /** Error Message */ + error_message?: string | null; + /** Result */ + result?: Record | null; + /** Status */ + status?: string | null; + /** Warning Message */ + warning_message?: string | null; + }; + /** + * Client + * @description ユーザ(常駐先/クライアント企業)。 + * + * ``is_vacation=True`` の場合は取引先ではなく在籍中の休暇(育児/介護/留学等)を表し、 + * name / projects の代わりに ``vacation_*`` 期間と詳細を保持する。 + */ + "Client-Input": { + /** + * Has Client + * @default true + */ + has_client: boolean; + /** + * Is Vacation + * @default false + */ + is_vacation: boolean; + /** + * Name + * @default + */ + name: string; + /** Projects */ + projects?: components["schemas"]["Project-Input"][]; + /** + * Vacation Description + * @default + */ + vacation_description: string; + /** + * Vacation End Date + * @default + */ + vacation_end_date: string; + /** + * Vacation Is Current + * @default false + */ + vacation_is_current: boolean; + /** + * Vacation Start Date + * @default + */ + vacation_start_date: string; + }; + /** + * Client + * @description ユーザ(常駐先/クライアント企業)。 + * + * ``is_vacation=True`` の場合は取引先ではなく在籍中の休暇(育児/介護/留学等)を表し、 + * name / projects の代わりに ``vacation_*`` 期間と詳細を保持する。 + */ + "Client-Output": { + /** + * Has Client + * @default true + */ + has_client: boolean; + /** + * Is Vacation + * @default false + */ + is_vacation: boolean; + /** + * Name + * @default + */ + name: string; + /** Projects */ + projects?: components["schemas"]["Project-Output"][]; + /** + * Vacation Description + * @default + */ + vacation_description: string; + /** + * Vacation End Date + * @default + */ + vacation_end_date: string; + /** + * Vacation Is Current + * @default false + */ + vacation_is_current: boolean; + /** + * Vacation Start Date + * @default + */ + vacation_start_date: string; + }; + /** Experience */ + "Experience-Input": { + /** Business Description */ + business_description: string; + /** + * Capital + * @default + */ + capital: string; + /** + * Capital Unit + * @default 千万円 + * @enum {string} + */ + capital_unit: "万円" | "百万円" | "千万円" | "億円"; + /** Clients */ + clients?: components["schemas"]["Client-Input"][]; + /** Company */ + company: string; + /** + * Description + * @default + */ + description: string; + /** + * Employee Count + * @default + */ + employee_count: string; + /** + * End Date + * @default + */ + end_date: string; + /** + * Is Current + * @default false + */ + is_current: boolean; + /** + * Is It Company + * @default true + */ + is_it_company: boolean; + /** + * Start Date + * @default + */ + start_date: string; + }; + /** Experience */ + "Experience-Output": { + /** Business Description */ + business_description: string; + /** + * Capital + * @default + */ + capital: string; + /** + * Capital Unit + * @default 千万円 + * @enum {string} + */ + capital_unit: "万円" | "百万円" | "千万円" | "億円"; + /** Clients */ + clients?: components["schemas"]["Client-Output"][]; + /** Company */ + company: string; + /** + * Description + * @default + */ + description: string; + /** + * Employee Count + * @default + */ + employee_count: string; + /** + * End Date + * @default + */ + end_date: string; + /** + * Is Current + * @default false + */ + is_current: boolean; + /** + * Is It Company + * @default true + */ + is_it_company: boolean; + /** + * Start Date + * @default + */ + start_date: string; + }; + /** GitHubCallbackRequest */ + GitHubCallbackRequest: { + /** Code */ + code: string; + /** State */ + state: string; + }; + /** GitHubLinkRequest */ + GitHubLinkRequest: { + /** + * Include Forks + * @description 連携にフォークしたリポジトリを含めるかどうか + * @default false + */ + include_forks: boolean; + }; + /** + * GitHubLoginUrlResponse + * @description GitHub OAuth 認可 URL と CSRF 検証用 state を返すレスポンス。 + * + * state はフロントが sessionStorage に保持し、コールバック時に CSRF 検証する。 + */ + GitHubLoginUrlResponse: { + /** Authorization Url */ + authorization_url: string; + /** State */ + state: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** + * MarkAllReadResponse + * @description 全件既読レスポンス。 + */ + MarkAllReadResponse: { + /** Updated */ + updated: number; + }; + /** + * MasterItem + * @description マスタデータ共通レスポンス(資格など)。 + */ + MasterItem: { + /** + * Id + * Format: uuid + */ + id: string; + /** Name */ + name: string; + /** Sort Order */ + sort_order: number; + }; + /** + * MasterItemCreate + * @description マスタデータ共通の作成リクエスト(資格など)。 + */ + MasterItemCreate: { + /** Name */ + name: string; + /** + * Sort Order + * @default 0 + */ + sort_order: number; + }; + /** + * MasterItemUpdate + * @description マスタデータ共通の更新リクエスト(資格など)。 + */ + MasterItemUpdate: { + /** Name */ + name: string; + /** + * Sort Order + * @default 0 + */ + sort_order: number; + }; + /** + * NotificationResponse + * @description 通知レスポンス。 + */ + NotificationResponse: { + /** Created At */ + created_at: string; + /** Id */ + id: string; + /** Is Read */ + is_read: boolean; + /** Message */ + message: string | null; + /** Status */ + status: string; + /** Task Type */ + task_type: string; + /** Title */ + title: string; + }; + /** + * ProgressResponse + * @description 非同期タスクの進捗情報。 + * + * GitHub 連携 / resume_import など、複数ステップを持つタスクで共通利用される。 + */ + ProgressResponse: { + /** + * Step Index + * @description 現在のステップ番号(0 は未開始) + * @default 0 + */ + step_index: number; + /** + * Step Label + * @description 現在のステップラベル + */ + step_label?: string | null; + /** @description ステップ内の細粒度進捗(任意) */ + sub_progress?: components["schemas"]["SubProgress"] | null; + /** Task Id */ + task_id: string; + /** + * Total Steps + * @description 全ステップ数 + * @default 5 + */ + total_steps: number; + }; + /** Project */ + "Project-Input": { + /** + * Description + * @default + */ + description: string; + /** + * Name + * @default + */ + name: string; + /** Periods */ + periods?: components["schemas"]["ProjectPeriod"][]; + /** Phases */ + phases?: string[]; + /** + * Role + * @default + */ + role: string; + team?: components["schemas"]["ProjectTeam"]; + /** Technology Stacks */ + technology_stacks?: components["schemas"]["TechnologyStackItem"][]; + }; + /** Project */ + "Project-Output": { + /** + * Description + * @default + */ + description: string; + /** + * Name + * @default + */ + name: string; + /** Periods */ + periods?: components["schemas"]["ProjectPeriod"][]; + /** Phases */ + phases?: string[]; + /** + * Role + * @default + */ + role: string; + team?: components["schemas"]["ProjectTeam"]; + /** Technology Stacks */ + technology_stacks?: components["schemas"]["TechnologyStackItem"][]; + }; + /** + * ProjectPeriod + * @description プロジェクトの在籍期間(1 案件に複数持てる)。 + */ + ProjectPeriod: { + /** + * End Date + * @default + */ + end_date: string; + /** + * Is Current + * @default false + */ + is_current: boolean; + /** + * Start Date + * @default + */ + start_date: string; + }; + /** + * ProjectTeam + * @description プロジェクト体制(全体人数 + 役割別内訳)。 + */ + ProjectTeam: { + /** Members */ + members?: components["schemas"]["TeamMember"][]; + /** + * Total + * @default + */ + total: string; + }; + /** ResumeCreate */ + ResumeCreate: { + /** Career Summary */ + career_summary: string; + /** Experiences */ + experiences?: components["schemas"]["Experience-Input"][]; + /** Full Name */ + full_name: string; + /** Qualifications */ + qualifications?: components["schemas"]["ResumeQualificationItem"][]; + /** Self Pr */ + self_pr: string; + }; + /** ResumeQualificationItem */ + ResumeQualificationItem: { + /** Acquired Date */ + acquired_date: string; + /** Name */ + name: string; + }; + /** ResumeResponse */ + ResumeResponse: { + /** Career Summary */ + career_summary: string; + /** Created At */ + created_at: string; + /** Experiences */ + experiences?: components["schemas"]["Experience-Output"][]; + /** Full Name */ + full_name: string; + /** + * Id + * Format: uuid + */ + id: string; + /** Qualifications */ + qualifications?: components["schemas"]["ResumeQualificationItem"][]; + /** Self Pr */ + self_pr: string; + /** Updated At */ + updated_at: string; + }; + /** ResumeUpdate */ + ResumeUpdate: { + /** Career Summary */ + career_summary: string; + /** Experiences */ + experiences?: components["schemas"]["Experience-Input"][]; + /** Full Name */ + full_name: string; + /** Qualifications */ + qualifications?: components["schemas"]["ResumeQualificationItem"][]; + /** Self Pr */ + self_pr: string; + }; + /** + * SubProgress + * @description ステップ内の細粒度な進捗(例: リポジトリ詳細取得ステップ)。 + */ + SubProgress: { + /** Done */ + done: number; + /** Total */ + total: number; + }; + /** + * TaskAcceptedResponse + * @description 非同期タスクの受付応答(202 Accepted)。 + * + * ``POST /github/run`` / ``POST /github/run/retry`` など、 + * バックグラウンドタスクを開始するエンドポイントで共通利用される受付レスポンス。 + * 現在のタスクステータス(``pending`` 等)のみを返す。 + */ + TaskAcceptedResponse: { + /** Status */ + status: string; + }; + /** + * TaskStatusResponse + * @description 非同期タスクのステータスを返す軽量レスポンス。 + * + * blog / intelligence など複数の router で共通利用される。 + */ + TaskStatusResponse: { + /** Error Code */ + error_code?: string | null; + /** Error Message */ + error_message?: string | null; + /** Status */ + status: string; + }; + /** + * TeamMember + * @description 体制の役割ごとの人数。 + */ + TeamMember: { + /** Count */ + count: number; + /** Role */ + role: string; + }; + /** + * TechStackMasterCreate + * @description 技術スタックマスタの作成リクエスト。 + */ + TechStackMasterCreate: { + /** Category */ + category: string; + /** Name */ + name: string; + /** + * Sort Order + * @default 0 + */ + sort_order: number; + }; + /** + * TechStackMasterItem + * @description 技術スタックマスタのレスポンス。 + */ + TechStackMasterItem: { + /** Category */ + category: string; + /** + * Id + * Format: uuid + */ + id: string; + /** Name */ + name: string; + /** Sort Order */ + sort_order: number; + }; + /** + * TechStackMasterUpdate + * @description 技術スタックマスタの更新リクエスト。 + */ + TechStackMasterUpdate: { + /** Category */ + category: string; + /** Name */ + name: string; + /** + * Sort Order + * @default 0 + */ + sort_order: number; + }; + /** TechnologyStackItem */ + TechnologyStackItem: { + /** + * Category + * @enum {string} + */ + category: "language" | "framework" | "os" | "db" | "cloud_provider" | "container" | "iac" | "vcs" | "ci_cd" | "project_tool" | "monitoring" | "middleware" | "ai_agent"; + /** Name */ + name: string; + }; + /** TokenResponse */ + TokenResponse: { + /** + * Is Github User + * @default false + */ + is_github_user: boolean; + /** Username */ + username: string; + }; + /** + * UnreadCountResponse + * @description 未読件数レスポンス。 + */ + UnreadCountResponse: { + /** Count */ + count: number; + }; + /** ValidationError */ + ValidationError: { + /** Context */ + ctx?: Record; + /** Input */ + input?: unknown; + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + list_accounts_api_blog_accounts_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAccountResponse"][]; + }; + }; + }; + }; + add_account_api_blog_accounts_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAccountCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAccountResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_account_api_blog_accounts__account_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + account_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + sync_account_api_blog_accounts__account_id__sync_post: { + parameters: { + query?: never; + header?: never; + path: { + account_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogSyncResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_account_api_blog_accounts__platform__patch: { + parameters: { + query?: never; + header?: never; + path: { + platform: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAccountUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAccountResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_articles_api_blog_articles_get: { + parameters: { + query?: { + platform?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogArticleResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_blog_score_api_blog_score_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogScoreResponse"]; + }; + }; + }; + }; + get_cache_api_github_link_cache_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CachedGitHubLinkResponse"]; + }; + }; + }; + }; + get_cache_status_api_github_link_cache_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskStatusResponse"]; + }; + }; + }; + }; + get_link_progress_api_github_link_progress_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProgressResponse"]; + }; + }; + }; + }; + start_github_link_api_github_link_run_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GitHubLinkRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskAcceptedResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + retry_github_link_api_github_link_run_retry_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GitHubLinkRequest"] | null; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskAcceptedResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_items_api_master_data_qualification_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MasterItem"][]; + }; + }; + }; + }; + create_item_api_master_data_qualification_post: { + parameters: { + query?: never; + header?: { + authorization?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MasterItemCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MasterItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_item_api_master_data_qualification__item_id__put: { + parameters: { + query?: never; + header?: { + authorization?: string | null; + }; + path: { + item_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MasterItemUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MasterItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_item_api_master_data_qualification__item_id__delete: { + parameters: { + query?: never; + header?: { + authorization?: string | null; + }; + path: { + item_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_technology_stacks_api_master_data_technology_stack_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TechStackMasterItem"][]; + }; + }; + }; + }; + create_technology_stack_api_master_data_technology_stack_post: { + parameters: { + query?: never; + header?: { + authorization?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TechStackMasterCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TechStackMasterItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_technology_stack_api_master_data_technology_stack__item_id__put: { + parameters: { + query?: never; + header?: { + authorization?: string | null; + }; + path: { + item_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TechStackMasterUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TechStackMasterItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_technology_stack_api_master_data_technology_stack__item_id__delete: { + parameters: { + query?: never; + header?: { + authorization?: string | null; + }; + path: { + item_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_notifications_api_notifications_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NotificationResponse"][]; + }; + }; + }; + }; + mark_all_as_read_api_notifications_read_all_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MarkAllReadResponse"]; + }; + }; + }; + }; + get_unread_count_api_notifications_unread_count_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UnreadCountResponse"]; + }; + }; + }; + }; + mark_as_read_api_notifications__notification_id__read_patch: { + parameters: { + query?: never; + header?: never; + path: { + notification_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NotificationResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_resume_api_resumes_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ResumeCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResumeResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_resume_api_resumes_delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + }; + }; + get_latest_resume_api_resumes_latest_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResumeResponse"]; + }; + }; + }; + }; + get_resume_api_resumes__resume_id__get: { + parameters: { + query?: never; + header?: never; + path: { + resume_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResumeResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_resume_api_resumes__resume_id__put: { + parameters: { + query?: never; + header?: never; + path: { + resume_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ResumeUpdate"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResumeResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + download_resume_markdown_api_resumes__resume_id__markdown_get: { + parameters: { + query?: never; + header?: never; + path: { + resume_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + download_resume_pdf_api_resumes__resume_id__pdf_get: { + parameters: { + query?: never; + header?: never; + path: { + resume_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + github_callback_redirect_auth_github_callback_get: { + parameters: { + query?: { + code?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + github_callback_auth_github_callback_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GitHubCallbackRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TokenResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + github_login_auth_github_login_get: { + parameters: { + query?: { + return_to?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + github_login_url_auth_github_login_url_get: { + parameters: { + query?: { + return_to?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GitHubLoginUrlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + logout_auth_logout_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + me_auth_me_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TokenResponse"]; + }; + }; + }; + }; + refresh_auth_refresh_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TokenResponse"]; + }; + }; + }; + }; + healthcheck_health_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + }; + }; + handle_task_internal_tasks__task_type__post: { + parameters: { + query?: never; + header?: never; + path: { + task_type: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} From 76f407996fac436a997ed0672b2afed8f6439c8e Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 29 May 2026 22:27:49 +0900 Subject: [PATCH 2/4] CI fail fix --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b294172e..ea35f77f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,6 +249,10 @@ jobs: libpango-1.0-0 libpangoft2-1.0-0 libpangocairo-1.0-0 \ libglib2.0-0 libgobject-2.0-0 libffi-dev libcairo2 + - name: Create virtualenv + working-directory: backend + run: uv venv + - name: Install backend dependencies working-directory: backend run: uv pip install -r requirements.txt From 9a4e8f48b6c61e4c4a42f5305401b50aeac1e818 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 29 May 2026 22:35:29 +0900 Subject: [PATCH 3/4] codegen fail --- .github/workflows/ci.yml | 30 ++++++++++++++++++------------ backend/scripts/export_openapi.py | 18 +++++++++++++----- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea35f77f..dcb3efb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -283,13 +283,14 @@ jobs: deploy-frontend: runs-on: ubuntu-latest - needs: [detect-changes, test-frontend, test-backend] + needs: [detect-changes, test-frontend, test-backend, codegen-drift] if: | github.ref == 'refs/heads/dev' && github.event_name == 'push' && needs.detect-changes.outputs.app == 'true' && (needs.test-frontend.result == 'success' || needs.test-frontend.result == 'skipped') && - (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') + (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') && + (needs.codegen-drift.result == 'success' || needs.codegen-drift.result == 'skipped') timeout-minutes: 10 steps: - name: Checkout @@ -327,13 +328,14 @@ jobs: deploy-backend: runs-on: ubuntu-latest - needs: [detect-changes, test-frontend, test-backend] + needs: [detect-changes, test-frontend, test-backend, codegen-drift] if: | github.ref == 'refs/heads/dev' && github.event_name == 'push' && needs.detect-changes.outputs.app == 'true' && (needs.test-frontend.result == 'success' || needs.test-frontend.result == 'skipped') && - (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') + (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') && + (needs.codegen-drift.result == 'success' || needs.codegen-drift.result == 'skipped') timeout-minutes: 20 env: # 値は GitHub Repo Variables / Secrets で上書き可能。未設定時は既存値にフォールバック。 @@ -383,13 +385,14 @@ jobs: # stg フロントエンドデプロイ(stg ブランチ push 時) deploy-frontend-stg: runs-on: ubuntu-latest - needs: [detect-changes, test-frontend, test-backend] + needs: [detect-changes, test-frontend, test-backend, codegen-drift] if: | github.ref == 'refs/heads/stg' && github.event_name == 'push' && needs.detect-changes.outputs.app == 'true' && (needs.test-frontend.result == 'success' || needs.test-frontend.result == 'skipped') && - (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') + (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') && + (needs.codegen-drift.result == 'success' || needs.codegen-drift.result == 'skipped') timeout-minutes: 10 steps: - name: Checkout @@ -428,13 +431,14 @@ jobs: # stg バックエンドデプロイ(stg ブランチ push 時) deploy-backend-stg: runs-on: ubuntu-latest - needs: [detect-changes, test-frontend, test-backend] + needs: [detect-changes, test-frontend, test-backend, codegen-drift] if: | github.ref == 'refs/heads/stg' && github.event_name == 'push' && needs.detect-changes.outputs.app == 'true' && (needs.test-frontend.result == 'success' || needs.test-frontend.result == 'skipped') && - (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') + (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') && + (needs.codegen-drift.result == 'success' || needs.codegen-drift.result == 'skipped') timeout-minutes: 20 env: # 値は GitHub Repo Variables / Secrets で上書き可能。未設定時は既存値にフォールバック。 @@ -484,13 +488,14 @@ jobs: # prod フロントエンドデプロイ(main ブランチ push 時) deploy-frontend-prod: runs-on: ubuntu-latest - needs: [detect-changes, test-frontend, test-backend] + needs: [detect-changes, test-frontend, test-backend, codegen-drift] if: | github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.detect-changes.outputs.app == 'true' && (needs.test-frontend.result == 'success' || needs.test-frontend.result == 'skipped') && - (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') + (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') && + (needs.codegen-drift.result == 'success' || needs.codegen-drift.result == 'skipped') timeout-minutes: 10 steps: - name: Checkout @@ -529,13 +534,14 @@ jobs: # prod バックエンドデプロイ(main ブランチ push 時) deploy-backend-prod: runs-on: ubuntu-latest - needs: [detect-changes, test-frontend, test-backend] + needs: [detect-changes, test-frontend, test-backend, codegen-drift] if: | github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.detect-changes.outputs.app == 'true' && (needs.test-frontend.result == 'success' || needs.test-frontend.result == 'skipped') && - (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') + (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') && + (needs.codegen-drift.result == 'success' || needs.codegen-drift.result == 'skipped') timeout-minutes: 20 env: # 値は GitHub Repo Variables / Secrets で上書き可能。未設定時は既存値にフォールバック。 diff --git a/backend/scripts/export_openapi.py b/backend/scripts/export_openapi.py index 6bc8a189..536d4a50 100644 --- a/backend/scripts/export_openapi.py +++ b/backend/scripts/export_openapi.py @@ -11,6 +11,9 @@ 注意: - `app.main` を import するだけで FastAPI app は構築される(lifespan の bootstrap は実行されない)。 - ENVIRONMENT 未設定時は local 扱いとなり、INTERNAL_SECRET 等の必須チェックは走らない。 +- `app.db.database` は import 時に `build_sqlalchemy_database_url()` を評価するため + TURSO_DATABASE_URL が必須。OpenAPI 出力では DB 接続しない(URL を組み立てて engine を + 作るだけで connect しない)ので、未設定時はダミーの sqlite URL を充てて import を通す。 - diff の安定化のため `sort_keys=True` で出力する(openapi-typescript はキー順に依存しない)。 """ @@ -19,16 +22,21 @@ import sys from pathlib import Path -# import 時の設定チェックを避けるため、未設定なら local 環境として扱う。 -os.environ.setdefault("ENVIRONMENT", "local") - # scripts/ から見たプロジェクトルート(backend/)配下に openapi.json を出力する。 +# `app` パッケージ解決を cwd に依存させないため、env_keys の import より前に path を通す。 _BACKEND_DIR = Path(__file__).resolve().parent.parent _DEFAULT_OUTPUT = _BACKEND_DIR / "openapi.json" - -# backend ディレクトリを import パスに追加し、`app.main` を解決可能にする。 sys.path.insert(0, str(_BACKEND_DIR)) +from app.core import env_keys # noqa: E402 + +# import 時の設定チェックを避けるため、未設定なら local 環境として扱う。 +os.environ.setdefault(env_keys.ENVIRONMENT, "local") + +# `app.db.database` の import 時に URL 構築が走る。OpenAPI 出力は DB へ接続しないため、 +# CI など TURSO_DATABASE_URL 未設定の環境では接続されないダミー sqlite URL を充てる。 +os.environ.setdefault(env_keys.TURSO_DATABASE_URL, "file:openapi-export-dummy.sqlite") + def main() -> None: from app.main import app From 783bcce68ea630243fec595791cc5c616509add3 Mon Sep 17 00:00:00 2001 From: Wada Yusuke Date: Fri, 29 May 2026 22:42:56 +0900 Subject: [PATCH 4/4] =?UTF-8?q?GitHub=20Actions=20=E3=81=AE=E3=82=B5?= =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=A4=E3=83=81=E3=82=A7=E3=83=BC=E3=83=B3?= =?UTF-8?q?=E4=BF=9D=E8=AD=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcb3efb0..4c2ec8be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,10 +230,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + # サプライチェーン保護のためコミット SHA で pin する(ADR-0007 codegen-drift を先行対応)。 + # 型生成のドリフト検知のみで push はしないため認証情報は保持しない。 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4 with: version: "latest" enable-cache: true @@ -258,7 +262,7 @@ jobs: run: uv pip install -r requirements.txt - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "20" cache: npm