Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)。
Expand All @@ -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` してから作り直す)。

Expand Down Expand Up @@ -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 件も出なかった。
Expand Down Expand Up @@ -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
Expand Down
35 changes: 33 additions & 2 deletions tools/api-inventory/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 の列を埋める(下記)
Expand All @@ -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` が減る。
Expand Down Expand Up @@ -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` の内部ヘルパ |
Expand Down Expand Up @@ -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列 に統合
Expand Down
55 changes: 51 additions & 4 deletions tools/api-inventory/scripts/add_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@

派生列(priority / test_* / cleanup)は空のままでよい。後で
test_coverage.py → prioritize.py が付与する。

`no` は台帳の主キー。**一度振った番号は変えず、使い回さない**。払い出した番号は
`no_registry.tsv`(廃止した番号も残る)にあるので、新しい番号はそこと台帳の
両方の最大値の次にする。台帳だけを見ると、末尾の行を廃止した直後にその番号を
もう一度振ってしまう。`--append` では `no_registry.tsv` にも同じ番号で書き足す。
"""
import argparse
import fcntl
import json
import os
import re
Expand All @@ -32,6 +38,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')

Expand Down Expand Up @@ -131,13 +139,29 @@ 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)')
p.add_argument('--uri', help='URI の一部で検索して特定する')
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')
Expand All @@ -164,21 +188,44 @@ def full_rules(v):
for k in keys:
print(' ' + k)

# 番号を読んでから両ファイルに書き終えるまでを排他にする。並行して --append を
# 回すと、同じ最大値を読んで同じ番号を二度払い出してしまうため。ロックは台帳
# そのものに掛ける(ロック用のファイルを作ると台帳の隣に紛れて commit される)。
# 'r+' で開く。'a' だと --full の打ち間違いで空の台帳を黙って作ってしまう。
lock = open(full, 'r+') 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')
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
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

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:
print(f'{registry} に同じ番号を払い出しました。')
else:
print(f'注意: {registry} が無いため、番号の払い出しを記録していません。')
print(' 次に: TODO の列をソースを読んで埋め、')
print(' test_coverage.py → prioritize.py → build_checklist.py を実行')
else:
Expand Down
6 changes: 5 additions & 1 deletion tools/api-inventory/scripts/merge.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions tools/api-inventory/scripts/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
weko3_api_list.tsv 台帳(24列)
api_snapshot.json 経路のベースライン
reconcile_allow.json 実機に無い行の許可リスト
no_registry.tsv no(主キー)の払い出し記録。廃止した番号も残す
"""
import os
import sys
Expand Down
127 changes: 127 additions & 0 deletions tools/api-inventory/tests/test_add_row.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
"""add_row.py — 新しい行に振る `no`。

`no` は台帳の主キーで、一度振った番号は変えず、使い回さない。消した行の番号は
`no_registry.tsv` に `廃止` として残るので、台帳だけを見て「最大値 + 1」にすると、
末尾の行を廃止した直後にその番号をもう一度振ってしまう。
"""
import json
import os
import subprocess
import sys

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


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)]


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()
Loading