diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 42e53d081..a0cebcf5b 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -27,6 +27,27 @@ git push origin vX.Y.Z-alpha.1 The stable MSIX package identity is `OpenClaw.Companion`. Alpha-tagged MSIX packages are patched during CI to use `OpenClaw.Companion.Alpha`, which lets testers install the signed alpha package without upgrading a stable MSIX install. +## Alpha update testing + +Normal update checks stay on stable releases. To test alpha release updates from a local Release build, isolate the run and opt into the alpha update channel before launching: + +```powershell +$env:OPENCLAW_TRAY_DATA_DIR = "$env:TEMP\OpenClawTray-alpha-test" +$env:OPENCLAW_UPDATE_CHANNEL = "alpha" +.\run-app-local.ps1 -Configuration Release -AllowNonMaster +``` + +`OPENCLAW_UPDATE_CHANNEL=alpha` enables pre-release checks and release-list fetching. The in-app updater downloads ZIP assets only; MSIX alpha packages are installed or sideloaded separately under the `OpenClaw.Companion.Alpha` identity. + +To force a local lower-version baseline against an already-published alpha, build the Release output with an explicit older version, then launch that output: + +```powershell +dotnet build .\src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj -c Release -r win-x64 -p:Version=0.5.0 +$env:OPENCLAW_TRAY_DATA_DIR = "$env:TEMP\OpenClawTray-alpha-test" +$env:OPENCLAW_UPDATE_CHANNEL = "alpha" +.\run-app-local.ps1 -Configuration Release -NoBuild -AllowNonMaster +``` + ## Why this is the correct flow - `GitVersion.yml` is configured for `ContinuousDelivery` with `tag-prefix: 'v'`. diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index de1ad8f03..ade6032a9 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -6,10 +6,10 @@ This project uses GitVersion for automatic semantic versioning based on git tags ### Version Properties in .csproj -The project file (`OpenClaw.Tray.WinUI.csproj`) defines only the `` property: +The project file (`OpenClaw.Tray.WinUI.csproj`) defines only the `` property. CI release builds override this value from the pushed tag; the checked-in value is the local-development fallback: ```xml -0.3.0 +0.6.0-alpha.1 ``` Other version-related properties (`FileVersion` and `AssemblyVersion`) are **not** explicitly set in the csproj files. This is intentional. @@ -17,9 +17,9 @@ Other version-related properties (`FileVersion` and `AssemblyVersion`) are **not ### Automatic Version Derivation When only `` is set in a .NET project: -- **AssemblyVersion**: Automatically set to the numeric part of `Version` (e.g., `0.3.0` → `0.3.0.0`) -- **FileVersion**: Automatically set to the numeric part of `Version` (e.g., `0.3.0` → `0.3.0.0`) -- **InformationalVersion**: Set to the full `Version` value including suffixes (e.g., `0.3.0-beta.1`) +- **AssemblyVersion**: Automatically set to the numeric part of `Version` (e.g., `0.6.0-alpha.1` -> `0.6.0.0`) +- **FileVersion**: Automatically set to the numeric part of `Version` (e.g., `0.6.0-alpha.1` -> `0.6.0.0`) +- **InformationalVersion**: Set to the full `Version` value including suffixes (e.g., `0.6.0-alpha.1`) This ensures all version properties stay in sync automatically. @@ -31,7 +31,7 @@ During CI builds (`.github/workflows/ci.yml`), GitVersion determines the semanti dotnet build -p:Version=${{ needs.test.outputs.semVer }} ``` -This `-p:Version=...` argument overrides the `` property in the csproj, and consequently also sets `FileVersion` and `AssemblyVersion` to match. +This `-p:Version=...` argument overrides the `` property in the csproj, and consequently also sets `FileVersion`, `AssemblyVersion`, and `InformationalVersion` from the release tag. ### Auto-Updater Version Detection @@ -41,7 +41,9 @@ The Updatum auto-updater determines the current application version by reading t Assembly.GetExecutingAssembly().GetName().Version ``` -This is why it's critical that `AssemblyVersion` (and `FileVersion`) match the semantic version - otherwise, the updater will get confused and keep offering the same update repeatedly. +This is why it's critical that `AssemblyVersion` (and `FileVersion`) match the numeric release version - otherwise, the updater will get confused and keep offering the same update repeatedly. Pre-release labels are preserved in `InformationalVersion` for display and diagnostics, but the updater's comparison is numeric. + +Stable update checks use GitHub's latest release endpoint and ignore pre-releases. Alpha testing is opt-in: set `OPENCLAW_UPDATE_CHANNEL=alpha` before launching a Release build. That enables Updatum pre-release checks and makes it fetch the release list instead of only the latest stable release. The updater is also constrained to `.zip` release assets, because the portable ZIP payloads are the assets intended for in-app replacement; MSIX packages are installed/sideloaded separately. ## Historical Issue @@ -79,8 +81,8 @@ By removing the hardcoded `FileVersion` and `AssemblyVersion` properties, they n `src/OpenClaw.Shared/AppVersionInfo.cs` exposes: -- `AppVersionInfo.Version` → bare string, e.g. `"0.4.7"` -- `AppVersionInfo.DisplayVersion` → `"v"` prefix, e.g. `"v0.4.7"` +- `AppVersionInfo.Version` -> bare string, e.g. `"0.6.0-alpha.1"` +- `AppVersionInfo.DisplayVersion` -> `"v"` prefix, e.g. `"v0.6.0-alpha.1"` It resolves the version by: @@ -88,8 +90,8 @@ It resolves the version by: still report the tray's version rather than the testhost / dotnet host). 2. Falling back to `Assembly.GetEntryAssembly()`, then to the Shared assembly. 3. Reading `AssemblyInformationalVersionAttribute` (preferred) or `AssemblyVersion`. -4. Stripping SourceLink build metadata (`+abc123`) **and** the SemVer pre-release suffix (`-beta.1`) so the - displayed value matches what Updatum compares (Updatum reads the numeric `AssemblyVersion` only). +4. Stripping SourceLink build metadata (`+abc123`) while preserving SemVer pre-release suffixes (`-alpha.1`) so + alpha builds are visibly identifiable in About, diagnostics, handshakes, and update-check status. For tests that need a deterministic value regardless of host process, set the `internal` test hook: diff --git a/src/OpenClaw.Shared/AppVersionInfo.cs b/src/OpenClaw.Shared/AppVersionInfo.cs index 3f292bc58..26d5b72f5 100644 --- a/src/OpenClaw.Shared/AppVersionInfo.cs +++ b/src/OpenClaw.Shared/AppVersionInfo.cs @@ -29,10 +29,10 @@ public static class AppVersionInfo /// internal static string? TestOverride { get; set; } - /// Bare version string, e.g. "0.4.7". + /// Bare version string, e.g. "0.6.0-alpha.1". public static string Version => TestOverride ?? _version; - /// Version prefixed with v, e.g. "v0.4.7". + /// Version prefixed with v, e.g. "v0.6.0-alpha.1". public static string DisplayVersion => "v" + Version; private static string ResolveVersion() @@ -48,7 +48,7 @@ private static string ResolveVersion() ?.InformationalVersion; if (!string.IsNullOrWhiteSpace(informational)) { - return NormalizeSemVer(informational); + return NormalizeInformationalVersion(informational); } var name = assembly.GetName().Version; @@ -82,20 +82,12 @@ private static string ResolveVersion() return null; } - private static string NormalizeSemVer(string s) + internal static string NormalizeInformationalVersion(string s) { - // Strip SourceLink build metadata, e.g. "0.4.7+abc123" -> "0.4.7". + // Strip SourceLink build metadata, e.g. "0.6.0-alpha.1+abc123" -> "0.6.0-alpha.1". var plus = s.IndexOf('+'); if (plus >= 0) s = s.Substring(0, plus); - // Strip the SemVer pre-release suffix, e.g. "0.4.7-beta.1" -> "0.4.7". - // This keeps the UI string aligned with Updatum, which compares the - // numeric AssemblyVersion only. Revisit if pre-release labels should - // ever be surfaced to users. - var dash = s.IndexOf('-'); - if (dash >= 0) s = s.Substring(0, dash); - return s; } } - diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index 57f0a6cd5..95e3c3e81 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -32,9 +32,12 @@ namespace OpenClawTray; public partial class App : Application, OpenClawTray.Services.IAppCommands { + internal static readonly UpdateChannelSettings UpdateChannel = UpdateChannelPolicy.Resolve(); internal static readonly UpdatumManager AppUpdater = new("shanselman", "openclaw-windows-hub") { - FetchOnlyLatestRelease = true, + FetchOnlyLatestRelease = UpdateChannel.FetchOnlyLatestRelease, + AllowPreReleases = UpdateChannel.AllowPreReleases, + AssetExtensionFilter = UpdateChannel.AssetExtensionFilter, InstallUpdateSingleFileExecutableName = "OpenClaw.Tray.WinUI", }; diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index 83a911f1f..47e997402 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -9,7 +9,7 @@ true Assets\openclaw.ico OpenClawTray - 0.4.7 + 0.6.0-alpha.1 en-US x64;ARM64 win-x64;win-arm64 diff --git a/src/OpenClaw.Tray.WinUI/Services/UpdateChannelPolicy.cs b/src/OpenClaw.Tray.WinUI/Services/UpdateChannelPolicy.cs new file mode 100644 index 000000000..829574c5b --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Services/UpdateChannelPolicy.cs @@ -0,0 +1,40 @@ +using System; + +namespace OpenClawTray.Services; + +internal readonly record struct UpdateChannelSettings( + bool AllowPreReleases, + bool FetchOnlyLatestRelease, + string AssetExtensionFilter); + +internal static class UpdateChannelPolicy +{ + internal const string ChannelEnvironmentVariable = "OPENCLAW_UPDATE_CHANNEL"; + internal const string StableChannel = "stable"; + internal const string AlphaChannel = "alpha"; + internal const string PreReleaseChannel = "prerelease"; + internal const string ReleaseAssetExtension = "zip"; + + internal static UpdateChannelSettings Resolve(Func? envLookup = null) + { + envLookup ??= Environment.GetEnvironmentVariable; + var channel = envLookup(ChannelEnvironmentVariable); + var allowPreReleases = IsPreReleaseChannel(channel); + + return new UpdateChannelSettings( + AllowPreReleases: allowPreReleases, + FetchOnlyLatestRelease: !allowPreReleases, + AssetExtensionFilter: ReleaseAssetExtension); + } + + internal static bool IsPreReleaseChannel(string? channel) + { + if (string.IsNullOrWhiteSpace(channel)) + return false; + + var normalized = channel.Trim(); + return string.Equals(normalized, AlphaChannel, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalized, PreReleaseChannel, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalized, "pre-release", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/tests/OpenClaw.Shared.Tests/AppVersionInfoTests.cs b/tests/OpenClaw.Shared.Tests/AppVersionInfoTests.cs index 45b22e6eb..ed86fc5f6 100644 --- a/tests/OpenClaw.Shared.Tests/AppVersionInfoTests.cs +++ b/tests/OpenClaw.Shared.Tests/AppVersionInfoTests.cs @@ -67,10 +67,23 @@ public void Version_AfterClearingOverride_DoesNotReturnClearedValue() [Theory] [InlineData("1.2.3", "1.2.3")] [InlineData("0.4.7", "0.4.7")] + [InlineData("0.6.0-alpha.1", "0.6.0-alpha.1")] [InlineData("10.0.0", "10.0.0")] public void DisplayVersion_AlwaysEqualsVPlusVersion(string version, string expected) { AppVersionInfo.TestOverride = version; Assert.Equal("v" + expected, AppVersionInfo.DisplayVersion); } + + [Theory] + [InlineData("0.6.0", "0.6.0")] + [InlineData("0.6.0+abc123", "0.6.0")] + [InlineData("0.6.0-alpha.1", "0.6.0-alpha.1")] + [InlineData("0.6.0-alpha.1+abc123", "0.6.0-alpha.1")] + public void NormalizeInformationalVersion_StripsBuildMetadataButKeepsPreRelease( + string input, + string expected) + { + Assert.Equal(expected, AppVersionInfo.NormalizeInformationalVersion(input)); + } } diff --git a/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj b/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj index 4f6fdd2f2..757eb5b05 100644 --- a/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj +++ b/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj @@ -54,6 +54,7 @@ + diff --git a/tests/OpenClaw.Tray.Tests/UpdateChannelPolicyTests.cs b/tests/OpenClaw.Tray.Tests/UpdateChannelPolicyTests.cs new file mode 100644 index 000000000..32379817d --- /dev/null +++ b/tests/OpenClaw.Tray.Tests/UpdateChannelPolicyTests.cs @@ -0,0 +1,45 @@ +using OpenClawTray.Services; + +namespace OpenClaw.Tray.Tests; + +public sealed class UpdateChannelPolicyTests +{ + [Fact] + public void Resolve_DefaultsToStableLatestOnlyZipUpdates() + { + var settings = UpdateChannelPolicy.Resolve(_ => null); + + Assert.False(settings.AllowPreReleases); + Assert.True(settings.FetchOnlyLatestRelease); + Assert.Equal("zip", settings.AssetExtensionFilter); + } + + [Theory] + [InlineData("alpha")] + [InlineData("Alpha")] + [InlineData(" prerelease ")] + [InlineData("pre-release")] + public void Resolve_AlphaChannelAllowsPreReleasesAndFetchesReleaseList(string channel) + { + var settings = UpdateChannelPolicy.Resolve(key => + key == UpdateChannelPolicy.ChannelEnvironmentVariable ? channel : null); + + Assert.True(settings.AllowPreReleases); + Assert.False(settings.FetchOnlyLatestRelease); + Assert.Equal("zip", settings.AssetExtensionFilter); + } + + [Theory] + [InlineData("")] + [InlineData("stable")] + [InlineData("beta")] + public void Resolve_NonAlphaChannelStaysOnStableLatestOnly(string? channel) + { + var settings = UpdateChannelPolicy.Resolve(key => + key == UpdateChannelPolicy.ChannelEnvironmentVariable ? channel : null); + + Assert.False(settings.AllowPreReleases); + Assert.True(settings.FetchOnlyLatestRelease); + Assert.Equal("zip", settings.AssetExtensionFilter); + } +} diff --git a/tests/OpenClaw.Tray.UITests/A2UIDashboardScaleTest.cs b/tests/OpenClaw.Tray.UITests/A2UIDashboardScaleTest.cs index bea35e8c2..7bd701c0f 100644 --- a/tests/OpenClaw.Tray.UITests/A2UIDashboardScaleTest.cs +++ b/tests/OpenClaw.Tray.UITests/A2UIDashboardScaleTest.cs @@ -6,6 +6,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Shapes; +using OpenClaw.Shared; using static OpenClaw.Tray.UITests.A2UI; using static OpenClaw.Tray.UITests.TestSupport; @@ -320,7 +321,7 @@ private static string BuildDashboardJsonl() // ── Footer row ──────────────────────────────────────────────────── components.Add(Component("ftr", "Row", new() { ["children"] = Children("ftrVer", "ftrDiv", "ftrConn") })); - components.Add(Component("ftrVer", "Text", new() { ["text"] = Lit("v0.4.7"), ["usageHint"] = "caption" })); + components.Add(Component("ftrVer", "Text", new() { ["text"] = Lit(AppVersionInfo.DisplayVersion), ["usageHint"] = "caption" })); components.Add(Component("ftrDiv", "Divider", new() { ["axis"] = "vertical" })); components.Add(Component("ftrConn", "Text", new() { ["text"] = Lit("Connected"), ["usageHint"] = "caption" }));