Files
json/tools/api_checker/snapshot_release.py
T
Niels Lohmann 4c73319d3b Use one-line module docstrings in the API checker scripts
Codacy runs two docstring checkers with opposite rules for module
docstrings: with the summary on the first line it reported D213, with
the summary on the second line it reports D212. A one-line docstring
satisfies both. Keep the summary as the docstring and move the details
into a comment below it; nothing reads the module docstrings.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-28 18:42:34 +02:00

110 lines
4.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Capture immutable, per-release API surface records into tools/api_checker/history/."""
# These are the durable, committed counterpart to diff_api.py's live git-archive-and-extract path:
# once a release is tagged, run this once to capture tools/api_checker/history/<tag>.json, commit
# it, and future diffs against that tag hit the fast, no-libclang-needed stored-file path in
# diff_api.py automatically. See tools/api_checker/README.md's "Workflow: Release Checklist" and
# POLICY.md for the full policy (manual step, not CI-automated; files are immutable once committed
# -- regenerate only via --force, and only as a deliberate, reviewed choice).
import argparse
import datetime
import json
import os
# subprocess is only called with fixed argument lists, never through a shell.
import subprocess # nosec B404
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
HISTORY_DIR = os.path.join(SCRIPT_DIR, 'history')
sys.path.insert(0, SCRIPT_DIR)
from diff_api import extract_surface_for_ref # noqa: E402
def discover_v3_tags() -> list:
"""
List every v3.* git tag, sorted by dotted version.
Sorting is numeric, not lexicographic: v3.9.0 must sort before v3.10.0.
"""
result = subprocess.run(['git', 'tag', '--list', 'v3.*'], capture_output=True, text=True, check=True) # nosec B603 B607
tags = [t for t in result.stdout.splitlines() if t.strip()]
def version_key(tag):
parts = tag.lstrip('v').replace('-', '.').split('.')
return tuple(int(p) if p.isdigit() else p for p in parts)
return sorted(tags, key=version_key)
def snapshot_one(ref: str, output_dir: str, force: bool, header: str, include: str) -> tuple:
"""Capture one ref's surface. Returns (ref, success, message)."""
output_path = os.path.join(output_dir, f"{ref}.json")
if os.path.exists(output_path) and not force:
return ref, True, f"skipped (already exists at {output_path}; use --force to overwrite)"
try:
surface = extract_surface_for_ref(ref, header, include)
except SystemExit:
return ref, False, "extraction failed (see output above)"
except Exception as e: # noqa: BLE001 -- a batch backfill must not die on one bad tag
return ref, False, f"unexpected error: {e}"
surface['meta']['generated_at'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
surface['meta']['generator'] = 'tools/api_checker/extract_api.py'
os.makedirs(output_dir, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(surface, f, indent=2, sort_keys=True)
f.write('\n')
return ref, True, f"{len(surface['public_api'])} entries -> {output_path}"
def main():
parser = argparse.ArgumentParser(
description='Capture per-release API surface snapshots into tools/api_checker/history/')
parser.add_argument('--ref', action='append', default=[],
help='Git ref (tag) to snapshot; repeatable')
parser.add_argument('--all-tags', action='store_true',
help='Snapshot every v3.* tag (existing history files are skipped unless --force)')
parser.add_argument('--output-dir', default=HISTORY_DIR,
help='Directory to write history files into (default: tools/api_checker/history/)')
parser.add_argument('--force', action='store_true',
help='Overwrite an existing history file (history files are immutable by '
'convention -- only pass this for a deliberate, reviewed regeneration)')
parser.add_argument('--header', default='include/nlohmann/json.hpp')
parser.add_argument('--include', default='include')
args = parser.parse_args()
refs = list(args.ref)
if args.all_tags:
refs = discover_v3_tags()
if not refs:
parser.error('specify at least one --ref, or --all-tags')
print(f"Snapshotting {len(refs)} ref(s)...")
results = []
for ref in refs:
print(f"\n=== {ref} ===")
result = snapshot_one(ref, args.output_dir, args.force, args.header, args.include)
print(result[2])
results.append(result)
failures = [(ref, msg) for ref, ok, msg in results if not ok]
print("\n" + "=" * 70)
print(f"Done: {len(results) - len(failures)} succeeded, {len(failures)} failed")
if failures:
print("\nFailures (review and record accepted gaps in tools/api_checker/history/README.md):")
for ref, msg in failures:
print(f" {ref}: {msg}")
sys.exit(1)
if __name__ == '__main__':
main()