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:
WangShen007 committed 2026-10-24 07:26:41 +00:00
1 parent ed8dc01ed9
commit f3ad539998
28 files changed
+1346 -1

No files matched your search

@@ -0,0 +1,55 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { computed, ref } from 'vue'
import SystemUpdates from '@/views/settings/SystemUpdates.vue'
import type { UpdateStatus } from '@/services/systemUpdates'
const state = ref<UpdateStatus>({ configured: true, can_manage: true })
const apply = vi.fn().mockResolvedValue(undefined)
const check = vi.fn().mockResolvedValue(undefined)
vi.mock('@/composables/useSystemUpdates', () => ({ useSystemUpdates: () => ({
state, busy: computed(() => state.value.update?.status === 'building'), error: ref(''),
refresh: vi.fn().mockResolvedValue(undefined), check, apply,
}) }))
vi.mock('@/layouts/DashboardLayout.vue', () => ({ default: { template: '<div><slot /></div>' } }))
beforeEach(() => {
vi.clearAllMocks()
state.value = { configured: true, can_manage: true, current: { revision: 'a'.repeat(40), subject: 'current' },
latest: { revision: 'b'.repeat(40), subject: 'new' }, check_interval_minutes: 30, update_available: true,
update: { status: 'idle', message: '' } }
})
describe('SystemUpdates', () => {
it('requires explicit confirmation before starting an update', async () => {
const wrapper = mount(SystemUpdates)
await flushPromises()
const button = wrapper.findAll('button').find(b => b.text() === '更新到此版本')!
await button.trigger('click')
expect(wrapper.find('[role=dialog]').exists()).toBe(true)
expect(apply).not.toHaveBeenCalled()
await wrapper.findAll('button').find(b => b.text() === '备份并开始更新')!.trigger('click')
await flushPromises()
expect(apply).toHaveBeenCalledOnce()
wrapper.unmount()
})
it('disables installation when there is no newer version or an update is running', async () => {
state.value.update_available = false
const wrapper = mount(SystemUpdates)
await flushPromises()
expect(wrapper.find('.update-actions button').attributes('disabled')).toBeDefined()
state.value.update_available = true
state.value.update = { status: 'building', message: '正在构建' }
await flushPromises()
expect(wrapper.find('.update-actions button').attributes('disabled')).toBeDefined()
expect(wrapper.text()).toContain('正在构建')
wrapper.unmount()
})
it('shows a failed update and recovery result', async () => {
state.value.update = { status: 'rolled_back', message: '已恢复原版本', error: '构建失败' }
const wrapper = mount(SystemUpdates)
await flushPromises()
expect(wrapper.text()).toContain('已恢复原版本')
expect(wrapper.text()).toContain('构建失败')
wrapper.unmount()
})
})