upgrade: adopt V8.33.1 — final merge, our patches upstreamed natively

V8.33.0 absorbed our entire platform patch set as native features
(discovery/env_for macro override/blocks.css priority/type-mapping fix/
frontend entries/locale override) and synced trade-pro from our remote;
V8.33.1 adds HESU PIPE industrial-theme home fixes (1.3.1).

Zero patch re-application this round — all verified native. Theme
suites 35/35; full run 428 passed + same 10 known Windows env fails
(FAILED set unchanged). Live: container 8.33.1-dev.1 healthy, hesupipe
192 entries intact, trade-pro build 203 pages 0 errors.

Patch registry retired to archive — future upgrades are plain overlay.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 14:27:17 +08:00
parent b5996bf2b1
commit e21dbb6518
66 changed files with 2137 additions and 24 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Isolated Chromium smoke of shipped standalone theme comparison; no real server."""
import asyncio,json
from pathlib import Path
from playwright.async_api import async_playwright
ROOT=Path(__file__).resolve().parents[1];REPORT=ROOT/'reports/v833';REPORT.mkdir(parents=True,exist_ok=True)
async def main():
async with async_playwright() as p:
browser=await p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage'])
records=[]
for device,width in (('desktop',1365),('phone',390)):
page=await browser.new_page(viewport={'width':width,'height':850},device_scale_factor=1)
errs=[];page.on('pageerror',lambda e:errs.append(str(e)))
html=(ROOT/'examples/V8_33_五套主题真实页面_离线交互预览.html').read_text(encoding='utf-8')
await page.set_content(html,wait_until='load',timeout=60000)
await page.select_option('#theme','trade-pro')
await page.select_option('#page','product')
await page.frame_locator('#preview').locator('.sf-tp-product-hero').wait_for(state='visible',timeout=12000)
await page.wait_for_timeout(1600)
if device=='phone':await page.locator('[data-width="390px"]').click()
title=await page.title()
count=await page.frame_locator('#preview').locator('.pro-trade-pro').count()
overflow=await page.evaluate('document.documentElement.scrollWidth>window.innerWidth')
img=REPORT/f'trade-pro-{device}.png'
await page.screenshot(path=str(img),full_page=True)
assert count>0,(device,'theme body missing')
assert not overflow,(device,'overflow')
assert not errs,(device,errs)
records.append({'device':device,'width':width,'theme_body_found':count,'horizontal_overflow':overflow,'script_errors':errs,'screenshot':str(img.relative_to(ROOT))})
await page.close()
await browser.close()
(REPORT/'browser-results.json').write_text(json.dumps(records,ensure_ascii=False,indent=2),encoding='utf-8')
print('Chromium PASS:', records)
if __name__=='__main__':asyncio.run(main())
+46
View File
@@ -0,0 +1,46 @@
"""Chromium isolated visual check for industrial features and collection."""
import json
from pathlib import Path
from playwright.sync_api import sync_playwright
root=Path(__file__).resolve().parents[1]
pagehtml=(root/'examples/v8331-real-built-page.html').read_text()
dest=root/'reports/v8331';dest.mkdir(parents=True,exist_ok=True)
rows=[]
with sync_playwright() as p:
browser=p.chromium.launch(headless=True,executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage'])
for width in [1440,820,390,375]:
page=browser.new_page(viewport={'width':width,'height':920},device_scale_factor=1)
errors=[]
page.on('pageerror',lambda exc: errors.append(str(exc)))
page.set_content(pagehtml,wait_until='load',timeout=30000)
page.wait_for_timeout(150)
facts=page.evaluate('''() => {
const shell=document.querySelector('.pro-custom-content');
const outerFeature=document.querySelector('.sf-section.sf-features');
const outerList=document.querySelector('.sf-section.sf-collection');
const innerFeature=document.querySelector('.sf-section.sf-features .sf-container > .sf-features');
const innerList=document.querySelector('.sf-section.sf-collection .sf-container > .sf-collection');
const box=el=>el?({x:Math.round(el.getBoundingClientRect().x),width:Math.round(el.getBoundingClientRect().width),display:getComputedStyle(el).display,cols:getComputedStyle(el).gridTemplateColumns}):null;
return {bodyScroll:document.documentElement.scrollWidth,viewport:innerWidth,
featuresOuter:box(outerFeature),listOuter:box(outerList),featuresInner:box(innerFeature),listInner:box(innerList),
featureCount:document.querySelectorAll('.sf-feature').length,
productCount:document.querySelectorAll('.sf-item').length,
filterCount:document.querySelectorAll('.pro-custom-content .sf-filters').length,
module:box(shell)};
}''')
assert facts['featuresOuter']['display']=='block',facts
assert facts['listOuter']['display']=='block',facts
assert facts['featuresInner']['display']=='grid',facts
assert facts['listInner']['display']=='grid',facts
assert facts['featuresInner']['width']>=min(900,width-80),facts
assert facts['productCount']==6 and facts['featureCount']==4,facts
assert facts['filterCount']==0,facts
assert facts['bodyScroll']<=width+1,facts
assert not errors,errors
shot=dest/f'hesupipe-home-blocks-{width}.png'
page.locator('.pro-custom-content').screenshot(path=str(shot),timeout=30000)
rows.append({'width':width,'screenshot':shot.name,'facts':facts,'script_errors':errors})
page.close()
browser.close()
(dest/'browser-results.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2))
for x in rows:print(x['width'], 'feature grid:',x['facts']['featuresInner']['cols'], 'product grid:', x['facts']['listInner']['cols'], 'filters:',x['facts']['filterCount'])
+62
View File
@@ -0,0 +1,62 @@
"""Make a fully offline industrial-theme homepage preview from the real static builder."""
import os,sys,json,html,re,tempfile
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
sys.path[:0]=[str(ROOT),str(ROOT/'tests')]
from tests.legacy import LegacyTestClient
from app import db,theme_store
from app.main import app
def generate():
target=ROOT/'examples'; target.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory() as temporary:
db.DATA=Path(temporary);db.DB_FILE=db.DATA/'data.sqlite3';theme_store.THEME_ROOT=db.DATA/'theme_packages'
os.environ['SITEFORGE_ALLOW_LEGACY_DIRECT_DESIGN_WRITE']='1'
os.environ['SITEFORGE_ADMIN_PASSWORD']='demo-test-admin-secret-2026'
os.environ['SITEFORGE_SESSION_SECRET']='demo-secret-not-for-use-outside-test-2026'
with LegacyTestClient(app) as c:
assert c.post('/api/auth/login',json={'username':'admin','password':os.environ['SITEFORGE_ADMIN_PASSWORD']}).status_code==200
c.headers.update({'X-SiteForge-CSRF':c.get('/api/auth/me').json()['csrf']})
r=c.post('/api/sites',json={'name':'HESU PIPE · 排版验证样站','theme':'industrial','domain':'https://example.test','mode':'migration','tagline':'工业管道与工程材料 · 完整产品方案'})
assert r.status_code==201,r.text; sid=r.json()['id']
home=next(e for e in c.get(f'/api/sites/{sid}/entries').json() if e['route']=='/')
assert c.put(f'/api/factory/sites/{sid}/theme',json={'theme':'industrial','version':'1.3.1'}).status_code==200
products=[('HDPE 给水管','适用于市政给水及工业供水管网。'),('双壁波纹排水管','适用于市政雨污水管网和地下排水工程。'),('PVC-U 排水管','耐腐蚀、规格丰富的塑料管材系统。'),('PPR 冷热水管','建筑冷热水输送常用管道系列。'),('钢丝网骨架复合管','适用于承压输水与工程管线系统。'),('HDPE 管道连接件','用于热熔及电熔连接的各类管件。')]
for i,(title,summary) in enumerate(products,1):
res=c.post(f'/api/sites/{sid}/entries',json={'type':'product','title':title,'route':f'/products/pipe-{i}/','summary':summary})
assert res.status_code==201,res.text
blocks=[{'id':'features-hp-v8331','type':'features','title':'核心优势','items':[
{'title':'管道产品供应','text':'覆盖多种工程管材和配件,便于项目集中采购。'},
{'title':'规格与质量管理','text':'根据实际应用提供产品选型和规格资料。'},
{'title':'工程应用支持','text':'面向供水、排水及工业管网项目的应用场景。'},
{'title':'全球交付服务','text':'从订单沟通到物流交付,提供清晰的合作流程。'},
]}, {'id':'collection-hp-v8331','type':'collection','title':'动态内容列表 · 精选产品',
'subtitle':'PRODUCT COLLECTION','source':'product','limit':6,'columns':3,
'href':'/products/','button':'查看全部产品'}]
saved=c.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'})
assert saved.status_code==200,saved.text
built=c.post(f'/api/sites/{sid}/builds',json={})
assert built.status_code==200,built.text
bid=built.json()['version']; dist=db.DATA/'builds'/sid/bid/'dist'
assert (dist/'index.html').exists(),dist
source=(dist/'index.html').read_text()
# Inline exact packaged CSS/JS, so the demo is readable without a server or external requests.
def stylesheet(m):
path=m.group(1)
local=dist/path
return '<style>\n'+local.read_text(encoding='utf-8')+'\n</style>' if local.is_file() else ''
source=re.sub(r'<link rel="stylesheet" href="(assets/[A-Za-z0-9._-]+\.css)">',stylesheet,source)
def script(m):
local=dist/m.group(1)
if local.is_file():
return '<script>\n'+local.read_text(encoding='utf-8').replace('</script','<\\/script')+'\n</script>'
return ''
source=re.sub(r'<script defer src="(assets/[A-Za-z0-9._-]+\.js)"></script>',script,source)
# These paths are demonstrative and intentionally not published.
isolated=target/'V8_33_1_HesuPipe_首页布局真实构建预览.html'
snippet=json.dumps(source,ensure_ascii=False).replace('<','\\u003c')
wrapper='''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>HesuPipe · 首页核心优势与产品列表</title><style>*{box-sizing:border-box}body{margin:0;background:#eef2f5;color:#1d3344;font-family:system-ui,-apple-system,sans-serif}.bar{display:flex;gap:15px;align-items:center;justify-content:space-between;padding:14px 23px;background:#fff;border-bottom:1px solid #dce4e9;flex-wrap:wrap}.bar strong{font-size:16px}.bar small{display:block;font-size:11px;color:#657a89;margin-top:4px}.buttons{display:flex;gap:6px}.buttons button{border:1px solid #cbdce4;background:#f6f9fa;padding:9px 14px;border-radius:6px;font-weight:650;color:#1e4359;cursor:pointer}.buttons button[aria-pressed=true]{background:#0f5e78;color:#fff}iframe{border:0;width:100%;height:calc(100vh - 74px);min-height:550px;background:#fff;display:block;margin:auto;transition:width .15s}.note{position:fixed;right:20px;bottom:18px;border:1px solid #d0dce3;border-radius:9px;background:#fff;padding:10px 13px;font-size:12px;box-shadow:0 4px 15px #0001}</style></head><body><div class="bar"><div><strong>HESU PIPE · 核心优势 / 动态内容列表</strong><small>V8.33.1 从实际静态构建生成的离线页面 · 示范文字与占位产品图</small></div><div class="buttons"><button data-size="100%" aria-pressed="true">电脑版</button><button data-size="820px" aria-pressed="false">平板</button><button data-size="390px" aria-pressed="false">手机版</button></div></div><iframe title="工业建材主题首页实际构建预览" sandbox="allow-scripts" id="preview"></iframe><span class="note">仅检查预览 · 不连接线上网站</span><script id="payload" type="application/json">PAYLOAD</script><script>const raw=JSON.parse(document.getElementById('payload').textContent);const target=document.getElementById('preview');target.srcdoc=raw;document.querySelectorAll('[data-size]').forEach(btn=>btn.addEventListener('click',()=>{target.style.width=btn.dataset.size;document.querySelectorAll('[data-size]').forEach(item=>item.setAttribute('aria-pressed',String(item===btn)));}));</script></body></html>'''
isolated.write_text(wrapper.replace('PAYLOAD',snippet),encoding='utf-8')
(target/'v8331-real-built-page.html').write_text(source,encoding='utf-8')
print('generated',isolated,'bytes',isolated.stat().st_size,'build',bid)
if __name__=='__main__':generate()
+78
View File
@@ -0,0 +1,78 @@
"""End-to-end HTTP + Worker smoke test on an isolated disposable SQLite database.
Launches an actual uvicorn process, calls authenticated HTTP APIs, runs a real
Worker claim + isolated subprocess build, then downloads and checks the archive.
Never points at production data or remote deployment services.
"""
from __future__ import annotations
import io
import json
import os
import socket
import subprocess
import sys
import tempfile
import time
import zipfile
from pathlib import Path
from urllib.error import URLError
from urllib.request import urlopen
import httpx
ROOT=Path(__file__).resolve().parent.parent
def run():
with tempfile.TemporaryDirectory(prefix='siteforge-e2e-') as folder:
with socket.socket() as sock:
sock.bind(('127.0.0.1',0)); port=sock.getsockname()[1]
base=f'http://127.0.0.1:{port}'
env=os.environ.copy()
env.update(SITEFORGE_ADMIN_PASSWORD='e2e-only-dummy-password-123456',
SITEFORGE_SESSION_SECRET='e2e-only-session-secret-long-value-123456',
SITEFORGE_DATA_DIR=folder,SITEFORGE_DB_FILE=str(Path(folder)/'siteforge.sqlite3'),
SITEFORGE_DATABASE_URL='',SITEFORGE_REDIS_URL='',SITEFORGE_COOKIE_SECURE='false')
proc=subprocess.Popen([sys.executable,'-m','uvicorn','app.main:app','--host','127.0.0.1','--port',str(port)],
cwd=ROOT,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
try:
for n in range(120):
if proc.poll() is not None:raise RuntimeError('Web startup failed: '+proc.stderr.read().decode(errors='replace')[-3000:])
try:
with urlopen(base+'/api/ready',timeout=0.5) as result:
if result.status==200:break
except (OSError,URLError):time.sleep(0.1)
else:raise RuntimeError('Web readiness timeout')
with httpx.Client(base_url=base,timeout=20,follow_redirects=False) as client:
auth=client.post('/api/auth/login',json={'username':'admin','password':env['SITEFORGE_ADMIN_PASSWORD']})
assert auth.status_code==200,auth.text
me=client.get('/api/auth/me')
assert me.status_code==200,me.text
client.headers['X-SiteForge-CSRF']=me.json()['csrf']
site=client.post('/api/sites',json={'name':'V8.33 Trade Pro Worker','theme':'trade-pro'})
assert site.status_code==201,site.text
sid=site.json()['id']
page=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Test about','route':'/about/','body':'E2E smoke content'})
assert page.status_code==201,page.text
job=client.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':'e2e-build-000001'})
assert job.status_code==202,job.text
completed=subprocess.run([sys.executable,'-c','from app import db,enterprise,worker; db.init(); enterprise.setup(); assert worker.run_once()'],cwd=ROOT,env=env,capture_output=True,text=True,timeout=70)
assert completed.returncode==0,completed.stderr[-2500:]
builds=client.get(f'/api/sites/{sid}/builds')
assert builds.status_code==200,builds.text
done=[x for x in builds.json() if x['status']=='complete']
if not done:
raise AssertionError('No completed static build: '+str(builds.json())[:1200])
archive=client.get(f"/api/sites/{sid}/builds/{done[0]['id']}/download")
assert archive.status_code==200,archive.text[:300]
with zipfile.ZipFile(io.BytesIO(archive.content)) as z:
assert 'index.html' in z.namelist()
assert 'about/index.html' in z.namelist()
assert '<html lang="en"' in z.read('index.html').decode('utf-8')
preview=client.get(f"/preview/{sid}/{done[0]['id']}/about/")
assert preview.status_code==200 and 'E2E smoke content' in preview.text
print('PASS: ready -> login/CSRF -> create site/page -> enqueue -> Worker build -> ZIP -> preview')
finally:
proc.terminate()
try:proc.communicate(timeout=5)
except subprocess.TimeoutExpired:proc.kill();proc.communicate()
if __name__=='__main__':run()
+28
View File
@@ -0,0 +1,28 @@
"""V8.29 isolated regression with per-batch deadline and raw logs."""
import argparse,datetime,json,subprocess,sys,time
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
def main():
ap=argparse.ArgumentParser();ap.add_argument('--timeout',type=int,default=90);ap.add_argument('--group-size',type=int,default=3);ap.add_argument('--start',type=int,default=1);ap.add_argument('--end',type=int,default=999);args=ap.parse_args()
all_files=sorted((ROOT/'tests').glob('test_*.py'))
groups=[all_files[i:i+args.group_size] for i in range(0,len(all_files),args.group_size)]
dest=ROOT/'reports/v833-regression';dest.mkdir(parents=True,exist_ok=True)
outcomes=[]
for index,files in enumerate(groups,1):
if not args.start <= index <= args.end:continue
start=time.monotonic();command=[sys.executable,'-m','pytest','-q',*[str(f.relative_to(ROOT)) for f in files]]
try:
proc=subprocess.run(command,cwd=ROOT,capture_output=True,text=True,timeout=args.timeout)
code=proc.returncode;output=proc.stdout+'\n'+proc.stderr;status='passed' if code==0 else 'failed'
except subprocess.TimeoutExpired as e:
output=(e.stdout or b'').decode(errors='replace') if isinstance(e.stdout,bytes) else (e.stdout or '')
output+='\nTIMEOUT exceeded';status='timeout'
elapsed=round(time.monotonic()-start,2)
(dest/f'batch-{index:02}.log').write_text(output,encoding='utf-8')
outcomes.append({'batch':index,'files':[f.name for f in files],'status':status,'seconds':elapsed,'summary':output[-350:]})
print(f'[{index:02}/{len(groups)}] {status.upper()} {elapsed:.1f}s: '+', '.join(f.name for f in files),flush=True)
result={'version':'8.33.1-dev.1','generated_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'groups':outcomes,'all_passed':all(x['status']=='passed' for x in outcomes)}
(dest/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
print('RESULT:',result['all_passed'],sum(x['status']=='passed' for x in outcomes),len(groups),flush=True)
return 0 if result['all_passed'] else 1
if __name__=='__main__':raise SystemExit(main())
+2 -2
View File
@@ -6,7 +6,7 @@ import sys
ROOT=Path(__file__).resolve().parent.parent
sys.path.insert(0,str(ROOT))
from app.version import VERSION
assert VERSION=='8.32.0-dev.1'
assert VERSION=='8.33.1-dev.1'
api=(ROOT/'app'/'main.py').read_text(encoding='utf-8')
assert 'return builder.build(' not in api, 'A synchronous request-thread build has reappeared'
assert "@app.post('/api/sites/{site_id}/builds', status_code=202)" in api
@@ -22,4 +22,4 @@ for file in (ROOT/'requirements.lock',ROOT/'requirements-dev.lock'):
assert (ROOT/'.github/workflows/ci.yml').is_file()
for file in (ROOT/'app/static').glob('*.js'):
subprocess.run(['node','--check',str(file)],check=True)
print('PASS: version, 4× themes, worker route, pinned dependencies, CI workflow, JS syntax')
print('PASS: version, 5× themes, worker route, pinned dependencies, CI workflow, JS syntax')