49 lines
3.8 KiB
Python
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)
|