Release notarized 1.3.1 installer with installed Motion effect resources

这个提交包含在:
dayu 于 2026-10-22 16:57:42 +08:00 提交
1 父节点 83ed140c2e
当前提交 b35b4a5d1c
17 个文件已更改
+594 -16

没有与搜索条件相匹配的文件

+99
查看文件
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Update the existing Dayu release article, retaining its cover and access rules.
Only --publish performs writes. Credentials stay in memory and go only to Dayu.
"""
import argparse, base64, hashlib, html, json, pathlib, ssl, subprocess, urllib.parse, urllib.request, uuid, zipfile
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--version', required=True)
parser.add_argument('--post-id', type=int, default=15593)
parser.add_argument('--publish', action='store_true')
args = parser.parse_args()
root = pathlib.Path(__file__).resolve().parent.parent
release = root / 'releases' / args.version
work = root / 'build' / ('publish-' + args.version)
work.mkdir(parents=True, exist_ok=True)
state_path = work / 'operation.json'
state = json.loads(state_path.read_text()) if state_path.exists() else {'idempotencyKey': str(uuid.uuid4())}
def save():
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2) + '\n')
save()
secret = subprocess.run(['security', 'find-generic-password', '-s', 'dayu-article-publisher', '-a', 'dayu', '-w'], text=True, capture_output=True, check=True).stdout.strip()
auth = 'Basic ' + base64.b64encode(('dayu:' + secret).encode()).decode()
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
opener = urllib.request.build_opener(NoRedirect, urllib.request.HTTPSHandler(context=ssl.create_default_context(cafile='/etc/ssl/cert.pem')))
def request(path, method='GET', body=None, headers=None):
assert path.startswith(('/wp/v2/', '/dayu-publisher/v1/')) and '\n' not in path
h = {'Authorization': auth, 'Accept': 'application/json'}
h.update(headers or {})
if isinstance(body, dict):
body = json.dumps(body, ensure_ascii=False).encode()
h['Content-Type'] = 'application/json'
with opener.open(urllib.request.Request('https://dayu.media/wp-json' + path, data=body, headers=h, method=method), timeout=180) as response:
return json.load(response)
def public_bytes(url):
assert urllib.parse.urlparse(url).hostname == 'dayu.media'
with urllib.request.urlopen(url, context=ssl.create_default_context(cafile='/etc/ssl/cert.pem'), timeout=180) as response:
return response.read()
digest = lambda data: hashlib.sha256(data).hexdigest()
route = '/dayu-publisher/v1/posts/' + str(args.post_id)
status = request('/dayu-publisher/v1/status')
assert status['can_publish'] and status['can_upload'] and 'seo' in status['fields']
before = request(route)['article']
assert before['id'] == args.post_id and before['status'] == 'publish'
if not args.publish:
(work / 'article-before.json').write_text(json.dumps({'article': before}, ensure_ascii=False, indent=2) + '\n')
print(json.dumps({'id': before['id'], 'url': before['url'], 'status': before['status'], 'writePerformed': False}))
raise SystemExit(0)
baseline = json.loads((work / 'article-before.json').read_text())['article']
payload = json.loads((release / 'article-payload.json').read_text())
package = release / ('大鱼人像美化-' + args.version + '.pkg')
source = root / 'build' / ('Dayu-Portrait-' + args.version + '-source.zip')
with zipfile.ZipFile(source) as z:
assert z.testzip() is None and any(n.endswith('/Wrapper Application/AppDelegate.m') for n in z.namelist())
assert not any(n.endswith(('.app', '.pkg', '.p12', '.key', '.env')) for n in z.namelist())
operation_digest = digest(package.read_bytes() + source.read_bytes() + json.dumps(payload, sort_keys=True, ensure_ascii=False).encode())
assert state.get('digest', operation_digest) == operation_digest, 'Prepared release changed; use a new reviewed operation state'
state['digest'] = operation_digest
save()
if not state.get('response'):
assert before == baseline, 'Article changed since review; reconcile the current article first'
if 'source' not in state:
title = 'Dayu Portrait ' + args.version + ' source'
matches = request('/wp/v2/media?context=edit&per_page=100&search=' + urllib.parse.quote(title))
for item in matches:
if item['title'].get('raw', item['title'].get('rendered')) == title and digest(public_bytes(item['source_url'])) == digest(source.read_bytes()):
state['source'] = {'id': item['id'], 'url': item['source_url']}
break
if 'source' not in state:
item = request('/wp/v2/media?title=' + urllib.parse.quote(title), 'POST', source.read_bytes(), {'Content-Type': 'application/zip', 'Content-Disposition': 'attachment; filename="' + source.name + '"'})
state['source'] = {'id': item['id'], 'url': item['source_url']}
save()
assert digest(public_bytes(state['source']['url'])) == digest(source.read_bytes())
payload['content'] = payload['content'].replace('{{source_url}}', html.escape(state['source']['url'], quote=True))
assert '{{' not in payload['content']
if not state.get('response'):
boundary = 'DayuRelease' + state['idempotencyKey'].replace('-', '')
parts = []
for name, data, filename, mime in [('payload', json.dumps(payload, ensure_ascii=False).encode(), None, 'application/json'), ('package', package.read_bytes(), 'Dayu-Portrait-' + args.version + '.pkg', 'application/octet-stream')]:
head = '--' + boundary + '\r\nContent-Disposition: form-data; name="' + name + '"'
if filename:
head += '; filename="' + filename + '"'
parts.append((head + '\r\nContent-Type: ' + mime + '\r\n\r\n').encode() + data + b'\r\n')
state['response'] = request(route, 'POST', b''.join(parts) + ('--' + boundary + '--\r\n').encode(), {'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Idempotency-Key': state['idempotencyKey']})
save()
after = request(route)['article']
for key in ['author', 'categories', 'tags', 'featured_media']:
assert after[key] == baseline[key], key
for key, value in baseline['resource'].items():
if key not in ['cao_downurl', 'cao_original_filename', 'cao_info']:
assert after['resource'][key] == value, key
assert after['status'] == 'publish' and after['title'] == payload['title']
assert after['seo'] == payload['seo'] and after['resource']['cao_info'] == payload['resource']['cao_info']
assert after['content'].count('<!-- wp:code -->') == 1
assert state['source']['url'] in after['content'] and ('/releases/tag/v' + args.version) in after['content']
assert digest(public_bytes(after['resource']['cao_downurl'])) == digest(package.read_bytes())
(work / 'article-after.json').write_text(json.dumps({'article': after}, ensure_ascii=False, indent=2) + '\n')
print(json.dumps({'id': after['id'], 'url': after['url'], 'status': after['status'], 'packageHashVerified': True, 'sourceHashVerified': True, 'coverAndAccessPreserved': True}, ensure_ascii=False))
+14 -6
查看文件
@@ -1,9 +1,15 @@
#!/usr/bin/env python3
"""Verify an expanded release payload and run template-only update/rollback in isolation."""
from pathlib import Path
import hashlib,json,os,plistlib,shutil,subprocess,sys
import argparse,hashlib,json,os,plistlib,shutil,subprocess,sys
root=Path(__file__).resolve().parent.parent
expanded=root/'build/pkg-expanded/Payload.pkg';payload=expanded/'Payload'
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--version',default='1.2.0')
parser.add_argument('--build',default='14')
parser.add_argument('--expanded',default='build/pkg-expanded')
parser.add_argument('--fixture',default='build/pkg-isolation')
args=parser.parse_args()
expanded=root/args.expanded/'Payload.pkg';payload=expanded/'Payload'
formal=Path('/Library/Plug-Ins/FxPlug/beatygril.app');packed=payload/'Library/Plug-Ins/FxPlug/beatygril.app'
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
def files(p):
@@ -23,7 +29,7 @@ assert len(locks)==2 and any('/large.png' in l for l in locks) and any('/small.p
assert 'Template Backups' in post and 'restore_original_locks' in post
# Keep the exact generated functions and template body. Only identity lookup,
# chown and external FxPlug registration are replaced to confine execution.
base=root/'build/pkg-isolation';base.mkdir(exist_ok=False)
base=root/args.fixture;base.mkdir(exist_ok=False)
volume=base/'volume';home=base/'user';home.mkdir()
start=post.index('CURRENT_ACTIVE=0')
isolated='#!/bin/zsh\nset -eu\nTARGET_VOLUME="'+str(volume)+'"\nCONSOLE_USER="'+os.getlogin()+'"\nCONSOLE_UID="'+str(os.getuid())+'"\nCONSOLE_GROUP="staff"\nUSER_HOME="'+str(home)+'"\n'+post[start:]
@@ -60,8 +66,10 @@ assert files(final)==original
assert all((final/n).stat().st_flags & 2 for n in ['large.png','small.png'])
backups=list((home/'Library/Application Support/PKGForge/Template Backups').rglob('.locked-paths'))
assert backups and all('/Movies/' not in str(x) for x in backups)
package=root/'releases/1.2.0/大鱼人像美化-1.2.0.pkg'
report={'package':str(package.relative_to(root)),'bytes':package.stat().st_size,'sha256':sha(package),'version':'1.2.0','build':'14','app_files_compared':len(files(formal)),'app_byte_and_symlink_equality':True,'template_files':{n:sha(template/n) for n in runtime},'locked_relative_paths':['大鱼人像美化/large.png','大鱼人像美化/small.png'],'script_sha256':{'preinstall':sha(expanded/'Scripts/preinstall'),'postinstall':sha(expanded/'Scripts/postinstall')},'isolated_script_tests':['fresh install','replace old locked thumbnail','partial copy failure restores old bytes and locks'],'isolation_changes':['Console identity/home redirected to task fixture','chown replaced with true','FxPlug verification skipped; formal app signature and discovery verified separately'],'real_installer_execution':False,'app_and_template_installed':'APP installed through signed delivery workflow; user thumbnail directory updated separately, not by running this PKG','fonts':'No editable third-party font dependency; promotional lettering baked into PNG. No optional font ZIP needed.'}
(root/'docs/verification/v1.2.0/pkg-verification.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n');print(json.dumps(report,ensure_ascii=False,indent=2))
package=root/('releases/'+args.version+'/大鱼人像美化-'+args.version+'.pkg')
info=plistlib.loads((packed/'Contents/Info.plist').read_bytes())
assert info['CFBundleShortVersionString']==args.version and info['CFBundleVersion']==args.build
report={'package':str(package.relative_to(root)),'bytes':package.stat().st_size,'sha256':sha(package),'version':args.version,'build':args.build,'app_files_compared':len(files(formal)),'app_byte_and_symlink_equality':True,'template_files':{n:sha(template/n) for n in runtime},'locked_relative_paths':['大鱼人像美化/large.png','大鱼人像美化/small.png'],'script_sha256':{'preinstall':sha(expanded/'Scripts/preinstall'),'postinstall':sha(expanded/'Scripts/postinstall')},'isolated_script_tests':['fresh install','replace old locked thumbnail','partial copy failure restores old bytes and locks'],'isolation_changes':['Console identity/home redirected to task fixture','chown replaced with true','FxPlug verification skipped; formal app signature and discovery verified separately'],'real_installer_execution':False,'app_and_template_installed':'APP installed through signed delivery workflow; formal template files matched the specified installed category, not changed by running this PKG','fonts':'No editable third-party font dependency; promotional lettering baked into PNG. No optional font ZIP needed.'}
(root/('docs/verification/v'+args.version+'/pkg-verification.json')).write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n');print(json.dumps(report,ensure_ascii=False,indent=2))
# This fixture belongs solely to this verifier; remove its locks before cleanup.
subprocess.run(['chflags','-R','nouchg',str(base)],check=True);shutil.rmtree(base)