feat: restart to apply update, privileged helper, rollback

This commit is contained in:
fsylvain committed 2026-10-01 21:36:40 +02:00
1 parent 70460e4170
commit 54e8fd5fed
63 files changed
+8635 -164

No files matched your search

+89 -9
View File
@@ -187,6 +187,7 @@ function updateStatusBody(overrides: Record<string, unknown> = {}) {
progress: null,
error: null,
started_at: null,
finished_at: null,
...overrides,
}
}
@@ -248,6 +249,41 @@ function updateDownloading() {
const updateCheckHangs: ApiMock = { status: 200, hang: true }
const restartStartedAt = '2026-09-30T09:51:00'
const restartFinishedAt = '2026-09-30T09:52:00'
const restartMemoryKey = 'renewcms:update-restart'
function restartApi(status: unknown, restart: ApiMock): Record<string, ApiMock> {
return { ...updateApi('owner', status), 'POST /v1/system/update/restart': restart }
}
function restartAccepted(status: Record<string, unknown>): ApiMock {
return { status: 202, body: { ...status, state: 'restarting', started_at: restartStartedAt } }
}
async function openRestartDialog(page: Page) {
await page.getByRole('button', { name: 'Restart', exact: true }).click()
await page.getByRole('alertdialog').waitFor()
}
function confirmRestart(label: string) {
return async (page: Page) => {
await openRestartDialog(page)
await page.getByRole('button', { name: label, exact: true }).click()
}
}
async function rememberRestartAndReload(page: Page) {
await page.evaluate(
([key, startedAt]) =>
sessionStorage.setItem(key!, JSON.stringify({ kind: 'update', started_at: startedAt })),
[restartMemoryKey, restartStartedAt],
)
await page.reload()
await page.getByText('Your site is back on v2.2.0-beta.').first().waitFor()
await page.mouse.move(0, 0)
}
function updateApi(
role: 'owner' | 'admin',
status: unknown,
@@ -2089,12 +2125,42 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
Downloading: { path: '/settings', api: updateApi('owner', updateDownloading()) },
Verifying: { path: '/settings', api: updateApi('owner', updateJob('verifying')) },
'Ready to install': { path: '/settings', api: updateApi('owner', updateJob('ready')) },
'Restart: confirm': { path: '/settings', skip: 'pending' },
'Restart: request failed': { path: '/settings', skip: 'pending' },
'Restarting to install': { path: '/settings', skip: 'pending' },
'Restarting, taking longer': { path: '/settings', skip: 'pending' },
'Update done': { path: '/settings', skip: 'pending' },
'Update failed, rolled back': { path: '/settings', skip: 'pending' },
'Restart: confirm': {
path: '/settings',
api: restartApi(updateJob('ready'), { status: 500 }),
steps: openRestartDialog,
},
'Restart: request failed': {
path: '/settings',
api: restartApi(updateJob('ready'), { status: 500 }),
steps: async (page) => {
await confirmRestart('Restart now')(page)
await page.getByText("Couldn't start the restart").waitFor()
await page.mouse.move(0, 0)
},
},
'Restarting to install': {
path: '/settings',
api: restartApi(updateJob('ready'), restartAccepted(updateJob('ready'))),
steps: async (page) => {
await confirmRestart('Restart now')(page)
await page.getByText('Installing version 2.3.0').waitFor()
},
},
'Restarting, taking longer': { path: '/settings', skip: 'clock-sensitive' },
'Update done': { path: '/settings', skip: 'clock-sensitive' },
'Update failed, rolled back': {
path: '/settings',
api: updateApi(
'owner',
updateJob('failed', {
error: 'rolled_back',
started_at: restartStartedAt,
finished_at: restartFinishedAt,
}),
),
steps: rememberRestartAndReload,
},
'Download failed': {
path: '/settings',
api: updateApi('owner', updateJob('failed', { error: 'download' })),
@@ -2123,9 +2189,23 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
await page.mouse.move(0, 0)
},
},
'Plain restart: confirm': { path: '/settings', skip: 'pending' },
'Plain restart: restarting': { path: '/settings', skip: 'pending' },
'Plain restart: done': { path: '/settings', skip: 'pending' },
'Plain restart: confirm': {
path: '/settings',
api: restartApi(updateStatusBody({ can_apply: true }), { status: 500 }),
steps: openRestartDialog,
},
'Plain restart: restarting': {
path: '/settings',
api: restartApi(
updateStatusBody({ can_apply: true }),
restartAccepted(updateStatusBody({ can_apply: true })),
),
steps: async (page) => {
await confirmRestart('Restart site')(page)
await page.getByText('Restarting your site').waitFor()
},
},
'Plain restart: done': { path: '/settings', skip: 'clock-sensitive' },
'Admin view': {
path: '/settings',
api: updateApi('admin', updateAvailable()),