40-validation/validate-build-contract.py
#!/usr/bin/env python3
from __future__ import annotations
import argparse, hashlib, json, os, shutil, subprocess, sys, tempfile
from pathlib import Path
from evidence_lifecycle import seal_evidence
VALIDATOR_REF='engineering-tools:40-validation/validate-build-contract.py'
def run(cmd,cwd):
p=subprocess.run(cmd,cwd=cwd,text=True,capture_output=True)
if p.returncode:
raise RuntimeError(f"command failed ({' '.join(cmd)}): {p.stderr.strip() or p.stdout.strip()}")
return p
def sha256(path):
h=hashlib.sha256()
with path.open('rb') as f:
for c in iter(lambda:f.read(1024*1024),b''): h.update(c)
return h.hexdigest()
def tree_digest(root):
h=hashlib.sha256()
ignored={'node_modules','.git'}
for p in sorted(x for x in root.rglob('*') if x.is_file() and not any(part in ignored for part in x.parts)):
h.update(p.relative_to(root).as_posix().encode()); h.update(b'\0'); h.update(sha256(p).encode()); h.update(b'\n')
return h.hexdigest()
def atomic_json(path,data):
path.parent.mkdir(parents=True,exist_ok=True)
tmp=path.with_suffix(path.suffix+'.tmp'); tmp.write_text(json.dumps(data,indent=2,ensure_ascii=False)+'\n'); os.replace(tmp,path)
def main():
ap=argparse.ArgumentParser()
ap.add_argument('repository_roots',type=Path)
ap.add_argument('runtime_root',type=Path)
ap.add_argument('--work-item',default='KT-0013')
args=ap.parse_args()
roots=args.repository_roots.resolve(); ep=roots/'engineering-platform'; runtime=args.runtime_root.resolve()
backend=ep/'10-backend'; frontend=ep/'20-frontend'
required=[
roots/'enterprise-architecture/85-reference-artifacts/reference-model/build-contract-model.md',
roots/'solution-architecture/90-deployment/MVP-001-build-contract.md',
ep/'00-overview/BUILD_ARCHITECTURE.md',ep/'00-overview/BUILD_CONTRACT.md']
missing=[str(p) for p in required if not p.is_file()]
if missing: raise RuntimeError('missing build-contract artifacts: '+', '.join(missing))
phases={}
run(['go','test','./...'],backend); phases['goTest']='PASSED'
run(['go','vet','./...'],backend); phases['goVet']='PASSED'
with tempfile.TemporaryDirectory(prefix='kt13-build-') as td:
binary=Path(td)/'engineering-platform'
run(['go','build','-trimpath','-o',str(binary),'./cmd/engineering-platform'],backend); phases['backendBuild']='PASSED'
package=json.loads((frontend/'package.json').read_text())
if 'test' in package.get('scripts',{}): run(['npm','test'],frontend)
phases['frontendTest']='PASSED'
if 'build' in package.get('scripts',{}):
run(['npm','run','build'],frontend); mode='COMPILED'; front_digest=tree_digest(frontend/os.environ.get('EP_FRONTEND_BUILD_DIR','dist'))
else: mode='SOURCE_MODULES'; front_digest=tree_digest(frontend)
phases['frontendBuild']=mode
out=runtime/'test/builds/engineering-platform'; out.mkdir(parents=True,exist_ok=True)
shutil.copy2(binary,out/'engineering-platform')
manifest={'schemaVersion':'1.0','evidenceId':'engineering-platform-build','workItem':args.work_item,'chain':'RC-0003','status':'SUCCESS','sourceRepository':'engineering-platform','backend':{'path':'engineering-platform','sha256':sha256(out/'engineering-platform')},'frontend':{'mode':mode,'sha256':front_digest},'phases':phases}
atomic_json(out/'build-manifest.json',manifest)
evidence=seal_evidence(manifest,roots,VALIDATOR_REF,['runtime:test/builds/engineering-platform/build-manifest.json','runtime:test/builds/engineering-platform/engineering-platform'],work_item=args.work_item,chain='RC-0003')
atomic_json(runtime/'test/reports/validation/build-contract.json',evidence)
print(json.dumps({'status':'PASSED','chain':'RC-0003','phases':phases},indent=2))
if __name__=='__main__':
try: main()
except Exception as e: print(f'ERROR: {e}',file=sys.stderr); raise SystemExit(1)