Skip to content
Merged
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
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"gitversion.tool": {
"version": "6.4.0",
"commands": [
"dotnet-gitversion"
],
"rollForward": false
}
}
}
29 changes: 23 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ jobs:

steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup .NET 10
uses: actions/setup-dotnet@v5
Expand All @@ -371,17 +373,32 @@ jobs:
run: dotnet restore src/OpenClaw.Tray.WinUI -r ${{ matrix.rid }}

- name: Build WinUI Tray App (Release)
run: dotnet build src/OpenClaw.Tray.WinUI --no-restore -c Release -r ${{ matrix.rid }} -p:Version=${{ needs.test.outputs.semVer }}
run: dotnet build src/OpenClaw.Tray.WinUI --no-restore -c Release -r ${{ matrix.rid }}

- name: Publish WinUI Tray App
run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -p:Version=${{ needs.test.outputs.semVer }} -o publish
run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r ${{ matrix.rid }} --self-contained --no-restore -o publish

- name: Publish SetupEngine.UI
run: |
dotnet publish src/OpenClaw.SetupEngine.UI -c Release -r ${{ matrix.rid }} --self-contained -p:Version=${{ needs.test.outputs.semVer }} -o publish-setup
dotnet publish src/OpenClaw.SetupEngine.UI -c Release -r ${{ matrix.rid }} --self-contained -o publish-setup
mkdir publish\SetupEngine
copy publish-setup\* publish\SetupEngine\ -Recurse

- name: Verify GitVersion assembly metadata
shell: pwsh
run: |
$expected = "${{ needs.test.outputs.semVer }}"
$assemblyPath = Resolve-Path "publish\OpenClaw.Tray.WinUI.dll"
$assembly = [System.Reflection.Assembly]::LoadFile($assemblyPath)
$attribute = $assembly.GetCustomAttributes([System.Reflection.AssemblyInformationalVersionAttribute], $false) | Select-Object -First 1
if (-not $attribute) {
throw "OpenClaw.Tray.WinUI.dll is missing AssemblyInformationalVersionAttribute."
}
$actual = $attribute.InformationalVersion -replace '\+.*$', ''
if ($actual -ne $expected) {
throw "AssemblyInformationalVersion '$actual' did not match GitVersion SemVer '$expected'."
}

- name: Disable NuGet source mapping for signing
if: startsWith(github.ref, 'refs/tags/v') && matrix.rid != 'win-arm64'
shell: pwsh
Expand Down Expand Up @@ -450,6 +467,8 @@ jobs:

steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup .NET 10 for VS MSBuild
uses: actions/setup-dotnet@v5
Expand Down Expand Up @@ -505,7 +524,6 @@ jobs:
/p:AppxPackageSigningEnabled=false
/p:AppxBundle=Never
/p:UapAppxPackageBuildMode=SideloadOnly
/p:Version=${{ needs.test.outputs.majorMinorPatch }}
/p:AppxPackageDir=AppPackages\

- name: Find MSIX Package
Expand Down Expand Up @@ -661,14 +679,13 @@ jobs:
- 🦞 System tray integration with gateway status
- 🔄 Auto-updates from GitHub Releases
- ✅ Code-signed with Azure Trusted Signing
- 📦 MSIX package available for native camera/microphone consent prompts

