Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'`.
Expand Down
24 changes: 13 additions & 11 deletions docs/VERSIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ 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 `<Version>` property:
The project file (`OpenClaw.Tray.WinUI.csproj`) defines only the `<Version>` property. CI release builds override this value from the pushed tag; the checked-in value is the local-development fallback:

```xml
<Version>0.3.0</Version>
<Version>0.6.0-alpha.1</Version>
```

Other version-related properties (`FileVersion` and `AssemblyVersion`) are **not** explicitly set in the csproj files. This is intentional.

### Automatic Version Derivation

When only `<Version>` 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.

Expand All @@ -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 `<Version>` property in the csproj, and consequently also sets `FileVersion` and `AssemblyVersion` to match.
This `-p:Version=...` argument overrides the `<Version>` property in the csproj, and consequently also sets `FileVersion`, `AssemblyVersion`, and `InformationalVersion` from the release tag.

### Auto-Updater Version Detection

Expand All @@ -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

Expand Down Expand Up @@ -79,17 +81,17 @@ 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:

1. Looking for the `OpenClaw.Tray.WinUI` assembly in the current `AppDomain` (so `dotnet test` and CLI siblings
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:

Expand Down
18 changes: 5 additions & 13 deletions src/OpenClaw.Shared/AppVersionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ public static class AppVersionInfo
/// </summary>
internal static string? TestOverride { get; set; }

/// <summary>Bare version string, e.g. <c>"0.4.7"</c>.</summary>
/// <summary>Bare version string, e.g. <c>"0.6.0-alpha.1"</c>.</summary>
public static string Version => TestOverride ?? _version;

/// <summary>Version prefixed with <c>v</c>, e.g. <c>"v0.4.7"</c>.</summary>
/// <summary>Version prefixed with <c>v</c>, e.g. <c>"v0.6.0-alpha.1"</c>.</summary>
public static string DisplayVersion => "v" + Version;

private static string ResolveVersion()
Expand All @@ -48,7 +48,7 @@ private static string ResolveVersion()
?.InformationalVersion;
if (!string.IsNullOrWhiteSpace(informational))
{
return NormalizeSemVer(informational);
return NormalizeInformationalVersion(informational);
}

var name = assembly.GetName().Version;
Expand Down Expand Up @@ -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;
}
}

5 changes: 4 additions & 1 deletion src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};

Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<EnableMsixTooling>true</EnableMsixTooling>
<ApplicationIcon>Assets\openclaw.ico</ApplicationIcon>
<RootNamespace>OpenClawTray</RootNamespace>
<Version>0.4.7</Version>
<Version>0.6.0-alpha.1</Version>
<DefaultLanguage>en-US</DefaultLanguage>
<Platforms>x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
Expand Down
40 changes: 40 additions & 0 deletions src/OpenClaw.Tray.WinUI/Services/UpdateChannelPolicy.cs
Original file line number Diff line number Diff line change
@@ -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<string, string?>? 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);
}
}
13 changes: 13 additions & 0 deletions tests/OpenClaw.Shared.Tests/AppVersionInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
1 change: 1 addition & 0 deletions tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<Compile Include="..\..\src\OpenClaw.Tray.WinUI\Services\GatewayNavVisibilityDebouncePolicy.cs" Link="Services\GatewayNavVisibilityDebouncePolicy.cs" />
<Compile Include="..\..\src\OpenClaw.Tray.WinUI\Services\NodeInvokeActivityFormatter.cs" Link="Services\NodeInvokeActivityFormatter.cs" />
<Compile Include="..\..\src\OpenClaw.Tray.WinUI\Services\NodeCapabilityGating.cs" Link="Services\NodeCapabilityGating.cs" />
<Compile Include="..\..\src\OpenClaw.Tray.WinUI\Services\UpdateChannelPolicy.cs" Link="Services\UpdateChannelPolicy.cs" />
<Compile Include="..\..\src\OpenClaw.Tray.WinUI\Services\ToastActivationRouter.cs" Link="Services\ToastActivationRouter.cs" />
<Compile Include="..\..\src\OpenClaw.Tray.WinUI\Helpers\TrayTooltipFormatter.cs" Link="Helpers\TrayTooltipFormatter.cs" />
<Compile Include="..\..\src\OpenClaw.Tray.WinUI\Helpers\GatewayDashboardUrlBuilder.cs" Link="Helpers\GatewayDashboardUrlBuilder.cs" />
Expand Down
45 changes: 45 additions & 0 deletions tests/OpenClaw.Tray.Tests/UpdateChannelPolicyTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 2 additions & 1 deletion tests/OpenClaw.Tray.UITests/A2UIDashboardScaleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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" }));

Expand Down
Loading