mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
✨ feat(updates): обновление установки из интерфейса
Установка раз в шесть часов и по кнопке проверяет страницу релизов GitHub. Администратор установки видит баннер о новой версии и ставит её одной кнопкой; карточка «Обновления» — в «Настройки → Платформа». Установку выполняет отдельный сервис updater с Docker-сокетом: backend-app общается с ним только файлами в томе chatballs-updates (heartbeat, request.json, status.json). Updater принимает лишь релизы своего репозитория с образами по digest и применяет compose.yaml в одноразовом контейнере-помощнике, после чего кладёт файл в каталог установки. Запрос установки попадает в аудит. Релизный workflow собирает образ updater и пришпиливает его в compose.yaml; версия бэкенда зашивается в образ (CHATBALLS_VERSION). Миграции updates.0001 и tenancy.0034 (гранты на таблицу состояния). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
29164db094
commit
5396dda8ad
38 files changed
+1381
-9
No files matched your search
@@ -6,8 +6,9 @@
|
||||
стек падал на первом старте, уже у человека.
|
||||
|
||||
Эти тесты держат свойство, а не текущий текст файла: в production-манифесте нет
|
||||
ни одного bind-mount (кроме сертификатов TURN у опционального профиля calls), а
|
||||
все ссылки на образы поддаются закреплению по digest.
|
||||
ни одного bind-mount (кроме сертификатов TURN у опционального профиля calls и
|
||||
Docker-сокета у сервиса обновлений), а все ссылки на образы поддаются
|
||||
закреплению по digest.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -23,9 +24,10 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
COMPOSE = REPO_ROOT / "compose.yaml"
|
||||
PIN_SCRIPT = REPO_ROOT / "scripts" / "pin-release-compose.py"
|
||||
|
||||
# Единственное исключение: сертификат TURN-хоста кладёт на хост renewal-хук,
|
||||
# и только при включённом профиле calls.
|
||||
BIND_MOUNT_EXCEPTIONS = {"coturn"}
|
||||
# Исключения: сертификат TURN-хоста кладёт на хост renewal-хук (только при
|
||||
# включённом профиле calls), а сервис обновлений управляет стеком через
|
||||
# Docker-сокет хоста — это его назначение (ADR-CHATBALLS-0049).
|
||||
BIND_MOUNT_EXCEPTIONS = {"coturn", "updater"}
|
||||
|
||||
DIGEST = "sha256:" + "a" * 64
|
||||
IMAGE_KEYS = (
|
||||
@@ -35,6 +37,7 @@ IMAGE_KEYS = (
|
||||
"CHATBALLS_REDIS_IMAGE",
|
||||
"CHATBALLS_GATEWAY_IMAGE",
|
||||
"CHATBALLS_COTURN_IMAGE",
|
||||
"CHATBALLS_UPDATER_IMAGE",
|
||||
)
|
||||
|
||||
|
||||
@@ -82,7 +85,7 @@ def test_named_volumes_are_declared() -> None:
|
||||
|
||||
|
||||
def test_every_image_reference_can_be_pinned(tmp_path: Path) -> None:
|
||||
"""Все шесть ключей образов присутствуют и закрепляются по digest.
|
||||
"""Все семь ключей образов присутствуют и закрепляются по digest.
|
||||
|
||||
Если из манифеста уйдёт (или переименуется) хоть один ключ, релизный
|
||||
compose.yaml уедет с плавающим тегом — а найдётся это уже у человека.
|
||||
|
||||
@@ -350,6 +350,34 @@ test("гость по ссылке-приглашению задаёт имя и
|
||||
expect(registered[0]).toContain("guest-token");
|
||||
});
|
||||
|
||||
test("администратор установки видит баннер о новой версии и запускает обновление", async ({ page }) => {
|
||||
await mockSession(page, OWNER_IDENTITY);
|
||||
await mockInstance(page);
|
||||
await mockEmployees(page);
|
||||
const update = {
|
||||
currentVersion: "1.4.0", latestVersion: "1.5.0", latestName: "v1.5.0", latestNotes: "", latestPublishedAt: null,
|
||||
latestPageUrl: "https://github.com/dartdavros/chatballs/releases/tag/v1.5.0", available: true, checkedAt: null,
|
||||
checkError: "", updaterOnline: true, install: { version: null, status: "IDLE", message: "", requestedAt: null, updatedAt: null },
|
||||
};
|
||||
await page.route("**/api/v1/instance/update/", (route) => route.fulfill({ json: { update } }));
|
||||
const installs: string[] = [];
|
||||
await page.route("**/api/v1/instance/update/install/", (route) => {
|
||||
installs.push(route.request().method());
|
||||
return route.fulfill({ status: 202, json: { update: { ...update, install: { ...update.install, version: "1.5.0", status: "REQUESTED" } } } });
|
||||
});
|
||||
|
||||
await page.goto(`/organizations/${ORGANIZATION_PUBLIC_ID}/chat`);
|
||||
|
||||
const banner = page.locator(".update-banner");
|
||||
await expect(banner).toContainText("Доступна версия 1.5.0");
|
||||
await banner.getByRole("button", { name: "Обновить", exact: true }).click();
|
||||
await expect(page.getByText("Обновить до 1.5.0?")).toBeVisible();
|
||||
await page.locator(".decision-dialog").getByRole("button", { name: "Обновить", exact: true }).click();
|
||||
|
||||
await expect.poll(() => installs).toEqual(["POST"]);
|
||||
await expect(page.locator(".update-banner")).toContainText("Обновление до 1.5.0");
|
||||
});
|
||||
|
||||
test("с несколькими организациями вход открывает первую, переключатель ведёт во вторую", async ({ page }) => {
|
||||
const secondMembership = membershipFor("OWNER", {
|
||||
id: 3,
|
||||
|
||||
Reference in new issue
Block a user