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
+42 -5
View File
@@ -1,7 +1,7 @@
'''Durable SQLite build queue. A single job is at most running once per site.
'''Durable build queue: SQLite single-host or PostgreSQL with DB-owned leases.
This implementation is deliberately single-host. For multi-host HA, replace
SQLite with PostgreSQL and Redis (outbox+leases) before production scaling.
Redis is an optional wake-up signal, not an authoritative queue. PostgreSQL
lock/lease semantics require actual multi-worker acceptance on infrastructure.
'''
from __future__ import annotations
import hashlib
@@ -101,9 +101,15 @@ def recover_expired():
if row['cancel_requested']:
state='cancelled'
else:
state='retrying' if row['attempts']<row['max_attempts'] else 'failed'
# Publishing is an external side effect: it may have completed remotely
# even though the Worker lost its lease. Never automatically replay it.
state=('failed' if row['kind'] in ('remote_deploy','remote_rollback')
else ('retrying' if row['attempts']<row['max_attempts'] else 'failed'))
message=('Release lease expired; reconcile remote release before manual retry'
if row['kind'] in ('remote_deploy','remote_rollback') else
'Worker lease expired; verify partial builds')
c.execute('UPDATE build_jobs SET state=?,worker_id=NULL,lease_until=NULL,available_at=?,error=? WHERE id=?',
(state,db.now(),'Worker lease expired; verify partial builds',row['id']))
(state,db.now(),message,row['id']))
return len(rows)
@@ -140,3 +146,34 @@ def submit_migration(site_id,session_id,actor,key=None):
from . import redis_signal
redis_signal.notify()
return {'id':job_id,'state':'queued','existing':False}
def submit_delivery(site_id,kind,payload,actor,key=None):
# One logical release per job; serialized with build jobs by Worker.
if kind not in ('remote_deploy','remote_rollback'):
raise JobConflict('Unknown delivery job kind')
if key is not None and not (8<=len(key)<=128 and all(c.isalnum() or c in '_-:.' for c in key)):
raise JobConflict('无效的幂等键')
normalized=json.dumps(payload,sort_keys=True,separators=(',',':'))
with db.connect() as c:
c.execute('BEGIN IMMEDIATE')
if key:
row=c.execute('SELECT * FROM build_jobs WHERE site_id=? AND idempotency_key=?',(site_id,key)).fetchone()
if row:
if row['kind']!=kind or row['payload_json']!=normalized:raise JobConflict('幂等键存在冲突')
return {'id':row['id'],'state':row['state'],'existing':True}
row=c.execute("SELECT id,state FROM build_jobs WHERE site_id=? AND kind=? AND payload_json=? AND state IN ('queued','running','retrying') ORDER BY created_at LIMIT 1",(site_id,kind,normalized)).fetchone()
if row:return {'id':row['id'],'state':row['state'],'existing':True}
active="state IN ('queued','running','retrying')"
if c.execute(f'SELECT COUNT(*) FROM build_jobs WHERE {active}').fetchone()[0]>=_limit('SITEFORGE_MAX_PENDING_JOBS',100):
raise JobCapacityError('系统任务队列已满')
if c.execute(f'SELECT COUNT(*) FROM build_jobs WHERE site_id=? AND {active}',(site_id,)).fetchone()[0]>=_limit('SITEFORGE_MAX_PENDING_JOBS_PER_SITE',5):
raise JobCapacityError('网站任务队列已满')
job_id=uuid.uuid4().hex[:16]
c.execute('''INSERT INTO build_jobs(id,site_id,kind,state,created_by,created_at,payload_json,
idempotency_key,max_attempts,timeout_seconds,available_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?)''',
(job_id,site_id,kind,'queued',actor,db.now(),normalized,key,3,900,db.now()))
from . import redis_signal
redis_signal.notify()
return {'id':job_id,'state':'queued','existing':False}