From a8ba07284548e981e1a75600f255e3e066061750 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Tue, 25 Aug 2026 00:53:35 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat(tools):=20=E5=AE=9F=E6=B8=AC=E3=81=AE?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=E3=81=A8=E3=83=AF=E3=83=BC=E3=82=AF=E3=83=95?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E6=B8=AC=E5=AE=9A=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=80=81=E9=80=9A=E3=81=97=E5=AE=9F=E8=A1=8C=E3=82=92?= =?UTF-8?q?=E3=83=84=E3=83=BC=E3=83=AB=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fixtures.py にワークフロー activity の作成を追加。自分所有/他人所有の2件を作る。 no.601-636 は を要求するため、これが無いと probe が 未解決プレースホルダで skip していた。 - probe_ci.py が /// を解決し、 activity は自分所有と他人所有の両方で測る(所有者チェックの検証のため)。 - apply_probe_results.py を新規追加。probe.json を台帳の dynamic_verified に 反映する。既定では空欄の行だけを埋め、★実証など人手で精査した既存値は残す。 - remeasure.sh を新規追加。フィクスチャ投入 → 実測 → 反映 → 再計算 を通しで行う。 --all-unmeasured / --nos / --read-only に対応。 - scripts/README.md にケース2c(実測の測り直し)と、測定できる範囲を追記。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HXo9u6PoTf6VRKr3aiGvZ3 --- tools/api-inventory/scripts/README.md | 38 ++++++ .../scripts/apply_probe_results.py | 110 ++++++++++++++++++ tools/api-inventory/scripts/fixtures.py | 43 ++++++- tools/api-inventory/scripts/probe_ci.py | 25 +++- tools/api-inventory/scripts/remeasure.sh | 60 ++++++++++ 5 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 tools/api-inventory/scripts/apply_probe_results.py create mode 100755 tools/api-inventory/scripts/remeasure.sh diff --git a/tools/api-inventory/scripts/README.md b/tools/api-inventory/scripts/README.md index 687feed8d2..1781e0bf9a 100644 --- a/tools/api-inventory/scripts/README.md +++ b/tools/api-inventory/scripts/README.md @@ -140,6 +140,42 @@ python3 tools/api-inventory/scripts/build_checklist.py 判定の入力側(`security_finding` / `dynamic_verified` / `data_op` / `deprecated`)を 直すか、`prioritize.py` のルールを変える。 +## ケース2c: 実測(dynamic_verified)を測り直す + +`remeasure.sh` が「フィクスチャ投入 → 実測 → 台帳反映 → 再計算」を通しで行う。 + +```bash +export WEKO_API_INVENTORY_DIR=/path/to/weko-secret +./install.sh # スタックを起動しておく + +tools/api-inventory/scripts/remeasure.sh # 未測定の P0/P1 を測る +tools/api-inventory/scripts/remeasure.sh --all-unmeasured # 未測定を全件 +tools/api-inventory/scripts/remeasure.sh --nos 607,618 # no を直接指定 +tools/api-inventory/scripts/remeasure.sh --read-only # GET/HEAD のみ(データを変えない) +``` + +**既定では書き込み系も実測するため実機のデータが変わる。** 著者DB・サイト情報・ +ワークフローの状態などが書き換わるので、使い捨て環境で回すか、終了後に +`./install.sh` で作り直すこと。`--read-only` なら副作用はない。 + +反映は `apply_probe_results.py` が行い、**`dynamic_verified` が空の行だけ**を埋める。 +既存の実測値(★実証など人手で精査した記述を含む)は残す。差し替えるときは +`--overwrite` を明示する。 + +### 測定できる範囲 + +`fixtures.py` が作る対象で決まる。 + +| プレースホルダ | 解決先 | +|---|---| +| `` `` | 公開アイテム / 非公開アイテムの**両方**で測る | +| `` `` `` | 非公開アイテムに添付したファイル(IIIF の三つ組も) | +| `` `` | ワークフロー activity を**自分所有/他人所有の両方**で測る | +| `` `` `` | フィクスチャで作成したもの | + +解決できないプレースホルダ(`` など)を含む行は `未解決プレースホルダ` として +skip し、台帳には反映しない。**「測っていない」ことが分かる状態を保つ**ため。 + ## ケース3: WEKO3 のバージョンアップに伴う全面更新 ```bash @@ -176,6 +212,8 @@ git push origin main --follow-tags | `prioritize.py` | full.tsv | full.tsv の 58-59, 65列 + 末尾列順の正規化 | | `build_checklist.py` | full.tsv | **`weko3_api_list.tsv` を全体再生成** | | `add_row.py` | `api_snapshot.json` + git | full.tsv に新規行の雛形を追記(`--append`) | +| `apply_probe_results.py` | probe.json | full.tsv の `dynamic_verified`(空欄のみ) | +| `remeasure.sh` | — | 上記を通しで実行するドライバ | | `add_cols.py` / `add_ssrf_redirect.py` / `add_idempotency.py` / `add_dataop4.py` / `add_authmech.py` | full.tsv + 実装ソース | full.tsv の**空欄/TODO セルのみ**を機械付与 | `test_coverage.py` → `prioritize.py` → `build_checklist.py` は**何度流しても結果が変わらない** diff --git a/tools/api-inventory/scripts/apply_probe_results.py b/tools/api-inventory/scripts/apply_probe_results.py new file mode 100644 index 0000000000..c7e801b9e4 --- /dev/null +++ b/tools/api-inventory/scripts/apply_probe_results.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +"""probe_ci.py の実測結果を台帳の dynamic_verified に反映する。 + + python3 apply_probe_results.py probe.json + python3 apply_probe_results.py probe.json --overwrite # 既存の実測値も差し替える + +既定では **`dynamic_verified` が空/`-` の行だけ**を埋める。台帳の既存値には +★実証など人手で精査した記述が含まれており、機械生成の要約で上書きすると +情報が落ちるため(add_cols.py 等と同じ方針)。 + +書式は既存の台帳に合わせる: + [実測·2026-08-25] 未認証で到達 | anon=200(到達); general=200(到達); ... +対象を変えて複数回測った行(公開/非公開、activity の自分/他人)は、 +それぞれを ` || ` で連結する。 +""" +import argparse +import datetime +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from paths import data_path # noqa: E402 + +IDENT = ['anon', 'general', 'contributor', 'comadmin', 'repoadmin', 'sysadmin'] + + +def summarize(obs): + """identity 別の判定から、台帳の見出し語を決める。""" + def v(k): + return (obs.get(k) or {}).get('verdict') + if v('anon') == '到達': + return '未認証で到達' + if v('general') == '到達' or v('contributor') == '到達': + return 'ログインのみで到達' + if v('comadmin') == '到達' or v('repoadmin') == '到達': + return '管理者で到達' + if v('sysadmin') == '到達': + return 'sysadminのみ到達' + if any(v(k) == '到達' for k in IDENT): + return '一部で到達' + if all(v(k) in ('遮断', None) for k in IDENT): + return '測定範囲では遮断' + return '判定不能' + + +def render(results, date): + """no ごとに dynamic_verified 用の文字列を作る。""" + by = {} + for r in results: + if r.get('status') != 'measured': + continue + by.setdefault(r['no'], []).append(r) + out = {} + for no, rs in by.items(): + parts = [] + for r in rs: + obs = r['observed'] + detail = '; '.join(f"{k}={obs[k]['code']}({obs[k]['verdict']})" + for k in IDENT if k in obs) + tgt = r.get('target', '-') + label = summarize(obs) + head = f"{label}" if tgt in ('-', None) else f"{label}[{tgt}]" + parts.append(f"{head} | {detail}") + out[no] = f"[実測·{date}] " + ' || '.join(parts) + return out + + +def main(): + p = argparse.ArgumentParser(description='実測結果を台帳に反映する') + p.add_argument('probe_json') + p.add_argument('--full', default=None) + p.add_argument('--date', default=None, help='既定: probe.json の更新日') + p.add_argument('--overwrite', action='store_true', + help='既存の dynamic_verified も差し替える(既定は空欄のみ)') + p.add_argument('--dry-run', action='store_true') + a = p.parse_args() + full = a.full or data_path('weko3_api_list_full.tsv') + date = a.date or datetime.date.fromtimestamp( + os.path.getmtime(a.probe_json)).isoformat() + + d = json.load(open(a.probe_json, encoding='utf-8')) + rendered = render(d.get('results', []), date) + + lines = open(full, encoding='utf-8').read().rstrip('\n').split('\n') + hdr = lines[0].split('\t') + H = {n: i for i, n in enumerate(hdr)} + i_no, i_dyn = H['no'], H['dynamic_verified'] + out = [lines[0]] + filled = kept = 0 + for raw in lines[1:]: + c = raw.split('\t') + v = rendered.get(c[i_no]) + if v: + if c[i_dyn] in ('', '-') or a.overwrite: + c[i_dyn] = v.replace('\t', ' ') + filled += 1 + else: + kept += 1 + out.append('\t'.join(c)) + if not a.dry_run: + open(full, 'w', encoding='utf-8').write('\n'.join(out) + '\n') + print(f'{"(dry-run) " if a.dry_run else ""}{full}') + print(f' 実測がある行: {len(rendered)}') + print(f' dynamic_verified を埋めた: {filled}') + print(f' 既存値を残した(--overwrite で差し替え可): {kept}') + + +if __name__ == '__main__': + main() diff --git a/tools/api-inventory/scripts/fixtures.py b/tools/api-inventory/scripts/fixtures.py index d622820886..ca79b41919 100644 --- a/tools/api-inventory/scripts/fixtures.py +++ b/tools/api-inventory/scripts/fixtures.py @@ -38,7 +38,7 @@ PASSWORD = "%(password)s" OUT = {"password": PASSWORD, "users": {}, "records": {}, "file": {}, "index": None, "token": None, "community": None, "group": None, - "errors": []} + "activity": None, "activity_other": None, "errors": []} # 1x1 透明PNG(75B)。ファイル露出検証はバイト列が取れれば十分 PNG = base64.b64decode( @@ -279,6 +279,46 @@ def _group(): OUT["group"] = {"id": g.id, "name": name} +# ---------------------------------------------------------------- ワークフロー +@step("activity") +def _activity(): + """ワークフローの activity を2件作る。 + + no.601-636 のワークフロー系は `` を要求するため、これが無いと + probe が「未解決プレースホルダ」で skip する。所有者チェック欠落の検証には + **他人所有の activity** が要るので2件作る。 + """ + from weko_workflow.api import WorkActivity + from weko_workflow.models import Activity as _Act, WorkFlow + + wf = WorkFlow.query.first() + if wf is None: + raise RuntimeError("ワークフロー定義が無い(install.sh の defaultworkflow.sql 未投入)") + + def ensure(title, uid, key): + act = _Act.query.filter_by(title=title).first() + if act is None: + act = WorkActivity().init_activity({ + "workflow_id": wf.id, + "flow_id": wf.flow_id, + "itemtype_id": wf.itemtype_id, + "activity_login_user": uid, + "activity_update_user": uid, + "title": title, + }) + db.session.flush() + OUT[key] = {"activity_id": act.activity_id, + "owner": uid, + "action_id": act.action_id, + "workflow_id": wf.id, + "flow_id": str(wf.flow_id)} + + own = OUT["users"].get("contributor@example.org", {}).get("id", 3) + other = OUT["users"].get("user@example.org", {}).get("id", 4) + ensure("APIInventory 検証用(自分)", own, "activity") + ensure("APIInventory 検証用(他人所有)", other, "activity_other") + + # ---------------------------------------------------------------- ES反映 @step("reindex") def _reindex(): @@ -342,6 +382,7 @@ def main(): f"token={'あり' if data['token'] else 'なし'} " f"community={'あり' if data['community'] else 'なし'} " f"group={'あり' if data['group'] else 'なし'} " + f"activity={'あり' if data.get('activity') else 'なし'} " f"errors={len(data['errors'])}") if data['errors']: print(' ※ 失敗した投入があります。probe の測定範囲が狭まります。') diff --git a/tools/api-inventory/scripts/probe_ci.py b/tools/api-inventory/scripts/probe_ci.py index 7fbd009625..2f1a4590dd 100644 --- a/tools/api-inventory/scripts/probe_ci.py +++ b/tools/api-inventory/scripts/probe_ci.py @@ -116,18 +116,23 @@ def build_resolver(fx): (r'<[^>]*group_id[^>]*>', str((fx.get('group') or {}).get('id', ''))), (r'<[^>]*user_id[^>]*>', str(other.get('owner', ''))), (r'<[^>]*api_code[^>]*>', 'crf'), + # ワークフロー(no.601-636)。fixtures.py が作った activity を使う + (r'<[^>]*activity_id[^>]*>', (fx.get('activity') or {}).get('activity_id', '')), + (r'<[^>]*action_id[^>]*>', str((fx.get('activity') or {}).get('action_id', ''))), + (r'<[^>]*workflow_id[^>]*>', str((fx.get('activity') or {}).get('workflow_id', ''))), + (r'<[^>]*flow_id[^>]*>', str((fx.get('activity') or {}).get('flow_id', ''))), # IIIF Image API のパラメータ(no.34) (r'<[^>]*region[^>]*>', 'full'), (r'<[^>]*size[^>]*>', 'full'), (r'<[^>]*rotation[^>]*>', '0'), (r'<[^>]*quality[^>]*>', 'default'), (r'<[^>]*image_format[^>]*>', 'png'), - # ワークフロー(activity)はフィクスチャに無いので解決しない → skip 扱い ] return table PID_PAT = r'<[^>]*(pid_value|recid|pid\()[^>]*>' +ACT_PAT = r'<[^>]*activity_id[^>]*>' def resolve_variants(uri, table, fx): @@ -148,6 +153,24 @@ def resolve_variants(uri, table, fx): continue base = re.sub(pat, val, base) + # activity は「自分所有」と「他人所有」の両方で測る(所有者チェックの検証) + if re.search(ACT_PAT, uri): + out = [] + for label, key in (('own', 'activity'), ('other', 'activity_other')): + a = fx.get(key) or {} + if not a.get('activity_id'): + continue + u = re.sub(ACT_PAT, a['activity_id'], uri) + for pat, val in table: + if val == '__VERSION__': + val = ver + if not val or pat == ACT_PAT: + continue + u = re.sub(pat, val, u) + out.append((f'activity:{label}', u)) + if out: + return out + if not re.search(PID_PAT, uri): return [('-', base)] out = [] diff --git a/tools/api-inventory/scripts/remeasure.sh b/tools/api-inventory/scripts/remeasure.sh new file mode 100755 index 0000000000..7c19ea3db5 --- /dev/null +++ b/tools/api-inventory/scripts/remeasure.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# 未測定/指定範囲のエンドポイントを実機で測り直し、台帳に反映する。 +# +# ./remeasure.sh # dynamic_verified が空の P0/P1 を測る +# ./remeasure.sh --all-unmeasured # 未測定を全件 +# ./remeasure.sh --nos 607,618 # no を直接指定 +# ./remeasure.sh --read-only # GET/HEAD だけ(データを変えない) +# +# 前提: $WEKO_API_INVENTORY_DIR(台帳) と WEKO スタックが起動していること。 +# +# 【注意】既定では書き込み系も実測するため **実機のデータが変わります**。 +# 著者DB・サイト情報・ワークフローの状態などが書き換わるので、 +# 使い捨て環境で回すか、終了後に環境を作り直してください。 +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +: "${WEKO_API_INVENTORY_DIR:?台帳の場所を指定してください (export WEKO_API_INVENTORY_DIR=...)}" +: "${WEKO_ROOT:=$(cd "$HERE/../../.." && pwd)}" +export WEKO_ROOT +WORK="${WORK:-$(mktemp -d)}" +mkdir -p "$WORK" +TSV="$WEKO_API_INVENTORY_DIR/weko3_api_list.tsv" + +SCOPE=p01; WRITES=--allow-writes; NOS="" +while [ $# -gt 0 ]; do + case "$1" in + --all-unmeasured) SCOPE=all ;; + --nos) NOS="$2"; SCOPE=nos; shift ;; + --read-only) WRITES="" ;; + *) echo "不明な引数: $1" >&2; exit 2 ;; + esac; shift +done + +echo "== 1. フィクスチャ投入 ==" +python3 "$HERE/fixtures.py" --out "$WORK/fixtures.json" + +echo "== 2. 対象の抽出 ==" +case "$SCOPE" in + p01) awk -F'\t' 'NR>1 && $25 ~ /^P[01]/ && $16=="-" {print $1}' "$TSV" > "$WORK/nos.txt" ;; + all) awk -F'\t' 'NR>1 && $16=="-" {print $1}' "$TSV" > "$WORK/nos.txt" ;; + nos) tr ',' '\n' <<< "$NOS" > "$WORK/nos.txt" ;; +esac +echo " 対象: $(wc -l < "$WORK/nos.txt") 件" +[ -s "$WORK/nos.txt" ] || { echo " 対象なし。終了"; exit 0; } + +echo "== 3. 実測 ==" +python3 "$HERE/probe_ci.py" --fixtures "$WORK/fixtures.json" \ + --only "$WORK/nos.txt" $WRITES --out "$WORK/probe.json" | tail -20 + +echo "== 4. 台帳へ反映 ==" +python3 "$HERE/apply_probe_results.py" "$WORK/probe.json" + +echo "== 5. 再計算 ==" +python3 "$HERE/prioritize.py" | tail -12 +python3 "$HERE/build_checklist.py" > /dev/null +python3 "$HERE/reconcile.py" --gate > /dev/null && echo " reconcile: 差分なし" + +echo +echo "成果物: $WORK/probe.json (明細)" +echo "※ 実機のデータが変わっています。継続利用する場合は fixtures.py が冪等なので" +echo " 再投入で復旧しますが、著者DB等の副作用は残ります。" From 0f4c499053cc3c982a275eab73a6217cfdcff392 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Tue, 25 Aug 2026 02:36:04 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat(tools):=20=E3=83=95=E3=82=A3=E3=82=AF?= =?UTF-8?q?=E3=82=B9=E3=83=81=E3=83=A3=E3=81=AE=E8=A7=A3=E6=B1=BA=E7=AF=84?= =?UTF-8?q?=E5=9B=B2=E3=82=92=E5=BA=83=E3=81=92=E3=82=8B(=E6=9C=AA?= =?UTF-8?q?=E8=A7=A3=E6=B1=BA=E3=83=97=E3=83=AC=E3=83=BC=E3=82=B9=E3=83=9B?= =?UTF-8?q?=E3=83=AB=E3=83=80=E3=81=AE=E5=89=8A=E6=B8=9B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 実測で skip していた行の原因を集計し、フィクスチャ側で解決できるようにした。 - 著者(authors)を1件作成。初期状態で0件のため が解決できなかった。 - 既存の初期データのIDを参照して fixtures.json に載せる(作成はしない)。 item_type_id / property_id / mail_template_id / prefix_id / affiliation_id / facet_search_id / oauth_client_id / oauth_token_id。install.sh が投入する データなので、フィクスチャで作ると二重になる。 - probe_ci.py が上記と定数(/=ja、=1)を解決。 CI も同じ fixtures.py / probe_ci.py を呼ぶため、拡張はそのまま CI に反映される。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HXo9u6PoTf6VRKr3aiGvZ3 --- tools/api-inventory/scripts/README.md | 19 +++++++---- tools/api-inventory/scripts/fixtures.py | 45 ++++++++++++++++++++++++- tools/api-inventory/scripts/probe_ci.py | 14 ++++++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/tools/api-inventory/scripts/README.md b/tools/api-inventory/scripts/README.md index 1781e0bf9a..cea5f95135 100644 --- a/tools/api-inventory/scripts/README.md +++ b/tools/api-inventory/scripts/README.md @@ -166,12 +166,19 @@ tools/api-inventory/scripts/remeasure.sh --read-only # GET/HEAD のみ( `fixtures.py` が作る対象で決まる。 -| プレースホルダ | 解決先 | -|---|---| -| `` `` | 公開アイテム / 非公開アイテムの**両方**で測る | -| `` `` `` | 非公開アイテムに添付したファイル(IIIF の三つ組も) | -| `` `` | ワークフロー activity を**自分所有/他人所有の両方**で測る | -| `` `` `` | フィクスチャで作成したもの | +| プレースホルダ | 解決先 | 出所 | +|---|---|---| +| `` `` | 公開アイテム / 非公開アイテムの**両方**で測る | 作成 | +| `` `` `` | 非公開アイテムに添付したファイル(IIIF の三つ組も) | 作成 | +| `` `` | ワークフロー activity を**自分所有/他人所有の両方**で測る | 作成 | +| `` `` `` | フィクスチャで作成したもの | 作成 | +| `` | 著者(authors は初期状態で0件のため作成) | 作成 | +| `` `` `` `` `` | `install.sh` が投入する初期データの先頭ID | **参照** | +| `` `` `` | 定数(`ja` / `1`) | 定数 | + +**既存の初期データは作らずに参照する。** アイテムタイプ・プロパティ・メール +テンプレート・著者プレフィクス/所属・ファセット検索・OAuthクライアントは +`install.sh` が入れるので、フィクスチャで作ると二重になる。 解決できないプレースホルダ(`` など)を含む行は `未解決プレースホルダ` として skip し、台帳には反映しない。**「測っていない」ことが分かる状態を保つ**ため。 diff --git a/tools/api-inventory/scripts/fixtures.py b/tools/api-inventory/scripts/fixtures.py index ca79b41919..cf3617baeb 100644 --- a/tools/api-inventory/scripts/fixtures.py +++ b/tools/api-inventory/scripts/fixtures.py @@ -38,7 +38,7 @@ PASSWORD = "%(password)s" OUT = {"password": PASSWORD, "users": {}, "records": {}, "file": {}, "index": None, "token": None, "community": None, "group": None, - "activity": None, "activity_other": None, "errors": []} + "activity": None, "activity_other": None, "ids": {}, "errors": []} # 1x1 透明PNG(75B)。ファイル露出検証はバイト列が取れれば十分 PNG = base64.b64decode( @@ -319,6 +319,48 @@ def ensure(title, uid, key): ensure("APIInventory 検証用(他人所有)", other, "activity_other") +# ---------------------------------------------------------------- 著者 +@step("author") +def _author(): + """著者を1件作る(no.243-267 の 用)。authors は初期状態で0件。""" + from weko_authors.models import Authors + a = Authors.query.filter_by(gather_flg=0).first() + if a is None: + a = Authors(gather_flg=0, is_deleted=False) + db.session.add(a) + db.session.flush() + OUT["ids"]["author_id"] = int(a.id) + + +# ------------------------------------------------- 既存データのID参照(作成しない) +@step("lookups") +def _lookups(): + """probe のプレースホルダ解決に使う既存レコードのIDを拾う。 + + install.sh が投入する初期データ(アイテムタイプ・プロパティ・メールテンプレート・ + 著者プレフィクス/所属・ファセット検索・OAuthクライアント)は作らずに参照する。 + 作ってしまうと実環境の初期データと二重になるため。 + """ + from sqlalchemy import text + q = lambda sql: db.session.execute(text(sql)).first() # noqa: E731 + for key, sql in ( + ("item_type_id", "SELECT id FROM item_type WHERE is_deleted=false ORDER BY id LIMIT 1"), + ("property_id", "SELECT id FROM item_type_property ORDER BY id LIMIT 1"), + ("prefix_id", "SELECT id FROM authors_prefix_settings ORDER BY id LIMIT 1"), + ("affiliation_id", "SELECT id FROM authors_affiliation_settings ORDER BY id LIMIT 1"), + ("mail_template_id", "SELECT id FROM mail_templates ORDER BY id LIMIT 1"), + ("facet_search_id", "SELECT id FROM facet_search_setting ORDER BY id LIMIT 1"), + ("oauth_client_id", "SELECT client_id FROM oauth2server_client LIMIT 1"), + ("oauth_token_id", "SELECT id FROM oauth2server_token LIMIT 1"), + ): + try: + r = q(sql) + if r is not None and r[0] is not None: + OUT["ids"][key] = str(r[0]) + except Exception as exc: + OUT["errors"].append("lookup %%s: %%s" %% (key, exc)) + + # ---------------------------------------------------------------- ES反映 @step("reindex") def _reindex(): @@ -383,6 +425,7 @@ def main(): f"community={'あり' if data['community'] else 'なし'} " f"group={'あり' if data['group'] else 'なし'} " f"activity={'あり' if data.get('activity') else 'なし'} " + f"ids={len(data.get('ids') or {})} " f"errors={len(data['errors'])}") if data['errors']: print(' ※ 失敗した投入があります。probe の測定範囲が狭まります。') diff --git a/tools/api-inventory/scripts/probe_ci.py b/tools/api-inventory/scripts/probe_ci.py index 2f1a4590dd..595d8dd3b2 100644 --- a/tools/api-inventory/scripts/probe_ci.py +++ b/tools/api-inventory/scripts/probe_ci.py @@ -100,6 +100,7 @@ def classify(code, body_path, redirect=''): def build_resolver(fx): """URI のプレースホルダをフィクスチャの実値に置き換える表を作る。""" + ids = fx.get('ids') or {} priv = fx['records'].get('private', {}) other = fx['records'].get('other_owner', {}) f = fx.get('file') or {} @@ -121,6 +122,19 @@ def build_resolver(fx): (r'<[^>]*action_id[^>]*>', str((fx.get('activity') or {}).get('action_id', ''))), (r'<[^>]*workflow_id[^>]*>', str((fx.get('activity') or {}).get('workflow_id', ''))), (r'<[^>]*flow_id[^>]*>', str((fx.get('activity') or {}).get('flow_id', ''))), + # fixtures.py が拾った既存レコードのID + (r'<[^>]*item_type_id[^>]*>', ids.get('item_type_id', '')), + (r'<[^>]*property_id[^>]*>', ids.get('property_id', '')), + (r'<[^>]*client_id[^>]*>', ids.get('oauth_client_id', '')), + (r'<[^>]*token_id[^>]*>', ids.get('oauth_token_id', '')), + (r'<[^>]*mail_id[^>]*>', ids.get('mail_template_id', '')), + (r'<[^>]*identifier[^>]*>', str(ids.get('author_id', ''))), + # 定数で決まるもの + (r'<[^>]*(lang_code|current_language|lang)[^>]*>', 'ja'), + (r'', '1'), + # 上記で解決しない汎用ID(facet-search の 等)は最後に当てる + (r'<(int|string):id>', ids.get('facet_search_id', '1')), + (r'', ids.get('prefix_id', '1')), # IIIF Image API のパラメータ(no.34) (r'<[^>]*region[^>]*>', 'full'), (r'<[^>]*size[^>]*>', 'full'), From ea9da182da0485863bf2ae66fca4c37d97257c5c Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Tue, 25 Aug 2026 02:49:35 +0000 Subject: [PATCH 3/6] =?UTF-8?q?refactor(tools):=20=E5=84=AA=E5=85=88?= =?UTF-8?q?=E5=BA=A6=E5=8C=BA=E5=88=86=E3=82=92=20P1-P5=20=E3=81=AB?= =?UTF-8?q?=E6=95=B4=E7=90=86=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧 P0(至急) と P0(最優先) を P1 に統合し、以降を繰り上げてかっこを外した。 P0 が2種類あるのが分かりにくく、かっこ内の補足も区分名として冗長だったため。 P0(至急) + P0(最優先) → P1 P1(高) → P2 P2(中) → P3 P3(低) → P4 P4(低・実装済) → P5 整理対象 / 環境依存 / 対象外 は変更なし(優先度ではなく扱いの区分のため) テスト観点による引き上げの上限も P2 → P3 に追随させた(同じ層を指す)。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HXo9u6PoTf6VRKr3aiGvZ3 --- tools/api-inventory/scripts/prioritize.py | 86 +++++++++++------------ 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/tools/api-inventory/scripts/prioritize.py b/tools/api-inventory/scripts/prioritize.py index c8fe78331e..f2e1320935 100644 --- a/tools/api-inventory/scripts/prioritize.py +++ b/tools/api-inventory/scripts/prioritize.py @@ -5,26 +5,25 @@ 判定基準(上から順に評価し、最初に合致したものを採用する): - P0(至急) 無認証で **既存のファイル実体** を上書き/削除できる - (「データ破壊」を「既存の実データを不可逆に壊すこと」と定義する。 - メタデータの更新や新規作成は破壊に含めない) - P0(最優先) 状態変更系(POST/PUT/DELETE/PATCH)なのに認証・権限チェックが一切ない、 + P1 無認証で既存のファイル実体を上書き/削除できる(データ破壊)、または + 状態変更系(POST/PUT/DELETE/PATCH)なのに認証・権限チェックが一切ない、 または権限チェック機構はあるように見えるが実装上機能していない - P1(高) ログイン必須のみで所有者/ロール/スコープの限定がない状態変更系(IDOR疑い)、 + P2 ログイン必須のみで所有者/ロール/スコープの限定がない状態変更系(IDOR疑い)、 ゲストトークン等のバイパス、ロールチェックが実質不問、または「不明」。 - 加えて、認証が無い状態変更系でも **新規作成のみで既存データを - 壊さない** ものはここに下げる - 対象外 認証必須かつ admin-access 相当の権限チェックがあり、指摘が無いもの - P3(低) 意図的な公開設計(static配信・ヘルスチェック・robots.txt・OAI-PMH等)、 - または deny_all 常時拒否 - P2(中) 読み取り系(GET/HEAD)で認証・権限チェックが無い、または + 認証が無い状態変更系でも新規作成のみで既存データを壊さないもの。 + 参照系でも露出内容が認証情報または非公開データの実体であるもの + P3 読み取り系(GET/HEAD)で認証・権限チェックが無い、または ログイン必須のみでロール/所有者スコープなし - P4(低・実装済) 具体的な権限/所有者チェック機構が明記されており破綻が見えない + P4 意図的な公開設計(static配信・ヘルスチェック・robots.txt・OAI-PMH等)、 + または deny_all 常時拒否 + P5 具体的な権限/所有者チェック機構が明記されており破綻が見えない + 整理対象 非利用(未使用/非推奨/呼出元なし)で、認可上の判定が P3 以下 + 環境依存 実機の url_map に存在しない(プラグイン未導入・config で無効・動的登録) + 対象外 認証必須かつ admin-access 相当の権限チェックがあり、指摘も★実証も無い -テスト観点による引き上げ(上限 P2): +テスト観点による引き上げ(上限 P3): 正常値/異常値/境界値/例外処理 のチェックが確認できない行は、認可上の問題が - 無くても確認対象に上げる。ただし **引き上げは P2 まで** とする(認可の欠陥と - 同列には扱わない)。既に P0/P1/P2 の行は変更しない。 + 無くても確認対象に上げる。ただし引き上げは P3 まで。 評価順について: 「対象外」と P3 は P2 より先に評価する。admin-access で保護された 管理画面や、意図的に公開している OAI-PMH を「認証なしの読み取り系」として @@ -145,9 +144,9 @@ def bump(pri, why): if not gap or gap == '-': return pri, why if gap == '特定不能': - return 'P2(中)', f'{why} / 対応するテスト関数を特定できずテスト観点を確認できない' + return 'P3', f'{why} / 対応するテスト関数を特定できずテスト観点を確認できない' if gap.count(',') == 3: - return 'P2(中)', f'{why} / テストの4観点(正常値・異常値・境界値・例外処理)が全て確認できない' + return 'P3', f'{why} / テストの4観点(正常値・異常値・境界値・例外処理)が全て確認できない' return pri, f'{why} / テスト観点の欠落: {gap}' methods = {m.strip() for m in method.split(',') if m.strip()} @@ -167,35 +166,35 @@ def bump(pri, why): unknown = (dyn.strip() in ('', '-')) has_finding = finding.strip() not in ('', '-') - # --- P0(至急) 無認証で既存のファイル実体を壊せる --- + # --- P1: 無認証で既存のファイル実体を壊せる(データ破壊) --- # 「データ破壊」= 既存の実データを不可逆に壊すこと。メタデータの更新や # 新規作成は含めない。この定義で 926 行中 no.503(POST /records/replace_file) # だけが該当する。 destroys_real_data = ('ファイル実体' in store) and ('更新' in data_op or '削除' in data_op) if (no_auth or unauth_reach) and destroys_real_data: - return 'P0(至急)', f'無認証で既存のファイル実体を上書き/削除できる (data_op={data_op})' + return 'P1', f'無認証で既存のファイル実体を上書き/削除できる (data_op={data_op})' - # --- P0(最優先) 状態変更系で認証・権限が無い/機能していない --- + # --- P1: 状態変更系で認証・権限が無い/機能していない --- # ただし新規作成しかせず既存データを壊さないものは P1 に下げる(下の P1 で拾う)。 creates_only = ('作成' in data_op) and not ('更新' in data_op or '削除' in data_op) if is_write_method and (no_auth or unauth_reach) and not creates_only: why = '認証チェックが無い' if no_auth else '認証はあるが未認証で到達(実測)' - return 'P0(最優先)', f'状態変更系({method})で{why}' + return 'P1', f'状態変更系({method})で{why}' if is_write_method and broken_authz and not creates_only: - return 'P0(最優先)', f'状態変更系({method})だが権限チェックが実装上機能していない' + return 'P1', f'状態変更系({method})だが権限チェックが実装上機能していない' - # --- P1(高) --- + # --- P2 --- # 認証が無い状態変更系でも、新規作成のみで既存データを壊さないものはここ。 if is_write_method and (no_auth or unauth_reach or broken_authz) and creates_only: - return 'P1(高)', f'状態変更系({method})で認証チェックが無いが、新規作成のみで既存データは壊さない' + return 'P2', f'状態変更系({method})で認証チェックが無いが、新規作成のみで既存データは壊さない' if is_write_method and guest_bypass: - return 'P1(高)', f'状態変更系({method})にゲストトークンのバイパス経路がある' + return 'P2', f'状態変更系({method})にゲストトークンのバイパス経路がある' if is_write_method and (login_only or scope_missing): why = 'ログイン必須のみで所有者/ロール限定なし(IDOR疑い)' if login_only \ else finding.split(';')[0].strip()[:60] - return 'P1(高)', f'状態変更系({method}): {why}' + return 'P2', f'状態変更系({method}): {why}' if is_write_method and unknown: - return 'P1(高)', f'状態変更系({method})だが到達可否が未測定(不明)' + return 'P2', f'状態変更系({method})だが到達可否が未測定(不明)' # --- 露出内容による引き上げ(参照系でも P1) --- # 「読み取り系だから情報漏洩リスクは限定的」は、露出するものが認証情報や @@ -214,9 +213,9 @@ def bump(pri, why): cred = [w for w in CREDENTIAL_WORDS if w in exposure] body = [w for w in NONPUBLIC_BODY_WORDS if w in exposure or w in store] if cred: - return 'P1(高)', f'参照系だが露出内容が認証情報({cred[0]})で、認可が緩い' + return 'P2', f'参照系だが露出内容が認証情報({cred[0]})で、認可が緩い' if body: - return 'P1(高)', f'参照系だが露出内容が非公開データの実体({body[0]})で、認可が緩い' + return 'P2', f'参照系だが露出内容が非公開データの実体({body[0]})で、認可が緩い' # --- 対象外: admin-access 相当で保護され、指摘も実証も無い --- if (not has_finding) and (not proven) and ( @@ -224,31 +223,31 @@ def bump(pri, why): or auth_req == '要(管理)'): return bump('対象外', '認証必須+admin-access 相当の権限チェックあり、指摘なし') - # --- P3(低) 意図的な公開設計 / deny_all --- + # --- P4: 意図的な公開設計 / deny_all --- for pat, label in PUBLIC_BY_DESIGN: if re.search(pat, uri): - return bump('P3(低)', f'意図的な公開設計({label})') + return bump('P4', f'意図的な公開設計({label})') if 'deny_all' in auth or 'deny_all' in cfg: - return bump('P3(低)', 'deny_all で常時拒否(逆方向の問題)') + return bump('P4', 'deny_all で常時拒否(逆方向の問題)') - # --- P2(中) 読み取り系 --- + # --- P3: 読み取り系 --- if not is_write_method: if no_auth or unauth_reach: - return 'P2(中)', f'読み取り系({method})で認証・権限チェックが無い' + return 'P3', f'読み取り系({method})で認証・権限チェックが無い' if login_only or scope_missing: why = 'ログイン必須のみでロール/所有者スコープなし' if login_only \ else finding.split(';')[0].strip()[:60] - return 'P2(中)', f'読み取り系({method}): {why}' + return 'P3', f'読み取り系({method}): {why}' if unknown and has_finding: - return 'P2(中)', f'読み取り系({method})に指摘があるが到達可否は未測定' + return 'P3', f'読み取り系({method})に指摘があるが到達可否は未測定' - # --- P4(低・実装済) --- + # --- P5 --- hit = [k for k in CONCRETE_AUTHZ if k in auth] if hit: - return bump('P4(低・実装済)', f'具体的な権限チェック機構あり({", ".join(hit[:3])})') + return bump('P5', f'具体的な権限チェック機構あり({", ".join(hit[:3])})') if auth_req.startswith('要'): - return bump('P4(低・実装済)', f'認証必須({auth_req})で破綻は見えない') - return 'P2(中)', '分類条件に合致せず。手動確認が必要' + return bump('P5', f'認証必須({auth_req})で破綻は見えない') + return 'P3', '分類条件に合致せず。手動確認が必要' def decide(c, H, allow=(frozenset(), frozenset())): @@ -263,7 +262,7 @@ def decide(c, H, allow=(frozenset(), frozenset())): if hit or field(c, H, 'no') in not_a_route: src = ('起動後に動的登録されるためURIが静的に定まらない' if not hit else 'この環境では未登録(プラグイン未導入・config で無効等)') - if pri in ('P0(至急)', 'P0(最優先)', 'P1(高)'): + if pri in ('P1', 'P1', 'P2'): return pri, f'{why} / 実機に無い({src})が、有効な環境では成立しうる', '-' return '環境依存', f'実機に無い: {src} / 認可上の判定は {pri}', '-' @@ -276,7 +275,7 @@ def decide(c, H, allow=(frozenset(), frozenset())): src = '経路なし(実機 url_map に未登録)' if not src: return pri, why, '-' - if pri in ('P0(至急)', 'P0(最優先)', 'P1(高)'): + if pri in ('P1', 'P1', 'P2'): return pri, f'{why} / 非利用({src[:40]})のため削除が最短の対応', src return '整理対象', f'非利用({src[:60]}) / 認可上の判定は {pri}', src @@ -331,8 +330,7 @@ def apply_to(path, out=None): return counts -ORDER = ['P0(至急)', 'P0(最優先)', 'P1(高)', 'P2(中)', 'P3(低)', - 'P4(低・実装済)', '整理対象', '環境依存', '対象外'] +ORDER = ['P1', 'P2', 'P3', 'P4', 'P5', '整理対象', '環境依存', '対象外'] def main(): From 7a4dc9ca6bcedb4400130532fa90a45bc7f4343f Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Tue, 25 Aug 2026 05:53:05 +0000 Subject: [PATCH 4/6] =?UTF-8?q?chore(ci):=20Claude=20=E3=81=AB=E3=82=88?= =?UTF-8?q?=E3=82=8BPR=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E3=82=92?= =?UTF-8?q?=E8=A9=A6=E8=A1=8C=E5=B0=8E=E5=85=A5(API=20=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E4=B8=8D=E4=BD=BF=E7=94=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic API キーではなく Claude サブスクリプションの長期トークンで認証する。 通信はアウトバウンドのみで、公開エンドポイントや常駐プロセスは不要。 cloud-hosted の claude ultrareview は本アカウントで利用できなかった ("Ultrareview is currently unavailable")ため、ヘッドレス実行(claude -p)を使う。 差分を stdin で渡し、--allowed-tools "" --permission-mode plan により ツール実行を一切許可しない。 public リポジトリ向けの配慮: - fork からの PR では起動しない(Secret が渡らないため) - レビュー結果は PR に投稿せず artifact 止まり(POST_TO_PR で切替) - 差分 200KB 超はスキップ 観点は認可の欠落・後退、破壊的操作、入力検証、既存挙動の変更に絞り、 差分から読み取れる事実のみを書かせる。 動作確認のため一時的に pull_request トリガを有効化している。 確認後は workflow_dispatch のみに戻す。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HXo9u6PoTf6VRKr3aiGvZ3 --- .github/workflows/claude-pr-review.yml | 151 ++++++++++++++++++++ tools/api-inventory/ci/claude-pr-review.yml | 151 ++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 .github/workflows/claude-pr-review.yml create mode 100644 tools/api-inventory/ci/claude-pr-review.yml diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml new file mode 100644 index 0000000000..63baeebee0 --- /dev/null +++ b/.github/workflows/claude-pr-review.yml @@ -0,0 +1,151 @@ +# Claude によるPRレビュー(Anthropic API キーを使わない構成) +# +# 認証は **Claude サブスクリプションの長期トークン**。従量課金の API キーは使わない。 +# ローカルで: claude setup-token # 1年有効・scope=user:inference +# 登録: gh secret set CLAUDE_CODE_AUTH_TOKEN --repo RCOSDP/weko +# +# 通信はすべてアウトバウンド(ランナー → Anthropic / GitHub)。 +# 公開エンドポイント・固定IP・ポート開放・常駐プロセスは不要。 +# +# 【このリポジトリは public】 +# Secret 名は CLAUDE_CODE_AUTH_TOKEN、CLI が読む環境変数は CLAUDE_CODE_OAUTH_TOKEN。 +# - Secret は fork からの PR には渡らない。下の if で同一リポジトリに限定する。 +# - レビュー結果を PR に投稿すると誰でも読める。既定では投稿せず artifact 止まり。 +# POST_TO_PR を true にすると投稿する。 +# +# 注: cloud-hosted の `claude ultrareview` は 2026-08 時点でこのアカウントでは +# 利用できなかった("Ultrareview is currently unavailable")。ここでは +# ヘッドレス実行(`claude -p`)を使う。動作は確認済み。 + +name: Claude PR Review + +on: + workflow_dispatch: + inputs: + pr_number: + description: 'レビュー対象の PR 番号' + required: true + # TODO(試行中): 動作確認のため有効化。確認後は workflow_dispatch のみに戻す。 + pull_request: + branches: ['**'] + +env: + POST_TO_PR: 'false' + MODEL: 'sonnet' + MAX_DIFF_BYTES: '200000' # これを超える差分はレビューしない(分割が必要) + +jobs: + review: + runs-on: ubuntu-latest + timeout-minutes: 30 + if: github.event_name == 'workflow_dispatch' || + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check token + id: cfg + env: + TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} + run: | + if [ -n "$TOKEN" ]; then echo "enabled=true" >> "$GITHUB_OUTPUT" + else echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::notice::CLAUDE_CODE_AUTH_TOKEN が未設定のためスキップします"; fi + + - name: Install Claude Code + if: steps.cfg.outputs.enabled == 'true' + run: | + curl -fsSL https://claude.ai/install.sh | bash + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Collect diff + if: steps.cfg.outputs.enabled == 'true' + id: diff + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.inputs.pr_number || github.event.pull_request.number }} + run: | + gh pr diff "$PR" > diff.patch + size=$(stat -c%s diff.patch) + echo "差分: ${size} bytes" + if [ "$size" -gt "${MAX_DIFF_BYTES}" ]; then + echo "::warning::差分が大きすぎます(${size} > ${MAX_DIFF_BYTES})。スキップします" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + + - name: Review + if: steps.cfg.outputs.enabled == 'true' && steps.diff.outputs.skip != 'true' + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} + run: | + set -o pipefail + # ツールを一切許可せず、渡した差分だけを読んで指摘を返させる。 + # リポジトリを読ませたい場合は --allowed-tools "Read,Grep,Glob" を足す。 + claude -p "$(cat <<'PROMPT' + 添付の差分をレビューしてください。返答は次のJSONのみとし、前後に文章を付けないこと。 + + {"findings":[{"file":"","line":0,"severity":"high|medium|low", + "title":"","detail":"","evidence":""}]} + + 観点(この順で重視): + 1. 認可の欠落・後退(デコレータの削除、permission factory の無効化、 + 所有者チェックの欠落) + 2. 破壊的操作(削除・上書き)の追加や条件緩和 + 3. 入力検証の不足 + 4. 既存挙動を変える変更で、呼び出し側への影響が考慮されていないもの + + 規則: + - 差分から読み取れる事実のみを書く。推測は書かない。 + - 根拠(evidence)には該当行の抜粋を入れる。 + - 指摘が無ければ {"findings":[]} を返す。 + PROMPT + )" --output-format json --model "$MODEL" \ + --allowed-tools "" --permission-mode plan \ + < diff.patch > raw.json + python3 - <<'PY' > review.md + import json, re + raw = json.load(open('raw.json')) + text = raw.get('result') or raw.get('text') or '' + m = re.search(r'\{.*\}', text, re.S) + data = json.loads(m.group(0)) if m else {'findings': []} + f = data.get('findings', []) + json.dump(data, open('findings.json', 'w'), ensure_ascii=False, indent=1) + order = {'high': 0, 'medium': 1, 'low': 2} + f.sort(key=lambda x: order.get(x.get('severity'), 9)) + print(f"## Claude によるレビュー\n\n指摘 {len(f)} 件" + f"(コスト ${raw.get('total_cost_usd', 0):.4f})\n") + for x in f: + loc = f"`{x.get('file','')}:{x.get('line','')}`" + print(f"- **[{x.get('severity','?')}] {x.get('title','')}** {loc}") + if x.get('detail'): + print(f" - {x['detail']}") + PY + cat review.md + + - name: Upload result + if: always() && steps.cfg.outputs.enabled == 'true' + uses: actions/upload-artifact@v4 + with: + name: claude-review + path: | + review.md + findings.json + + - name: Comment on PR + if: steps.cfg.outputs.enabled == 'true' && env.POST_TO_PR == 'true' && + github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let body = '(レビュー結果を生成できませんでした)'; + try { body = fs.readFileSync('review.md', 'utf8'); } catch (e) {} + await github.rest.issues.createComment({ + issue_number: context.issue.number, owner: context.repo.owner, + repo: context.repo.repo, body: body.slice(0, 60000), + }); diff --git a/tools/api-inventory/ci/claude-pr-review.yml b/tools/api-inventory/ci/claude-pr-review.yml new file mode 100644 index 0000000000..63baeebee0 --- /dev/null +++ b/tools/api-inventory/ci/claude-pr-review.yml @@ -0,0 +1,151 @@ +# Claude によるPRレビュー(Anthropic API キーを使わない構成) +# +# 認証は **Claude サブスクリプションの長期トークン**。従量課金の API キーは使わない。 +# ローカルで: claude setup-token # 1年有効・scope=user:inference +# 登録: gh secret set CLAUDE_CODE_AUTH_TOKEN --repo RCOSDP/weko +# +# 通信はすべてアウトバウンド(ランナー → Anthropic / GitHub)。 +# 公開エンドポイント・固定IP・ポート開放・常駐プロセスは不要。 +# +# 【このリポジトリは public】 +# Secret 名は CLAUDE_CODE_AUTH_TOKEN、CLI が読む環境変数は CLAUDE_CODE_OAUTH_TOKEN。 +# - Secret は fork からの PR には渡らない。下の if で同一リポジトリに限定する。 +# - レビュー結果を PR に投稿すると誰でも読める。既定では投稿せず artifact 止まり。 +# POST_TO_PR を true にすると投稿する。 +# +# 注: cloud-hosted の `claude ultrareview` は 2026-08 時点でこのアカウントでは +# 利用できなかった("Ultrareview is currently unavailable")。ここでは +# ヘッドレス実行(`claude -p`)を使う。動作は確認済み。 + +name: Claude PR Review + +on: + workflow_dispatch: + inputs: + pr_number: + description: 'レビュー対象の PR 番号' + required: true + # TODO(試行中): 動作確認のため有効化。確認後は workflow_dispatch のみに戻す。 + pull_request: + branches: ['**'] + +env: + POST_TO_PR: 'false' + MODEL: 'sonnet' + MAX_DIFF_BYTES: '200000' # これを超える差分はレビューしない(分割が必要) + +jobs: + review: + runs-on: ubuntu-latest + timeout-minutes: 30 + if: github.event_name == 'workflow_dispatch' || + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check token + id: cfg + env: + TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} + run: | + if [ -n "$TOKEN" ]; then echo "enabled=true" >> "$GITHUB_OUTPUT" + else echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::notice::CLAUDE_CODE_AUTH_TOKEN が未設定のためスキップします"; fi + + - name: Install Claude Code + if: steps.cfg.outputs.enabled == 'true' + run: | + curl -fsSL https://claude.ai/install.sh | bash + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Collect diff + if: steps.cfg.outputs.enabled == 'true' + id: diff + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.inputs.pr_number || github.event.pull_request.number }} + run: | + gh pr diff "$PR" > diff.patch + size=$(stat -c%s diff.patch) + echo "差分: ${size} bytes" + if [ "$size" -gt "${MAX_DIFF_BYTES}" ]; then + echo "::warning::差分が大きすぎます(${size} > ${MAX_DIFF_BYTES})。スキップします" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + + - name: Review + if: steps.cfg.outputs.enabled == 'true' && steps.diff.outputs.skip != 'true' + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} + run: | + set -o pipefail + # ツールを一切許可せず、渡した差分だけを読んで指摘を返させる。 + # リポジトリを読ませたい場合は --allowed-tools "Read,Grep,Glob" を足す。 + claude -p "$(cat <<'PROMPT' + 添付の差分をレビューしてください。返答は次のJSONのみとし、前後に文章を付けないこと。 + + {"findings":[{"file":"","line":0,"severity":"high|medium|low", + "title":"","detail":"","evidence":""}]} + + 観点(この順で重視): + 1. 認可の欠落・後退(デコレータの削除、permission factory の無効化、 + 所有者チェックの欠落) + 2. 破壊的操作(削除・上書き)の追加や条件緩和 + 3. 入力検証の不足 + 4. 既存挙動を変える変更で、呼び出し側への影響が考慮されていないもの + + 規則: + - 差分から読み取れる事実のみを書く。推測は書かない。 + - 根拠(evidence)には該当行の抜粋を入れる。 + - 指摘が無ければ {"findings":[]} を返す。 + PROMPT + )" --output-format json --model "$MODEL" \ + --allowed-tools "" --permission-mode plan \ + < diff.patch > raw.json + python3 - <<'PY' > review.md + import json, re + raw = json.load(open('raw.json')) + text = raw.get('result') or raw.get('text') or '' + m = re.search(r'\{.*\}', text, re.S) + data = json.loads(m.group(0)) if m else {'findings': []} + f = data.get('findings', []) + json.dump(data, open('findings.json', 'w'), ensure_ascii=False, indent=1) + order = {'high': 0, 'medium': 1, 'low': 2} + f.sort(key=lambda x: order.get(x.get('severity'), 9)) + print(f"## Claude によるレビュー\n\n指摘 {len(f)} 件" + f"(コスト ${raw.get('total_cost_usd', 0):.4f})\n") + for x in f: + loc = f"`{x.get('file','')}:{x.get('line','')}`" + print(f"- **[{x.get('severity','?')}] {x.get('title','')}** {loc}") + if x.get('detail'): + print(f" - {x['detail']}") + PY + cat review.md + + - name: Upload result + if: always() && steps.cfg.outputs.enabled == 'true' + uses: actions/upload-artifact@v4 + with: + name: claude-review + path: | + review.md + findings.json + + - name: Comment on PR + if: steps.cfg.outputs.enabled == 'true' && env.POST_TO_PR == 'true' && + github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let body = '(レビュー結果を生成できませんでした)'; + try { body = fs.readFileSync('review.md', 'utf8'); } catch (e) {} + await github.rest.issues.createComment({ + issue_number: context.issue.number, owner: context.repo.owner, + repo: context.repo.repo, body: body.slice(0, 60000), + }); From c14a2ed9d2f6996aa445ecea4b956d1fac9f0c9a Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Tue, 25 Aug 2026 05:58:23 +0000 Subject: [PATCH 5/6] =?UTF-8?q?chore(ci):=20Claude=20PR=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=81=AE=E5=A4=B1=E6=95=97=E5=8E=9F=E5=9B=A0?= =?UTF-8?q?=E3=82=92=E5=88=87=E3=82=8A=E5=88=86=E3=81=91=E3=82=8B=E3=81=9F?= =?UTF-8?q?=E3=82=81=E8=A8=BA=E6=96=AD=E5=87=BA=E5=8A=9B=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 初回実行が3秒で exit 1 になり、stdout を raw.json にリダイレクトしていたため エラー内容が取れなかった。stdout/stderr を両方残して表示し、artifact にも 上げるようにした。診断目的のためジョブは落とさない。 --allowed-tools は可変長引数で空文字を渡すと解釈が不安定なため指定をやめた (--permission-mode plan により変更系は行われない)。環境変数名は CLAUDE_CODE_OAUTH_TOKEN で正しいことをバイナリ内の文字列で確認済み。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HXo9u6PoTf6VRKr3aiGvZ3 --- .github/workflows/claude-pr-review.yml | 23 +++++++++++++++------ tools/api-inventory/ci/claude-pr-review.yml | 23 +++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index 63baeebee0..43e65cdc91 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -83,9 +83,10 @@ jobs: env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} run: | - set -o pipefail - # ツールを一切許可せず、渡した差分だけを読んで指摘を返させる。 - # リポジトリを読ませたい場合は --allowed-tools "Read,Grep,Glob" を足す。 + # 診断のため stdout/stderr を両方残す。--allowed-tools は可変長引数で + # 空文字を渡すと解釈が不安定なため指定しない(--permission-mode plan で + # 変更系は行われない)。 + set +e claude -p "$(cat <<'PROMPT' 添付の差分をレビューしてください。返答は次のJSONのみとし、前後に文章を付けないこと。 @@ -104,9 +105,17 @@ jobs: - 根拠(evidence)には該当行の抜粋を入れる。 - 指摘が無ければ {"findings":[]} を返す。 PROMPT - )" --output-format json --model "$MODEL" \ - --allowed-tools "" --permission-mode plan \ - < diff.patch > raw.json + )" --output-format json --model "$MODEL" --permission-mode plan \ + < diff.patch > raw.json 2> claude.err + rc=$? + set -e + echo "claude exit=$rc" + echo "----- stderr -----"; head -c 3000 claude.err || true + echo "----- stdout(先頭) -----"; head -c 1500 raw.json || true + if [ $rc -ne 0 ]; then + echo "::warning::claude の実行に失敗しました(exit=$rc)。診断のためジョブは継続します" + exit 0 + fi python3 - <<'PY' > review.md import json, re raw = json.load(open('raw.json')) @@ -135,6 +144,8 @@ jobs: path: | review.md findings.json + raw.json + claude.err - name: Comment on PR if: steps.cfg.outputs.enabled == 'true' && env.POST_TO_PR == 'true' && diff --git a/tools/api-inventory/ci/claude-pr-review.yml b/tools/api-inventory/ci/claude-pr-review.yml index 63baeebee0..43e65cdc91 100644 --- a/tools/api-inventory/ci/claude-pr-review.yml +++ b/tools/api-inventory/ci/claude-pr-review.yml @@ -83,9 +83,10 @@ jobs: env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} run: | - set -o pipefail - # ツールを一切許可せず、渡した差分だけを読んで指摘を返させる。 - # リポジトリを読ませたい場合は --allowed-tools "Read,Grep,Glob" を足す。 + # 診断のため stdout/stderr を両方残す。--allowed-tools は可変長引数で + # 空文字を渡すと解釈が不安定なため指定しない(--permission-mode plan で + # 変更系は行われない)。 + set +e claude -p "$(cat <<'PROMPT' 添付の差分をレビューしてください。返答は次のJSONのみとし、前後に文章を付けないこと。 @@ -104,9 +105,17 @@ jobs: - 根拠(evidence)には該当行の抜粋を入れる。 - 指摘が無ければ {"findings":[]} を返す。 PROMPT - )" --output-format json --model "$MODEL" \ - --allowed-tools "" --permission-mode plan \ - < diff.patch > raw.json + )" --output-format json --model "$MODEL" --permission-mode plan \ + < diff.patch > raw.json 2> claude.err + rc=$? + set -e + echo "claude exit=$rc" + echo "----- stderr -----"; head -c 3000 claude.err || true + echo "----- stdout(先頭) -----"; head -c 1500 raw.json || true + if [ $rc -ne 0 ]; then + echo "::warning::claude の実行に失敗しました(exit=$rc)。診断のためジョブは継続します" + exit 0 + fi python3 - <<'PY' > review.md import json, re raw = json.load(open('raw.json')) @@ -135,6 +144,8 @@ jobs: path: | review.md findings.json + raw.json + claude.err - name: Comment on PR if: steps.cfg.outputs.enabled == 'true' && env.POST_TO_PR == 'true' && From 10d26b7869c4854d6799089724c047aafad18afb Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Tue, 25 Aug 2026 08:10:42 +0000 Subject: [PATCH 6/6] =?UTF-8?q?chore(ci):=20PR=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E3=83=97=E3=83=AD=E3=83=B3=E3=83=97=E3=83=88?= =?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84=E3=81=97=E3=80=81=E8=A3=8F=E5=8F=96?= =?UTF-8?q?=E3=82=8A=E3=82=92=E5=BF=85=E9=A0=88=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 初回試行で「fixtures.py の %% は SyntaxError」という誤指摘が出た。実際は その文字列が PAYLOAD % {...} で展開される前提で %% が正しく、差分だけでは 文脈が見えないことが原因だった。 - --allowed-tools "Read,Grep,Glob" を許可し、リポジトリの実物を読ませる。 変更系のツールは許可せず --permission-mode plan も併用する。 - プロンプトの最重要規則を「指摘する前に必ず裏を取る」にし、確認せずに 指摘してはいけない例(未定義に見える変数、書式の誤り、呼び出し側の追随)を挙げた。 - 出力に verified 欄を追加し、どのファイルを読んで確認したかを書かせる。 埋まらない指摘は出力しないよう指示。 - 「指摘ゼロは正当な結論」と明記し、件数稼ぎを抑止。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HXo9u6PoTf6VRKr3aiGvZ3 --- .github/workflows/claude-pr-review.yml | 65 +++++++++++++++------ tools/api-inventory/ci/claude-pr-review.yml | 65 +++++++++++++++------ 2 files changed, 96 insertions(+), 34 deletions(-) diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index 43e65cdc91..61e6099573 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -83,29 +83,58 @@ jobs: env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} run: | - # 診断のため stdout/stderr を両方残す。--allowed-tools は可変長引数で - # 空文字を渡すと解釈が不安定なため指定しない(--permission-mode plan で - # 変更系は行われない)。 + # Read/Grep/Glob だけを許可してリポジトリを読ませる。差分だけを見せると + # 文脈不足で誤検知が出る(初回試行で「%% は SyntaxError」という誤指摘が出た。 + # 実際はその文字列が後で % 展開される前提だった)。 + # 変更系のツールは許可せず、--permission-mode plan も併用する。 set +e claude -p "$(cat <<'PROMPT' - 添付の差分をレビューしてください。返答は次のJSONのみとし、前後に文章を付けないこと。 + このリポジトリの Pull Request をレビューしてください。 + 差分は標準入力から渡されます。 + + ## 最重要の規則: 指摘する前に必ず裏を取る + + 差分は前後の文脈が欠けています。差分の見た目だけで判断すると誤検知になります。 + 指摘を書く前に、必ず Read/Grep/Glob で該当ファイルの実物を読み、 + その指摘が本当に成立するかを確認してください。 + + 確認せずに指摘してはいけない例: + - 「この変数は未定義に見える」→ ファイル全体を読めば定義されている + - 「この書式は誤り」→ その文字列が後で加工される前提かもしれない + - 「呼び出し側の追随が無い」→ 差分外のファイルを grep すれば分かる + + 裏が取れなかったものは **書かない**。件数を稼ぐ必要はありません。 + 指摘ゼロは正当な結論です。 + + ## 観点(この順で重視) + + 1. 認可の欠落・後退 + デコレータの削除、permission factory の無効化(None 代入等)、 + 所有者チェックの欠落、ロール判定の緩和 + 2. 破壊的操作の追加・条件緩和 + 削除/上書き処理の新設、既定値が安全側から危険側に変わる変更 + 3. 入力検証の不足 + 外部入力をそのまま使う、パス連結、スキーマ検証なし + 4. 既存挙動を変える変更で、呼び出し側への影響が未考慮のもの + 関数シグネチャ、戻り値の形、列名・キー名の変更など。 + **grep で実際に呼び出し箇所を確認してから指摘すること** + + ## 出力 + + 最後に次のJSONだけを出力してください。前後に文章を付けないこと。 {"findings":[{"file":"","line":0,"severity":"high|medium|low", - "title":"","detail":"","evidence":""}]} - - 観点(この順で重視): - 1. 認可の欠落・後退(デコレータの削除、permission factory の無効化、 - 所有者チェックの欠落) - 2. 破壊的操作(削除・上書き)の追加や条件緩和 - 3. 入力検証の不足 - 4. 既存挙動を変える変更で、呼び出し側への影響が考慮されていないもの - - 規則: - - 差分から読み取れる事実のみを書く。推測は書かない。 - - 根拠(evidence)には該当行の抜粋を入れる。 - - 指摘が無ければ {"findings":[]} を返す。 + "title":"","detail":"","evidence":"","verified":""}]} + + detail : 何が問題で何が起きるかを1〜2文で + evidence : 該当行の抜粋 + verified : **どのファイルを読んで裏を取ったか**(例 "utils.py:120-140 を確認") + ここが埋まらない指摘は出力しないこと + + 指摘が無ければ {"findings":[]} を返してください。 PROMPT )" --output-format json --model "$MODEL" --permission-mode plan \ + --allowed-tools "Read,Grep,Glob" \ < diff.patch > raw.json 2> claude.err rc=$? set -e @@ -133,6 +162,8 @@ jobs: print(f"- **[{x.get('severity','?')}] {x.get('title','')}** {loc}") if x.get('detail'): print(f" - {x['detail']}") + if x.get('verified'): + print(f" - 確認: {x['verified']}") PY cat review.md diff --git a/tools/api-inventory/ci/claude-pr-review.yml b/tools/api-inventory/ci/claude-pr-review.yml index 43e65cdc91..61e6099573 100644 --- a/tools/api-inventory/ci/claude-pr-review.yml +++ b/tools/api-inventory/ci/claude-pr-review.yml @@ -83,29 +83,58 @@ jobs: env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_AUTH_TOKEN }} run: | - # 診断のため stdout/stderr を両方残す。--allowed-tools は可変長引数で - # 空文字を渡すと解釈が不安定なため指定しない(--permission-mode plan で - # 変更系は行われない)。 + # Read/Grep/Glob だけを許可してリポジトリを読ませる。差分だけを見せると + # 文脈不足で誤検知が出る(初回試行で「%% は SyntaxError」という誤指摘が出た。 + # 実際はその文字列が後で % 展開される前提だった)。 + # 変更系のツールは許可せず、--permission-mode plan も併用する。 set +e claude -p "$(cat <<'PROMPT' - 添付の差分をレビューしてください。返答は次のJSONのみとし、前後に文章を付けないこと。 + このリポジトリの Pull Request をレビューしてください。 + 差分は標準入力から渡されます。 + + ## 最重要の規則: 指摘する前に必ず裏を取る + + 差分は前後の文脈が欠けています。差分の見た目だけで判断すると誤検知になります。 + 指摘を書く前に、必ず Read/Grep/Glob で該当ファイルの実物を読み、 + その指摘が本当に成立するかを確認してください。 + + 確認せずに指摘してはいけない例: + - 「この変数は未定義に見える」→ ファイル全体を読めば定義されている + - 「この書式は誤り」→ その文字列が後で加工される前提かもしれない + - 「呼び出し側の追随が無い」→ 差分外のファイルを grep すれば分かる + + 裏が取れなかったものは **書かない**。件数を稼ぐ必要はありません。 + 指摘ゼロは正当な結論です。 + + ## 観点(この順で重視) + + 1. 認可の欠落・後退 + デコレータの削除、permission factory の無効化(None 代入等)、 + 所有者チェックの欠落、ロール判定の緩和 + 2. 破壊的操作の追加・条件緩和 + 削除/上書き処理の新設、既定値が安全側から危険側に変わる変更 + 3. 入力検証の不足 + 外部入力をそのまま使う、パス連結、スキーマ検証なし + 4. 既存挙動を変える変更で、呼び出し側への影響が未考慮のもの + 関数シグネチャ、戻り値の形、列名・キー名の変更など。 + **grep で実際に呼び出し箇所を確認してから指摘すること** + + ## 出力 + + 最後に次のJSONだけを出力してください。前後に文章を付けないこと。 {"findings":[{"file":"","line":0,"severity":"high|medium|low", - "title":"","detail":"","evidence":""}]} - - 観点(この順で重視): - 1. 認可の欠落・後退(デコレータの削除、permission factory の無効化、 - 所有者チェックの欠落) - 2. 破壊的操作(削除・上書き)の追加や条件緩和 - 3. 入力検証の不足 - 4. 既存挙動を変える変更で、呼び出し側への影響が考慮されていないもの - - 規則: - - 差分から読み取れる事実のみを書く。推測は書かない。 - - 根拠(evidence)には該当行の抜粋を入れる。 - - 指摘が無ければ {"findings":[]} を返す。 + "title":"","detail":"","evidence":"","verified":""}]} + + detail : 何が問題で何が起きるかを1〜2文で + evidence : 該当行の抜粋 + verified : **どのファイルを読んで裏を取ったか**(例 "utils.py:120-140 を確認") + ここが埋まらない指摘は出力しないこと + + 指摘が無ければ {"findings":[]} を返してください。 PROMPT )" --output-format json --model "$MODEL" --permission-mode plan \ + --allowed-tools "Read,Grep,Glob" \ < diff.patch > raw.json 2> claude.err rc=$? set -e @@ -133,6 +162,8 @@ jobs: print(f"- **[{x.get('severity','?')}] {x.get('title','')}** {loc}") if x.get('detail'): print(f" - {x['detail']}") + if x.get('verified'): + print(f" - 確認: {x['verified']}") PY cat review.md