Separate radio defence and harden player and objective lifecycles
This commit is contained in:
+200
-18
@@ -1,19 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Refuse release qualification while acceptance evidence is incomplete."""
|
||||
import json,sys
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
ledger=json.loads((ROOT/'docs/acceptance.json').read_text())
|
||||
blocked=[]
|
||||
for name,case in ledger['cases'].items():
|
||||
if case['status']!='passed' or not case['evidence']:
|
||||
blocked.append(name)
|
||||
continue
|
||||
for evidence in case['evidence']:
|
||||
path=(ROOT/evidence).resolve()
|
||||
if not path.is_relative_to(ROOT) or not path.is_file():blocked.append(name);break
|
||||
if blocked:
|
||||
print('Not release-qualified; missing passed evidence:')
|
||||
for name in blocked:print(' - '+name)
|
||||
sys.exit(1)
|
||||
print('Acceptance ledger complete. Review provenance, native packages and publication metadata before release.')
|
||||
"""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())
|
||||
|
||||
Reference in New Issue
Block a user