Files

202 lines
9.3 KiB
Python

#!/usr/bin/env python3
"""Audit release qualification; never change acceptance, build or publish anything."""
import json
import re
from pathlib import Path, PurePosixPath
from native_report import analyze, clean
from source_manifest import addon_manifest
from native_provenance import qualify_archive, MANIFEST, METADATA, PROVENANCE
ROOT = Path(__file__).resolve().parents[1]
REQUIRED_CASES = (
'independent_native_build', 'prison_geometry_and_spawn', 'escape_loop',
'join_late', 'reconnect_identity', 'death_unconsciousness_revive',
'respawn_and_spectators', 'partial_escape', 'defeat_restart',
'interrupted_reports', 'stale_contacts', 'support_losses',
'ammunition_depletion', 'cleanup_no_duplicate_orders_or_spawns',
'twelve_players', 'equivalent_workload_fps_improvement',
'own_identity_release_package',
)
STATUSES = {'pending', 'passed', 'failed'}
def strict_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f'duplicate JSON key: {key}')
result[key] = value
return result
def read_json(path):
def invalid_constant(value):
raise ValueError(f'invalid JSON number: {value}')
return json.loads(path.read_text(encoding='utf-8'), object_pairs_hook=strict_object,
parse_constant=invalid_constant)
def evidence_path(root, name):
if not isinstance(name, str) or not name or '\\' in name or any(ord(c) < 32 for c in name):
raise ValueError('evidence path must be a nonempty repository-relative POSIX path')
path = PurePosixPath(name)
if (path.is_absolute() or path.as_posix() != name or ':' in name
or any(part in {'.', '..', '.git'} for part in path.parts)):
raise ValueError(f'unsafe or noncanonical evidence path: {name}')
full = root / name
try:
resolved = full.resolve()
except (OSError, RuntimeError) as error:
raise ValueError(f'evidence path cannot be resolved: {name}') from error
if not resolved.is_relative_to(root):
raise ValueError(f'evidence leaves repository: {name}')
cursor = root
for part in path.parts:
cursor /= part
if cursor.is_symlink():
raise ValueError(f'evidence symlinks are not supported: {name}')
if not full.is_file():
raise ValueError(f'evidence is not an existing regular file: {name}')
with full.open('rb') as stream:
if not any(chunk.strip() for chunk in iter(lambda: stream.read(65536), b'')):
raise ValueError(f'evidence is empty: {name}')
return full
def audit(root=ROOT):
root = Path(root).resolve()
blocked = []
try:
ledger = read_json(evidence_path(root, 'docs/acceptance.json'))
except (OSError, ValueError) as error:
return [f'docs/acceptance.json: {error}']
if not isinstance(ledger, dict):
return ['acceptance ledger must be an object']
if not isinstance(ledger.get('target'), str) or not ledger['target'].strip():
blocked.append('target must be a nonempty string')
cases = ledger.get('cases')
if not isinstance(cases, dict):
blocked.append('cases must be an object containing the fixed first-release requirements')
cases = {}
for name in REQUIRED_CASES:
if name not in cases:
blocked.append(f'cases.{name}: required first-release case is missing')
for name in sorted(set(cases) - set(REQUIRED_CASES)):
blocked.append(f'cases.{name}: unknown first-release case')
development = ledger.get('development_checks')
if not isinstance(development, dict):
blocked.append('development_checks must be an object')
development = {}
if 'clean_native_runtime' not in development:
blocked.append('development_checks.clean_native_runtime: required clean-runtime gate is missing')
def validate_entry(label, entry, required):
if not isinstance(entry, dict):
blocked.append(f'{label}: entry must be an object')
return
status = entry.get('status')
if not isinstance(status, str) or status not in STATUSES:
blocked.append(f'{label}: unknown or malformed status {status!r}')
elif required and status != 'passed':
blocked.append(f'{label}: status is {status}, not passed')
evidence = entry.get('evidence')
if not isinstance(evidence, list):
blocked.append(f'{label}: evidence must be a list of file paths')
return
if status == 'passed' and not evidence:
blocked.append(f'{label}: passed status requires nonempty evidence')
seen = set()
for name in evidence:
try:
evidence_path(root, name)
if name in seen:
raise ValueError(f'duplicate evidence path: {name}')
seen.add(name)
except (OSError, ValueError) as error:
blocked.append(f'{label}: {error}')
for name, entry in cases.items():
validate_entry(f'cases.{name}', entry, True)
for name, entry in development.items():
validate_entry(f'development_checks.{name}', entry, name == 'clean_native_runtime')
current = None
try:
current = addon_manifest(root)
except (OSError, ValueError) as error:
blocked.append(f'current addon source/resource manifest: {error}')
def native_qualification(label, entry):
if not isinstance(entry, dict) or entry.get('status') != 'passed':
return
paths = {}
for field in ('source_manifest', 'native_report'):
try:
paths[field] = evidence_path(root, entry.get(field))
if not isinstance(entry.get('evidence'), list) or entry[field] not in entry['evidence']:
raise ValueError(f'{field} must also appear in this entry\'s evidence list')
except (OSError, ValueError) as error:
blocked.append(f'{label}.{field}: {error}')
if len(paths) != 2:
return
if paths['source_manifest'].parent != paths['native_report'].parent:
blocked.append(f'{label}: native report and manifest must belong to the same evidence archive')
try:
manifest = read_json(paths['source_manifest'])
if (not isinstance(manifest, dict) or not manifest
or any(not isinstance(digest, str) or not re.fullmatch(r'[0-9a-f]{64}', digest)
for digest in manifest.values())):
raise ValueError('manifest must be a nonempty path-to-SHA256 object')
if current is not None and manifest != current:
missing = sorted(set(current) - set(manifest))
extra = sorted(set(manifest) - set(current))
changed = sorted(path for path in set(current) & set(manifest) if current[path] != manifest[path])
raise ValueError(f'manifest does not match current addon sources/resources '
f'(missing={len(missing)}, extra={len(extra)}, changed={len(changed)})')
except (OSError, ValueError) as error:
blocked.append(f'{label}.source_manifest: {error}')
required_rounds = 1
try:
archive = paths['native_report'].parent
for name in (MANIFEST, METADATA, PROVENANCE, 'runner.json.txt', 'source-sha256.json'):
evidence_path(root, (archive / name).relative_to(root).as_posix())
required_rounds = qualify_archive(archive, current)
except (OSError, ValueError) as error:
blocked.append(f'{label}.launch_provenance: {error}')
try:
report = read_json(paths['native_report'])
if not isinstance(report, dict):
raise ValueError('native report must be an object')
log = evidence_path(root, report.get('log'))
if log.parent != paths['native_report'].parent:
raise ValueError('native raw log must belong to the same evidence archive')
if log.name != 'console.log.txt':
raise ValueError('native report must analyze the exact console.log.txt sealed by the stopped runner')
actual = analyze(log.read_text(errors='replace'), report['log'])
# Exact JSON types matter: True must not masquerade as one completion.
if json.dumps(report, sort_keys=True) != json.dumps(actual, sort_keys=True):
raise ValueError('native report does not match analysis of its archived raw log')
if not clean(actual, required_rounds) or actual['assertions_passed'] <= 0:
raise ValueError('native raw log must contain successful completion/assertions and no script, engine or end-screen errors')
except (OSError, ValueError) as error:
blocked.append(f'{label}.native_report: {error}')
native_qualification('cases.independent_native_build', cases.get('independent_native_build'))
native_qualification('development_checks.clean_native_runtime', development.get('clean_native_runtime'))
return blocked
def main():
blocked = audit()
if blocked:
print('Not release-qualified:')
for reason in blocked:
print(' - ' + reason)
return 1
print('Release qualification audit passed. This audit does not build or publish a release.')
return 0
if __name__ == '__main__':
raise SystemExit(main())