### Requirements
- Windows 10 version 1903 or later
- [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
- OpenClaw gateway running locally

### Quick Start
1. Run the installer for your architecture (or sideload the MSIX for camera consent)
1. Run the installer for your architecture
2. Launch from Start Menu or system tray
3. Right-click tray icon → Settings to configure
7 changes: 4 additions & 3 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ Only tag when `HEAD == origin/master`.
## Versioning rules

- Do not manually bump project or manifest versions for routine releases.
- Treat csproj `<Version>` as a local fallback for dev builds.
- Do not add csproj `<Version>` release fallbacks; product versions come from
GitVersion/tag history.
- Release versions come from the tag (`vX.Y.Z` or `vX.Y.Z-alpha.N`).
- CI computes the version from git history/tags and passes it to builds.

- CI computes GitVersion outputs for artifact naming, while product builds use
GitVersion-backed assembly metadata.
122 changes: 47 additions & 75 deletions docs/VERSIONING.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,73 @@
# Versioning in OpenClaw Windows Hub

## How Versioning Works
## Source of truth

This project uses GitVersion for automatic semantic versioning based on git tags and commit history. The version is used in multiple places:
OpenClaw uses GitVersion and git tags for application versioning. Product
project files must not hardcode release versions with `<Version>` elements.

### Version Properties in .csproj
Canonical release tags use:

The project file (`OpenClaw.Tray.WinUI.csproj`) defines only the `<Version>` property:
- Stable: `vX.Y.Z`
- Alpha: `vX.Y.Z-alpha.N`

```xml
<Version>0.3.0</Version>
```

Other version-related properties (`FileVersion` and `AssemblyVersion`) are **not** explicitly set in the csproj files. This is intentional.
`GitVersion.yml` controls how tag history becomes SemVer. The product build
imports GitVersion through `src\Directory.Build.props`, so normal `dotnet build`,
`.\build.ps1`, `.\run-app-local.ps1`, and CI builds all derive assembly metadata
from the same tag history.

### Automatic Version Derivation
## Assembly metadata

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`)
GitVersion-derived builds set:

This ensures all version properties stay in sync automatically.
- `AssemblyVersion` and `FileVersion` to numeric versions Windows/.NET can
compare.
- `AssemblyInformationalVersion` to the SemVer identity used by user-visible
surfaces.

### CI Build Process
`OpenClaw.Shared.AppVersionInfo` reads `AssemblyInformationalVersionAttribute`
from the tray assembly and exposes:

During CI builds (`.github/workflows/ci.yml`), GitVersion determines the semantic version from git history and passes it to the build:

```bash
dotnet build -p:Version=${{ needs.test.outputs.semVer }}
```
- `AppVersionInfo.Version` -> bare SemVer, for example `1.2.3-alpha.4`
- `AppVersionInfo.DisplayVersion` -> `v`-prefixed SemVer, for example
`v1.2.3-alpha.4`

This `-p:Version=...` argument overrides the `<Version>` property in the csproj, and consequently also sets `FileVersion` and `AssemblyVersion` to match.
Build metadata after `+` is stripped before display, but prerelease labels are
preserved. That makes alpha builds identify themselves precisely in About,
diagnostics, support context, `device.info`, MCP handshake metadata, and update
diagnostics.

### Auto-Updater Version Detection
## CI release flow

The Updatum auto-updater determines the current application version by reading the **AssemblyVersion** from the running executable using:
The release workflow computes GitVersion in the `test` job for workflow outputs
and artifact naming. Product builds themselves also use GitVersion-backed
MSBuild metadata; CI should not pass a competing hardcoded `-p:Version=...`
value that could hide drift.

```csharp
Assembly.GetExecutingAssembly().GetName().Version
```
Release build jobs must check out full git history (`fetch-depth: 0`) so
GitVersion can see tags.

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.
## Local scripts

## Historical Issue
`scripts\Get-OpenClawVersion.ps1` uses the repository-local
`.config\dotnet-tools.json` manifest and `GitVersion.Tool` to print the same
GitVersion value local scripts need outside MSBuild.

Previously, the csproj files had hardcoded values:
For example:

```xml
<Version>0.3.0</Version>
<FileVersion>0.2.0</FileVersion>
<AssemblyVersion>0.2.0</AssemblyVersion>
```powershell
.\scripts\Get-OpenClawVersion.ps1 -Variable SemVer
.\scripts\Get-OpenClawVersion.ps1 -Variable MajorMinorPatch
```

This caused a version mismatch:
- The semantic version was 0.3.0
- But the file and assembly versions were stuck at 0.2.0
- Updatum would read 0.2.0 from the running EXE
- It would see 0.4.0 available on GitHub
- It would offer to update from "0.2.0" to "0.4.0" even though the user was already on 0.3.0 or 0.4.0

## Solution

By removing the hardcoded `FileVersion` and `AssemblyVersion` properties, they now automatically derive from `Version`. When CI overrides `Version` via command-line, all three properties are set correctly and consistently.

## Best Practices
`scripts\build-inno-local.ps1` uses that helper for Inno's `AppVersion` when
`-Version` is not explicitly supplied.

1. **Never hardcode `FileVersion` or `AssemblyVersion` in the csproj** - let them auto-derive from `Version`
2. **Let GitVersion and CI control the version** - the csproj's `<Version>` is just a fallback for local development builds
3. **Test version detection** - after building, check the EXE properties to ensure FileVersion matches expectations
4. **Use semantic versioning** - tags should follow `v{major}.{minor}.{patch}` format (e.g., `v0.4.0`)
5. **Use `OpenClaw.Shared.AppVersionInfo` for any user-visible or wire-exposed version string** - never re-roll
`typeof(...).Assembly.GetName().Version` or hardcode literals like `"v0.1.0"`. `AppVersionInfo` is the single
source of truth driven by `<Version>`, used by the About page, Update dialog, support-context dump,
`device.info` capability, MCP `serverVersion` handshake, and the update-check diagnostics.
## Guardrails

## Runtime Version Resolution (AppVersionInfo)

`src/OpenClaw.Shared/AppVersionInfo.cs` exposes:

- `AppVersionInfo.Version` → bare string, e.g. `"0.4.7"`
- `AppVersionInfo.DisplayVersion` → `"v"` prefix, e.g. `"v0.4.7"`

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).

For tests that need a deterministic value regardless of host process, set the `internal` test hook:

