UltrafastSecp256k1/scripts/release_diff.py
shrec 56413c0942
feat: CI preflight gate, assurance tooling, adversarial test depth
New CI workflow:
- preflight.yml: security+ABI hard-fail, coverage/freshness advisory,
  assurance report artifact upload

New scripts:
- validate_assurance.py: cross-ref ledger vs ufsecp.h, TEST_MATRIX vs CTest
- export_assurance.py: machine-readable JSON (subsystems, API coverage,
  security density, protocol status, routing summary)
- release_diff.py: release diff with ABI changes, categorized files, checklist

New docs:
- BACKEND_ASSURANCE_MATRIX.md: CPU/CUDA/OpenCL/Metal feature/audit/secret matrix
- RELEASE_VERIFICATION.md: SHA256/cosign/SLSA provenance verification guide

Modified:
- preflight.py: DOC_PAIRS expanded 5->21 (protocols, CT, GPU, headers)
- test_adversarial_protocol.cpp: +test_frost_stale_commitment_replay (B.7),
  +test_ffi_invalid_enums (G.21: network/compressed flag boundary values)
2026-03-15 12:49:58 +00:00

187 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""
release_diff.py -- Generate release diff summary between two tags/commits.
Produces a structured report of:
- New/removed ABI functions (ufsecp_*)
- Changed test targets
- Security pattern changes
- Doc changes
- Protocol/feature changes
Usage:
python3 scripts/release_diff.py v3.21.0 v3.22.0
python3 scripts/release_diff.py HEAD~10 HEAD
python3 scripts/release_diff.py v3.22.0 HEAD --json
"""
import re
import subprocess
import sys
import json
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
LIB_ROOT = SCRIPT_DIR.parent
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
CYAN = '\033[96m'
BOLD = '\033[1m'
RESET = '\033[0m'
def git_diff_files(ref_from, ref_to):
"""Get list of changed files between two refs."""
result = subprocess.run(
['git', 'diff', '--name-status', ref_from, ref_to],
capture_output=True, text=True, cwd=str(LIB_ROOT)
)
files = {'A': [], 'M': [], 'D': [], 'R': []}
for line in result.stdout.strip().split('\n'):
if not line.strip():
continue
parts = line.split('\t')
status = parts[0][0] # A/M/D/R
fname = parts[-1]
files.setdefault(status, []).append(fname)
return files
def git_diff_content(ref_from, ref_to, path_filter):
"""Get diff content for a specific path pattern."""
result = subprocess.run(
['git', 'diff', ref_from, ref_to, '--', path_filter],
capture_output=True, text=True, cwd=str(LIB_ROOT)
)
return result.stdout
def extract_abi_changes(ref_from, ref_to):
"""Find added/removed ufsecp_* function declarations."""
diff = git_diff_content(ref_from, ref_to, 'include/ufsecp/ufsecp.h')
fn_re = re.compile(r'(ufsecp_\w+)\s*\(')
added, removed = [], []
for line in diff.split('\n'):
if line.startswith('+') and not line.startswith('+++'):
for m in fn_re.finditer(line):
added.append(m.group(1))
elif line.startswith('-') and not line.startswith('---'):
for m in fn_re.finditer(line):
removed.append(m.group(1))
return sorted(set(added) - set(removed)), sorted(set(removed) - set(added))
def categorize_changes(changed_files):
"""Categorize changed files by area."""
categories = {
'abi': [], 'ct_layer': [], 'protocol': [], 'gpu': [],
'tests': [], 'docs': [], 'ci': [], 'build': [], 'other': [],
}
for status, files in changed_files.items():
for f in files:
entry = f"{status}\t{f}"
if 'include/ufsecp/' in f:
categories['abi'].append(entry)
elif 'ct_' in f or '/ct/' in f:
categories['ct_layer'].append(entry)
elif any(p in f for p in ['musig', 'frost', 'adaptor', 'silent_pay', 'ecies', 'dleq']):
categories['protocol'].append(entry)
elif any(p in f for p in ['cuda/', 'opencl/', 'metal/']):
categories['gpu'].append(entry)
elif any(p in f for p in ['test', 'audit/', 'fuzz']):
categories['tests'].append(entry)
elif f.startswith('docs/') or f.endswith('.md'):
categories['docs'].append(entry)
elif '.github/' in f:
categories['ci'].append(entry)
elif 'CMakeLists' in f or f.endswith('.cmake'):
categories['build'].append(entry)
else:
categories['other'].append(entry)
return categories
def main():
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <from-ref> <to-ref> [--json]")
sys.exit(1)
ref_from = sys.argv[1]
ref_to = sys.argv[2]
json_mode = '--json' in sys.argv
changed = git_diff_files(ref_from, ref_to)
abi_added, abi_removed = extract_abi_changes(ref_from, ref_to)
categories = categorize_changes(changed)
total_files = sum(len(v) for v in changed.values())
if json_mode:
report = {
'from': ref_from,
'to': ref_to,
'total_files_changed': total_files,
'abi_added': abi_added,
'abi_removed': abi_removed,
'categories': {k: v for k, v in categories.items() if v},
}
print(json.dumps(report, indent=2))
return
print(f"\n{BOLD}{'='*60}{RESET}")
print(f"{BOLD} Release Diff: {ref_from} -> {ref_to}{RESET}")
print(f"{BOLD}{'='*60}{RESET}\n")
print(f" Total files changed: {total_files}")
print(f" Added: {len(changed.get('A', []))}, Modified: {len(changed.get('M', []))}, "
f"Deleted: {len(changed.get('D', []))}\n")
# ABI changes
if abi_added or abi_removed:
print(f"{BOLD}ABI Surface Changes{RESET}")
for fn in abi_added:
print(f" {GREEN}+ {fn}{RESET}")
for fn in abi_removed:
print(f" {RED}- {fn}{RESET}")
print()
# By category
for cat, entries in categories.items():
if not entries:
continue
print(f"{BOLD}{cat.upper().replace('_', ' ')} ({len(entries)}){RESET}")
for e in entries[:15]:
print(f" {e}")
if len(entries) > 15:
print(f" ... +{len(entries) - 15} more")
print()
# Checklist
print(f"{BOLD}Release Checklist{RESET}")
checks = [
('ABI functions added/removed', bool(abi_added or abi_removed)),
('CT layer files changed', bool(categories['ct_layer'])),
('Protocol files changed', bool(categories['protocol'])),
('GPU backends changed', bool(categories['gpu'])),
('Test files changed', bool(categories['tests'])),
('CI workflows changed', bool(categories['ci'])),
('Build system changed', bool(categories['build'])),
]
for desc, triggered in checks:
marker = f"{YELLOW}[!]{RESET}" if triggered else f"{GREEN}[ ]{RESET}"
print(f" {marker} {desc}")
if abi_added or abi_removed:
print(f"\n {YELLOW}ACTION: Update binding READMEs for ABI changes{RESET}")
if categories['ct_layer']:
print(f" {YELLOW}ACTION: Verify CT security patterns preserved{RESET}")
if categories['protocol']:
print(f" {YELLOW}ACTION: Review protocol adversarial test coverage{RESET}")
print()
if __name__ == '__main__':
main()