From 157ed22d2a6181e895e7273c848d118a974e49c6 Mon Sep 17 00:00:00 2001 From: Ryuuji Yoshimoto Date: Fri, 14 Aug 2026 23:40:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?lint=20=E3=82=92=20autopep8=20=E3=81=8B?= =?UTF-8?q?=E3=82=89=20ruff=20=E3=81=AB=E7=BD=AE=E3=81=8D=E6=8F=9B?= =?UTF-8?q?=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit autopep8 2.3.2 を外して ruff 0.16.3 に統一する。 autopep8 は formatter であって linter ではないため、このリポジトリには これまで lint に相当するものが実質存在しなかった。設定ファイル (setup.cfg / tox.ini など) も1つも無く、autopep8 はデフォルト設定 だったので、移植すべき設定も無い。 main.py の import 構造を手で直した。もともと import 群の途中に app = FastAPI() と app.add_middleware(...) が挟まっていて、その下に io と zipfile と ndc_parser の import が続く形だった (E402 が3件)。 すべての import を先頭にまとめ、app の生成と add_middleware の 順序関係は変えていない。 ruff check --fix が入れた変更は I001 と SIM117 で、SIM117 は 入れ子の with を1つにまとめるもの。意味は変わらない。 --unsafe-fixes は使っていない。 Dockerfile は --no-dev があるので ruff は既定グループの dev に置いた。 コメントの autopep8 への言及も直した。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BsERiWbzZwB6EdGoyyqcpT --- Dockerfile | 2 +- main.py | 154 ++++++++++++++++++++++++++----------------------- pyproject.toml | 25 +++++++- uv.lock | 50 ++++++++-------- validate.py | 13 +++-- 5 files changed, 140 insertions(+), 104 deletions(-) diff --git a/Dockerfile b/Dockerfile index be7cd74..90c0539 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.12.0 /uv /uvx /bin/ # Python依存関係のインストール # uv.lock をそのまま使うので requirements.txt は要らない。 # --frozen: uv.lock を書き換えず、ロックどおりに入れる(ずれていれば失敗する) -# --no-dev: autopep8 など開発用の依存は入れない +# --no-dev: ruff など開発用の依存は入れない # --no-install-project: このプロジェクト自体はパッケージとして入れない COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev --no-install-project diff --git a/main.py b/main.py index 7986ba0..3a637ff 100644 --- a/main.py +++ b/main.py @@ -1,112 +1,120 @@ -import json import codecs +import io +import json +import zipfile +import ndc_parser from fastapi import FastAPI -from fastapi.responses import HTMLResponse, ORJSONResponse from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, ORJSONResponse + app = FastAPI() app.add_middleware(CORSMiddleware, allow_origins=['*']) - -import io -import zipfile -import ndc_parser - -with zipfile.ZipFile("zips/ndc8.zip") as zfile: - with zfile.open("ndc8.ttl") as readfile: - ndc8_items_source = ndc_parser.parse("8", io.TextIOWrapper(readfile, "utf-8")) - ndc8_items = {} - for key, item in ndc8_items_source.items(): - i = item.copy() - del i["source"] - ndc8_items[key] = i - -with zipfile.ZipFile("zips/ndc9.zip") as zfile: - with zfile.open("ndc9.ttl") as readfile: - ndc9_items_source = ndc_parser.parse("9", io.TextIOWrapper(readfile, "utf-8")) - ndc9_items = {} - for key, item in ndc9_items_source.items(): - i = item.copy() - del i["source"] - ndc9_items[key] = i - - - -@app.get("/", - tags=["index"], - summary="トップページ", - description="トップページの表示", - response_description="トップページのHTMLを返す", - response_class=HTMLResponse +with zipfile.ZipFile('zips/ndc8.zip') as zfile, zfile.open('ndc8.ttl') as readfile: + ndc8_items_source = ndc_parser.parse('8', io.TextIOWrapper(readfile, 'utf-8')) + ndc8_items = {} + for key, item in ndc8_items_source.items(): + i = item.copy() + del i['source'] + ndc8_items[key] = i + +with zipfile.ZipFile('zips/ndc9.zip') as zfile, zfile.open('ndc9.ttl') as readfile: + ndc9_items_source = ndc_parser.parse('9', io.TextIOWrapper(readfile, 'utf-8')) + ndc9_items = {} + for key, item in ndc9_items_source.items(): + i = item.copy() + del i['source'] + ndc9_items[key] = i + + +@app.get( + '/', + tags=['index'], + summary='トップページ', + description='トップページの表示', + response_description='トップページのHTMLを返す', + response_class=HTMLResponse, ) async def index(): - with codecs.open("./templates/index.html", "r", "utf-8") as file: + with codecs.open('./templates/index.html', 'r', 'utf-8') as file: return file.read() -@app.get("/schema", - tags=["schema"], - summary="JSONスキーマ", - description="JSONスキーマの取得", - response_description="JSONスキーマを返す" +@app.get( + '/schema', + tags=['schema'], + summary='JSONスキーマ', + description='JSONスキーマの取得', + response_description='JSONスキーマを返す', ) async def schema(): - with codecs.open("jsonschema.json", "r", "utf-8") as file: + with codecs.open('jsonschema.json', 'r', 'utf-8') as file: json_schema = json.load(file) return ORJSONResponse(json_schema, headers={'Access-Control-Allow-Origin': '*'}) -@app.get("/ndc8.json", - tags=["ndc8"], - summary="NDC8", - description="NDC8の全データの取得", - response_description="NDC8の全データを返す" +@app.get( + '/ndc8.json', + tags=['ndc8'], + summary='NDC8', + description='NDC8の全データの取得', + response_description='NDC8の全データを返す', ) async def ndc8_json(): return ORJSONResponse(ndc8_items, headers={'Access-Control-Allow-Origin': '*'}) -@app.get("/ndc8/", - tags=["ndc8"], - summary="NDC8トップ", - description="NDC8のトップの取得", - response_description="NDC8のトップを返す" + +@app.get( + '/ndc8/', + tags=['ndc8'], + summary='NDC8トップ', + description='NDC8のトップの取得', + response_description='NDC8のトップを返す', ) async def ndc8_top(): - return ORJSONResponse(ndc8_items_source[""], headers={'Access-Control-Allow-Origin': '*'}) + return ORJSONResponse(ndc8_items_source[''], headers={'Access-Control-Allow-Origin': '*'}) + -@app.get("/ndc8/{ndc}", - tags=["ndc8"], - summary="NDC8分類項目", - description="NDC8の分類項目の取得", - response_description="NDC8の分類項目を返す" +@app.get( + '/ndc8/{ndc}', + tags=['ndc8'], + summary='NDC8分類項目', + description='NDC8の分類項目の取得', + response_description='NDC8の分類項目を返す', ) async def ndc8(ndc: str): return ORJSONResponse(ndc8_items_source[ndc], headers={'Access-Control-Allow-Origin': '*'}) -@app.get("/ndc9.json", - tags=["ndc9"], - summary="NDC9", - description="NDC9の全データの取得", - response_description="NDC9の全データを返す" + +@app.get( + '/ndc9.json', + tags=['ndc9'], + summary='NDC9', + description='NDC9の全データの取得', + response_description='NDC9の全データを返す', ) async def ndc9_json(): return ORJSONResponse(ndc9_items, headers={'Access-Control-Allow-Origin': '*'}) -@app.get("/ndc9/", - tags=["ndc9"], - summary="NDC9トップ", - description="NDC9のトップの取得", - response_description="NDC9のトップを返す" + +@app.get( + '/ndc9/', + tags=['ndc9'], + summary='NDC9トップ', + description='NDC9のトップの取得', + response_description='NDC9のトップを返す', ) async def ndc9_top(): - return ORJSONResponse(ndc9_items_source[""], headers={'Access-Control-Allow-Origin': '*'}) + return ORJSONResponse(ndc9_items_source[''], headers={'Access-Control-Allow-Origin': '*'}) -@app.get("/ndc9/{ndc}", - tags=["ndc9"], - summary="NDC9分類項目", - description="NDC9の分類項目の取得", - response_description="NDC9の分類項目を返す" + +@app.get( + '/ndc9/{ndc}', + tags=['ndc9'], + summary='NDC9分類項目', + description='NDC9の分類項目の取得', + response_description='NDC9の分類項目を返す', ) async def ndc9(ndc: str): return ORJSONResponse(ndc9_items_source[ndc], headers={'Access-Control-Allow-Origin': '*'}) - diff --git a/pyproject.toml b/pyproject.toml index 101a8e5..21fcc69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,30 @@ dependencies = [ [dependency-groups] dev = [ - "autopep8>=2.3.2", "jsonschema>=4.26.0", "requests>=2.34.2", + "ruff>=0.16.3", +] + +[tool.ruff] +target-version = "py314" +line-length = 120 + +[tool.ruff.format] +quote-style = "single" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify + "ASYNC", # flake8-async + "RUF100", # 効かなくなった noqa を検出する +] +ignore = [ + "B008", # FastAPI の Depends() を既定値に置くパターン ] diff --git a/uv.lock b/uv.lock index 5983548..e099fb9 100644 --- a/uv.lock +++ b/uv.lock @@ -41,18 +41,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "autopep8" -version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycodestyle" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, -] - [[package]] name = "certifi" version = "2026.7.22" @@ -206,9 +194,9 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "autopep8" }, { name = "jsonschema" }, { name = "requests" }, + { name = "ruff" }, ] [package.metadata] @@ -223,9 +211,9 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "autopep8", specifier = ">=2.3.2" }, { name = "jsonschema", specifier = ">=4.26.0" }, { name = "requests", specifier = ">=2.34.2" }, + { name = "ruff", specifier = ">=0.16.3" }, ] [[package]] @@ -268,15 +256,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - [[package]] name = "pydantic" version = "2.13.4" @@ -427,6 +406,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, ] +[[package]] +name = "ruff" +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, +] + [[package]] name = "starlette" version = "1.3.1" diff --git a/validate.py b/validate.py index 1be1ff6..fb1ae24 100644 --- a/validate.py +++ b/validate.py @@ -1,13 +1,14 @@ +import codecs +import json + import jsonschema import requests -import json -import codecs -if __name__ == "__main__": - url = "http://127.0.0.1:8000/ndc9.json" - r = requests.get(url, headers={"content-type": "application/json"}) +if __name__ == '__main__': + url = 'http://127.0.0.1:8000/ndc9.json' + r = requests.get(url, headers={'content-type': 'application/json'}) data = r.json() - with codecs.open("jsonschema.json", "r", "utf-8") as file: + with codecs.open('jsonschema.json', 'r', 'utf-8') as file: json_schema = json.load(file) for item in data.values(): jsonschema.validate(item, json_schema) From d242397f7db70d71f48131e4b4dcf9101f3b66ae Mon Sep 17 00:00:00 2001 From: Ryuuji Yoshimoto Date: Fri, 14 Aug 2026 23:40:54 +0900 Subject: [PATCH 2/2] =?UTF-8?q?CI=20=E3=81=A8=20Dependabot=20=E3=81=A8=20z?= =?UTF-8?q?izmor=20=E3=82=92=E6=96=B0=E8=A8=AD=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit これまで .github ディレクトリそのものが存在せず、自動検証も依存更新も まったく動いていなかった。 CI は2ジョブ。静的解析ジョブは ruff check と ruff format --check、 それに読み込み確認。main.py は import するだけで zips を展開して NDC を全件パースする (NDC8 が 10340件、NDC9 が 12388件) ので、 これが実質のスモークテストになる。テストが無いため唯一の実行時チェック。 Docker ジョブは本番と同じ gunicorn と UvicornWorker で起動して /ndc9/123 が応答するところまで見る。Dockerfile が外部認証を必要と しない自己完結した構成なので、起動確認まで入れられる。 uv sync は --frozen ではなく --locked を使う。--frozen はロックの ずれを検出しないため (uv 0.12 のヘルプに "Instead of checking if the lockfile is up-to-date" と明記されている)。Dockerfile 側は --frozen なので、ずれの検出はこの CI でしか行われない。 CI 完了ジョブは、将来ブランチ保護を掛けるときに必須チェックを これ1つだけ指定できるようにするため。 zizmor.yml の unpinned-uses ポリシーには ndc-dev/* を足した。 CALIL/* も残しているのは、ワークフローを org 間でコピーしても判定が 変わらないようにするため。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BsERiWbzZwB6EdGoyyqcpT --- .github/dependabot.yml | 121 ++++++++++++++++++ .github/workflows/actions-security-check.yml | 77 ++++++++++++ .github/workflows/ci.yml | 125 +++++++++++++++++++ .github/zizmor.yml | 32 +++++ 4 files changed, 355 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/actions-security-check.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/zizmor.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a8309ac --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,121 @@ +# Dependabot 設定 +# https://docs.github.com/code-security/dependabot/working-with-dependabot/dependabot-options-reference +# +# これまで dependabot.yml が無く、Dependabot alerts 起点のセキュリティ更新しか +# 動いていなかったため、バージョン更新も定期的に回すようにする。 +# +# 方針: minor/patch はグループ化して 1 PR にまとめ、major は個別 PR で判断する。 +# リリース直後の不具合や yank を踏まないよう cooldown で寝かせてから PR を出す。 + +version: 2 + +updates: + # --------------------------------------------------------------------------- + # Python (uv: pyproject.toml + uv.lock) + # --------------------------------------------------------------------------- + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Tokyo" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "python" + commit-message: + prefix: "chore(deps)" + cooldown: + default-days: 7 + semver-major-days: 30 + groups: + python-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + + # --------------------------------------------------------------------------- + # GitHub Actions (.github/workflows/) + # --------------------------------------------------------------------------- + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Tokyo" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ci(deps)" + cooldown: + default-days: 7 + groups: + github-actions: + patterns: + - "*" + update-types: + - "minor" + - "patch" + + # --------------------------------------------------------------------------- + # Docker ベースイメージ + # タグベースのため semver 単位の cooldown (semver-major-days 等) は指定できない + # --------------------------------------------------------------------------- + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Tokyo" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "build(deps)" + cooldown: + default-days: 7 + groups: + container-images: + patterns: + - "*" + update-types: + - "minor" + - "patch" + ignore: + # Python 3.15 のプレリリースは除外し、正式版 3.15 は許可する + - dependency-name: "python" + versions: + - ">= 3.15.a, < 3.15" + + # --------------------------------------------------------------------------- + # Terraform (provider / module のバージョン制約) + # --------------------------------------------------------------------------- + - package-ecosystem: "terraform" + directory: "/terraform" + schedule: + interval: "monthly" + time: "09:00" + timezone: "Asia/Tokyo" + open-pull-requests-limit: 2 + labels: + - "dependencies" + - "terraform" + commit-message: + prefix: "build(deps)" + cooldown: + default-days: 14 + groups: + terraform-providers: + patterns: + - "*" + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/actions-security-check.yml b/.github/workflows/actions-security-check.yml new file mode 100644 index 0000000..72c6298 --- /dev/null +++ b/.github/workflows/actions-security-check.yml @@ -0,0 +1,77 @@ +name: Actionsのセキュリティチェック + +# ワークフロー自体に潜むセキュリティ上の問題を zizmor で検査します。 +# 対象は .github/ 配下の変更があったときだけです。 +# +# 落ちたときの読み方(よく出るもの): +# unpinned-uses サードパーティの uses: をタグでなくコミットSHAで指定してください +# 例) foo/bar@v1 → foo/bar@<40桁SHA> # v1 +# artipacked actions/checkout に persist-credentials: false を足してください +# (checkout は既定で書き込み権限つきトークンを .git に残すため) +# excessive-permissions permissions: を必要最小限に絞ってください +# template-injection テンプレート式を run: に直接書かず、env: 経由で渡してください +# +# 個別に許容したい指摘がある場合は、その行に次のコメントを付けられます: +# # zizmor: ignore[ルール名] +# +# 詳細な説明: https://docs.zizmor.sh/audits/ + +# 注意: このファイルの中に、テンプレート式の記法をリテラルで書かないこと。 +# 解説目的でも Actions がそれを式として評価しようとし、ワークフロー全体が +# 起動できなくなる(ジョブが1つも作られず failure になる)。 + +on: + push: + branches: [master] + paths: + - '.github/**' + pull_request: + paths: + - '.github/**' + workflow_dispatch: + +permissions: {} + +jobs: + zizmor: + name: zizmor による静的解析 + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run zizmor + id: zizmor + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + inputs: .github/workflows/ + # SARIF (advanced-security) は finding があっても失敗しないためゲートにならない。 + # annotations は zizmor の終了コードをそのまま返すのでゲートとして機能する + advanced-security: false + annotations: true + min-severity: low + + - name: 失敗したときの案内 + if: failure() + run: | + { + echo "## Actionsのセキュリティチェックで指摘が出ました" + echo "" + echo "上の **Annotations** に、該当する行と理由が出ています。" + echo "" + echo "| ルール | 直し方 |" + echo "|---|---|" + echo "| \`unpinned-uses\` | サードパーティの \`uses:\` をコミットSHAで指定する(例: \`foo/bar@ # v1\`)|" + echo "| \`artipacked\` | \`actions/checkout\` に \`persist-credentials: false\` を足す |" + echo "| \`excessive-permissions\` | \`permissions:\` を必要な範囲まで絞る |" + echo "| \`template-injection\` | テンプレート式を \`run:\` に直書きせず \`env:\` 経由で渡す |" + echo "" + echo "意図的にその書き方をしている場合は、該当行に \`# zizmor: ignore[ルール名]\` を付けると除外できます。" + echo "" + echo "ルールの詳しい説明: https://docs.zizmor.sh/audits/" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f294278 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,125 @@ +name: CI + +# 依存を更新したときに壊れていないかを見るためのチェック。 +# Dependabot の PR にもこれが走るので、更新して安全かどうかが分かる。 + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: {} + +# 同じブランチで新しい push があったら、古い実行は止める +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: 静的解析とスモークテスト + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + # Python 自体も uv が pyproject.toml の requires-python を見て用意する + - name: Setup uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: true + cache-dependency-glob: uv.lock + + # --locked は「uv.lock が pyproject.toml と揃っていること」も検査する。 + # --frozen は lock の中身をそのまま使うだけで、ずれを検出しない。 + # Dockerfile は --frozen なので、ずれの検出はここでしか行われない。 + - name: Install + run: uv sync --locked + + # --output-format=github を付けると、PR の該当行に注釈が出る + - name: Lint + run: uv run ruff check --output-format=github . + + - name: フォーマットの確認 + if: ${{ !cancelled() }} + run: uv run ruff format --check . + + # main.py は読み込むだけで zips/ を展開して NDC を全件パースするので、 + # import できること自体が ndc-parser と FastAPI のスモークテストになる。 + # テストが無いためこれが唯一の実行時チェック。 + - name: 読み込み確認 + if: ${{ !cancelled() }} + run: uv run python -c "import main; print(len(main.ndc8_items), len(main.ndc9_items))" + + docker: + name: Dockerイメージ + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Buildxをセットアップ + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + + - name: イメージをビルド + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + context: . + push: false + load: true + tags: ndc-dev-api:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + # 本番と同じ gunicorn + UvicornWorker で実際に応答するところまで見る。 + # 起動時に NDC を展開するので、応答までしばらくかかる。 + - name: 起動確認 (スモークテスト) + run: | + docker run -d --name ndc-dev-api-ci -p 8080:8080 ndc-dev-api:ci + for i in $(seq 1 30); do + if curl -fsS http://localhost:8080/ndc9/123 -o /dev/null; then + echo "分類項目の応答を確認しました" + exit 0 + fi + sleep 2 + done + echo "::error::コンテナが起動しませんでした" + docker logs ndc-dev-api-ci + exit 1 + + - name: コンテナを片付ける + if: always() + run: docker rm -f ndc-dev-api-ci || true + + done: + name: CI 完了 + # ブランチ保護の必須チェックにはこのジョブだけを指定する。 + # ジョブを増減しても保護の設定を変えずに済む。 + needs: [check, docker] + if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + steps: + - name: 各ジョブの結果を確認 + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + run: | + echo "各ジョブの結果: $RESULTS" + for r in $RESULTS; do + if [ "$r" != "success" ]; then + echo "::error::成功しなかったジョブがあります" + exit 1 + fi + done diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..60447e2 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,32 @@ +# Actionsのセキュリティチェック(zizmor)の設定 +# +# unpinned-uses: uses: の書き方をどこまで厳しくするか +# +# GitHub 公式(actions/*)と自社(ndc-dev/* と CALIL/*)は、タグやブランチでの +# 指定を許容します。これらの参照先を差し替えられるのは GitHub 自身か自社だけで、 +# 第三者が公開するアクションとはリスクの質が違うためです。 +# +# CALIL/* を含めているのは、この設定を CALIL org のリポジトリと共通の形に +# 保っておくためです(ワークフローを org 間でコピーしても判定が変わらない)。 +# +# それ以外はすべてコミットSHAでの固定を必須にします。第三者のアクションは +# タグを後から別のコミットに付け替えられるため、タグ指定だと +# 「レビューしたコードとは違うものが動く」ことが起こりえます。 +# +# 第三者のアクションを追加するときの書き方: +# - uses: foo/bar@0123456789abcdef0123456789abcdef01234567 # v1.2.3 +# SHA はタグのページか `gh api repos/foo/bar/commits/v1.2.3 --jq .sha` で取れます。 +# Dependabot はこの形式を認識し、更新時に SHA とコメントの両方を書き換えます。 +# +# 個別に許容したい指摘は、ワークフロー側の該当行に次のコメントを付けて除外します: +# # zizmor: ignore[ルール名] +# +# 設定の詳細: https://docs.zizmor.sh/configuration/ +rules: + unpinned-uses: + config: + policies: + "actions/*": ref-pin + "ndc-dev/*": ref-pin + "CALIL/*": ref-pin + "*": hash-pin