diff --git a/en/guide/download.md b/en/guide/download.md index 3e5ad06..fa9f26f 100644 --- a/en/guide/download.md +++ b/en/guide/download.md @@ -5,15 +5,16 @@ home: hello @@ -192,10 +203,18 @@ The command line program package includes four executables: -## EasyTier v{{ currentVersion }} { #latest } +

Nightly follows the main branch and is intended for testing the latest features. It may be unstable.

+ +## EasyTier {{ currentTitle }} { #latest } + +

+ Loading Nightly downloads… + Nightly downloads could not be loaded. Please visit the Nightly release page. +

- GitHub Acceleration
@@ -221,7 +240,7 @@ The command line program package includes four executables:
- +
@@ -239,14 +258,14 @@ The command line program package includes four executables: diff --git a/guide/download.md b/guide/download.md index 73160b6..86401f7 100644 --- a/guide/download.md +++ b/guide/download.md @@ -5,15 +5,16 @@ home: hello @@ -192,10 +203,18 @@ function renderUrlTmpl(url_tmpl: string): string { -## EasyTier v{{ currentVersion }} { #latest } +

Nightly 跟随 main 分支更新,适合测试最新功能,可能不稳定。

+ +## EasyTier {{ currentTitle }} { #latest } + +

+ 正在加载 Nightly 下载文件… + 暂时无法获取 Nightly 下载文件,请前往 Nightly 发布页。 +

- Github 加速
@@ -221,7 +240,7 @@ function renderUrlTmpl(url_tmpl: string): string {
-
{{ pkg.arch }} - -{{format}} + +{{link.format}} - -{{format}} + +{{link.format}}
+
@@ -239,14 +258,14 @@ function renderUrlTmpl(url_tmpl: string): string { diff --git a/metadata.data.ts b/metadata.data.ts index 6ca52c6..5f69297 100644 --- a/metadata.data.ts +++ b/metadata.data.ts @@ -2,7 +2,7 @@ export default { load() { return { easytier_latest_version: '2.6.4', - easytier_pre_release_version: '2.6.3', + easytier_pre_release_version: '2.7.0', github_accels: [ 'https://ghfast.top/', 'https://v6.gh-proxy.org/', diff --git a/releaseUtils.ts b/releaseUtils.ts new file mode 100644 index 0000000..dfa19f6 --- /dev/null +++ b/releaseUtils.ts @@ -0,0 +1,22 @@ +export interface Release { + tag_name: string + prerelease?: boolean + draft?: boolean + assets: Array<{ name: string, browser_download_url: string }> +} + +export function findPrerelease(releases: Release[]): Release | null { + return releases.find(release => release.prerelease && !release.draft && release.tag_name !== 'nightly') ?? null +} + +export function resolveDownloadUrl(template: string, release: Release | null, fallbackVersion?: string): string | undefined { + if (release) { + const filename = template.slice(template.lastIndexOf('/') + 1) + const pattern = filename.split('{}') + .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + .join('[^/]+') + return release.assets.find(asset => new RegExp(`^${pattern}$`).test(asset.name))?.browser_download_url + } + + return fallbackVersion ? template.replace(/\{\}/g, fallbackVersion) : undefined +} diff --git a/tests/releaseUtils.test.ts b/tests/releaseUtils.test.ts new file mode 100644 index 0000000..be47b30 --- /dev/null +++ b/tests/releaseUtils.test.ts @@ -0,0 +1,54 @@ +import type { Release } from '../releaseUtils.ts' +import assert from 'node:assert/strict' +// eslint-disable-next-line test/no-import-node-test -- Use the built-in runner without adding a test framework. +import { test } from 'node:test' +import { findPrerelease, resolveDownloadUrl } from '../releaseUtils.ts' + +const cliTemplate = 'https://github.com/EasyTier/EasyTier/releases/download/v{}/easytier-linux-x86_64-v{}.zip' +const guiTemplate = 'https://github.com/EasyTier/EasyTier/releases/download/v{}/easytier-gui_{}_x64-setup.exe' + +function release(tag_name: string, assets: string[] = [], prerelease = true): Release { + return { + tag_name, + prerelease, + assets: assets.map(name => ({ name, browser_download_url: `https://github.com/EasyTier/EasyTier/releases/download/${tag_name}/${name}` })), + } +} + +test('ordinary prereleases exclude rolling nightly and draft releases', () => { + const prerelease = release('v2.7.0') + assert.equal(findPrerelease([ + release('nightly'), + { ...release('v2.8.0'), draft: true }, + prerelease, + release('v2.6.4', [], false), + ]), prerelease) + assert.equal(findPrerelease([release('nightly'), release('v2.6.4', [], false)]), null) +}) + +test('nightly uses the actual release tag and versioned asset name', () => { + const nightly = release('nightly', ['easytier-linux-x86_64-v2.7.0.zip']) + assert.equal(resolveDownloadUrl(cliTemplate, nightly), 'https://github.com/EasyTier/EasyTier/releases/download/nightly/easytier-linux-x86_64-v2.7.0.zip') +}) + +test('GUI asset versions can differ from the release version', () => { + const prerelease = release('v2.6.1', ['easytier-gui_2.6.0_x64-setup.exe']) + assert.equal(resolveDownloadUrl(guiTemplate, prerelease), 'https://github.com/EasyTier/EasyTier/releases/download/v2.6.1/easytier-gui_2.6.0_x64-setup.exe') +}) + +test('Android assets without a version placeholder also resolve', () => { + const nightly = release('nightly', ['app-arm64-release.apk']) + assert.equal(resolveDownloadUrl('https://github.com/EasyTier/EasyTier/releases/download/v{}/app-arm64-release.apk', nightly), 'https://github.com/EasyTier/EasyTier/releases/download/nightly/app-arm64-release.apk') +}) + +test('missing assets do not produce invented download links', () => { + assert.equal(resolveDownloadUrl(cliTemplate, release('nightly', ['easytier-linux-aarch64-v2.7.0.zip'])), undefined) + assert.equal(resolveDownloadUrl(guiTemplate, release('v2.6.4'), '2.6.4'), undefined) + assert.equal(resolveDownloadUrl(cliTemplate, release('nightly', ['easytier-linux-x86_64-v2.7.0Xzip'])), undefined) +}) + +test('API failure preserves stable and prerelease fallbacks without guessing nightly assets', () => { + assert.equal(resolveDownloadUrl(cliTemplate, null, '2.6.4'), 'https://github.com/EasyTier/EasyTier/releases/download/v2.6.4/easytier-linux-x86_64-v2.6.4.zip') + assert.equal(resolveDownloadUrl(guiTemplate, null, '2.7.0'), 'https://github.com/EasyTier/EasyTier/releases/download/v2.7.0/easytier-gui_2.7.0_x64-setup.exe') + assert.equal(resolveDownloadUrl(cliTemplate, null), undefined) +}) diff --git a/useLatestVersion.ts b/useLatestVersion.ts index 0bfd786..a0e6fc8 100644 --- a/useLatestVersion.ts +++ b/useLatestVersion.ts @@ -1,40 +1,42 @@ -import { onMounted, ref } from 'vue' +import type { Release } from './releaseUtils' +import { computed, onMounted, ref } from 'vue' +import { findPrerelease } from './releaseUtils' export function useLatestVersion() { - const latestVersion = ref(null) - const prereleaseVersion = ref(null) + const latestRelease = ref(null) + const prereleaseRelease = ref(null) + const nightlyRelease = ref(null) + const latestVersion = computed(() => latestRelease.value?.tag_name.replace(/^v/, '') ?? null) + const prereleaseVersion = computed(() => prereleaseRelease.value?.tag_name.replace(/^v/, '') ?? null) const loading = ref(true) const error = ref(null) onMounted(async () => { - try { - const [latestRes, releasesRes] = await Promise.all([ - fetch('https://api.github.com/repos/EasyTier/EasyTier/releases/latest'), - fetch('https://api.github.com/repos/EasyTier/EasyTier/releases?per_page=5'), - ]) - - if (!latestRes.ok || !releasesRes.ok) { + async function fetchRelease(path: string): Promise { + const response = await fetch(`https://api.github.com/repos/EasyTier/EasyTier/releases${path}`, { signal: AbortSignal.timeout(10000) }) + if (!response.ok) throw new Error('Failed to fetch releases from GitHub API') - } - - const latestData = await latestRes.json() as { tag_name?: string } - const releases = await releasesRes.json() as Array<{ tag_name?: string, prerelease?: boolean }> - - latestVersion.value = latestData.tag_name ?? null - prereleaseVersion.value = releases.find(item => item.prerelease)?.tag_name ?? null - } - catch (err) { - error.value = err instanceof Error ? err.message : 'Unknown error' - } - finally { - loading.value = false + return response.json() } - if (latestVersion.value?.startsWith('v')) - latestVersion.value = latestVersion.value.slice(1) - if (prereleaseVersion.value?.startsWith('v')) - prereleaseVersion.value = prereleaseVersion.value.slice(1) + const [latest, releases, nightly] = await Promise.allSettled([ + fetchRelease('/latest'), + fetchRelease('?per_page=100'), + fetchRelease('/tags/nightly'), + ]) + + if (latest.status === 'fulfilled') + latestRelease.value = latest.value + if (releases.status === 'fulfilled') + prereleaseRelease.value = findPrerelease(releases.value) + if (nightly.status === 'fulfilled') + nightlyRelease.value = nightly.value + + const failure = [latest, releases, nightly].find(result => result.status === 'rejected') + if (failure?.status === 'rejected') + error.value = failure.reason instanceof Error ? failure.reason.message : 'Unknown error' + loading.value = false }) - return { latestVersion, prereleaseVersion, loading, error } + return { latestVersion, prereleaseVersion, latestRelease, prereleaseRelease, nightlyRelease, loading, error } }
{{ pkg.arch }} - -{{format}} + +{{link.format}} - -{{format}} + +{{link.format}}