diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d260b1b..0d6b4fb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -2,12 +2,14 @@ name: Release
on:
push:
+ branches:
+ - master
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
tag_name:
- description: 'Release tag (example: v2.1.0)'
+ description: 'Release tag (example: v2.2.0)'
required: true
type: string
@@ -28,20 +30,47 @@ jobs:
with:
dotnet-version: '9.0.x'
- - name: Resolve tag
- id: tag
+ - name: Resolve release tag
+ id: resolve
shell: pwsh
run: |
- if ("${{ github.event_name }}" -eq "workflow_dispatch") {
- "tag=${{ inputs.tag_name }}" >> $env:GITHUB_OUTPUT
- } else {
- "tag=${{ github.ref_name }}" >> $env:GITHUB_OUTPUT
+ $proceed = 'true'
+ if ($env:GITHUB_EVENT_NAME -eq 'push' -and $env:GITHUB_REF -like 'refs/tags/*') {
+ # Manual tag push: use the pushed tag.
+ $tag = $env:GITHUB_REF_NAME
}
+ elseif ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') {
+ # Manual run: use the provided tag.
+ $tag = '${{ inputs.tag_name }}'
+ }
+ else {
+ # Push to master: derive the tag from the project version and
+ # only release if that tag does not already exist.
+ $csproj = Get-Content "Proxmox Desktop/ProxmoxDesktop.csproj" -Raw
+ if ($csproj -notmatch '([^<]+)') {
+ Write-Error "InformationalVersion not found in csproj"; exit 1
+ }
+ $version = $Matches[1].Trim()
+ $tag = "v$version"
+ git fetch --tags --quiet
+ if (git tag -l $tag) {
+ Write-Host "Tag $tag already exists - nothing to release."
+ $proceed = 'false'
+ }
+ else {
+ Write-Host "New version detected: $tag"
+ }
+ }
+ "tag=$tag" >> $env:GITHUB_OUTPUT
+ "proceed=$proceed" >> $env:GITHUB_OUTPUT
+ Write-Host "Resolved tag=$tag proceed=$proceed"
- name: Restore
+ if: steps.resolve.outputs.proceed == 'true'
run: dotnet restore "Proxmox Desktop/ProxmoxDesktop.csproj"
- name: Publish (self-contained win-x64)
+ if: steps.resolve.outputs.proceed == 'true'
shell: pwsh
run: |
dotnet publish "Proxmox Desktop/ProxmoxDesktop.csproj" `
@@ -53,20 +82,22 @@ jobs:
-o output/ProxmoxDesktop
- name: Create ZIP
+ if: steps.resolve.outputs.proceed == 'true'
shell: pwsh
run: |
Compress-Archive `
-Path output/ProxmoxDesktop/* `
- -DestinationPath output/ProxmoxDesktop-${{ steps.tag.outputs.tag }}-win-x64.zip
+ -DestinationPath output/ProxmoxDesktop-${{ steps.resolve.outputs.tag }}-win-x64.zip
- name: Create GitHub Release
+ if: steps.resolve.outputs.proceed == 'true'
uses: softprops/action-gh-release@v2
with:
- tag_name: ${{ steps.tag.outputs.tag }}
- name: "Proxmox Desktop ${{ steps.tag.outputs.tag }}"
+ tag_name: ${{ steps.resolve.outputs.tag }}
+ name: "Proxmox Desktop ${{ steps.resolve.outputs.tag }}"
body: |
## 📦 Installation
- 1. Download `ProxmoxDesktop-${{ steps.tag.outputs.tag }}-win-x64.zip`
+ 1. Download `ProxmoxDesktop-${{ steps.resolve.outputs.tag }}-win-x64.zip`
2. Extract anywhere
3. Run `ProxmoxDesktop.exe`
@@ -76,9 +107,9 @@ jobs:
- Windows 10 (build 17763+) or Windows 11
- [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) *(pre-installed on Windows 11)*
- [Virt-Viewer + UsbDk](https://www.spice-space.org/download.html) *(SPICE only)*
- files: output/ProxmoxDesktop-${{ steps.tag.outputs.tag }}-win-x64.zip
+ files: output/ProxmoxDesktop-${{ steps.resolve.outputs.tag }}-win-x64.zip
generate_release_notes: true
draft: false
- prerelease: ${{ contains(steps.tag.outputs.tag, '-beta') || contains(steps.tag.outputs.tag, '-rc') }}
+ prerelease: ${{ contains(steps.resolve.outputs.tag, '-beta') || contains(steps.resolve.outputs.tag, '-rc') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Proxmox Desktop/Api/ApiClient.cs b/Proxmox Desktop/Api/ApiClient.cs
index 5f52d96..028c0bb 100644
--- a/Proxmox Desktop/Api/ApiClient.cs
+++ b/Proxmox Desktop/Api/ApiClient.cs
@@ -65,10 +65,14 @@ public async Task LoginAsync(
if (!string.IsNullOrWhiteSpace(otp)) form["otp"] = otp;
TicketResponse? result;
- try { result = await PostTicketAsync(form, ct); }
+ HttpStatusCode status;
+ try { (result, status) = await PostTicketWithStatusAsync(form, ct); }
catch (Exception ex) { return LoginResult.Failure($"Cannot reach server: {ex.Message}"); }
- if (result is null) return LoginResult.Failure("Invalid server response.");
+ if (result is null)
+ return LoginResult.Failure(status == HttpStatusCode.Unauthorized
+ ? "Login rejected — check username, password and realm, and enter the TOTP code if 2FA is enabled."
+ : $"Login rejected by the server (HTTP {(int)status}).");
if (result.Ticket?.Contains("PVE:!tfa!") == true)
return string.IsNullOrWhiteSpace(otp)
@@ -261,12 +265,16 @@ private async Task GetRawAsync(string path, CancellationToken ct)
private async Task PostTicketAsync(
Dictionary form, CancellationToken ct)
+ => (await PostTicketWithStatusAsync(form, ct)).Data;
+
+ private async Task<(TicketResponse? Data, HttpStatusCode Status)> PostTicketWithStatusAsync(
+ Dictionary form, CancellationToken ct)
{
var resp = await _http.PostAsync(
"access/ticket", new FormUrlEncodedContent(form), ct);
- if (!resp.IsSuccessStatusCode) return null;
var body = await resp.Content.ReadAsStringAsync(ct);
- return JsonSerializer.Deserialize>(body, _json)?.Data;
+ if (!resp.IsSuccessStatusCode) return (null, resp.StatusCode);
+ return (JsonSerializer.Deserialize>(body, _json)?.Data, resp.StatusCode);
}
private async Task> FetchMachinesAsync(
diff --git a/Proxmox Desktop/ProxmoxDesktop.csproj b/Proxmox Desktop/ProxmoxDesktop.csproj
index b583f92..1cb30de 100644
--- a/Proxmox Desktop/ProxmoxDesktop.csproj
+++ b/Proxmox Desktop/ProxmoxDesktop.csproj
@@ -10,8 +10,8 @@
true
ProxmoxDesktop
ProxmoxDesktop
- 2.2.0.0
- 2.2.0
+ 2.2.1.0
+ 2.2.1
x64
win-x64
win-x64
diff --git a/README.md b/README.md
index e87b9a6..35d2d74 100644
--- a/README.md
+++ b/README.md
@@ -93,18 +93,21 @@ Every push to `master` and every pull request automatically triggers a build. If
### Release
-Releases are created in two ways:
+Releases are created three ways:
-**Option 1 — Git tag** (from your machine):
+**Option 1 — Automatic on version bump** *(recommended)*:
+Bump `` in `Proxmox Desktop/ProxmoxDesktop.csproj`, commit and merge to `master`. The Release workflow reads the version, and if no `v` tag exists yet, it builds, zips and publishes the GitHub Release automatically. Pushing `master` without changing the version is a no-op, so it never spams releases.
+
+**Option 2 — Git tag** (from your machine):
```bash
-git tag v2.1.0
-git push origin v2.1.0
+git tag v2.2.0
+git push origin v2.2.0
```
-**Option 2 — Manual trigger** (from GitHub UI):
+**Option 3 — Manual trigger** (from GitHub UI):
1. Go to [Actions → Release](../../actions/workflows/release.yml)
2. Click **Run workflow**
-3. Enter the tag name (e.g. `v2.1.0`) and confirm
+3. Enter the tag name (e.g. `v2.2.0`) and confirm
Tags containing `-beta` or `-rc` are automatically marked as pre-releases.