Default update probe: GitHub plus Forgejo mirror, pick newest reachable.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-07 00:00:31 +08:00
parent 07a7e17e22
commit d0b260b67d
4 changed files with 110 additions and 93 deletions

View file

@ -120,11 +120,11 @@ NETX_UME_NOTIFICATION_TOPIC=ALARM
# bundled | external; unset = external (compatible with existing deploys) # bundled | external; unset = external (compatible with existing deploys)
# NETX_BUNDLED_PG_PORT=15432 # NETX_BUNDLED_PG_PORT=15432
# NETX_BUNDLED_PG_DATA_DIR= # NETX_BUNDLED_PG_DATA_DIR=
# Update sources: forgejo primary + github fallback (recommended for internal Forgejo) # Update: GitHub primary + Forgejo mirror (git.avelo.top); pick newest reachable
# NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest # NETX_UPDATE_SOURCES=github,forgejo
# NETX_UPDATE_SOURCES=forgejo,github # NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest
# NETX_UPDATE_GITHUB_REPO=hansjone/netx # NETX_UPDATE_GITHUB_REPO=hansjone/netx
# NETX_UPDATE_URL=https://cdn.example.com/netx/manifest.json # NETX_UPDATE_URL=
# NETX_UPDATE_FALLBACK_URL= # NETX_UPDATE_FALLBACK_URL=
# NETX_UPDATE_TOKEN= # NETX_UPDATE_TOKEN=
# NETX_UPDATE_CHANNEL=stable # NETX_UPDATE_CHANNEL=stable

View file

@ -87,38 +87,30 @@ Or reinstall a newer `NetX-Setup-*.exe` over the same program directory.
## Check / apply updates ## Check / apply updates
Default when nothing is configured: **GitHub Releases** (`hansjone/netx`). **Defaults (no `.env` needed):** probe **GitHub** (`hansjone/netx`) and Forgejo mirror (`https://git.avelo.top/hansjone/netx`), then use the **newest reachable** version. Same version → prefer GitHub.
You can put **Forgejo/Gitea first** and keep GitHub as fallback:
```env ```env
# Data-root .env (ProgramData\NetX\.env) # Optional overrides in ProgramData\NetX\.env
NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest # NETX_UPDATE_SOURCES=github,forgejo
NETX_UPDATE_SOURCES=forgejo,github # NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest
# Optional private repo token (Forgejo "token …" / GitHub Bearer) # NETX_UPDATE_GITHUB_REPO=hansjone/netx
# NETX_UPDATE_URL=https://cdn.example.com/netx/manifest.json
# NETX_UPDATE_TOKEN=****** # NETX_UPDATE_TOKEN=******
``` ```
Other options:
| Variable | Role | | Variable | Role |
|----------|------| |----------|------|
| `NETX_UPDATE_URL` | Custom JSON manifest (see `manifest.example.json`) | | `NETX_UPDATE_FORGEJO_URL` | Forgejo/Gitea `releases/latest` API (default: git.avelo.top mirror) |
| `NETX_UPDATE_FALLBACK_URL` | Second manifest URL | | `NETX_UPDATE_GITHUB_REPO` | GitHub `owner/repo` (default `hansjone/netx`) |
| `NETX_UPDATE_GITHUB_REPO` | Override GitHub `owner/repo` (default `hansjone/netx`) | | `NETX_UPDATE_SOURCES` | Probe order / tie-break order, e.g. `github,forgejo` |
| `NETX_UPDATE_SOURCES` | Explicit order, e.g. `forgejo,github` or `manifest,forgejo,github` | | `NETX_UPDATE_URL` | Optional custom JSON manifest |
If `NETX_UPDATE_FORGEJO_URL` is set and `SOURCES` is empty, order is: manifest (if URL set) → **forgejo** → fallback manifest → **github**.
```powershell ```powershell
# Report only (exit 0 = up to date, 10 = update available)
.\packaging\check_update.ps1 .\packaging\check_update.ps1
# Download zip + run update_netx.ps1
.\packaging\check_update.ps1 -Apply .\packaging\check_update.ps1 -Apply
``` ```
Forgejo must publish the same assets as GitHub (`NetX-*-win64.zip`, optional `NetX-Setup-*.exe`) on a release whose tag is like `v0.4.0`. Both sides should publish the same release assets (`NetX-*-win64.zip`). Code mirror alone is not enough—Forgejo Release 也需要挂上安装包(或仅用 GitHub 下包)。
## Tray & autostart ## Tray & autostart
```powershell ```powershell

