Forráskód Böngészése

修复系统发布源检查超时

LSamien 2 hónapja
szülő
commit
9b32dc9f9a

+ 1 - 1
bin/hermes-yaoyao-updater.mjs

@@ -247,7 +247,7 @@ function stageRelease(job, jobPath) {
   const staging = join(releasesRoot, `.staging-${id}`)
   removeInside(staging, releasesRoot)
   updateJob(jobPath, { state: 'downloading', message: `正在下载 ${target.gitTag}` })
-  run('git', ['clone', '--quiet', '--depth', '1', '--single-branch', '--branch', target.gitTag, plan.source, staging], { timeout: 180_000 })
+  run('git', ['clone', '--quiet', '--depth', '1', '--single-branch', '--branch', target.gitTag, plan.source, staging], { timeout: 600_000 })
   const manifest = validateManifest(readJSON(join(staging, 'release.json')))
   for (const field of ['schemaVersion', 'releaseVersion', 'webVersion', 'pluginVersion', 'gitTag']) {
     if (manifest[field] !== target[field]) fail('下载的发布清单与检查结果不一致')

+ 68 - 12
src/server/updateManager.ts

@@ -18,6 +18,7 @@ import type { ServerConfig } from './config.js'
 import { DEFAULT_YAOYAO_RELEASE_SOURCE } from './config.js'
 import {
   compareReleaseVersions,
+  parseReleaseManifest,
   readReleaseManifest,
   type ReleaseManifest,
 } from './releases.js'
@@ -91,13 +92,58 @@ export interface SystemUpdateManagerOptions {
 
 function run(command: string, args: string[], timeout = 30_000): Promise<string> {
   return new Promise((resolvePromise, reject) => {
-    execFile(command, args, { encoding: 'utf8', timeout, maxBuffer: 4 * 1_024 * 1_024 }, (error, stdout) => {
-      if (error) reject(error)
+    execFile(command, args, { encoding: 'utf8', timeout, maxBuffer: 4 * 1_024 * 1_024 }, (error, stdout, stderr) => {
+      if (error) {
+        const detail = stderr.trim()
+        reject(detail ? new Error(detail) : error)
+      }
       else resolvePromise(stdout)
     })
   })
 }
 
+export function releaseManifestURL(source: string, tag: string): URL | undefined {
+  try {
+    const url = new URL(source)
+    if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return undefined
+    url.hash = ''
+    url.search = ''
+    url.pathname = `${url.pathname.replace(/\/+$/, '').replace(/\.git$/, '')}/raw/${encodeURIComponent(tag)}/release.json`
+    return url
+  } catch {
+    return undefined
+  }
+}
+
+async function fetchReleaseManifest(source: string, tag: string): Promise<ReleaseManifest | undefined> {
+  const url = releaseManifestURL(source, tag)
+  if (!url) return undefined
+  let response: Response
+  try {
+    response = await fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(15_000) })
+  } catch {
+    return undefined
+  }
+  if (!response.ok) return undefined
+  try {
+    return parseReleaseManifest(await response.json())
+  } catch (error) {
+    if (error instanceof SyntaxError) throw new Error('远端 release.json 不是有效的 JSON')
+    throw error
+  }
+}
+
+function exactTagCommit(output: string): string | undefined {
+  let commit: string | undefined
+  for (const line of output.split('\n')) {
+    const [candidate = '', reference = ''] = line.trim().split(/\s+/, 2)
+    if (!/^[0-9a-f]{40,64}$/.test(candidate)) continue
+    if (reference.endsWith('^{}')) return candidate
+    if (reference) commit = candidate
+  }
+  return commit
+}
+
 export async function inspectGitRemote(source: string, current: ReleaseManifest): Promise<RemoteRelease | undefined> {
   const output = await run('git', ['ls-remote', '--tags', source, 'refs/tags/v*'])
   const byVersion = new Map<string, { version: string; commit: string; peeled: boolean }>()
@@ -115,17 +161,27 @@ export async function inspectGitRemote(source: string, current: ReleaseManifest)
   const latest = releases.at(-1)
   if (!latest || compareReleaseVersions(latest.version, current.releaseVersion) <= 0) return undefined
 
-  const temporary = mkdtempSync(join(tmpdir(), 'hermes-yaoyao-update-check-'))
-  try {
-    await run('git', ['clone', '--quiet', '--depth', '1', '--single-branch', '--branch', `v${latest.version}`, source, temporary], 60_000)
-    const commit = (await run('git', ['-C', temporary, 'rev-parse', 'HEAD'])).trim()
-    if (commit !== latest.commit) throw new Error('远端发布标签在检查期间发生变化')
-    const manifest = readReleaseManifest(join(temporary, 'release.json'))
-    if (manifest.releaseVersion !== latest.version) throw new Error('远端标签与 release.json 版本不一致')
-    return { manifest, commit }
-  } finally {
-    rmSync(temporary, { recursive: true, force: true })
+  const tag = `v${latest.version}`
+  let manifest = await fetchReleaseManifest(source, tag)
+  if (!manifest) {
+    const temporary = mkdtempSync(join(tmpdir(), 'hermes-yaoyao-update-check-'))
+    try {
+      await run('git', [
+        'clone', '--quiet', '--filter=blob:none', '--no-checkout', '--depth', '1',
+        '--single-branch', '--branch', tag, source, temporary,
+      ], 180_000)
+      const commit = (await run('git', ['-C', temporary, 'rev-parse', 'HEAD'])).trim()
+      if (commit !== latest.commit) throw new Error('远端发布标签在检查期间发生变化')
+      manifest = parseReleaseManifest(JSON.parse(await run('git', ['-C', temporary, 'show', 'HEAD:release.json'])))
+    } finally {
+      rmSync(temporary, { recursive: true, force: true })
+    }
+  } else {
+    const verification = await run('git', ['ls-remote', '--tags', source, `refs/tags/${tag}*`])
+    if (exactTagCommit(verification) !== latest.commit) throw new Error('远端发布标签在检查期间发生变化')
   }
+  if (manifest.releaseVersion !== latest.version) throw new Error('远端标签与 release.json 版本不一致')
+  return { manifest, commit: latest.commit }
 }
 
 function publicJob(job: StoredUpdateJob | undefined): UpdateJob | undefined {

+ 9 - 1
tests/server/updateManager.test.ts

@@ -5,7 +5,7 @@ import { join } from 'node:path'
 import { afterEach, describe, expect, it } from 'vitest'
 import type { ServerConfig } from '../../src/server/config.js'
 import { compareReleaseVersions, parseReleaseManifest } from '../../src/server/releases.js'
-import { inspectGitRemote, SystemUpdateManager } from '../../src/server/updateManager.js'
+import { inspectGitRemote, releaseManifestURL, SystemUpdateManager } from '../../src/server/updateManager.js'
 
 const roots: string[] = []
 const current = {
@@ -50,6 +50,14 @@ function fixture() {
 }
 
 describe('system release contract', () => {
+  it('maps an HTTPS Git source to the lightweight tagged release manifest', () => {
+    expect(releaseManifestURL(
+      'https://git.samien.cn/samien/hermes-yaoyao.git',
+      'v0.2.1',
+    )?.href).toBe('https://git.samien.cn/samien/hermes-yaoyao/raw/v0.2.1/release.json')
+    expect(releaseManifestURL('/srv/git/hermes-yaoyao.git', 'v0.2.1')).toBeUndefined()
+  })
+
   it('validates paired versions and semantic ordering', () => {
     expect(parseReleaseManifest(current)).toEqual(current)
     expect(compareReleaseVersions('0.3.0', '0.2.9')).toBeGreaterThan(0)