feat(updates): check Gitea automatically and deploy on owner confirmation
Add owner-only update controls, a read-only SSH checker and independent systemd deployment services. Build before maintenance; back up code and database; restore on failed deployment. Preserve runtime configuration and gate business writes during the switch.
This commit is contained in:
1 parent
ed8dc01ed9
commit
f3ad539998
28 files changed
+1346
-1
No files matched your search
@@ -0,0 +1,70 @@
|
||||
"""Persistent state shared by the independent update services."""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
|
||||
CONFIG_FILE = Path('/etc/replypier-updater/config.json')
|
||||
BUSY = {'queued', 'preparing', 'building', 'backing_up', 'deploying', 'migrating', 'verifying', 'rolling_back'}
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def config() -> dict[str, Any]:
|
||||
return json.loads(CONFIG_FILE.read_text())
|
||||
|
||||
|
||||
def directory(cfg: dict[str, Any]) -> Path:
|
||||
return Path(cfg['state_dir'])
|
||||
|
||||
|
||||
def read(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
p = directory(cfg) / 'status.json'
|
||||
return json.loads(p.read_text()) if p.exists() else {
|
||||
'current': {'revision': cfg['initial_revision'], 'subject': '已安装版本'},
|
||||
'latest': None, 'checked_at': None, 'checking': False,
|
||||
'check_error': None, 'update': {'status': 'idle', 'message': ''},
|
||||
}
|
||||
|
||||
|
||||
def write(cfg: dict[str, Any], state: dict[str, Any]) -> None:
|
||||
target = directory(cfg) / 'status.json'
|
||||
temp = target.with_suffix('.tmp')
|
||||
with temp.open('w') as out:
|
||||
json.dump(state, out, ensure_ascii=False, indent=2)
|
||||
out.flush()
|
||||
os.fsync(out.fileno())
|
||||
temp.chmod(0o644)
|
||||
temp.replace(target)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def transaction(cfg: dict[str, Any]) -> Iterator[dict[str, Any]]:
|
||||
directory(cfg).mkdir(parents=True, exist_ok=True)
|
||||
with (directory(cfg) / 'state.lock').open('a') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
state = read(cfg)
|
||||
yield state
|
||||
write(cfg, state)
|
||||
|
||||
|
||||
def progress(cfg: dict[str, Any], status: str, message: str, **values: Any) -> None:
|
||||
with transaction(cfg) as state:
|
||||
state['update'].update(status=status, message=message, **values)
|
||||
|
||||
|
||||
def public_status(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
state = read(cfg)
|
||||
current = state['current']['revision']
|
||||
latest = (state.get('latest') or {}).get('revision')
|
||||
return {**state, 'configured': True, 'can_manage': True,
|
||||
'repository_url': cfg['web_url'], 'branch': cfg['branch'],
|
||||
'check_interval_minutes': 30,
|
||||
'update_available': bool(latest and latest != current and not state.get('check_error'))}
|
||||
Reference in new issue
Block a user