```csharp
AppVersionInfo.TestOverride = "9.9.9";
```
- Do not add `<Version>` release literals to product `.csproj` files.
- Do not hardcode user-visible version strings like `vX.Y.Z` in active code or
tests; use `AppVersionInfo`.
- Keep release tags and `GitVersion.yml` as the versioning contract.

## References

Expand Down
46 changes: 46 additions & 0 deletions scripts/Get-OpenClawVersion.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<#
.SYNOPSIS
Prints the GitVersion-derived OpenClaw version.

.DESCRIPTION
Uses the repository-local GitVersion.Tool manifest so local scripts and CI
derive versions from the same GitVersion.yml/tag history as release builds.
#>

[CmdletBinding()]
param(
[ValidateSet("SemVer", "MajorMinorPatch")]
[string]$Variable = "SemVer",

[switch]$NoRestore
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Push-Location $repoRoot
try {
if (-not $NoRestore) {
dotnet tool restore | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "dotnet tool restore failed."
}
}

$gitVersionOutput = & dotnet tool run dotnet-gitversion -- /output json 2>&1
if ($LASTEXITCODE -ne 0) {
throw "GitVersion failed: $gitVersionOutput"
}

$gitVersion = $gitVersionOutput | ConvertFrom-Json
$value = $gitVersion.$Variable
if ([string]::IsNullOrWhiteSpace($value)) {
throw "GitVersion did not return '$Variable'."
}

Write-Output $value
}
finally {
Pop-Location
}
57 changes: 32 additions & 25 deletions scripts/build-inno-local.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,6 @@ function Resolve-InnoCompiler {
throw "Inno Setup compiler (ISCC.exe) was not found. Install it, or rerun with -InstallInno."
}

function Get-ProjectVersion {
$projectPath = Join-Path $repoRoot "src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj"
[xml]$project = Get-Content -LiteralPath $projectPath -Raw
$project.Project.PropertyGroup |
ForEach-Object { $_.Version } |
Where-Object { $_ } |
Select-Object -First 1
}

function Get-RidForArch {
param([string]$Architecture)
if ($Architecture -eq "arm64") {
Expand All @@ -104,24 +95,36 @@ function Publish-ArchitecturePayload {
Remove-Item -LiteralPath $publishDir, $setupPublishDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $publishDir | Out-Null

dotnet publish .\src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj `
-c $Configuration `
-r $RuntimeIdentifier `
--self-contained `
-p:Version=$PublishVersion `
-o $publishDir `
-v:minimal
$trayPublishArgs = @(
".\src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj",
"-c", $Configuration,
"-r", $RuntimeIdentifier,
"--self-contained",
"-o", $publishDir,
"-v:minimal"
)
if ($PublishVersion) {
$trayPublishArgs += "-p:Version=$PublishVersion"
}

dotnet publish @trayPublishArgs
if ($LASTEXITCODE -ne 0) {
throw "Tray publish failed for $Architecture."
}

dotnet publish .\src\OpenClaw.SetupEngine.UI\OpenClaw.SetupEngine.UI.csproj `
-c $Configuration `
-r $RuntimeIdentifier `
--self-contained `
-p:Version=$PublishVersion `
-o $setupPublishDir `
-v:minimal
$setupPublishArgs = @(
".\src\OpenClaw.SetupEngine.UI\OpenClaw.SetupEngine.UI.csproj",
"-c", $Configuration,
"-r", $RuntimeIdentifier,
"--self-contained",
"-o", $setupPublishDir,
"-v:minimal"
)
if ($PublishVersion) {
$setupPublishArgs += "-p:Version=$PublishVersion"
}

dotnet publish @setupPublishArgs
if ($LASTEXITCODE -ne 0) {
throw "SetupEngine.UI publish failed for $Architecture."
}
Expand Down Expand Up @@ -178,8 +181,11 @@ function Invoke-InnoCompiler {
}
}

$versionWasProvided = $PSBoundParameters.ContainsKey("Version")

if (-not $Version) {
$Version = Get-ProjectVersion
$versionScript = Join-Path $PSScriptRoot "Get-OpenClawVersion.ps1"
$Version = & $versionScript -Variable SemVer
}

if (-not $Version) {
Expand All @@ -198,7 +204,8 @@ Write-Host "No publish: $($NoPublish.IsPresent)"
foreach ($architecture in $architectures) {
$rid = Get-RidForArch $architecture
if (-not $NoPublish) {
Publish-ArchitecturePayload -Architecture $architecture -RuntimeIdentifier $rid -PublishVersion $Version
$publishVersion = if ($versionWasProvided) { $Version } else { $null }
Publish-ArchitecturePayload -Architecture $architecture -RuntimeIdentifier $rid -PublishVersion $publishVersion
}

$payload = Assert-PayloadReady $architecture
Expand Down
4 changes: 4 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="6.4.0" PrivateAssets="all" />
</ItemGroup>

</Project>
Loading
Loading