From d0b260b67d31721edbc059748bd082e470370e3d Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 7 Oct 2026 00:00:31 +0800 Subject: [PATCH] Default update probe: GitHub plus Forgejo mirror, pick newest reachable. Co-authored-by: Cursor --- .env.example | 8 +- packaging/README.md | 30 ++--- packaging/check_update.ps1 | 158 +++++++++++++++----------- packaging/config/database.example.env | 7 +- 4 files changed, 110 insertions(+), 93 deletions(-) diff --git a/.env.example b/.env.example index c307e8a..23d7cfd 100644 --- a/.env.example +++ b/.env.example @@ -120,11 +120,11 @@ NETX_UME_NOTIFICATION_TOPIC=ALARM # bundled | external; unset = external (compatible with existing deploys) # NETX_BUNDLED_PG_PORT=15432 # NETX_BUNDLED_PG_DATA_DIR= -# Update sources: forgejo primary + github fallback (recommended for internal Forgejo) -# NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest -# NETX_UPDATE_SOURCES=forgejo,github +# Update: GitHub primary + Forgejo mirror (git.avelo.top); pick newest reachable +# NETX_UPDATE_SOURCES=github,forgejo +# NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest # 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_TOKEN= # NETX_UPDATE_CHANNEL=stable diff --git a/packaging/README.md b/packaging/README.md index 3abd44b..26c2b98 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -87,38 +87,30 @@ Or reinstall a newer `NetX-Setup-*.exe` over the same program directory. ## Check / apply updates -Default when nothing is configured: **GitHub Releases** (`hansjone/netx`). - -You can put **Forgejo/Gitea first** and keep GitHub as fallback: +**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. ```env -# Data-root .env (ProgramData\NetX\.env) -NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest -NETX_UPDATE_SOURCES=forgejo,github -# Optional private repo token (Forgejo "token …" / GitHub Bearer) +# Optional overrides in ProgramData\NetX\.env +# NETX_UPDATE_SOURCES=github,forgejo +# NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest +# NETX_UPDATE_GITHUB_REPO=hansjone/netx +# NETX_UPDATE_URL=https://cdn.example.com/netx/manifest.json # NETX_UPDATE_TOKEN=****** ``` -Other options: - | Variable | Role | |----------|------| -| `NETX_UPDATE_URL` | Custom JSON manifest (see `manifest.example.json`) | -| `NETX_UPDATE_FALLBACK_URL` | Second manifest URL | -| `NETX_UPDATE_GITHUB_REPO` | Override GitHub `owner/repo` (default `hansjone/netx`) | -| `NETX_UPDATE_SOURCES` | Explicit order, e.g. `forgejo,github` or `manifest,forgejo,github` | - -If `NETX_UPDATE_FORGEJO_URL` is set and `SOURCES` is empty, order is: manifest (if URL set) → **forgejo** → fallback manifest → **github**. +| `NETX_UPDATE_FORGEJO_URL` | Forgejo/Gitea `releases/latest` API (default: git.avelo.top mirror) | +| `NETX_UPDATE_GITHUB_REPO` | GitHub `owner/repo` (default `hansjone/netx`) | +| `NETX_UPDATE_SOURCES` | Probe order / tie-break order, e.g. `github,forgejo` | +| `NETX_UPDATE_URL` | Optional custom JSON manifest | ```powershell -# Report only (exit 0 = up to date, 10 = update available) .\packaging\check_update.ps1 - -# Download zip + run update_netx.ps1 .\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 ```powershell diff --git a/packaging/check_update.ps1 b/packaging/check_update.ps1 index a7d7215..6563203 100644 --- a/packaging/check_update.ps1 +++ b/packaging/check_update.ps1 @@ -15,17 +15,17 @@ param( # Check for a newer NetX Windows package. # -# Source order (first success wins), controlled by NETX_UPDATE_SOURCES or defaults: -# manifest → NETX_UPDATE_URL JSON (manifest.example.json) -# forgejo → NETX_UPDATE_FORGEJO_URL (Forgejo/Gitea releases/latest API) -# github → GitHub releases/latest (NETX_UPDATE_GITHUB_REPO, default hansjone/netx) +# Default (no config needed): +# GitHub primary + Forgejo mirror fallback (git.avelo.top). +# Probe every reachable source and pick the highest version; +# on equal versions prefer GitHub (listed first). # -# Examples: -# NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest -# NETX_UPDATE_SOURCES=forgejo,github # Forgejo primary, GitHub fallback -# NETX_UPDATE_URL=https://cdn/.../manifest.json -# NETX_UPDATE_FALLBACK_URL=https://cdn/.../manifest-backup.json -# NETX_UPDATE_TOKEN=... # optional Bearer/token for private APIs +# Optional overrides: +# NETX_UPDATE_SOURCES=github,forgejo +# NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest +# NETX_UPDATE_GITHUB_REPO=hansjone/netx +# NETX_UPDATE_URL=... manifest JSON +# NETX_UPDATE_TOKEN=... private API token $ErrorActionPreference = "Stop" . "$PSScriptRoot\_common.ps1" @@ -47,6 +47,9 @@ function Get-Cfg([string]$Key, [string]$ParamVal = "") { $UpdateUrl = Get-Cfg "NETX_UPDATE_URL" $UpdateUrl $FallbackUrl = Get-Cfg "NETX_UPDATE_FALLBACK_URL" $FallbackUrl $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 if (-not $GithubRepo) { $GithubRepo = "hansjone/netx" } $Channel = Get-Cfg "NETX_UPDATE_CHANNEL" $Channel @@ -81,8 +84,28 @@ function Get-AuthHeaders { 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 { - param($Rel, [string]$SourceName) + param($Rel, [string]$SourceName, [string]$ApiLatestUrl = "") $tag = [string]$Rel.tag_name $ver = $tag.TrimStart('v', 'V') $assets = @($Rel.assets) @@ -90,16 +113,12 @@ function Convert-ReleaseToManifest { $setupAsset = $assets | Where-Object { $_.name -match 'Setup-.*\.exe$' } | Select-Object -First 1 if (-not $zipAsset) { throw "${SourceName}_release_missing_zip" } - $zipUrl = [string]$zipAsset.browser_download_url - 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 - } + $zipUrl = Resolve-AssetUrl -Asset $zipAsset -Rel $Rel -SourceName $SourceName -ApiLatestUrl $ApiLatestUrl if (-not $zipUrl) { throw "${SourceName}_zip_url_missing" } $setupUrl = "" - if ($setupAsset -and $setupAsset.browser_download_url) { - $setupUrl = [string]$setupAsset.browser_download_url + if ($setupAsset) { + $setupUrl = Resolve-AssetUrl -Asset $setupAsset -Rel $Rel -SourceName $SourceName -ApiLatestUrl $ApiLatestUrl } return [pscustomobject]@{ @@ -139,88 +158,94 @@ function Get-FromGitHubReleases { function Get-FromForgejoReleases { param([string]$ApiUrl) if (-not $ApiUrl) { throw "forgejo_url_empty" } - # Forgejo/Gitea: /api/v1/repos/{owner}/{repo}/releases/latest $headers = Get-AuthHeaders -Style "token" $headers["Accept"] = "application/json" $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 { if ($Sources) { 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() 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") } - # 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) } +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" $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 = @() foreach ($src in $sourceList) { try { - switch ($src) { - "manifest" { - if (-not $UpdateUrl) { throw "NETX_UPDATE_URL empty" } - 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" } + $m = Get-ManifestFromSource -src $src + if ($m.channel -and $Channel -and ($m.channel -ne $Channel)) { + Write-Host "[WARN] $src channel=$($m.channel) requested=$Channel" } - if ($manifest.channel -and $Channel -and ($manifest.channel -ne $Channel)) { - Write-Host "[WARN] manifest channel=$($manifest.channel) requested=$Channel" + if (-not $m.windows -or -not $m.windows.url) { + throw "missing_windows_download_url" } - Write-Host "==> Using source: $(if ($manifest.source) { $manifest.source } else { $src })" -ForegroundColor Green - break + Write-Host " OK $($m.source) latest=$($m.latest)" -ForegroundColor DarkGreen + $candidates += $m } catch { $msg = $_.Exception.Message $errors += "${src}: $msg" - Write-Host "[WARN] source '$src' failed: $msg" -ForegroundColor Yellow - $manifest = $null + Write-Host "[WARN] source '$src' unreachable/failed: $msg" -ForegroundColor Yellow } } -if (-not $manifest) { +if ($candidates.Count -eq 0) { $joined = ($errors -join "; ") if ($Quiet) { exit 2 } 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 $cmp = Compare-SemVer -A $current -B $latest $result = [pscustomobject]@{ @@ -232,6 +257,7 @@ $result = [pscustomobject]@{ setup_url = $(if ($manifest.windows.setup_url) { [string]$manifest.windows.setup_url } else { "" }) notes_url = $(if ($manifest.notes_url) { [string]$manifest.notes_url } else { "" }) sha256 = $(if ($manifest.windows.sha256) { [string]$manifest.windows.sha256 } else { "" }) + reachable = @($candidates | ForEach-Object { "$($_.source)=$($_.latest)" }) } if (-not $result.update_available) { diff --git a/packaging/config/database.example.env b/packaging/config/database.example.env index dc78103..f5e5555 100644 --- a/packaging/config/database.example.env +++ b/packaging/config/database.example.env @@ -5,12 +5,11 @@ # NETX_HOST=127.0.0.1 # NETX_PORT=8890 # NETX_UI_DIST_DIR=web/dist -# Prefer internal Forgejo, fall back to GitHub -# NETX_UPDATE_FORGEJO_URL=https://git.example.com/api/v1/repos/ops/netx/releases/latest -# NETX_UPDATE_SOURCES=forgejo,github +# Defaults already: github + forgejo mirror git.avelo.top (newest reachable wins) +# NETX_UPDATE_SOURCES=github,forgejo +# NETX_UPDATE_FORGEJO_URL=https://git.avelo.top/api/v1/repos/hansjone/netx/releases/latest # NETX_UPDATE_GITHUB_REPO=hansjone/netx # NETX_UPDATE_URL= -# NETX_UPDATE_FALLBACK_URL= # NETX_UPDATE_TOKEN= # NETX_UPDATE_CHANNEL=stable # NETX_UPDATE_AUTO=false