View file

@ -15,17 +15,17 @@ param(
# Check for a newer NetX Windows package. # Check for a newer NetX Windows package.
# #
# Source order (first success wins), controlled by NETX_UPDATE_SOURCES or defaults: # Default (no config needed):
# manifest → NETX_UPDATE_URL JSON (manifest.example.json) # GitHub primary + Forgejo mirror fallback (git.avelo.top).
# forgejo → NETX_UPDATE_FORGEJO_URL (Forgejo/Gitea releases/latest API) # Probe every reachable source and pick the highest version;
# github → GitHub releases/latest (NETX_UPDATE_GITHUB_REPO, default hansjone/netx) # on equal versions prefer GitHub (listed first).
# #
# Examples: # Optional overrides:
# NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest # NETX_UPDATE_SOURCES=github,forgejo
# NETX_UPDATE_SOURCES=forgejo,github # Forgejo primary, GitHub fallback # NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest
# NETX_UPDATE_URL=https://cdn/.../manifest.json # NETX_UPDATE_GITHUB_REPO=hansjone/netx
# NETX_UPDATE_FALLBACK_URL=https://cdn/.../manifest-backup.json # NETX_UPDATE_URL=... manifest JSON
# NETX_UPDATE_TOKEN=... # optional Bearer/token for private APIs # NETX_UPDATE_TOKEN=... private API token
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
. "$PSScriptRoot\_common.ps1" . "$PSScriptRoot\_common.ps1"
@ -47,6 +47,9 @@ function Get-Cfg([string]$Key, [string]$ParamVal = "") {
$UpdateUrl = Get-Cfg "NETX_UPDATE_URL" $UpdateUrl $UpdateUrl = Get-Cfg "NETX_UPDATE_URL" $UpdateUrl
$FallbackUrl = Get-Cfg "NETX_UPDATE_FALLBACK_URL" $FallbackUrl $FallbackUrl = Get-Cfg "NETX_UPDATE_FALLBACK_URL" $FallbackUrl
$ForgejoUrl = Get-Cfg "NETX_UPDATE_FORGEJO_URL" $ForgejoUrl $ForgejoUrl = Get-Cfg "NETX_UPDATE_FORGEJO_URL" $ForgejoUrl
if (-not $ForgejoUrl) {
$ForgejoUrl = "https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest"
}
$GithubRepo = Get-Cfg "NETX_UPDATE_GITHUB_REPO" $GithubRepo $GithubRepo = Get-Cfg "NETX_UPDATE_GITHUB_REPO" $GithubRepo
if (-not $GithubRepo) { $GithubRepo = "hansjone/netx" } if (-not $GithubRepo) { $GithubRepo = "hansjone/netx" }
$Channel = Get-Cfg "NETX_UPDATE_CHANNEL" $Channel $Channel = Get-Cfg "NETX_UPDATE_CHANNEL" $Channel
@ -81,8 +84,28 @@ function Get-AuthHeaders {
return $h return $h
} }
function Resolve-AssetUrl {
param($Asset, $Rel, [string]$SourceName, [string]$ApiLatestUrl = "")
$url = [string]$Asset.browser_download_url
if ($url) { return $url }
# Forgejo/Gitea sometimes omit browser_download_url; synthesize release download URL.
if ($SourceName -eq "forgejo" -and $ApiLatestUrl -and $Rel.tag_name -and $Asset.name) {
if ($ApiLatestUrl -match '^(https?://[^/]+)/api/v1/repos/([^/]+)/([^/]+)/releases/latest') {
$base = $Matches[1]
$owner = $Matches[2]
$repo = $Matches[3]
$tag = [string]$Rel.tag_name
$name = [uri]::EscapeDataString([string]$Asset.name).Replace('%2F', '/')
# EscapeDataString encodes too much; use raw name (assets shouldn't need query encoding).
$name = [string]$Asset.name
return "$base/$owner/$repo/releases/download/$tag/$name"
}
}
return ""
}
function Convert-ReleaseToManifest { function Convert-ReleaseToManifest {
param($Rel, [string]$SourceName) param($Rel, [string]$SourceName, [string]$ApiLatestUrl = "")
$tag = [string]$Rel.tag_name $tag = [string]$Rel.tag_name
$ver = $tag.TrimStart('v', 'V') $ver = $tag.TrimStart('v', 'V')
$assets = @($Rel.assets) $assets = @($Rel.assets)
@ -90,16 +113,12 @@ function Convert-ReleaseToManifest {
$setupAsset = $assets | Where-Object { $_.name -match 'Setup-.*\.exe$' } | Select-Object -First 1 $setupAsset = $assets | Where-Object { $_.name -match 'Setup-.*\.exe$' } | Select-Object -First 1
if (-not $zipAsset) { throw "${SourceName}_release_missing_zip" } if (-not $zipAsset) { throw "${SourceName}_release_missing_zip" }
$zipUrl = [string]$zipAsset.browser_download_url $zipUrl = Resolve-AssetUrl -Asset $zipAsset -Rel $Rel -SourceName $SourceName -ApiLatestUrl $ApiLatestUrl
if (-not $zipUrl -and $zipAsset.id -and $Rel.html_url) {
# Some Gitea/Forgejo builds omit browser_download_url; leave empty to fail clearly.
$zipUrl = [string]$zipAsset.browser_download_url
}
if (-not $zipUrl) { throw "${SourceName}_zip_url_missing" } if (-not $zipUrl) { throw "${SourceName}_zip_url_missing" }
$setupUrl = "" $setupUrl = ""
if ($setupAsset -and $setupAsset.browser_download_url) { if ($setupAsset) {
$setupUrl = [string]$setupAsset.browser_download_url $setupUrl = Resolve-AssetUrl -Asset $setupAsset -Rel $Rel -SourceName $SourceName -ApiLatestUrl $ApiLatestUrl
} }
return [pscustomobject]@{ return [pscustomobject]@{
@ -139,88 +158,94 @@ function Get-FromGitHubReleases {
function Get-FromForgejoReleases { function Get-FromForgejoReleases {
param([string]$ApiUrl) param([string]$ApiUrl)
if (-not $ApiUrl) { throw "forgejo_url_empty" } if (-not $ApiUrl) { throw "forgejo_url_empty" }
# Forgejo/Gitea: /api/v1/repos/{owner}/{repo}/releases/latest
$headers = Get-AuthHeaders -Style "token" $headers = Get-AuthHeaders -Style "token"
$headers["Accept"] = "application/json" $headers["Accept"] = "application/json"
$rel = Invoke-RestMethod -Uri $ApiUrl -Headers $headers -TimeoutSec 30 $rel = Invoke-RestMethod -Uri $ApiUrl -Headers $headers -TimeoutSec 30
return Convert-ReleaseToManifest -Rel $rel -SourceName "forgejo" return Convert-ReleaseToManifest -Rel $rel -SourceName "forgejo" -ApiLatestUrl $ApiUrl
} }
function Get-UpdateSourceList { function Get-UpdateSourceList {
if ($Sources) { if ($Sources) {
return @($Sources.Split(',') | ForEach-Object { $_.Trim().ToLowerInvariant() } | Where-Object { $_ }) return @($Sources.Split(',') | ForEach-Object { $_.Trim().ToLowerInvariant() } | Where-Object { $_ })
} }
# Default: GitHub primary, Forgejo mirror backup. Optional manifests prepended if configured.
$list = [System.Collections.Generic.List[string]]::new() $list = [System.Collections.Generic.List[string]]::new()
if ($UpdateUrl) { [void]$list.Add("manifest") } if ($UpdateUrl) { [void]$list.Add("manifest") }
if ($ForgejoUrl) { [void]$list.Add("forgejo") } [void]$list.Add("github")
[void]$list.Add("forgejo")
if ($FallbackUrl) { [void]$list.Add("manifest_fallback") } if ($FallbackUrl) { [void]$list.Add("manifest_fallback") }
# Always keep GitHub as last resort unless explicitly disabled via SOURCES.
if (-not $list.Contains("github")) { [void]$list.Add("github") }
# Prefer Forgejo before GitHub when both configured and no explicit SOURCES.
if ($ForgejoUrl -and $list.Contains("forgejo") -and $list.Contains("github")) {
$ordered = [System.Collections.Generic.List[string]]::new()
foreach ($s in @("manifest", "forgejo", "manifest_fallback", "github")) {
if ($list.Contains($s) -and -not $ordered.Contains($s)) { [void]$ordered.Add($s) }
}
foreach ($s in $list) {
if (-not $ordered.Contains($s)) { [void]$ordered.Add($s) }
}
return @($ordered)
}
return @($list) return @($list)
} }
function Get-ManifestFromSource([string]$src) {
switch ($src) {
"manifest" {
if (-not $UpdateUrl) { throw "NETX_UPDATE_URL empty" }
Write-Host "==> Probing manifest: $UpdateUrl"
return Get-ManifestFromUrl -Url $UpdateUrl
}
"manifest_fallback" {
if (-not $FallbackUrl) { throw "NETX_UPDATE_FALLBACK_URL empty" }
Write-Host "==> Probing fallback manifest: $FallbackUrl"
$m = Get-ManifestFromUrl -Url $FallbackUrl
if ($m -and -not $m.source) {
$m | Add-Member -NotePropertyName source -NotePropertyValue "manifest_fallback" -Force
}
return $m
}
"forgejo" {
Write-Host "==> Probing Forgejo: $ForgejoUrl"
return Get-FromForgejoReleases -ApiUrl $ForgejoUrl
}
"github" {
Write-Host "==> Probing GitHub: $GithubRepo"
return Get-FromGitHubReleases
}
default { throw "unknown_source: $src" }
}
}
Write-Host "==> Current version: $current" Write-Host "==> Current version: $current"
$sourceList = Get-UpdateSourceList $sourceList = Get-UpdateSourceList
Write-Host "==> Update sources: $($sourceList -join ' -> ')" Write-Host "==> Probe sources (pick newest reachable; tie → earlier in list): $($sourceList -join ', ')"
$manifest = $null $candidates = @()
$errors = @() $errors = @()
foreach ($src in $sourceList) { foreach ($src in $sourceList) {
try { try {
switch ($src) { $m = Get-ManifestFromSource -src $src
"manifest" { if ($m.channel -and $Channel -and ($m.channel -ne $Channel)) {
if (-not $UpdateUrl) { throw "NETX_UPDATE_URL empty" } Write-Host "[WARN] $src channel=$($m.channel) requested=$Channel"
Write-Host "==> Trying manifest: $UpdateUrl"
$manifest = Get-ManifestFromUrl -Url $UpdateUrl
}
"manifest_fallback" {
if (-not $FallbackUrl) { throw "NETX_UPDATE_FALLBACK_URL empty" }
Write-Host "==> Trying fallback manifest: $FallbackUrl"
$manifest = Get-ManifestFromUrl -Url $FallbackUrl
if ($manifest -and -not $manifest.source) {
$manifest | Add-Member -NotePropertyName source -NotePropertyValue "manifest_fallback" -Force
}
}
"forgejo" {
Write-Host "==> Trying Forgejo/Gitea: $ForgejoUrl"
$manifest = Get-FromForgejoReleases -ApiUrl $ForgejoUrl
}
"github" {
Write-Host "==> Trying GitHub Releases: $GithubRepo"
$manifest = Get-FromGitHubReleases
}
default { throw "unknown_source: $src" }
} }
if ($manifest.channel -and $Channel -and ($manifest.channel -ne $Channel)) { if (-not $m.windows -or -not $m.windows.url) {
Write-Host "[WARN] manifest channel=$($manifest.channel) requested=$Channel" throw "missing_windows_download_url"
} }
Write-Host "==> Using source: $(if ($manifest.source) { $manifest.source } else { $src })" -ForegroundColor Green Write-Host " OK $($m.source) latest=$($m.latest)" -ForegroundColor DarkGreen
break $candidates += $m
} catch { } catch {
$msg = $_.Exception.Message $msg = $_.Exception.Message
$errors += "${src}: $msg" $errors += "${src}: $msg"
Write-Host "[WARN] source '$src' failed: $msg" -ForegroundColor Yellow Write-Host "[WARN] source '$src' unreachable/failed: $msg" -ForegroundColor Yellow
$manifest = $null
} }
} }
if (-not $manifest) { if ($candidates.Count -eq 0) {
$joined = ($errors -join "; ") $joined = ($errors -join "; ")
if ($Quiet) { exit 2 } if ($Quiet) { exit 2 }
throw "update_check_failed: all sources failed ($joined)" throw "update_check_failed: all sources failed ($joined)"
} }
# Prefer highest semver; on tie keep earlier source in $sourceList (GitHub before Forgejo by default).
$manifest = $candidates[0]
foreach ($c in $candidates) {
$cmpCand = Compare-SemVer -A ([string]$manifest.latest) -B ([string]$c.latest)
if ($cmpCand -lt 0) {
$manifest = $c
}
}
Write-Host "==> Selected source=$($manifest.source) latest=$($manifest.latest) (from $($candidates.Count) reachable)" -ForegroundColor Green
$latest = [string]$manifest.latest $latest = [string]$manifest.latest
$cmp = Compare-SemVer -A $current -B $latest $cmp = Compare-SemVer -A $current -B $latest
$result = [pscustomobject]@{ $result = [pscustomobject]@{
@ -232,6 +257,7 @@ $result = [pscustomobject]@{
setup_url = $(if ($manifest.windows.setup_url) { [string]$manifest.windows.setup_url } else { "" }) setup_url = $(if ($manifest.windows.setup_url) { [string]$manifest.windows.setup_url } else { "" })
notes_url = $(if ($manifest.notes_url) { [string]$manifest.notes_url } else { "" }) notes_url = $(if ($manifest.notes_url) { [string]$manifest.notes_url } else { "" })
sha256 = $(if ($manifest.windows.sha256) { [string]$manifest.windows.sha256 } else { "" }) sha256 = $(if ($manifest.windows.sha256) { [string]$manifest.windows.sha256 } else { "" })
reachable = @($candidates | ForEach-Object { "$($_.source)=$($_.latest)" })
} }
if (-not $result.update_available) { if (-not $result.update_available) {

View file

@ -5,12 +5,11 @@
# NETX_HOST=127.0.0.1 # NETX_HOST=127.0.0.1
# NETX_PORT=8890 # NETX_PORT=8890
# NETX_UI_DIST_DIR=web/dist # NETX_UI_DIST_DIR=web/dist
# Prefer internal Forgejo, fall back to GitHub # Defaults already: github + forgejo mirror git.avelo.top (newest reachable wins)
# NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest # NETX_UPDATE_SOURCES=github,forgejo
# NETX_UPDATE_SOURCES=forgejo,github # NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest
# NETX_UPDATE_GITHUB_REPO=hansjone/netx # NETX_UPDATE_GITHUB_REPO=hansjone/netx
# NETX_UPDATE_URL= # NETX_UPDATE_URL=
# NETX_UPDATE_FALLBACK_URL=
# NETX_UPDATE_TOKEN= # NETX_UPDATE_TOKEN=
# NETX_UPDATE_CHANNEL=stable # NETX_UPDATE_CHANNEL=stable
# NETX_UPDATE_AUTO=false # NETX_UPDATE_AUTO=false