feat(updater): GitHub-first update checks with 20s connectivity probe
checkWithFallback now probes api.github.com through a dedicated system-proxy session with a 20s AbortSignal timeout before choosing the feed: reachable -> GitHub releases (with Gitea as the error fallback), unreachable/timeout -> straight to the domestic Gitea channel (forced direct), so proxy-less users never hang on a dead proxy. Feed is decided before touching the updater, so there is never a raced concurrent checkForUpdates. Docs updated: AGENTS.md update-mechanism section and the release flow (GitHub assets ship per version again, release.cjs runs with no skip flags).
This commit is contained in:
1 parent
680254cad6
commit
dba2b14c33
2 files changed
+59
-20
No files matched your search
@@ -28,18 +28,18 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
|
|||||||
1. `package.json` 版本号 +1,写 `RELEASE_NOTES.md`(仓库根,已 gitignore;可选 `.zh-TW/.en/.ja` 译文,缺译文回退简体)
|
1. `package.json` 版本号 +1,写 `RELEASE_NOTES.md`(仓库根,已 gitignore;可选 `.zh-TW/.en/.ja` 译文,缺译文回退简体)
|
||||||
2. `node scripts/sync-changelog.cjs` 把发布说明并入 `CHANGELOG*.md` —— **必须在 `npm run dist` 之前**:更新日志以 `?raw` 打进安装包(「关于」页离线读),`release.cjs` 也校验 `CHANGELOG.md` 已含该版本号
|
2. `node scripts/sync-changelog.cjs` 把发布说明并入 `CHANGELOG*.md` —— **必须在 `npm run dist` 之前**:更新日志以 `?raw` 打进安装包(「关于」页离线读),`release.cjs` 也校验 `CHANGELOG.md` 已含该版本号
|
||||||
3. `npm run dist` 构建
|
3. `npm run dist` 构建
|
||||||
4. `node scripts/release.cjs <版本号> --skip-github`,例如 `node scripts/release.cjs 1.0.14 --skip-github`
|
4. `node scripts/release.cjs <版本号>`,例如 `node scripts/release.cjs 1.0.14`(**不带 skip 参数**,Gitea 与 GitHub 一起发)
|
||||||
- **只发国内**(2026-09-15 起的产品决定):Gitea release(msi + exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`:exe.blockmap → exe → release-notes.md → **latest.yml 最后**)
|
- Gitea:release(msi + exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`:exe.blockmap → exe → release-notes.md → **latest.yml 最后**)
|
||||||
|
- GitHub:release 资产再次随版本同步发布(exe + exe.blockmap + latest.yml),electron-updater 标准 GitHub provider 直接吃 release 资产
|
||||||
- 通道不再先删旧版:新版本文件全部传完、latest.yml 生效后才清掉上一版 exe/blockmap,中途失败不会把通道打空;同一版本可重复运行(release 复用、已传资产跳过)
|
- 通道不再先删旧版:新版本文件全部传完、latest.yml 生效后才清掉上一版 exe/blockmap,中途失败不会把通道打空;同一版本可重复运行(release 复用、已传资产跳过)
|
||||||
- 只补通道:`node scripts/release.cjs <版本号> --channel-only`(不建 release、不发 GitHub)
|
- 只补通道:`node scripts/release.cjs <版本号> --channel-only`(不建 release、不发 GitHub)
|
||||||
- 国内通道全程直连,**不需要设代理**;代理只在显式补发 GitHub 时才用(`HTTPS_PROXY=http://127.0.0.1:7897`,脚本只把它用于 GitHub 请求)
|
- 国内通道全程直连,**不需要设代理**;GitHub 请求走 `HTTPS_PROXY=http://127.0.0.1:7897`(脚本只把它用于 GitHub 请求)
|
||||||
- GitHub 那一步的状态:`README` 与更新机制里仍保留 GitHub 作为更新回退源,但**release 资产不再随版本发布同步**;若某天需要补,跑一次 `node scripts/release.cjs <版本号> --skip-gitea` 即可
|
|
||||||
5. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号
|
5. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号
|
||||||
6. `git tag v<版本号>` 并推送两个远程(代码/tag 的镜像保持同步,仅 release 资产不发 GitHub)
|
6. `git tag v<版本号>` 并推送两个远程(代码/tag 与 release 资产的镜像保持同步)
|
||||||
|
|
||||||
## 更新机制
|
## 更新机制
|
||||||
|
|
||||||
- 检查更新:先 Gitea 更新通道(强制直连,不走系统代理),失败回退 GitHub(走系统代理)。 electron-updater 用独立 session(partition `electron-updater`),代理模式在 `useFeed` 里按源切换
|
- 检查更新:**GitHub 优先**(走系统代理;前置 20 秒连通性探测 `probeGithub`——探测失败/超时直接兜底 Gitea,不给 electron-updater 挂起的机会),失败回退国内 Gitea 通用包通道(强制直连,不走系统代理)。 electron-updater 用独立 session(partition `electron-updater`),代理模式在 `useFeed` 里按源切换
|
||||||
- 更新日志:Gitea 仓库是私有的(匿名 API 404),改为从更新通道的 `release-notes.md` 读取(直连 session `openterminal-update-direct`),再回退 Gitea/GitHub releases API
|
- 更新日志:Gitea 仓库是私有的(匿名 API 404),改为从更新通道的 `release-notes.md` 读取(直连 session `openterminal-update-direct`),再回退 Gitea/GitHub releases API
|
||||||
- electron-updater 不支持 MSI 自动更新,自动更新只走 NSIS exe
|
- electron-updater 不支持 MSI 自动更新,自动更新只走 NSIS exe
|
||||||
|
|
||||||
|
|||||||
+53
-14
@@ -8,9 +8,11 @@ import { loadSettings } from './settingsStore'
|
|||||||
import { markQuitting } from './tray'
|
import { markQuitting } from './tray'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update feeds: domestic Gitea generic package is tried first (fast in
|
* Update feeds: GitHub releases is tried first (system proxy, gated by a
|
||||||
* China); GitHub releases is the fallback. Both feeds carry the NSIS .exe —
|
* 20s connectivity probe so proxy-less users don't hang); the domestic
|
||||||
* electron-updater does NOT support MSI auto-updates, MSI is manual only.
|
* Gitea generic package is the fallback (forced direct). Both feeds carry
|
||||||
|
* the NSIS .exe — electron-updater does NOT support MSI auto-updates, MSI
|
||||||
|
* is manual only.
|
||||||
*/
|
*/
|
||||||
const GITEA_FEED =
|
const GITEA_FEED =
|
||||||
'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/'
|
'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/'
|
||||||
@@ -19,6 +21,9 @@ const GITEA_RELEASES_API =
|
|||||||
const GITHUB_REPO = { owner: 'billowliu2', repo: 'OpenTerminal' }
|
const GITHUB_REPO = { owner: 'billowliu2', repo: 'OpenTerminal' }
|
||||||
const GITHUB_RELEASES_API =
|
const GITHUB_RELEASES_API =
|
||||||
'https://api.github.com/repos/billowliu2/OpenTerminal/releases?per_page=10'
|
'https://api.github.com/repos/billowliu2/OpenTerminal/releases?per_page=10'
|
||||||
|
const GITHUB_PROBE_URL =
|
||||||
|
'https://api.github.com/repos/billowliu2/OpenTerminal/releases/latest'
|
||||||
|
const GITHUB_PROBE_TIMEOUT_MS = 20_000
|
||||||
|
|
||||||
let state: UpdateState = { status: 'idle', currentVersion: app.getVersion() }
|
let state: UpdateState = { status: 'idle', currentVersion: app.getVersion() }
|
||||||
let activeFeed: 'gitea' | 'github' = 'gitea'
|
let activeFeed: 'gitea' | 'github' = 'gitea'
|
||||||
@@ -70,23 +75,38 @@ function wireEvents(): void {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check the active feed; on failure switch Gitea → GitHub and retry once. */
|
/**
|
||||||
|
* Check updates: GitHub releases first (system proxy, gated by a
|
||||||
|
* connectivity probe), domestic Gitea generic package as fallback.
|
||||||
|
*
|
||||||
|
* The feed is decided BEFORE touching the updater: checkForUpdates has no
|
||||||
|
* timeout, and racing it while mid-flight risks two concurrent checks
|
||||||
|
* polluting state.
|
||||||
|
*/
|
||||||
async function checkWithFallback(): Promise<void> {
|
async function checkWithFallback(): Promise<void> {
|
||||||
// The feed choice is per attempt: a transient Gitea failure must not pin
|
// The feed choice is per attempt: a transient GitHub failure must not pin
|
||||||
// every later check to GitHub (and its system proxy) for the whole session.
|
// every later check to Gitea (and its forced direct connection) — or vice
|
||||||
|
// versa — for the whole session.
|
||||||
|
let githubErr: string | undefined
|
||||||
|
if (await probeGithub()) {
|
||||||
|
useFeed('github')
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates()
|
||||||
|
return
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[updater] github feed failed, falling back to gitea:', err)
|
||||||
|
githubErr = err instanceof Error ? err.message : String(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[updater] github probe failed or timed out, using gitea feed directly')
|
||||||
|
}
|
||||||
useFeed('gitea')
|
useFeed('gitea')
|
||||||
try {
|
try {
|
||||||
await autoUpdater.checkForUpdates()
|
await autoUpdater.checkForUpdates()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[updater] gitea feed failed, falling back to github:', err)
|
if (githubErr === undefined) throw err
|
||||||
const giteaErr = err instanceof Error ? err.message : String(err)
|
const giteaErr = err instanceof Error ? err.message : String(err)
|
||||||
useFeed('github')
|
throw new Error(t('main.updater.feedFailed', { gitea: giteaErr, github: githubErr }))
|
||||||
try {
|
|
||||||
await autoUpdater.checkForUpdates()
|
|
||||||
} catch (err2) {
|
|
||||||
const ghErr = err2 instanceof Error ? err2.message : String(err2)
|
|
||||||
throw new Error(t('main.updater.feedFailed', { gitea: giteaErr, github: ghErr }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +141,25 @@ async function directFetch(url: string): Promise<Response> {
|
|||||||
return s.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
|
return s.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connectivity probe for the GitHub feed: dedicated session in the same
|
||||||
|
* proxy mode as the feed (system), hard timeout so proxy-less users fall
|
||||||
|
* back to Gitea instead of hanging on a dead proxy/DNS.
|
||||||
|
*/
|
||||||
|
async function probeGithub(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const s = session.fromPartition('openterminal-github-probe', { cache: false })
|
||||||
|
await s.setProxy({ mode: 'system' })
|
||||||
|
const resp = await s.fetch(GITHUB_PROBE_URL, {
|
||||||
|
headers: { 'User-Agent': 'OpenTerminal' },
|
||||||
|
signal: AbortSignal.timeout(GITHUB_PROBE_TIMEOUT_MS)
|
||||||
|
})
|
||||||
|
return resp.ok
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changelog sources, in order:
|
* Changelog sources, in order:
|
||||||
* 1. update channel's release-notes.md — the Gitea repo itself is private
|
* 1. update channel's release-notes.md — the Gitea repo itself is private
|
||||||
|
|||||||
Reference in new issue
Block a user