@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply the mandatory post-release grant and group announcement."""
|
||||
"""Apply the mandatory release lifecycle notices, grant, and announcement."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -39,6 +39,8 @@ PRIMOGEM_COMPENSATION_RELEASES = {
|
||||
MAX_BALANCE = 1_000_000_000
|
||||
MAX_SUMMARY_CHARS = 300
|
||||
MAX_ANNOUNCEMENT_CHARS = 900
|
||||
MAX_LIFECYCLE_CHARS = 300
|
||||
LIFECYCLE_STAGES = frozenset({"shutdown", "startup"})
|
||||
RELEASE_PATTERN = re.compile(
|
||||
r"^beta (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<date>\d{4})$"
|
||||
)
|
||||
@@ -140,6 +142,24 @@ def build_announcement(release: str, summary: str) -> str:
|
||||
return announcement
|
||||
|
||||
|
||||
def build_lifecycle_message(release: str, stage: str) -> str:
|
||||
if stage == "shutdown":
|
||||
message = (
|
||||
f"Taffy 准备停机更新到 {release},会短暂离线一下;"
|
||||
"别慌,我很快回来喵"
|
||||
)
|
||||
elif stage == "startup":
|
||||
message = (
|
||||
f"Taffy 刚上线,正在做 {release} 的最后自检;"
|
||||
"确认就绪后会发送更新内容和补偿说明喵"
|
||||
)
|
||||
else:
|
||||
raise ReleaseUpdateError(f"unsupported lifecycle stage: {stage}")
|
||||
if len(message) > MAX_LIFECYCLE_CHARS:
|
||||
raise ReleaseUpdateError("release lifecycle message is too long")
|
||||
return message
|
||||
|
||||
|
||||
def compensation_key(release: str) -> str:
|
||||
return f"release_compensation:{release}"
|
||||
|
||||
@@ -157,6 +177,23 @@ def release_slug(release: str) -> str:
|
||||
return re.sub(r"[^a-zA-Z0-9]+", "-", release).strip("-").lower()
|
||||
|
||||
|
||||
def release_groups(database_path: Path) -> list[str]:
|
||||
connection = sqlite3.connect(f"file:{database_path}?mode=ro", uri=True)
|
||||
try:
|
||||
if connection.execute("PRAGMA quick_check").fetchone()[0] != "ok":
|
||||
raise ReleaseUpdateError("Edge SQLite integrity check failed")
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT DISTINCT group_fingerprint
|
||||
FROM player_accounts
|
||||
ORDER BY group_fingerprint
|
||||
"""
|
||||
).fetchall()
|
||||
finally:
|
||||
connection.close()
|
||||
return [str(row[0]) for row in rows]
|
||||
|
||||
|
||||
def checked_marker(raw: str, release: str) -> dict[str, Any]:
|
||||
expected_primogems = primogem_compensation_amount(release)
|
||||
try:
|
||||
@@ -566,6 +603,34 @@ def run_release_update(
|
||||
}
|
||||
|
||||
|
||||
def run_release_lifecycle(
|
||||
*,
|
||||
release_file: Path,
|
||||
database_path: Path,
|
||||
delivery_helper: Path,
|
||||
stage: str,
|
||||
deliverer: Callable[[Path, str, str, str], None] = run_delivery_helper,
|
||||
) -> dict[str, Any]:
|
||||
if stage not in LIFECYCLE_STAGES:
|
||||
raise ReleaseUpdateError(f"unsupported lifecycle stage: {stage}")
|
||||
release = read_release(release_file)
|
||||
message = build_lifecycle_message(release, stage)
|
||||
groups = release_groups(database_path)
|
||||
delivered = 0
|
||||
for group in groups:
|
||||
group_hash = hashlib.sha256(group.encode()).hexdigest()[:12]
|
||||
event_id = f"release-{release_slug(release)}-{stage}-{group_hash}"
|
||||
deliverer(delivery_helper, group, message, event_id)
|
||||
delivered += 1
|
||||
return {
|
||||
"status": "ok",
|
||||
"release": release,
|
||||
"stage": stage,
|
||||
"groups": len(groups),
|
||||
"delivered": delivered,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
home = Path.home()
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
@@ -599,20 +664,33 @@ def parse_args() -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="retry only after visually confirming that the prior send did not land",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lifecycle-stage",
|
||||
choices=sorted(LIFECYCLE_STAGES),
|
||||
help="send a guarded lifecycle notice instead of release settlement",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
result = run_release_update(
|
||||
release_file=args.release_file,
|
||||
changelog=args.changelog,
|
||||
database_path=args.database,
|
||||
backup_dir=args.backup_dir,
|
||||
delivery_helper=args.delivery_helper,
|
||||
retry_uncertain=args.retry_uncertain_announcement,
|
||||
)
|
||||
if args.lifecycle_stage:
|
||||
result = run_release_lifecycle(
|
||||
release_file=args.release_file,
|
||||
database_path=args.database,
|
||||
delivery_helper=args.delivery_helper,
|
||||
stage=args.lifecycle_stage,
|
||||
)
|
||||
else:
|
||||
result = run_release_update(
|
||||
release_file=args.release_file,
|
||||
changelog=args.changelog,
|
||||
database_path=args.database,
|
||||
backup_dir=args.backup_dir,
|
||||
delivery_helper=args.delivery_helper,
|
||||
retry_uncertain=args.retry_uncertain_announcement,
|
||||
)
|
||||
except ReleaseUpdateError as error:
|
||||
print(
|
||||
json.dumps(
|
||||
|
||||
Reference in New Issue
Block a user