GitHubReleaseUpdater 0.7.0
dotnet add package GitHubReleaseUpdater --version 0.7.0
NuGet\Install-Package GitHubReleaseUpdater -Version 0.7.0
<PackageReference Include="GitHubReleaseUpdater" Version="0.7.0" />
<PackageVersion Include="GitHubReleaseUpdater" Version="0.7.0" />
<PackageReference Include="GitHubReleaseUpdater" />
paket add GitHubReleaseUpdater --version 0.7.0
#r "nuget: GitHubReleaseUpdater, 0.7.0"
#:package GitHubReleaseUpdater@0.7.0
#addin nuget:?package=GitHubReleaseUpdater&version=0.7.0
#tool nuget:?package=GitHubReleaseUpdater&version=0.7.0
GitHubReleaseUpdater
English | 中文
A dependency-free .NET 10 library that turns GitHub Releases into an update source: check for a newer version → pick an asset → download with progress → verify SHA‑256 → (optionally) launch the installer.
Works with github.com and GitHub Enterprise Server, anonymously or with a token (private repositories / higher rate limits). It deliberately does not extract archives, replace files or restart the app — that is up to the caller (or the installer you launch).
See CHANGELOG.md for release notes and breaking changes.
Quick start
using GitHubReleaseUpdater;
using GitHubReleaseUpdater.Assets;
using var updater = new ReleaseUpdater(new UpdaterOptions
{
Owner = "cli",
Repo = "cli",
CurrentVersion = "2.90.0",
Token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), // optional
// BaseUrl = new Uri("https://ghe.example.com/api/v3/"), // GitHub Enterprise
AssetSelector = new PatternAssetSelector("gh_{version}_windows_amd64.zip"),
});
var check = await updater.CheckForUpdateAsync(); // never throws (except cancellation)
if (!check.Success)
{
Console.WriteLine($"Check failed: {check.Error}");
return;
}
if (check.IsUpdateAvailable)
{
Console.WriteLine($"New version {check.Update!.Version}: {check.ReleaseNotes}");
var progress = new Progress<DownloadProgress>(p => Console.Write($"\r{p.Percentage:0.0}%"));
var file = await updater.DownloadAsync(check, downloadDir, progress);
Console.WriteLine($"{file.FilePath} sha256={file.Sha256} verified={file.Verified}");
}
The current version usually comes from the running app: SemanticVersion.TryFromAssembly(typeof(App).Assembly, out var current) reads AssemblyInformationalVersion.
Core concepts
| Type | Purpose |
|---|---|
ReleaseUpdater |
Facade: CheckForUpdateAsync(), ListAssets(), DownloadAsync(), LaunchInstallerAsync(), DownloadAndLaunchInstallerAsync(). |
UpdaterOptions |
Owner/Repo/CurrentVersion are required. Optional: Token, BaseUrl, IncludePrerelease, TagPrefix, AssetSelector, ChecksumProvider, RequireChecksum, HttpClient, Timeout, LastCheckStore, MinimumCheckInterval, Download* retry/resume options, AssetUrlRewriter, InstallerLauncher, TimeProvider. |
UpdateCheckResult |
Outcome of a check: Success/Error, LatestVersion/Release, Throttled, and Update (non-null exactly when an update is available). |
SemanticVersion |
Minimal SemVer 2.0. Tolerates a v prefix, a missing patch (1.2) and a custom TagPrefix. |
IAssetSelector |
RuntimeAssetSelector (default, matches current OS/arch), PatternAssetSelector (wildcards plus {os} {arch} {rid} {version} {tagversion} {tag}), DelegateAssetSelector (custom predicate). |
IChecksumProvider |
ReleaseChecksumProvider (default: <asset>.sha256 → SHA256SUMS/*checksums.txt → GitHub digest), StaticChecksumProvider, NoChecksumProvider. |
IGitHubReleaseClient |
GitHub API abstraction; inject your own for tests or custom transports. |
ILastCheckStore |
Optional persistence for throttling and skipped versions: InMemoryLastCheckStore, JsonFileLastCheckStore(path), or your own. |
Version resolution
IncludePrerelease = false(default): uses/releases/latest(no pre-releases or drafts).IncludePrerelease = true: scans the latestReleaseScanCountreleases (default 30) and takes the highest SemVer; drafts and unparseable tags are skipped (the latter listed inSkippedTags).IsUpdateAvailableis true only whenlatest > currentand the version is not recorded as skipped.
Throttling and skipped versions
No throttling by default. Set LastCheckStore and MinimumCheckInterval to opt in:
var options = new UpdaterOptions
{
Owner = "cli", Repo = "cli", CurrentVersion = "2.90.0",
LastCheckStore = new JsonFileLastCheckStore(Path.Combine(dataDir, "update-state.json")),
MinimumCheckInterval = TimeSpan.FromHours(24),
};
- A check inside the window returns
Throttled == truewithout calling GitHub. - A failure to record the check time keeps the check result; the error is in
LastCheckStoreError. SetSkippedVersionAsync(version)/ClearSkippedVersionAsync()suppress or restore a version (it still appears inLatestVersion).CheckForUpdateAsync(bypassSkippedVersion: true, bypassThrottle: true)suits a user-initiated "check now".
Automatic update flow (GitHubReleaseUpdater.AutoUpdate)
UpdateController is a UI-agnostic state machine: check in the background (first after 30 s, then every 24 h), announce a version once, ask before installing, download, start the installer. Implement IUpdatePresenter (tray balloon, message box, closing the app, …); every member has a do-nothing default, so nothing is installed until you ask the user.
var controller = new UpdateController(updater, new MyPresenter(), new UpdateControllerOptions
{
IsAutoCheckEnabled = () => settings.CheckForUpdates,
InstallerLaunchOptions = InstallerLaunchOptions.InnoSetupSilent,
});
_ = controller.RunAsync(shutdownToken); // background loop
await controller.CheckAsync(manual: true); // "Check for updates" button
await controller.InstallAsync(); // "Update to x.y.z"
- States:
Idle,Checking,UpToDate,Available,Failed,Installing,Downloading,Downloaded;Changedfires on every change.LastFailure(Stage: check, download or install;Error;UserInitiated) andProgress(withProgressChanged) expose the latest failure and download progress;LastErroris its exception.UserInitiatedis false for a failed automatic check, which an application usually does not show. CanDownload,CanInstallandIsBusysay whatDownloadAsync()/InstallAsync()would do now, so a UI can bind to them instead of mappingStateitself.- Manual checks bypass throttling and the skipped version; one requested during an automatic check runs right after it.
SkipVersionAsync()records the found version as skipped. DownloadAsync()pre-downloads and verifies;InstallAsync()then starts the installer without downloading again.- A newer release without a matching package stays
AvailablewithHasInstaller == false— show the install entry only whenHasInstalleris true. LaunchInstaller = falseis for non-installer packages (e.g. an archive): the file is downloaded, verified and handed toIUpdatePresenter.PackageDownloaded.BeforeInstallAsyncruns just before the installer starts (e.g. to save state).WatchInstaller = truewaits for the installer process to end.CleanDownloadDirectory = truedeletes earlier downloads (use a dedicated directory only);DownloadDirectorydefaults to a folder under the system temp directory.- Verification is required by default (
RequireVerifiedDownload): an installer whose checksum cannot be verified is not started. AskToInstallAfterManualCheck = falsemakes a manual check only look;SkipChecksWhileUpdateKnown = truestops background rounds from asking again while an update is known.- Inno Setup silent update that restarts the app: use
InstallerLaunchOptions.InnoSetupSilent, close the app fromIUpdatePresenter.InstallerStarted, and leaveskipifsilentoff the[Run]entry that starts the app. Extra switches:InnoSetupSilent.WithArguments("/CLOSEAPPLICATIONS"). - Exceptions from your presenter,
Changedhandlers orIsAutoCheckEnabledgo toUpdateControllerOptions.OnError. Call the controller from one thread (normally the UI thread).
Downloading
- Streams to
<name>.partial, then renames atomically. ThrowsUpdaterExceptionwhenContent-Lengthdoes not match the received bytes. - With a known hash the file is verified before it is moved into place; a mismatch discards it and throws
ChecksumMismatchException. Without one,DownloadResult.Verifiedisfalse(RequireChecksum = truefails instead, before any transfer). - Private repository assets are downloaded through the API with a token.
- Retries: transient failures (I/O error, timeout, truncated or stalled body) are retried with exponential backoff —
DownloadMaxRetryAttempts(default 2),DownloadRetryDelay(default 1 s),DownloadIdleTimeout(default 30 s,nullto wait forever). Checksum mismatches, disk errors and cancellation are never retried. - Resume: an interrupted download resumes via HTTP Range, tracked by a
<name>.partial.metasidecar; a partial from a different asset is discarded.DownloadAllowResume = falsealways restarts and leaves no partial file. - Timeout:
UpdaterOptions.Timeout(default 10 s,nullto rely onHttpClient.Timeout) bounds each API call and the wait for asset response headers; expiry throwsTimeoutException. Valid range: 1 ms to ~49.7 days. - HttpClient: when
HttpClientis null, a process-wide shared one is used, so creating aReleaseUpdaterper check is fine.
Mirrors
Where GitHub's download servers are slow or blocked, AssetUrlRewriter maps a package's download address to another one:
AssetUrlRewriter = url => new Uri("https://mirror.example.com/" + url), // return the same address to download as usual
Only the package is fetched from the mirror, and only when its checksum is known. The token is never sent there; checksums and release data still come from GitHub. If the mirror fails or delivers something unusable, the package is downloaded from GitHub once. The option is ignored when you pass your own IGitHubReleaseClient (set GitHubReleaseClient.AssetUrlRewriter instead).
Choosing assets
RuntimeAssetSelector never guesses: a name like MyApp-Setup-1.2.3.exe with no OS or architecture is not selected. For a single platform-less installer use new RuntimeAssetSelector { AllowUnknownPlatform = true } or new PatternAssetSelector("*Setup*.exe").
To let the user pick a platform, list the assets:
foreach (var a in updater.ListAssets(check)) // checksum/signature files left out by default
Console.WriteLine($"{a.Asset.Name,-40} os={a.Platform.Os ?? "?"} arch={a.Platform.Arch ?? "?"} {(a.MatchesCurrent ? "<- this machine" : "")}");
var chosen = updater.ListAssets(check).First(a => a.Platform is { Os: "linux", Arch: "arm64" });
await updater.DownloadAsync(check.Release!, chosen.Asset, downloadDir);
AssetPlatformDetector.Detect(name) returns an AssetPlatform (Os, Arch, Rid); unknown parts are null. To auto-pick for another platform use new RuntimeAssetSelector(new RuntimeInfo("linux", "arm64")).
Launching the installer
After the download is verified, the library can start an .exe/.msi through the OS shell (UAC prompts as usual) and return immediately; the installer is expected to close and restart your app.
using GitHubReleaseUpdater.Installation;
// One step: download + verify + start.
await updater.DownloadAndLaunchInstallerAsync(check, downloadDir, InstallerLaunchOptions.InnoSetupSilent, progress);
// Or two steps:
var file = await updater.DownloadAsync(check, downloadDir, progress);
await updater.LaunchInstallerAsync(file, new InstallerLaunchOptions { Arguments = "/S", RequireVerified = true });
InstallerLaunchOptions:Arguments,WorkingDirectory,RequireVerified; presetsInnoSetupSilentandInnoSetupVerySilent.- The installer never starts after a failed, cancelled or unverified download. The file is re-hashed right before launch, so a file modified after download is not run (
ChecksumMismatchException). - Provide
UpdaterOptions.InstallerLauncherto customize or stub out the launch. - Signature check: the checksum comes from the same release, so whoever can change the release can change both. Set
UpdaterOptions.InstallerVerifierto check the installer just before it starts; one that is refused is not started (InstallerVerificationExceptionwithReason).UpdateControllerasks right after the download (also when it only downloads, withLaunchInstalleroff), so a refused package is never offered, and asks again just before the start.AuthenticodeInstallerVerifierchecks the Authenticode signature of a Windows installer with the operating system and, withExpectedSubjects(the full subject name of the signing certificate, such asCN=Contoso Ltd, O=Contoso Ltd, L=Redmond, C=US— the usual way to pin a publisher, since a common name alone is not unique and a thumbprint changes at every renewal),ExpectedPublishers(the common name only) orExpectedThumbprints(SHA-1 or SHA-256), who signed it; each kind that is given must match, and without any a valid signature of anyone passes. On Windows the file is held open for reading from its hash check until the installer has started, so it cannot be replaced in between (elsewhere .NET file sharing is only advisory, so keep the download directory private). An installer the verifier refused is deleted and not downloaded again for the same asset (IsInstallerRefused); a manual check allows another try. Revocation is not checked online. On other systems it refuses everything (AuthenticodeInstallerVerifier.IsSupported), so set it only on Windows. ImplementIInstallerVerifierfor other schemes.
Exceptions
All derive from UpdaterException: GitHubApiException (StatusCode, IsRateLimited, RateLimitResetAt), AssetNotFoundException (AvailableAssets), ChecksumMismatchException (Expected/Actual), InstallerLaunchException (FilePath, IsUserCancelled), InstallerVerificationException (FilePath, Reason).
CheckForUpdateAsync() catches everything and reports it via Success/Error; only caller cancellation throws OperationCanceledException. DownloadAsync() throws.
Sample CLI
dotnet run --project samples/GitHubReleaseUpdater.Cli -- check --owner cli --repo cli --current 1.0.0
dotnet run --project samples/GitHubReleaseUpdater.Cli -- download --owner cli --repo cli --current 1.0.0 --asset "gh_{version}_windows_amd64.zip" --require-checksum --out ./downloads
Other options: --token, --base-url, --prerelease, --tag-prefix, --sha256, --no-verify.
Build and test
dotnet build
dotnet test # HTTP stubs only, no network
dotnet pack src/GitHubReleaseUpdater -c Release
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.