🐛 Fix(update): correct latest version lookup with beta channel (#1396) (#1397)

This commit is contained in:
PiEgg
2026-03-03 16:03:29 +08:00
committed by GitHub
parent 0e452e1459
commit c7ca0de0c3
2 changed files with 206 additions and 23 deletions
+108
View File
@@ -0,0 +1,108 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { RELEASE_URL, RELEASE_URL_BACKUP } from '#/utils/static'
import { getLatestVersion } from '~/main/utils/getLatestVersion'
const { axiosGetMock } = vi.hoisted(() => {
return {
axiosGetMock: vi.fn()
}
})
vi.mock('axios', () => {
return {
default: {
get: axiosGetMock
}
}
})
describe('main/utils/getLatestVersion', () => {
beforeEach(() => {
axiosGetMock.mockReset()
})
it('returns stable release when beta channel is older', async () => {
axiosGetMock.mockResolvedValueOnce({
data: [
{ tag_name: 'v2.5.2', prerelease: false, draft: false },
{ tag_name: 'v2.4.2-beta.0', prerelease: true, draft: false }
]
})
const version = await getLatestVersion(true)
expect(version).toBe('2.5.2')
expect(axiosGetMock).toHaveBeenCalledWith(RELEASE_URL, {
headers: {
Referer: 'https://github.com'
}
})
})
it('returns prerelease when beta channel has a newer version', async () => {
axiosGetMock.mockResolvedValueOnce({
data: [
{ tag_name: 'v2.5.2', prerelease: false, draft: false },
{ tag_name: 'v2.6.0-beta.1', prerelease: true, draft: false }
]
})
const version = await getLatestVersion(true)
expect(version).toBe('2.6.0-beta.1')
})
it('ignores prerelease when beta updates are disabled', async () => {
axiosGetMock.mockResolvedValueOnce({
data: [
{ tag_name: 'v2.6.0-beta.1', prerelease: true, draft: false },
{ tag_name: 'v2.5.2', prerelease: false, draft: false }
]
})
const version = await getLatestVersion(false)
expect(version).toBe('2.5.2')
})
it('fallback compares stable and beta backup metadata when beta updates are enabled', async () => {
axiosGetMock.mockRejectedValueOnce(new Error('network down'))
axiosGetMock.mockResolvedValueOnce({
data: 'version: 2.5.2'
})
axiosGetMock.mockResolvedValueOnce({
data: 'version: 2.4.2-beta.0'
})
const version = await getLatestVersion(true)
expect(version).toBe('2.5.2')
expect(axiosGetMock).toHaveBeenNthCalledWith(2, `${RELEASE_URL_BACKUP}/latest.yml`, {
headers: {
Referer: 'https://github.com'
}
})
expect(axiosGetMock).toHaveBeenNthCalledWith(3, `${RELEASE_URL_BACKUP}/latest.beta.yml`, {
headers: {
Referer: 'https://github.com'
}
})
})
it('fallback uses only stable backup metadata when beta updates are disabled', async () => {
axiosGetMock.mockRejectedValueOnce(new Error('network down'))
axiosGetMock.mockResolvedValueOnce({
data: 'version: 2.5.2'
})
const version = await getLatestVersion(false)
expect(version).toBe('2.5.2')
expect(axiosGetMock).toHaveBeenCalledTimes(2)
expect(axiosGetMock).toHaveBeenNthCalledWith(2, `${RELEASE_URL_BACKUP}/latest.yml`, {
headers: {
Referer: 'https://github.com'
}
})
})
})
+98 -23
View File
@@ -1,34 +1,109 @@
// for referer policy, we can't use it in renderer
import axios from 'axios'
import { RELEASE_URL, RELEASE_URL_BACKUP } from '../../universal/utils/static'
import semver from 'semver'
import yaml from 'js-yaml'
interface IGithubRelease {
tag_name?: string
name?: string
prerelease?: boolean
draft?: boolean
}
interface IReleaseYAML {
version?: unknown
}
const REQUEST_HEADERS = {
Referer: 'https://github.com'
}
function normalizeVersion (version: unknown): string {
if (typeof version !== 'string') {
return ''
}
const normalized = version.trim().replace(/^v/i, '')
return semver.valid(normalized) ?? ''
}
function pickLatestVersion (versions: string[]): string {
return versions.reduce((latest, current) => {
if (latest === '' || semver.gt(current, latest)) {
return current
}
return latest
}, '')
}
async function fetchLatestVersionFromGitHub (isCheckBetaUpdate: boolean): Promise<string> {
const response = await axios.get(RELEASE_URL, {
headers: REQUEST_HEADERS
})
const releaseList: IGithubRelease[] = Array.isArray(response.data) ? response.data : []
const versions = releaseList.flatMap((release) => {
if (release.draft) {
return []
}
if (!isCheckBetaUpdate && release.prerelease) {
return []
}
const version = normalizeVersion(release.tag_name ?? release.name)
return version ? [version] : []
})
return pickLatestVersion(versions)
}
async function fetchVersionFromBackupYAML (fileName: 'latest.yml' | 'latest.beta.yml'): Promise<string> {
const response = await axios.get(`${RELEASE_URL_BACKUP}/${fileName}`, {
headers: REQUEST_HEADERS
})
const releaseInfo = yaml.load(response.data)
if (typeof releaseInfo !== 'object' || releaseInfo === null) {
return ''
}
return normalizeVersion((releaseInfo as IReleaseYAML).version)
}
async function fetchLatestVersionFromBackup (isCheckBetaUpdate: boolean): Promise<string> {
if (!isCheckBetaUpdate) {
return fetchVersionFromBackupYAML('latest.yml')
}
const settled = await Promise.allSettled([
fetchVersionFromBackupYAML('latest.yml'),
fetchVersionFromBackupYAML('latest.beta.yml')
])
const versions: string[] = []
settled.forEach((item) => {
if (item.status === 'fulfilled' && item.value) {
versions.push(item.value)
}
})
return pickLatestVersion(versions)
}
export const getLatestVersion = async (isCheckBetaUpdate: boolean = false) => {
let res: string = ''
try {
res = await axios.get(RELEASE_URL, {
headers: {
Referer: 'https://github.com'
}
}).then(r => {
const list = r.data as IStringKeyMap[]
if (isCheckBetaUpdate) {
const betaList = list.filter(item => item.name.includes('beta'))
return betaList[0].name
}
const normalList = list.filter(item => !item.name.includes('beta'))
return normalList[0].name
}).catch(async () => {
const result = await axios.get(isCheckBetaUpdate ? `${RELEASE_URL_BACKUP}/latest.beta.yml` : `${RELEASE_URL_BACKUP}/latest.yml`, {
headers: {
Referer: 'https://github.com'
}
})
const r = yaml.load(result.data) as IStringKeyMap
return r.version
})
const version = await fetchLatestVersionFromGitHub(isCheckBetaUpdate)
if (version) {
return version
}
} catch (err) {
console.log(err)
}
return res
try {
return await fetchLatestVersionFromBackup(isCheckBetaUpdate)
} catch (err) {
console.log(err)
return ''
}
}