From 609fa8f0c149912b98ff9cd7a40fd4d886a23b5c Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Fri, 25 Sep 2026 01:53:20 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix(tools):=20add=5Frow.py=20=E3=81=8C?= =?UTF-8?q?=E5=BB=83=E6=AD=A2=E3=81=97=E3=81=9F=20no=20=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E3=81=84=E5=9B=9E=E3=81=95=E3=81=AA=E3=81=84=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 台帳の no は主キーで、起票・許可リスト・所見の本文から参照される。 プライベートリポジトリ側で、払い出した番号を no_registry.tsv に残し (廃止した番号も消さない)、振り直しと使い回しを検査で落とすようにした。 add_row.py は台帳だけを見て「最大値 + 1」を振っていたので、末尾の行を 廃止した直後にその番号をもう一度振ってしまう。検査で落ちるので事故には ならないが、手で直す手間が出る。 add_row.py 新しい番号は、台帳と no_registry.tsv の両方の最大値の次にする。 --append のとき no_registry.tsv にも同じ番号で書き足す。 no_registry.tsv が無ければ従来どおり台帳だけで決め、注意を出す。 ヘッダが想定と違えば何も書かずに止める。 merge.py / scripts/README.md merge.py は全行を振り直すので、初回生成専用であることを明記した。 行の追加・削除・経路の改名で no をどう扱うかの手順を書いた。 tests/test_add_row.py 修正前の add_row.py では 4 件が落ちることを確かめた。 Co-Authored-By: Claude Opus 5.5 --- tools/api-inventory/scripts/README.md | 35 +++++++++- tools/api-inventory/scripts/add_row.py | 42 ++++++++++-- tools/api-inventory/scripts/merge.py | 6 +- tools/api-inventory/scripts/paths.py | 1 + tools/api-inventory/tests/test_add_row.py | 83 +++++++++++++++++++++++ 5 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 tools/api-inventory/tests/test_add_row.py diff --git a/tools/api-inventory/scripts/README.md b/tools/api-inventory/scripts/README.md index bb39e005ad..b24952f40c 100644 --- a/tools/api-inventory/scripts/README.md +++ b/tools/api-inventory/scripts/README.md @@ -113,7 +113,7 @@ python3 tools/api-inventory/scripts/reconcile.py python3 tools/api-inventory/scripts/add_row.py --endpoint api:weko_admin.foo # URI の一部でも探せる: --uri /api/items/import-task -# 3) 追記する +# 3) 追記する(no_registry.tsv にも同じ番号で書き足される) python3 tools/api-inventory/scripts/add_row.py --endpoint api:weko_admin.foo --append # 4) TODO の列を埋める(下記) @@ -130,6 +130,33 @@ python3 tools/api-inventory/scripts/build_checklist.py python3 tools/api-inventory/scripts/reconcile.py --gate # 差分0になること ``` +### `no` は主キー。振り直さない・使い回さない + +`no` は起票(`fix_ticket`)・許可リスト・所見の本文から参照される。振り直すと +それらが**黙って別の経路を指す**。払い出した番号はプライベートリポジトリ側の +`no_registry.tsv` に、廃止した番号も含めて全部残っている。 + +`add_row.py` は、台帳と `no_registry.tsv` の両方の最大値の次を新しい番号にし、 +`--append` のとき `no_registry.tsv` にも同じ番号で書き足す。台帳だけを見て +「最大値 + 1」にすると、末尾の行を廃止した直後にその番号をもう一度振ってしまう。 +手で行を足すときも、この二つを揃えること。 + +### 台帳から行を消す + +経路が無くなった行は台帳から消してよいが、**番号は詰めない**。 + +1. 台帳(`weko3_api_list_full.tsv`)から行を消す +2. `no_registry.tsv` の同じ番号の行は**消さずに**、`status` を `廃止` にし、 + `note` に理由を書く +3. 手順6(派生列の再計算 → 32列版の再生成 → 突き合わせ)を回す + +経路の表記だけが変わった(uri の改名、method の追加など)ときは、行を消して +足し直すのではなく、同じ番号のまま台帳と `no_registry.tsv` を書き換え、 +`no_registry.tsv` の `note` に旧値を書く。 + +振り直しや使い回しは、プライベートリポジトリ側の `tests/test_no_stability.py` が +リリースタグと `main` の台帳と突き合わせて落とす。 + ### 機械付与スクリプトで TODO を減らす `add_row.py --append` の直後に、Phase 2 の機械付与スクリプトを流すと `TODO` が減る。 @@ -695,7 +722,7 @@ git push origin main --follow-tags | `test_coverage.py` | full.tsv + テストコード | full.tsv の 59-63列 | | `prioritize.py` | full.tsv | full.tsv の 57-58, 64列 + 末尾列順の正規化 | | `build_checklist.py` | full.tsv | **`weko3_api_list.tsv` を全体再生成** | -| `add_row.py` | `api_snapshot.json` + git | full.tsv に新規行の雛形を追記(`--append`) | +| `add_row.py` | `api_snapshot.json` + git | full.tsv に新規行の雛形を追記し、`no_registry.tsv` に番号を払い出す(`--append`) | | `apply_probe_results.py` | probe.json | full.tsv の `dynamic_verified`(空欄のみ / `--overwrite` で差し替え、`--keep-history` で旧値を ` ‖ 旧: ` として残す) | | `measure.sh` | `measure_profile.json` | 実測の唯一の入口。上記を固定順で回し `measure_report.md` を書く | | `_ensure_profile.py` / `_read_profile.py` / `_targets.py` / `_report.py` | — | `measure.sh` の内部ヘルパ | @@ -1080,6 +1107,10 @@ python3 tools/api-inventory/probe.py probe_results.json # 未認証+各ロー python3 tools/api-inventory/merge.py out/ merged.tsv # 分割TSVを結合・重複排除・採番 ``` +**初回生成専用。** 全行に `no` を振り直すので、既存の台帳に使ってはいけない +(`no` は主キーで、振り直すと起票や許可リストが別の経路を指す)。既存の台帳に +行を足すのは `add_row.py`。 + ## Phase 5: チェックリスト版(32列)を生成 ```bash python3 tools/api-inventory/scripts/build_checklist.py # 64列 full → 32列 に統合 diff --git a/tools/api-inventory/scripts/add_row.py b/tools/api-inventory/scripts/add_row.py index 85e5096499..4da6ebe3c9 100644 --- a/tools/api-inventory/scripts/add_row.py +++ b/tools/api-inventory/scripts/add_row.py @@ -19,6 +19,11 @@ 派生列(priority / test_* / cleanup)は空のままでよい。後で test_coverage.py → prioritize.py が付与する。 + +`no` は台帳の主キー。**一度振った番号は変えず、使い回さない**。払い出した番号は +`no_registry.tsv`(廃止した番号も残る)にあるので、新しい番号はそこと台帳の +両方の最大値の次にする。台帳だけを見ると、末尾の行を廃止した直後にその番号を +もう一度振ってしまう。`--append` では `no_registry.tsv` にも同じ番号で書き足す。 """ import argparse import json @@ -32,6 +37,8 @@ from snapshot import default_weko_root # noqa: E402 AUTO_TODO = 'TODO' +REGISTRY = 'no_registry.tsv' +REGISTRY_COLUMNS = ['no', 'app', 'method', 'uri', 'endpoint', 'status', 'note'] DERIVED = ('priority', 'priority_reason', 'test_normal', 'test_abnormal', 'test_boundary', 'test_exception', 'test_gap', 'cleanup') @@ -131,6 +138,20 @@ def build(hdr, snap_key, e, root, next_no): return [v[n] for n in hdr] +def _nos(lines): + return [int(l.split('\t')[0]) for l in lines[1:] if l.split('\t')[0].isdigit()] + + +def next_no(full_lines, registry_lines): + """台帳と払い出し記録のどちらでもまだ使われていない、最小の番号。""" + return max(_nos(full_lines) + _nos(registry_lines) + [0]) + 1 + + +def registry_entry(hdr, row): + v = dict(zip(hdr, row)) + return [v['no'], v['app'], v['method'], v['uri'], v['endpoint'], '現役', ''] + + def main(): p = argparse.ArgumentParser(description='台帳に新規行の雛形を作る') p.add_argument('--endpoint', help='api_snapshot.json のキー(例 api:weko_admin.foo)') @@ -138,6 +159,8 @@ def main(): p.add_argument('--snapshot', default=None) p.add_argument('--full', default=None) p.add_argument('--weko-root', default=None) + p.add_argument('--registry', default=None, + help=f'番号の払い出し記録(既定は台帳と同じ場所の {REGISTRY})') p.add_argument('--append', action='store_true', help='full.tsv に追記する(既定は表示のみ)') a = p.parse_args() snap_p = a.snapshot or data_path('api_snapshot.json') @@ -166,19 +189,30 @@ def full_rules(v): lines = open(full, encoding='utf-8').read().rstrip('\n').split('\n') hdr = lines[0].split('\t') - next_no = max(int(l.split('\t')[0]) for l in lines[1:] - if l.split('\t')[0].isdigit()) + 1 + registry = a.registry or os.path.join(os.path.dirname(os.path.abspath(full)), REGISTRY) + reg_lines = (open(registry, encoding='utf-8').read().rstrip('\n').split('\n') + if os.path.isfile(registry) else []) + if reg_lines and reg_lines[0].split('\t') != REGISTRY_COLUMNS: + sys.exit(f'{registry} のヘッダが想定と違います: {reg_lines[0]}') + no = next_no(lines, reg_lines) rows = [] for k in keys: - rows.append(build(hdr, k, E[k], root, next_no)) - next_no += 1 + rows.append(build(hdr, k, E[k], root, no)) + no += 1 if a.append: with open(full, 'a', encoding='utf-8') as f: for r in rows: f.write('\t'.join(r) + '\n') print(f'{full} に {len(rows)} 行を追記しました。') + if reg_lines: + with open(registry, 'a', encoding='utf-8') as f: + for r in rows: + f.write('\t'.join(registry_entry(hdr, r)) + '\n') + print(f'{registry} に同じ番号を払い出しました。') + else: + print(f'注意: {registry} が無いため、番号の払い出しを記録していません。') print(' 次に: TODO の列をソースを読んで埋め、') print(' test_coverage.py → prioritize.py → build_checklist.py を実行') else: diff --git a/tools/api-inventory/scripts/merge.py b/tools/api-inventory/scripts/merge.py index e611b39776..930c1471b0 100644 --- a/tools/api-inventory/scripts/merge.py +++ b/tools/api-inventory/scripts/merge.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- -"""out/*.tsv をマージ・整形・採番して 1本の TSV にする。""" +"""out/*.tsv をマージ・整形・採番して 1本の TSV にする。 + +**台帳の初回生成専用。** 全行に `no` を振り直すので、既存の台帳には使わない +(`no` は主キーで、一度振った番号は変えない)。既存の台帳に行を足すのは add_row.py。 +""" import glob, os, sys NCOL = 41 diff --git a/tools/api-inventory/scripts/paths.py b/tools/api-inventory/scripts/paths.py index d67d7f8b5a..3b38286e65 100644 --- a/tools/api-inventory/scripts/paths.py +++ b/tools/api-inventory/scripts/paths.py @@ -11,6 +11,7 @@ weko3_api_list.tsv 台帳(24列) api_snapshot.json 経路のベースライン reconcile_allow.json 実機に無い行の許可リスト + no_registry.tsv no(主キー)の払い出し記録。廃止した番号も残す """ import os import sys diff --git a/tools/api-inventory/tests/test_add_row.py b/tools/api-inventory/tests/test_add_row.py new file mode 100644 index 0000000000..1ccac47fe7 --- /dev/null +++ b/tools/api-inventory/tests/test_add_row.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +"""add_row.py — 新しい行に振る `no`。 + +`no` は台帳の主キーで、一度振った番号は変えず、使い回さない。消した行の番号は +`no_registry.tsv` に `廃止` として残るので、台帳だけを見て「最大値 + 1」にすると、 +末尾の行を廃止した直後にその番号をもう一度振ってしまう。 +""" +import json + +import add_row +from conftest import FULL_HEADER, make_row, run + +REG_HEADER = '\t'.join(add_row.REGISTRY_COLUMNS) + + +def _setup(tmp_path, full_nos, registry=None): + full = tmp_path / 'weko3_api_list_full.tsv' + lines = ['\t'.join(FULL_HEADER)] + for n in full_nos: + r = make_row(no=str(n), uri=f'/demo{n}', endpoint=f'demo.v{n}') + lines.append('\t'.join(r[h] for h in FULL_HEADER)) + full.write_text('\n'.join(lines) + '\n', encoding='utf-8') + if registry is not None: + (tmp_path / 'no_registry.tsv').write_text( + '\n'.join([REG_HEADER] + registry) + '\n', encoding='utf-8') + snap = tmp_path / 'api_snapshot.json' + snap.write_text(json.dumps({'endpoints': {'ui:demo.new': { + 'app': 'ui', 'endpoint': 'demo.new', 'view': 'weko_demo.views.new', + 'routes': [{'rule': '/demo/new', 'methods': ['GET']}]}}}), encoding='utf-8') + return full, snap + + +def _append(tmp_path, full, snap): + return run('add_row.py', '--endpoint', 'ui:demo.new', '--append', + '--full', full, '--snapshot', snap, '--weko-root', tmp_path, expect=0) + + +def _last(path): + return open(path, encoding='utf-8').read().rstrip('\n').split('\n')[-1].split('\t') + + +def test_次の番号は払い出し記録と台帳の両方の最大値の次(tmp_path): + # 3 を廃止して台帳から消した直後。台帳だけ見ると 3 を振ってしまう + full, snap = _setup(tmp_path, [1, 2], registry=[ + '1\tUIアプリ\tGET\t/demo1\tdemo.v1\t現役\t', + '2\tUIアプリ\tGET\t/demo2\tdemo.v2\t現役\t', + '3\tUIアプリ\tGET\t/demo3\tdemo.v3\t廃止\t経路が消えた', + ]) + _append(tmp_path, full, snap) + assert _last(full)[0] == '4' + + +def test_追記した行を同じ番号で払い出し記録に書き足す(tmp_path): + full, snap = _setup(tmp_path, [1], registry=['1\tUIアプリ\tGET\t/demo1\tdemo.v1\t現役\t']) + _append(tmp_path, full, snap) + assert _last(tmp_path / 'no_registry.tsv') == \ + ['2', 'UIアプリ', 'GET', '/demo/new', 'demo.new', '現役', ''] + + +def test_表示だけなら払い出し記録を触らない(tmp_path): + full, snap = _setup(tmp_path, [1], registry=['1\tUIアプリ\tGET\t/demo1\tdemo.v1\t現役\t']) + before = (tmp_path / 'no_registry.tsv').read_text(encoding='utf-8') + run('add_row.py', '--endpoint', 'ui:demo.new', + '--full', full, '--snapshot', snap, '--weko-root', tmp_path, expect=0) + assert (tmp_path / 'no_registry.tsv').read_text(encoding='utf-8') == before + + +def test_払い出し記録が無ければ台帳の最大値の次で注意を出す(tmp_path): + full, snap = _setup(tmp_path, [1, 5]) + p = _append(tmp_path, full, snap) + assert _last(full)[0] == '6' + assert '払い出しを記録していません' in p.stdout + assert not (tmp_path / 'no_registry.tsv').exists() + + +def test_払い出し記録のヘッダが違えば書き込まない(tmp_path): + full, snap = _setup(tmp_path, [1], registry=[]) + (tmp_path / 'no_registry.tsv').write_text('no\turi\n1\t/demo1\n', encoding='utf-8') + before = full.read_text(encoding='utf-8') + p = run('add_row.py', '--endpoint', 'ui:demo.new', '--append', + '--full', full, '--snapshot', snap, '--weko-root', tmp_path) + assert p.returncode != 0 + assert full.read_text(encoding='utf-8') == before From fe167438c3e24b9be9133d1b1a7ab81701193ebf Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Fri, 25 Sep 2026 05:28:32 +0000 Subject: [PATCH 2/4] =?UTF-8?q?docs(ops):=20=E5=8F=B0=E5=B8=B3=E3=81=AE?= =?UTF-8?q?=E6=A4=9C=E6=9F=BB=E3=82=92=E6=89=8B=E5=85=83=E3=81=A7=E5=9B=9E?= =?UTF-8?q?=E3=81=99=E9=81=8B=E7=94=A8=E3=81=A8=E3=80=81no=20=E3=82=92?= =?UTF-8?q?=E4=B8=BB=E3=82=AD=E3=83=BC=E3=81=A8=E3=81=97=E3=81=A6=E6=89=B1?= =?UTF-8?q?=E3=81=86=E6=89=8B=E9=A0=86=E3=82=92=E6=9B=B8=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit private 側(RCOSDP/weko-secret)は GitHub Actions の利用枠の制限を受けて ジョブが起動しないため、台帳の検査は手元の ci/local.sh を pre-push フックで 回す運用にした。Actions で回るのは public 側だけ。 - §2-3: clone 直後に git config core.hooksPath .githooks を実行する手順と、 private 側で Actions を使わない理由を書いた。 - 台帳の検査のコマンドを python3 -m pytest から ci/local.sh に改めた。 ci/local.sh はツールを同名ブランチの先頭から、解析対象を台帳の測定 リビジョンから取り出すので、手元の worktree の状態に左右されない。 - 手順 4: no は主キーで、振り直さない・使い回さないこと、行の追加・削除・ 改名での no_registry.tsv の扱いを書いた。 - 手順 10: private 側の push でフックが検査を回すこと、private 側の PR には Actions のチェックが付かないことを書いた。 Co-Authored-By: Claude Opus 5.5 --- docs/OPERATIONS.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 33e8fdc7e6..513eda4ab1 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -91,6 +91,16 @@ git clone https://github.com/RCOSDP/weko-secret.git ~/weko-secret **private 側を public リポジトリの中に置かない。** 誤って commit する事故を防ぐため、 必ず別の場所に clone する(`docs/RULE.md` §1)。 +clone したら、private 側で pre-push フックを有効にしておく(clone ごとに 1 回)。 + +```bash +git -C ~/weko-secret config core.hooksPath .githooks +``` + +**private 側は GitHub Actions を使わない。** private リポジトリは Actions の利用枠の +制限を受けるため、台帳の検査は手元の `ci/local.sh` で回す運用にしている。このフックが +push のたびにそれを回し、落ちたら push を止める。Actions で回るのは public 側だけ。 + #### 解析対象のソースを測定リビジョンに固定する 台帳は「どのリビジョンを測ったか」とセットでしか意味を持たない(`docs/RULE.md` 規則 2-3)。 @@ -111,10 +121,14 @@ git -C ~/weko worktree add --detach ~/weko3 "$REV" 向けないこと。** 探索順で古いツールが先に当たる。何も指定しないのが正しい。 ```bash -cd ~/weko-secret && python3 -m pytest # 実機も Docker も要らない +cd ~/weko-secret && ci/local.sh # 実機も Docker も要らない。約 1 分 ``` -列・語彙・派生列の再現・経路の漏れをまとめて見る。**台帳を触ったら必ず回す。** +列・語彙・派生列の再現・経路の漏れ・`no` の固定をまとめて見る。**台帳を触ったら必ず回す。** +push 時にはフックが同じものを回す。`ci/local.sh` はツールを同名ブランチの先頭から、 +解析対象のソースを台帳の測定リビジョンから、それぞれ `~/.cache/weko-secret-ci/` に +取り出して使うので、この worktree の状態には左右されない。編集中にさっと確かめるだけなら +`python3 -m pytest`(この worktree を解析対象に使う)でもよい。 バージョンを上げて台帳の測定先を移したら、この worktree は貼り替える (`git -C ~/weko worktree remove ~/weko3` してから作り直す)。 @@ -439,6 +453,19 @@ python3 $INV/reconcile.py --gate # 「✅ 一致(0件)」になる 消えた経路(B)は `reconcile_allow.json` に**理由付きで**登録する。理由なしの登録は禁止(`docs/RULE.md` §3-3)。 +**`no` は台帳の主キー。振り直さない・使い回さない。** 起票(`fix_ticket`)や許可リストが +番号で行を指しているので、振り直すと黙って別の経路を指す。払い出した番号は private 側の +`no_registry.tsv` に全部残っている。 + +- 行を足す: `add_row.py --append` を使う。台帳と `no_registry.tsv` の両方に次の番号で書く。 +- 行を消す: 台帳から消してよいが、**番号は詰めない**。`no_registry.tsv` の行は残し、 + `status` を `廃止`、`note` に理由を書く。 +- 経路の表記だけが変わった(uri の改名、method の追加など): 同じ番号のまま書き換え、 + `no_registry.tsv` の `note` に旧値を書く。 + +詳しくは `tools/api-inventory/scripts/README.md` のケース2。守れていなければ、private 側の +検査(`tests/test_no_stability.py`)がリリースタグと `main` の台帳と突き合わせて落とす。 + **外部調査と件数が合わないときは、まず相手の環境を疑う。** v2.1.0 ではベンダ資料が新規 23 件としていたが、ソースに存在したのは 5 件だけだった。 残り 18 件は `modules/` ではなく site-packages 側の pip パッケージの版差が原因で、grep しても 1 件も出なかった。 @@ -634,6 +661,10 @@ tools/release/open-pr.sh --base develop_v2.1.0 --inventory-base main --run --inv gh pr checks --watch ``` +private 側の push では pre-push フックが `ci/local.sh` を回す(約 1 分)。落ちたら push は +止まるので、直してから出し直す。private 側の PR には Actions のチェックが付かないので、 +`gh pr checks` に出るのは public 側だけ。 + **マージしてから**、両リポジトリに**同名のタグ**を打つ(`docs/RULE.md` 規則 2-3)。 ```bash From 7ea3eb8162a2d8422cfc61e00c255afd426fe8d6 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Fri, 25 Sep 2026 05:55:15 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix(tools):=20add=5Frow.py=20=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E8=A8=98=E3=82=92=E6=8E=92=E4=BB=96=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=80=81=E6=89=95=E3=81=84=E5=87=BA=E3=81=97=E8=A8=98=E9=8C=B2?= =?UTF-8?q?=E3=82=92=E5=85=88=E3=81=AB=E6=9B=B8=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1917 のレビュー指摘(3件)への対応。 - 払い出し記録を台帳より先に書く。台帳を書いた後に記録の書き込みが 失敗すると、台帳にだけある記録漏れの番号が残っていた。先に記録を 書けば、後で台帳が失敗しても欠番が1つできるだけで済む。 - 番号を読んでから両ファイルに書き終えるまでを flock で排他にする。 並行して --append を回すと同じ番号を二度払い出せた。ロックは台帳 そのものに掛ける(ロック用のファイルを作ると台帳の隣に紛れて commit される)。 - next_no の docstring を実装どおり「最大値の次」に直した。 テストを2件足した。並行実行のテストは、ロックを外すと3回とも落ちる ことを確かめた。 Co-Authored-By: Claude Opus 5.5 --- tools/api-inventory/scripts/add_row.py | 20 ++++++++++--- tools/api-inventory/tests/test_add_row.py | 34 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/tools/api-inventory/scripts/add_row.py b/tools/api-inventory/scripts/add_row.py index 4da6ebe3c9..2ad98baec3 100644 --- a/tools/api-inventory/scripts/add_row.py +++ b/tools/api-inventory/scripts/add_row.py @@ -26,6 +26,7 @@ もう一度振ってしまう。`--append` では `no_registry.tsv` にも同じ番号で書き足す。 """ import argparse +import fcntl import json import os import re @@ -143,7 +144,7 @@ def _nos(lines): def next_no(full_lines, registry_lines): - """台帳と払い出し記録のどちらでもまだ使われていない、最小の番号。""" + """台帳と払い出し記録の最大値の次の番号(欠番は埋めない)。""" return max(_nos(full_lines) + _nos(registry_lines) + [0]) + 1 @@ -187,6 +188,13 @@ def full_rules(v): for k in keys: print(' ' + k) + # 番号を読んでから両ファイルに書き終えるまでを排他にする。並行して --append を + # 回すと、同じ最大値を読んで同じ番号を二度払い出してしまうため。ロックは台帳 + # そのものに掛ける(ロック用のファイルを作ると台帳の隣に紛れて commit される)。 + lock = open(full, 'a') if a.append else None + if lock: + fcntl.flock(lock, fcntl.LOCK_EX) + lines = open(full, encoding='utf-8').read().rstrip('\n').split('\n') hdr = lines[0].split('\t') registry = a.registry or os.path.join(os.path.dirname(os.path.abspath(full)), REGISTRY) @@ -202,14 +210,18 @@ def full_rules(v): no += 1 if a.append: + # 払い出し記録を先に書く。台帳の書き込みが後で失敗しても、番号が1つ + # 欠番になるだけで済む(逆順だと、台帳にだけある記録漏れの番号が残る)。 + if reg_lines: + with open(registry, 'a', encoding='utf-8') as f: + for r in rows: + f.write('\t'.join(registry_entry(hdr, r)) + '\n') with open(full, 'a', encoding='utf-8') as f: for r in rows: f.write('\t'.join(r) + '\n') + lock.close() print(f'{full} に {len(rows)} 行を追記しました。') if reg_lines: - with open(registry, 'a', encoding='utf-8') as f: - for r in rows: - f.write('\t'.join(registry_entry(hdr, r)) + '\n') print(f'{registry} に同じ番号を払い出しました。') else: print(f'注意: {registry} が無いため、番号の払い出しを記録していません。') diff --git a/tools/api-inventory/tests/test_add_row.py b/tools/api-inventory/tests/test_add_row.py index 1ccac47fe7..827c012d4d 100644 --- a/tools/api-inventory/tests/test_add_row.py +++ b/tools/api-inventory/tests/test_add_row.py @@ -6,6 +6,9 @@ 末尾の行を廃止した直後にその番号をもう一度振ってしまう。 """ import json +import os +import subprocess +import sys import add_row from conftest import FULL_HEADER, make_row, run @@ -81,3 +84,34 @@ def test_払い出し記録のヘッダが違えば書き込まない(tmp_path): '--full', full, '--snapshot', snap, '--weko-root', tmp_path) assert p.returncode != 0 assert full.read_text(encoding='utf-8') == before + + +def test_払い出し記録に書けなければ台帳を触らない(tmp_path): + """記録を先に書く。逆順だと、台帳にだけある記録漏れの番号が残る。""" + full, snap = _setup(tmp_path, [1], registry=['1\tUIアプリ\tGET\t/demo1\tdemo.v1\t現役\t']) + reg = tmp_path / 'no_registry.tsv' + reg.chmod(0o444) + if os.access(reg, os.W_OK): # root では読み取り専用にならない + import pytest + pytest.skip('読み取り専用のファイルに書けてしまう環境') + before = full.read_text(encoding='utf-8') + p = run('add_row.py', '--endpoint', 'ui:demo.new', '--append', + '--full', full, '--snapshot', snap, '--weko-root', tmp_path) + assert p.returncode != 0 + assert full.read_text(encoding='utf-8') == before + + +def test_並行して追記しても同じ番号を二度払い出さない(tmp_path): + full, snap = _setup(tmp_path, [1], registry=['1\tUIアプリ\tGET\t/demo1\tdemo.v1\t現役\t']) + e = {k: v for k, v in os.environ.items() if k != 'WEKO_API_INVENTORY_DIR'} + cmd = [sys.executable, os.path.join(os.path.dirname(add_row.__file__), 'add_row.py'), + '--endpoint', 'ui:demo.new', '--append', + '--full', str(full), '--snapshot', str(snap), '--weko-root', str(tmp_path)] + procs = [subprocess.Popen(cmd, env=e, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + for _ in range(8)] + assert all(p.wait() == 0 for p in procs) + nos = [l.split('\t')[0] for l in full.read_text(encoding='utf-8').rstrip('\n').split('\n')[1:]] + assert sorted(nos, key=int) == [str(i) for i in range(1, 10)] + reg_nos = [l.split('\t')[0] for l in + (tmp_path / 'no_registry.tsv').read_text(encoding='utf-8').rstrip('\n').split('\n')[1:]] + assert reg_nos == [str(i) for i in range(1, 10)] From 75af0d858fc4f3216fb4717f9cfcc85b355b04d5 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Fri, 25 Sep 2026 08:34:56 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix(tools):=20add=5Frow.py=20=E3=81=8C?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E3=81=97=E3=81=AA=E3=81=84=E5=8F=B0=E5=B8=B3?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E3=82=89=E3=81=AA=E3=81=84=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1917 のレビュー追加指摘への対応。ロックを取るために台帳を 'a' で 開いていたので、--full を打ち間違えると空の台帳ファイルを黙って作り、 意味のない行を書き込んでいた。'r+' で開き、無ければその場で止める。 テストを1件足した。'a' に戻すと落ちることを確かめた。 Co-Authored-By: Claude Opus 5.5 --- tools/api-inventory/scripts/add_row.py | 3 ++- tools/api-inventory/tests/test_add_row.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/api-inventory/scripts/add_row.py b/tools/api-inventory/scripts/add_row.py index 2ad98baec3..bf1ce44c6c 100644 --- a/tools/api-inventory/scripts/add_row.py +++ b/tools/api-inventory/scripts/add_row.py @@ -191,7 +191,8 @@ def full_rules(v): # 番号を読んでから両ファイルに書き終えるまでを排他にする。並行して --append を # 回すと、同じ最大値を読んで同じ番号を二度払い出してしまうため。ロックは台帳 # そのものに掛ける(ロック用のファイルを作ると台帳の隣に紛れて commit される)。 - lock = open(full, 'a') if a.append else None + # 'r+' で開く。'a' だと --full の打ち間違いで空の台帳を黙って作ってしまう。 + lock = open(full, 'r+') if a.append else None if lock: fcntl.flock(lock, fcntl.LOCK_EX) diff --git a/tools/api-inventory/tests/test_add_row.py b/tools/api-inventory/tests/test_add_row.py index 827c012d4d..713f952c63 100644 --- a/tools/api-inventory/tests/test_add_row.py +++ b/tools/api-inventory/tests/test_add_row.py @@ -115,3 +115,13 @@ def test_並行して追記しても同じ番号を二度払い出さない(tmp_ reg_nos = [l.split('\t')[0] for l in (tmp_path / 'no_registry.tsv').read_text(encoding='utf-8').rstrip('\n').split('\n')[1:]] assert reg_nos == [str(i) for i in range(1, 10)] + + +def test_台帳が無ければ作らずに止まる(tmp_path): + """--full の打ち間違いで、空の台帳を黙って作って書き込まない。""" + full, snap = _setup(tmp_path, [1]) + missing = tmp_path / 'typo.tsv' + p = run('add_row.py', '--endpoint', 'ui:demo.new', '--append', + '--full', missing, '--snapshot', snap, '--weko-root', tmp_path) + assert p.returncode != 0 + assert not missing.exists()