upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)

Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 12:31:03 +08:00
parent 257c89e159
commit 601451f22b
218 changed files with 10441 additions and 628 deletions
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""REAL PostgreSQL data migration CI smoke with a disposable isolated target.
This script deliberately never runs on ambiguous production-looking DB names.
Set SITEFORGE_ALLOW_PG_DATA_SMOKE=1 and database suffix _ci/_test.
"""
# Allow direct `python scripts/name.py` from a fresh source checkout.
import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parents[1]))
import os
import subprocess
import sys
import tempfile
import uuid
from pathlib import Path
from urllib.parse import unquote,urlsplit
from app import db
from app.sqlite_pg_transfer import migrate
from app.pg_disaster_recovery import RecoveryError
url = os.environ.get('SITEFORGE_DATABASE_URL','')
name = unquote(urlsplit(url).path.lstrip('/'))
if not db.POSTGRES or os.environ.get('SITEFORGE_ALLOW_PG_DATA_SMOKE')!='1' or not name.endswith(('_ci','_test')):
raise SystemExit('Requires explicit CI-only PG smoke flag and _ci/_test database')
with tempfile.TemporaryDirectory(prefix='sf-v818-ci-') as temp:
source=Path(temp)/'siteforge.sqlite3'
env=os.environ.copy(); env.pop('SITEFORGE_DATABASE_URL',None)
env['SITEFORGE_DATA_DIR']=temp
env['SITEFORGE_DB_FILE']=str(source)
marker='migration-smoke-'+uuid.uuid4().hex[:10]
code=r'''from app import (db,enterprise,factory_api,migration_audit,reusables,release_api,
cms_pipeline,v8_workflows,v8_templates,wp_migration_v81,wp_migration_v82,
wp_migration_v83,connectors_v84,connectors_v85,studio_v86,site_design_v87,multilingual)
import os
assert not db.POSTGRES
assert not os.path.exists(db.DB_FILE)
db.init()
for x in (enterprise,factory_api,migration_audit,reusables,release_api,
cms_pipeline,v8_workflows,v8_templates,wp_migration_v81,wp_migration_v82,
wp_migration_v83,connectors_v84,connectors_v85,studio_v86,site_design_v87,multilingual):x.setup()
s=os.environ['V818_MARKER']
with db.connect() as c:
c.execute('INSERT INTO sites(id,name,created_at,updated_at) VALUES (?,?,?,?)',(s,'Migration Sample',db.now(),db.now()))
for i in range(3):
c.execute('INSERT INTO entries(id,site_id,type,title,route,slug,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)',
(s+'e'+str(i),s,'product','Migrated Car '+str(i),'/products/'+str(i)+'/','p'+str(i),db.now(),db.now()))
c.execute('INSERT INTO redirects(id,site_id,old_path,new_path) VALUES (?,?,?,?)',(s+'r',s,'/old/','/products/0/'))
'''
env['V818_MARKER']=marker
subprocess.run([sys.executable,'-c',code],check=True,env=env)
try:
result=migrate(source,confirmed_offline=True)
assert result['verified'] and result['details']['entries']['rows']==3
assert result['details']['redirects']['rows']==1
assert db.one('SELECT COUNT(*) AS n FROM entries WHERE site_id=?',(marker,))['n']==3
assert db.one('SELECT COUNT(*) AS n FROM redirects WHERE site_id=?',(marker,))['n']==1
# Re-running must fail: no duplicate rows, no silent upsert.
try:migrate(source,confirmed_offline=True)
except RecoveryError as exc:
assert 'not empty' in str(exc)
else:raise AssertionError('Migration unexpectedly allowed nonempty target')
print(f'PASS: actual PostgreSQL transfer {result["tables"]} tables, {result["records"]} records; rerun blocked')
finally:
with db.connect() as c:
c.execute('DELETE FROM sites WHERE id=?',(marker,))