Files

49 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Validate local release assets; use --submission-ready to require owner setup too."""
from pathlib import Path
from PIL import Image,ImageStat
import json,subprocess,plistlib,hashlib,sys
R=Path(__file__).resolve().parents[1]
errors=[];checks=[]
def check(ok,label):
(checks if ok else errors).append(label)
for locale in ['en-GB','en-US']:
for name,limit in [('name',30),('subtitle',30),('promotional_text',170),('description',4000),('keywords',100)]:
value=(R/f'metadata/{locale}/{name}.txt').read_text().strip()
check(bool(value) and (len(value.encode()) if name=='keywords' else len(value))<=limit,f'{locale}/{name}: length within {limit}')
manifest=json.loads((R/'asset-manifest.json').read_text())
check(len(manifest['screenshots'])==12,'12 upload screenshots')
for item in manifest['screenshots']:
for name in [item['file'],item['source']]:
im=Image.open(R/name)
check(im.size==(item['width'],item['height']) and (im.mode=='RGB' or ('A' in im.getbands() and im.getchannel('A').getextrema()==(255,255))),f'{name}: dimensions and opacity')
check(max(ImageStat.Stat(im.resize((100,100))).stddev)>15,f'{name}: nonblank image')
im=Image.open(R/'icons/Tarnax-AppStore-1024.png')
check(im.size==(1024,1024) and im.mode=='RGB','App Store icon: opaque 1024 square')
for device,size in [('iphone-6.9',(886,1920)),('ipad-13',(1200,1600))]:
file=R/f'previews/Tarnax-{device}-preview.mp4'
if not file.exists(): check(False,f'{device}: preview exists');continue
data=json.loads(subprocess.check_output(['ffprobe','-v','error','-show_streams','-show_format','-of','json',str(file)]))
video=next((s for s in data['streams'] if s['codec_type']=='video'),{})
audio=next((s for s in data['streams'] if s['codec_type']=='audio'),{})
check((video.get('width'),video.get('height'))==size and video.get('codec_name')=='h264' and video.get('level',999)<=40,f'{device}: video format')
check(15<=float(data['format']['duration'])<=30 and file.stat().st_size<500_000_000,f'{device}: duration and size')
check(audio.get('codec_name')=='aac' and audio.get('channels')==2 and audio.get('sample_rate')=='48000',f'{device}: audio format')
a=plistlib.loads((R/'legal/PrivacyInfo.xcprivacy').read_bytes())
check(a==plistlib.loads((R.parent/'Tarnax/PrivacyInfo.xcprivacy').read_bytes()),'Privacy manifest matches shipping source')
for name in ['index.html','support.html','privacy.html','terms.html','style.css']:
check((R/'website'/name).exists(),f'Website {name}')
config=json.loads((R/'release-config.json').read_text())
owner=[key for key in ['support_url','privacy_policy_url','price','territories'] if config[key] is None]
if not config['review_contact']['phone']:owner.append('review contact phone')
if not config['review_server']['https_url']:owner.append('dedicated reviewer server and credentials')
if not (R/'build/latest.json').exists():owner.append('App Store signed IPA export')
owner+=['Confirm naming rights, privacy answers, age rating, agreements and final physical-device QA']
report={'asset_checks_passed':len(checks),'asset_errors':errors,'submission_ready':not(errors or owner),'owner_actions':owner}
(R/'validation-report.json').write_text(json.dumps(report,indent=2)+'\n')
(R/'validation-report.md').write_text('# Release asset validation\n\n'+f'{len(checks)} checks passed. {len(errors)} asset errors.\n\n'+ '\n'.join('- '+v for v in errors)+'\n\n## Owner actions before submission\n\n'+'\n'.join('- '+v for v in owner)+'\n')
files=sorted(p for p in R.rglob('*') if p.is_file() and 'build' not in p.relative_to(R).parts and p.name not in ['SHA256SUMS'] and '__pycache__' not in p.parts)
(R/'SHA256SUMS').write_text(''.join(hashlib.sha256(p.read_bytes()).hexdigest()+' '+str(p.relative_to(R))+'\n' for p in files))
print(json.dumps(report,indent=2))
sys.exit(1 if errors else 2 if '--submission-ready' in sys.argv and owner else 0)